1 /*
2  * Copyright (C) 2016 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.tools.build.apkzlib.zip;
18 
19 import com.android.tools.build.apkzlib.zip.utils.LittleEndianUtils;
20 import com.google.common.base.Preconditions;
21 import com.google.common.collect.ImmutableList;
22 import java.io.IOException;
23 import java.nio.ByteBuffer;
24 import java.util.ArrayList;
25 import java.util.List;
26 import java.util.stream.Collectors;
27 import javax.annotation.Nonnull;
28 import javax.annotation.Nullable;
29 
30 /**
31  * Contains an extra field.
32  *
33  * <p>According to the zip specification, the extra field is composed of a sequence of fields.
34  * This class provides a way to access, parse and modify that information.
35  *
36  * <p>The zip specification calls fields to the fields inside the extra field. Because this
37  * terminology is confusing, we use <i>segment</i> to refer to a part of the extra field. Each
38  * segment is represented by an instance of {@link Segment} and contains a header ID and data.
39  *
40  * <p>Each instance of {@link ExtraField} is immutable. The extra field of a particular entry can
41  * be changed by creating a new instanceof {@link ExtraField} and pass it to
42  * {@link StoredEntry#setLocalExtra(ExtraField)}.
43  *
44  * <p>Instances of {@link ExtraField} can be created directly from the list of segments in it
45  * or from the raw byte data. If created from the raw byte data, the data will only be parsed
46  * on demand. So, if neither {@link #getSegments()} nor {@link #getSingleSegment(int)} is
47  * invoked, the extra field will not be parsed. This guarantees low performance impact of the
48  * using the extra field unless its contents are needed.
49  */
50 public class ExtraField {
51 
52     /**
53      * Header ID for field with zip alignment.
54      */
55     static final int ALIGNMENT_ZIP_EXTRA_DATA_FIELD_HEADER_ID = 0xd935;
56 
57     /**
58      * The field's raw data, if it is known. Either this variable or {@link #segments} must be
59      * non-{@code null}.
60      */
61     @Nullable
62     private final byte[] rawData;
63 
64     /**
65      * The list of field's segments. Will be populated if the extra field is created based on a
66      * list of segments; will also be populated after parsing if the extra field is created based
67      * on the raw bytes.
68      */
69     @Nullable
70     private ImmutableList<Segment> segments;
71 
72     /**
73      * Creates an extra field based on existing raw data.
74      *
75      * @param rawData the raw data; will not be parsed unless needed
76      */
ExtraField(@onnull byte[] rawData)77     public ExtraField(@Nonnull byte[] rawData) {
78         this.rawData = rawData;
79         segments = null;
80     }
81 
82     /**
83      * Creates a new extra field with no segments.
84      */
ExtraField()85     public ExtraField() {
86         rawData = null;
87         segments = ImmutableList.of();
88     }
89 
90     /**
91      * Creates a new extra field with the given segments.
92      *
93      * @param segments the segments
94      */
ExtraField(@onnull ImmutableList<Segment> segments)95     public ExtraField(@Nonnull ImmutableList<Segment> segments) {
96         rawData = null;
97         this.segments = segments;
98     }
99 
100     /**
101      * Obtains all segments in the extra field.
102      *
103      * @return all segments
104      * @throws IOException failed to parse the extra field
105      */
getSegments()106     public ImmutableList<Segment> getSegments() throws IOException {
107         if (segments == null) {
108             parseSegments();
109         }
110 
111         Preconditions.checkNotNull(segments);
112         return segments;
113     }
114 
115     /**
116      * Obtains the only segment with the provided header ID.
117      *
118      * @param headerId the header ID
119      * @return the segment found or {@code null} if no segment contains the provided header ID
120      * @throws IOException there is more than one header with the provided header ID
121      */
122     @Nullable
getSingleSegment(int headerId)123     public Segment getSingleSegment(int headerId) throws IOException {
124         List<Segment> found =
125                 getSegments().stream()
126                         .filter(s -> s.getHeaderId() == headerId)
127                         .collect(Collectors.toList());
128         if (found.isEmpty()) {
129             return null;
130         } else if (found.size() == 1) {
131             return found.get(0);
132         } else {
133             throw new IOException(found.size() + " segments with header ID " + headerId + "found");
134         }
135     }
136 
137     /**
138      * Parses the raw data and generates all segments in {@link #segments}.
139      *
140      * @throws IOException failed to parse the data
141      */
parseSegments()142     private void parseSegments() throws IOException {
143         Preconditions.checkNotNull(rawData);
144         Preconditions.checkState(segments == null);
145 
146         List<Segment> segments = new ArrayList<>();
147         ByteBuffer buffer = ByteBuffer.wrap(rawData);
148 
149         while (buffer.remaining() > 0) {
150             int headerId = LittleEndianUtils.readUnsigned2Le(buffer);
151             int dataSize = LittleEndianUtils.readUnsigned2Le(buffer);
152             if (dataSize < 0) {
153                 throw new IOException(
154                         "Invalid data size for extra field segment with header ID "
155                                 + headerId
156                                 + ": "
157                                 + dataSize);
158             }
159 
160             byte[] data = new byte[dataSize];
161             if (buffer.remaining() < dataSize) {
162                 throw new IOException(
163                         "Invalid data size for extra field segment with header ID "
164                                 + headerId
165                                 + ": "
166                                 + dataSize
167                                 + " (only "
168                                 + buffer.remaining()
169                                 + " bytes are available)");
170             }
171             buffer.get(data);
172 
173             SegmentFactory factory = identifySegmentFactory(headerId);
174             Segment seg = factory.make(headerId, data);
175             segments.add(seg);
176         }
177 
178         this.segments = ImmutableList.copyOf(segments);
179     }
180 
181     /**
182      * Obtains the size of the extra field.
183      *
184      * @return the size
185      */
size()186     public int size() {
187         if (rawData != null) {
188             return rawData.length;
189         } else {
190             Preconditions.checkNotNull(segments);
191             int sz = 0;
192             for (Segment s : segments) {
193                 sz += s.size();
194             }
195 
196             return sz;
197         }
198     }
199 
200     /**
201      * Writes the extra field to the given output buffer.
202      *
203      * @param out the output buffer to write the field; exactly {@link #size()} bytes will be
204      * written
205      * @throws IOException failed to write the extra fields
206      */
write(@onnull ByteBuffer out)207     public void write(@Nonnull ByteBuffer out) throws IOException {
208         if (rawData != null) {
209             out.put(rawData);
210         } else {
211             Preconditions.checkNotNull(segments);
212             for (Segment s : segments) {
213                 s.write(out);
214             }
215         }
216     }
217 
218     /**
219      * Identifies the factory to create the segment with the provided header ID.
220      *
221      * @param headerId the header ID
222      * @return the segmnet factory that creates segments with the given header
223      */
224     @Nonnull
identifySegmentFactory(int headerId)225     private static SegmentFactory identifySegmentFactory(int headerId) {
226         if (headerId == ALIGNMENT_ZIP_EXTRA_DATA_FIELD_HEADER_ID) {
227             return AlignmentSegment::new;
228         }
229 
230         return RawDataSegment::new;
231     }
232 
233     /**
234      * Field inside the extra field. A segment contains a header ID and data. Specific types of
235      * segments implement this interface.
236      */
237     public interface Segment {
238 
239         /**
240          * Obtains the segment's header ID.
241          *
242          * @return the segment's header ID
243          */
getHeaderId()244         int getHeaderId();
245 
246         /**
247          * Obtains the size of the segment including the header ID.
248          *
249          * @return the number of bytes needed to write the segment
250          */
size()251         int size();
252 
253         /**
254          * Writes the segment to a buffer.
255          *
256          * @param out the buffer where to write the segment to; exactly {@link #size()} bytes will
257          * be written
258          * @throws IOException failed to write segment data
259          */
write(@onnull ByteBuffer out)260         void write(@Nonnull ByteBuffer out) throws IOException;
261     }
262 
263     /**
264      * Factory that creates a segment.
265      */
266     @FunctionalInterface
267     interface SegmentFactory {
268 
269         /**
270          * Creates a new segment.
271          *
272          * @param headerId the header ID
273          * @param data the segment's data
274          * @return the created segment
275          * @throws IOException failed to create the segment from the data
276          */
277         @Nonnull
make(int headerId, @Nonnull byte[] data)278         Segment make(int headerId, @Nonnull byte[] data) throws IOException;
279     }
280 
281     /**
282      * Segment of raw data: this class represents a general segment containing an array of bytes
283      * as data.
284      */
285     public static class RawDataSegment implements Segment {
286 
287         /**
288          * Header ID.
289          */
290         private final int headerId;
291 
292         /**
293          * Data in the segment.
294          */
295         @Nonnull
296         private final byte[] data;
297 
298         /**
299          * Creates a new raw data segment.
300          *
301          * @param headerId the header ID
302          * @param data the segment data
303          */
RawDataSegment(int headerId, @Nonnull byte[] data)304         RawDataSegment(int headerId, @Nonnull byte[] data) {
305             this.headerId = headerId;
306             this.data = data;
307         }
308 
309         @Override
getHeaderId()310         public int getHeaderId() {
311             return headerId;
312         }
313 
314         @Override
write(@onnull ByteBuffer out)315         public void write(@Nonnull ByteBuffer out) throws IOException {
316             LittleEndianUtils.writeUnsigned2Le(out, headerId);
317             LittleEndianUtils.writeUnsigned2Le(out, data.length);
318             out.put(data);
319         }
320 
321         @Override
size()322         public int size() {
323             return 4 + data.length;
324         }
325     }
326 
327     /**
328      * Segment with information on an alignment: this segment contains information on how an entry
329      * should be aligned and contains zero-filled data to force alignment.
330      *
331      * <p>An alignment segment contains the header ID, the size of the data, the alignment value
332      * and zero bytes to pad
333      */
334     public static class AlignmentSegment implements Segment {
335 
336         /**
337          * Minimum size for an alignment segment.
338          */
339         public static final int MINIMUM_SIZE = 6;
340 
341         /**
342          * The alignment value.
343          */
344         private int alignment;
345 
346         /**
347          * How many bytes of padding are in this segment?
348          */
349         private int padding;
350 
351         /**
352          * Creates a new alignment segment.
353          *
354          * @param alignment the alignment value
355          * @param totalSize how many bytes should this segment take?
356          */
AlignmentSegment(int alignment, int totalSize)357         public AlignmentSegment(int alignment, int totalSize) {
358             Preconditions.checkArgument(alignment > 0, "alignment <= 0");
359             Preconditions.checkArgument(totalSize >= MINIMUM_SIZE, "totalSize < MINIMUM_SIZE");
360 
361             /*
362              * We have 6 bytes of fixed data: header ID (2 bytes), data size (2 bytes), alignment
363              * value (2 bytes).
364              */
365             this.alignment = alignment;
366             padding = totalSize - MINIMUM_SIZE;
367         }
368 
369         /**
370          * Creates a new alignment segment from extra data.
371          *
372          * @param headerId the header ID
373          * @param data the segment data
374          * @throws IOException failed to create the segment from the data
375          */
AlignmentSegment(int headerId, @Nonnull byte[] data)376         public AlignmentSegment(int headerId, @Nonnull byte[] data) throws IOException {
377             Preconditions.checkArgument(headerId == ALIGNMENT_ZIP_EXTRA_DATA_FIELD_HEADER_ID);
378 
379             ByteBuffer dataBuffer = ByteBuffer.wrap(data);
380             alignment = LittleEndianUtils.readUnsigned2Le(dataBuffer);
381             if (alignment <= 0) {
382                 throw new IOException("Invalid alignment in alignment field: " + alignment);
383             }
384 
385             padding = data.length - 2;
386         }
387 
388         @Override
write(@onnull ByteBuffer out)389         public void write(@Nonnull ByteBuffer out) throws IOException {
390             LittleEndianUtils.writeUnsigned2Le(out, ALIGNMENT_ZIP_EXTRA_DATA_FIELD_HEADER_ID);
391             LittleEndianUtils.writeUnsigned2Le(out, padding + 2);
392             LittleEndianUtils.writeUnsigned2Le(out, alignment);
393             out.put(new byte[padding]);
394         }
395 
396         @Override
size()397         public int size() {
398             return padding + 6;
399         }
400 
401         @Override
getHeaderId()402         public int getHeaderId() {
403             return ALIGNMENT_ZIP_EXTRA_DATA_FIELD_HEADER_ID;
404         }
405     }
406 }
407