Home | History | Annotate | Download | only in zip
      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      */
     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      */
     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      */
     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      */
    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
    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      */
    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      */
    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      */
    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
    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          */
    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          */
    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          */
    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
    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          */
    304         RawDataSegment(int headerId, @Nonnull byte[] data) {
    305             this.headerId = headerId;
    306             this.data = data;
    307         }
    308 
    309         @Override
    310         public int getHeaderId() {
    311             return headerId;
    312         }
    313 
    314         @Override
    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
    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          */
    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          */
    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
    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
    397         public int size() {
    398             return padding + 6;
    399         }
    400 
    401         @Override
    402         public int getHeaderId() {
    403             return ALIGNMENT_ZIP_EXTRA_DATA_FIELD_HEADER_ID;
    404         }
    405     }
    406 }
    407