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
18package android.util.jar;
19
20import java.io.IOException;
21import java.io.InputStream;
22import java.io.OutputStream;
23import java.nio.ByteBuffer;
24import java.nio.CharBuffer;
25import java.nio.charset.CharsetEncoder;
26import java.nio.charset.CoderResult;
27import java.nio.charset.StandardCharsets;
28import java.util.HashMap;
29import java.util.Iterator;
30import java.util.Map;
31import java.util.jar.Attributes;
32import libcore.io.Streams;
33
34/**
35 * The {@code StrictJarManifest} class is used to obtain attribute information for a
36 * {@code StrictJarFile} and its entries.
37 *
38 * @hide
39 */
40public class StrictJarManifest implements Cloneable {
41    static final int LINE_LENGTH_LIMIT = 72;
42
43    private static final byte[] LINE_SEPARATOR = new byte[] { '\r', '\n' };
44
45    private static final byte[] VALUE_SEPARATOR = new byte[] { ':', ' ' };
46
47    private final Attributes mainAttributes;
48    private final HashMap<String, Attributes> entries;
49
50    static final class Chunk {
51        final int start;
52        final int end;
53
54        Chunk(int start, int end) {
55            this.start = start;
56            this.end = end;
57        }
58    }
59
60    private HashMap<String, Chunk> chunks;
61
62    /**
63     * The end of the main attributes section in the manifest is needed in
64     * verification.
65     */
66    private int mainEnd;
67
68    /**
69     * Creates a new {@code StrictJarManifest} instance.
70     */
71    public StrictJarManifest() {
72        entries = new HashMap<String, Attributes>();
73        mainAttributes = new Attributes();
74    }
75
76    /**
77     * Creates a new {@code StrictJarManifest} instance using the attributes obtained
78     * from the input stream.
79     *
80     * @param is
81     *            {@code InputStream} to parse for attributes.
82     * @throws IOException
83     *             if an IO error occurs while creating this {@code StrictJarManifest}
84     */
85    public StrictJarManifest(InputStream is) throws IOException {
86        this();
87        read(Streams.readFully(is));
88    }
89
90    /**
91     * Creates a new {@code StrictJarManifest} instance. The new instance will have the
92     * same attributes as those found in the parameter {@code StrictJarManifest}.
93     *
94     * @param man
95     *            {@code StrictJarManifest} instance to obtain attributes from.
96     */
97    @SuppressWarnings("unchecked")
98    public StrictJarManifest(StrictJarManifest man) {
99        mainAttributes = (Attributes) man.mainAttributes.clone();
100        entries = (HashMap<String, Attributes>) ((HashMap<String, Attributes>) man
101                .getEntries()).clone();
102    }
103
104    StrictJarManifest(byte[] manifestBytes, boolean readChunks) throws IOException {
105        this();
106        if (readChunks) {
107            chunks = new HashMap<String, Chunk>();
108        }
109        read(manifestBytes);
110    }
111
112    /**
113     * Resets the both the main attributes as well as the entry attributes
114     * associated with this {@code StrictJarManifest}.
115     */
116    public void clear() {
117        entries.clear();
118        mainAttributes.clear();
119    }
120
121    /**
122     * Returns the {@code Attributes} associated with the parameter entry
123     * {@code name}.
124     *
125     * @param name
126     *            the name of the entry to obtain {@code Attributes} from.
127     * @return the Attributes for the entry or {@code null} if the entry does
128     *         not exist.
129     */
130    public Attributes getAttributes(String name) {
131        return getEntries().get(name);
132    }
133
134    /**
135     * Returns a map containing the {@code Attributes} for each entry in the
136     * {@code StrictJarManifest}.
137     *
138     * @return the map of entry attributes.
139     */
140    public Map<String, Attributes> getEntries() {
141        return entries;
142    }
143
144    /**
145     * Returns the main {@code Attributes} of the {@code JarFile}.
146     *
147     * @return main {@code Attributes} associated with the source {@code
148     *         JarFile}.
149     */
150    public Attributes getMainAttributes() {
151        return mainAttributes;
152    }
153
154    /**
155     * Creates a copy of this {@code StrictJarManifest}. The returned {@code StrictJarManifest}
156     * will equal the {@code StrictJarManifest} from which it was cloned.
157     *
158     * @return a copy of this instance.
159     */
160    @Override
161    public Object clone() {
162        return new StrictJarManifest(this);
163    }
164
165    /**
166     * Writes this {@code StrictJarManifest}'s name/attributes pairs to the given {@code OutputStream}.
167     * The {@code MANIFEST_VERSION} or {@code SIGNATURE_VERSION} attribute must be set before
168     * calling this method, or no attributes will be written.
169     *
170     * @throws IOException
171     *             If an error occurs writing the {@code StrictJarManifest}.
172     */
173    public void write(OutputStream os) throws IOException {
174        write(this, os);
175    }
176
177    /**
178     * Merges name/attribute pairs read from the input stream {@code is} into this manifest.
179     *
180     * @param is
181     *            The {@code InputStream} to read from.
182     * @throws IOException
183     *             If an error occurs reading the manifest.
184     */
185    public void read(InputStream is) throws IOException {
186        read(Streams.readFullyNoClose(is));
187    }
188
189    private void read(byte[] buf) throws IOException {
190        if (buf.length == 0) {
191            return;
192        }
193
194        StrictJarManifestReader im = new StrictJarManifestReader(buf, mainAttributes);
195        mainEnd = im.getEndOfMainSection();
196        im.readEntries(entries, chunks);
197    }
198
199    /**
200     * Returns the hash code for this instance.
201     *
202     * @return this {@code StrictJarManifest}'s hashCode.
203     */
204    @Override
205    public int hashCode() {
206        return mainAttributes.hashCode() ^ getEntries().hashCode();
207    }
208
209    /**
210     * Determines if the receiver is equal to the parameter object. Two {@code
211     * StrictJarManifest}s are equal if they have identical main attributes as well as
212     * identical entry attributes.
213     *
214     * @param o
215     *            the object to compare against.
216     * @return {@code true} if the manifests are equal, {@code false} otherwise
217     */
218    @Override
219    public boolean equals(Object o) {
220        if (o == null) {
221            return false;
222        }
223        if (o.getClass() != this.getClass()) {
224            return false;
225        }
226        if (!mainAttributes.equals(((StrictJarManifest) o).mainAttributes)) {
227            return false;
228        }
229        return getEntries().equals(((StrictJarManifest) o).getEntries());
230    }
231
232    Chunk getChunk(String name) {
233        return chunks.get(name);
234    }
235
236    void removeChunks() {
237        chunks = null;
238    }
239
240    int getMainAttributesEnd() {
241        return mainEnd;
242    }
243
244    /**
245     * Writes out the attribute information of the specified manifest to the
246     * specified {@code OutputStream}
247     *
248     * @param manifest
249     *            the manifest to write out.
250     * @param out
251     *            The {@code OutputStream} to write to.
252     * @throws IOException
253     *             If an error occurs writing the {@code StrictJarManifest}.
254     */
255    static void write(StrictJarManifest manifest, OutputStream out) throws IOException {
256        CharsetEncoder encoder = StandardCharsets.UTF_8.newEncoder();
257        ByteBuffer buffer = ByteBuffer.allocate(LINE_LENGTH_LIMIT);
258
259        Attributes.Name versionName = Attributes.Name.MANIFEST_VERSION;
260        String version = manifest.mainAttributes.getValue(versionName);
261        if (version == null) {
262            versionName = Attributes.Name.SIGNATURE_VERSION;
263            version = manifest.mainAttributes.getValue(versionName);
264        }
265        if (version != null) {
266            writeEntry(out, versionName, version, encoder, buffer);
267            Iterator<?> entries = manifest.mainAttributes.keySet().iterator();
268            while (entries.hasNext()) {
269                Attributes.Name name = (Attributes.Name) entries.next();
270                if (!name.equals(versionName)) {
271                    writeEntry(out, name, manifest.mainAttributes.getValue(name), encoder, buffer);
272                }
273            }
274        }
275        out.write(LINE_SEPARATOR);
276        Iterator<String> i = manifest.getEntries().keySet().iterator();
277        while (i.hasNext()) {
278            String key = i.next();
279            writeEntry(out, Attributes.Name.NAME, key, encoder, buffer);
280            Attributes attributes = manifest.entries.get(key);
281            Iterator<?> entries = attributes.keySet().iterator();
282            while (entries.hasNext()) {
283                Attributes.Name name = (Attributes.Name) entries.next();
284                writeEntry(out, name, attributes.getValue(name), encoder, buffer);
285            }
286            out.write(LINE_SEPARATOR);
287        }
288    }
289
290    private static void writeEntry(OutputStream os, Attributes.Name name,
291            String value, CharsetEncoder encoder, ByteBuffer bBuf) throws IOException {
292        String nameString = name.toString();
293        os.write(nameString.getBytes(StandardCharsets.US_ASCII));
294        os.write(VALUE_SEPARATOR);
295
296        encoder.reset();
297        bBuf.clear().limit(LINE_LENGTH_LIMIT - nameString.length() - 2);
298
299        CharBuffer cBuf = CharBuffer.wrap(value);
300
301        while (true) {
302            CoderResult r = encoder.encode(cBuf, bBuf, true);
303            if (CoderResult.UNDERFLOW == r) {
304                r = encoder.flush(bBuf);
305            }
306            os.write(bBuf.array(), bBuf.arrayOffset(), bBuf.position());
307            os.write(LINE_SEPARATOR);
308            if (CoderResult.UNDERFLOW == r) {
309                break;
310            }
311            os.write(' ');
312            bBuf.clear().limit(LINE_LENGTH_LIMIT - 1);
313        }
314    }
315}
316