blob: b17abc8a0ffaf1f8b510f884ef4e492fab55bb51 [file] [log] [blame]
Przemyslaw Szczepaniak8a7c1602015-11-03 09:47:56 +00001/*
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()) {
Tobias Thierer8a0845e2018-07-13 16:44:45 +010061 if (!StrictJarManifest.ATTRIBUTE_NAME_NAME.equals(name)) {
Przemyslaw Szczepaniak8a7c1602015-11-03 09:47:56 +000062 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}