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