| /* |
| * 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()); |
| } |
| } |