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 java.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 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 */ 30 class 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