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