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 
18 package android.util.jar;
19 
20 import java.io.ByteArrayOutputStream;
21 import java.io.IOException;
22 import java.nio.charset.StandardCharsets;
23 import java.util.HashMap;
24 import java.util.Map;
25 import 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  */
31 class 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 
StrictJarManifestReader(byte[] buf, Attributes main)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 
readEntries(Map<String, Attributes> entries, Map<String, StrictJarManifest.Chunk> chunks)58     public void readEntries(Map<String, Attributes> entries, Map<String, StrictJarManifest.Chunk> chunks) throws IOException {
59         int mark = pos;
60         while (readHeader()) {
61             if (!StrictJarManifest.ATTRIBUTE_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 
getEndOfMainSection()93     public int getEndOfMainSection() {
94         return endOfMainSection;
95     }
96 
97     /**
98      * Read a single line from the manifest buffer.
99      */
readHeader()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 
readName()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 
readValue()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