1/*
2 * Licensed to the Apache Software Foundation (ASF) under one or more
3 * contributor license agreements.  See the NOTICE file distributed with
4 * this work for additional information regarding copyright ownership.
5 * The ASF licenses this file to You under the Apache License, Version 2.0
6 * (the "License"); you may not use this file except in compliance with
7 * the License.  You may obtain a copy of the License at
8 *
9 *     http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18package java.util.jar;
19
20import java.io.ByteArrayOutputStream;
21import java.io.IOException;
22import java.nio.charset.StandardCharsets;
23import java.util.HashMap;
24import java.util.Map;
25
26/**
27 * Reads a JAR file manifest. The specification is here:
28 * http://java.sun.com/javase/6/docs/technotes/guides/jar/jar.html
29 */
30class ManifestReader {
31    // There are relatively few unique attribute names,
32    // but a manifest might have thousands of entries.
33    private final HashMap<String, Attributes.Name> attributeNameCache = new HashMap<String, Attributes.Name>();
34
35    private final ByteArrayOutputStream valueBuffer = new ByteArrayOutputStream(80);
36
37    private final byte[] buf;
38
39    private final int endOfMainSection;
40
41    private int pos;
42
43    private Attributes.Name name;
44
45    private String value;
46
47    private int consecutiveLineBreaks = 0;
48
49    public ManifestReader(byte[] buf, Attributes main) throws IOException {
50        this.buf = buf;
51        while (readHeader()) {
52            main.put(name, value);
53        }
54        this.endOfMainSection = pos;
55    }
56
57    public void readEntries(Map<String, Attributes> entries, Map<String, Manifest.Chunk> chunks) throws IOException {
58        int mark = pos;
59        while (readHeader()) {
60            if (!Attributes.Name.NAME.equals(name)) {
61                throw new IOException("Entry is not named");
62            }
63            String entryNameValue = value;
64
65            Attributes entry = entries.get(entryNameValue);
66            if (entry == null) {
67                entry = new Attributes(12);
68            }
69
70            while (readHeader()) {
71                entry.put(name, value);
72            }
73
74            if (chunks != null) {
75                if (chunks.get(entryNameValue) != null) {
76                    // TODO A bug: there might be several verification chunks for
77                    // the same name. I believe they should be used to update
78                    // signature in order of appearance; there are two ways to fix
79                    // this: either use a list of chunks, or decide on used
80                    // signature algorithm in advance and reread the chunks while
81                    // updating the signature; for now a defensive error is thrown
82                    throw new IOException("A jar verifier does not support more than one entry with the same name");
83                }
84                chunks.put(entryNameValue, new Manifest.Chunk(mark, pos));
85                mark = pos;
86            }
87
88            entries.put(entryNameValue, entry);
89        }
90    }
91
92    public int getEndOfMainSection() {
93        return endOfMainSection;
94    }
95
96    /**
97     * Read a single line from the manifest buffer.
98     */
99    private boolean readHeader() throws IOException {
100        if (consecutiveLineBreaks > 1) {
101            // break a section on an empty line
102            consecutiveLineBreaks = 0;
103            return false;
104        }
105        readName();
106        consecutiveLineBreaks = 0;
107        readValue();
108        // if the last line break is missed, the line
109        // is ignored by the reference implementation
110        return consecutiveLineBreaks > 0;
111    }
112
113    private void readName() throws IOException {
114        int mark = pos;
115
116        while (pos < buf.length) {
117            if (buf[pos++] != ':') {
118                continue;
119            }
120
121            String nameString = new String(buf, mark, pos - mark - 1, StandardCharsets.US_ASCII);
122
123            if (buf[pos++] != ' ') {
124                throw new IOException(String.format("Invalid value for attribute '%s'", nameString));
125            }
126
127            try {
128                name = attributeNameCache.get(nameString);
129                if (name == null) {
130                    name = new Attributes.Name(nameString);
131                    attributeNameCache.put(nameString, name);
132                }
133            } catch (IllegalArgumentException e) {
134                // new Attributes.Name() throws IllegalArgumentException but we declare IOException
135                throw new IOException(e.getMessage());
136            }
137            return;
138        }
139    }
140
141    private void readValue() throws IOException {
142        boolean lastCr = false;
143        int mark = pos;
144        int last = pos;
145        valueBuffer.reset();
146        while (pos < buf.length) {
147            byte next = buf[pos++];
148            switch (next) {
149            case 0:
150                throw new IOException("NUL character in a manifest");
151            case '\n':
152                if (lastCr) {
153                    lastCr = false;
154                } else {
155                    consecutiveLineBreaks++;
156                }
157                continue;
158            case '\r':
159                lastCr = true;
160                consecutiveLineBreaks++;
161                continue;
162            case ' ':
163                if (consecutiveLineBreaks == 1) {
164                    valueBuffer.write(buf, mark, last - mark);
165                    mark = pos;
166                    consecutiveLineBreaks = 0;
167                    continue;
168                }
169            }
170
171            if (consecutiveLineBreaks >= 1) {
172                pos--;
173                break;
174            }
175            last = pos;
176        }
177
178        valueBuffer.write(buf, mark, last - mark);
179        // A bit frustrating that that Charset.forName will be called
180        // again.
181        value = valueBuffer.toString(StandardCharsets.UTF_8.name());
182    }
183}
184