/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.util.jar; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; import java.util.jar.Attributes; /** * Reads a JAR file manifest. The specification is here: * http://java.sun.com/javase/6/docs/technotes/guides/jar/jar.html */ class StrictJarManifestReader { // There are relatively few unique attribute names, // but a manifest might have thousands of entries. private final HashMap<String, Attributes.Name> attributeNameCache = new HashMap<String, Attributes.Name>(); private final ByteArrayOutputStream valueBuffer = new ByteArrayOutputStream(80); private final byte[] buf; private final int endOfMainSection; private int pos; private Attributes.Name name; private String value; private int consecutiveLineBreaks = 0; public StrictJarManifestReader(byte[] buf, Attributes main) throws IOException { this.buf = buf; while (readHeader()) { main.put(name, value); } this.endOfMainSection = pos; } public void readEntries(Map<String, Attributes> entries, Map<String, StrictJarManifest.Chunk> chunks) throws IOException { int mark = pos; while (readHeader()) { if (!Attributes.Name.NAME.equals(name)) { throw new IOException("Entry is not named"); } String entryNameValue = value; Attributes entry = entries.get(entryNameValue); if (entry == null) { entry = new Attributes(12); } while (readHeader()) { entry.put(name, value); } if (chunks != null) { if (chunks.get(entryNameValue) != null) { // TODO A bug: there might be several verification chunks for // the same name. I believe they should be used to update // signature in order of appearance; there are two ways to fix // this: either use a list of chunks, or decide on used // signature algorithm in advance and reread the chunks while // updating the signature; for now a defensive error is thrown throw new IOException("A jar verifier does not support more than one entry with the same name"); } chunks.put(entryNameValue, new StrictJarManifest.Chunk(mark, pos)); mark = pos; } entries.put(entryNameValue, entry); } } public int getEndOfMainSection() { return endOfMainSection; } /** * Read a single line from the manifest buffer. */ private boolean readHeader() throws IOException { if (consecutiveLineBreaks > 1) { // break a section on an empty line consecutiveLineBreaks = 0; return false; } readName(); consecutiveLineBreaks = 0; readValue(); // if the last line break is missed, the line // is ignored by the reference implementation return consecutiveLineBreaks > 0; } private void readName() throws IOException { int mark = pos; while (pos < buf.length) { if (buf[pos++] != ':') { continue; } String nameString = new String(buf, mark, pos - mark - 1, StandardCharsets.US_ASCII); if (buf[pos++] != ' ') { throw new IOException(String.format("Invalid value for attribute '%s'", nameString)); } try { name = attributeNameCache.get(nameString); if (name == null) { name = new Attributes.Name(nameString); attributeNameCache.put(nameString, name); } } catch (IllegalArgumentException e) { // new Attributes.Name() throws IllegalArgumentException but we declare IOException throw new IOException(e.getMessage()); } return; } } private void readValue() throws IOException { boolean lastCr = false; int mark = pos; int last = pos; valueBuffer.reset(); while (pos < buf.length) { byte next = buf[pos++]; switch (next) { case 0: throw new IOException("NUL character in a manifest"); case '\n': if (lastCr) { lastCr = false; } else { consecutiveLineBreaks++; } continue; case '\r': lastCr = true; consecutiveLineBreaks++; continue; case ' ': if (consecutiveLineBreaks == 1) { valueBuffer.write(buf, mark, last - mark); mark = pos; consecutiveLineBreaks = 0; continue; } } if (consecutiveLineBreaks >= 1) { pos--; break; } last = pos; } valueBuffer.write(buf, mark, last - mark); // A bit frustrating that that Charset.forName will be called // again. value = valueBuffer.toString(StandardCharsets.UTF_8.name()); } }