View Javadoc
1   /*
2    * Copyright (C) 2005-2015 Schlichtherle IT Services.
3    * All rights reserved. Use is subject to license terms.
4    */
5   package net.java.truevfs.comp.zip;
6   
7   import java.util.Map;
8   import java.util.TreeMap;
9   import javax.annotation.CheckForNull;
10  import javax.annotation.Nullable;
11  import javax.annotation.concurrent.NotThreadSafe;
12  import static net.java.truevfs.comp.zip.Constants.EMPTY;
13  import static net.java.truevfs.comp.zip.LittleEndian.readUShort;
14  import static net.java.truevfs.comp.zip.LittleEndian.writeShort;
15  
16  /**
17   * Represents a collection of {@link ExtraField extra fields} as they may
18   * be present at several locations in ZIP files.
19   *
20   * @author  Christian Schlichtherle
21   */
22  @NotThreadSafe
23  final class ExtraFields implements Cloneable {
24  
25      /**
26       * The map of extra fields.
27       * Maps from Header ID [{@link Integer}] to extra field [{@link ExtraField}].
28       * Must not be {@code null}, but may be empty if no extra fields are used.
29       * The map is sorted by Header IDs in ascending order.
30       */
31      private Map<Integer, ExtraField> extra = new TreeMap<>();
32  
33      /** Returns a shallow clone of this collection. */
34      @Override
35      @SuppressWarnings("AccessingNonPublicFieldOfAnotherObject")
36      public ExtraFields clone() {
37          try {
38              final ExtraFields clone = (ExtraFields) super.clone();
39              clone.extra = new TreeMap<>(extra);
40              return clone;
41          } catch (CloneNotSupportedException cannotHappen) {
42              throw new AssertionError(cannotHappen);
43          }
44      }
45  
46      /** Returns the number of extra fields in this collection. */
47      int size() { return extra.size(); }
48  
49      /**
50       * Returns the extra field with the given Header ID or {@code null}
51       * if no such extra field exists.
52       *
53       * @param headerId The requested Header ID.
54       * @return The extra field with the given Header ID or {@code null}
55       *         if no such extra field exists.
56       * @throws IllegalArgumentException If {@code headerID} is not in
57       *         the range of {@code 0} to {@link UShort#MAX_VALUE}
58       *         ({@value net.truevfs.driver.zip.io.UShort#MAX_VALUE}).
59       */
60      @CheckForNull ExtraField get(final int headerId) {
61          assert UShort.check(headerId);
62          final ExtraField ef = extra.get(headerId);
63          assert null == ef || ef.getHeaderId() == headerId;
64          return ef;
65      }
66  
67      /**
68       * Stores the given extra field in this collection.
69       *
70       * @param ef The extra field to store in this collection.
71       * @return The extra field previously associated with the Header ID of
72       *         of the given extra field or {@code null} if no such
73       *         extra field existed.
74       * @throws NullPointerException If {@code ef} is {@code null}.
75       * @throws IllegalArgumentException If the Header ID of the given Extra
76       *         Field is not in the range of {@code 0} to
77       *         {@link UShort#MAX_VALUE}
78       *         ({@value net.truevfs.driver.zip.io.UShort#MAX_VALUE}).
79       */
80      ExtraField add(final ExtraField ef) {
81          final int headerId = ef.getHeaderId();
82          assert UShort.check(headerId);
83          return extra.put(headerId, ef);
84      }
85  
86      /**
87       * Removes the extra field with the given Header ID.
88       *
89       * @param headerId The requested Header ID.
90       * @return The extra field with the given Header ID or {@code null}
91       *         if no such extra field exists.
92       * @throws IllegalArgumentException If {@code headerID} is not in
93       *         the range of {@code 0} to {@link UShort#MAX_VALUE}
94       *         ({@value net.truevfs.driver.zip.io.UShort#MAX_VALUE}).
95       */
96      @Nullable ExtraField remove(final int headerId) {
97          assert UShort.check(headerId);
98          final ExtraField ef = extra.remove(headerId);
99          assert null == ef || ef.getHeaderId() == headerId;
100         return ef;
101     }
102 
103     /**
104      * Returns the number of bytes required to hold the extra fields.
105      *
106      * @return The length of the extra fields in bytes.
107      *         May be {@code 0}.
108      * @see #getDataBlock
109      */
110     int getDataSize() {
111         final Map<Integer, ExtraField> extra = this.extra;
112         if (extra.isEmpty()) return 0;
113         int l = 0;
114         for (ExtraField ef : extra.values()) l += 4 + ef.getDataSize();
115         return l;
116     }
117 
118     /**
119      * Returns a protective copy of the extra fields.
120      * {@code null} is never returned.
121      *
122      * @see #getDataSize
123      */
124     byte[] getDataBlock() {
125         final int size = getDataSize();
126         assert UShort.check(size);
127         if (0 == size) return EMPTY;
128         final byte[] data = new byte[size];
129         writeTo(data, 0);
130         return data;
131     }
132 
133     /**
134      * Deserializes this collection of extra fields from
135      * the data block starting at the zero based offset {@code off} with
136      * {@code len} bytes length in the byte array {@code buf}.
137      * After return, this collection does not access {@code buf} anymore
138      * and {@link #getDataSize} equals {@code len}.
139      *
140      * @param  buf The byte array to read the data block from.
141      * @param  off The zero based offset in the byte array where the first byte
142      *         of the data block is read from.
143      * @param  len The length of the data block in bytes.
144      * @throws IndexOutOfBoundsException If the byte array
145      *         {@code buf} does not hold at least {@code len}
146      *         bytes at the zero based offset {@code off}
147      *         or if {@code len} is smaller than the extra field data requires.
148      * @throws IllegalArgumentException If the data block does not conform to
149      *         the ZIP File Format Specification.
150      * @see    #getDataSize
151      */
152     void readFrom(final byte[] buf, int off, final int len)
153     throws IndexOutOfBoundsException, IllegalArgumentException {
154         assert UShort.check(len);
155         final Map<Integer, ExtraField> map = new TreeMap<>();
156         if (null != buf && 0 < len) {
157             final int end = off + len;
158             while (off < end) {
159                 final int headerId = readUShort(buf, off);
160                 off += 2;
161                 final int dataSize = readUShort(buf, off);
162                 off += 2;
163                 final ExtraField ef = ExtraField.create(headerId);
164                 ef.readFrom(buf, off, dataSize);
165                 off += dataSize;
166                 map.put(headerId, ef);
167             }
168             assert off == end;
169         }
170         extra = map;
171     }
172 
173     /**
174      * Serializes this collection of extra fields to
175      * the data block starting at the zero based offset {@code off} with
176      * {@link #getDataSize} bytes length in the byte array {@code buf}.
177      * Upon return, this collection shall not access {@code data}
178      * subsequently.
179      *
180      * @param  buf The byte array to write the data block to.
181      * @param  off The zero based offset in the byte array where the first byte
182      *         of the data block is written to.
183      * @throws IndexOutOfBoundsException If the byte array
184      *         {@code buf} does not hold at least {@link #getDataSize}
185      *         bytes at the zero based offset {@code off}.
186      * @see    #getDataSize
187      */
188     void writeTo(final byte[] buf, int off)
189     throws IndexOutOfBoundsException {
190        for (final ExtraField ef : extra.values()) {
191             writeShort(ef.getHeaderId(), buf, off);
192             off += 2;
193             writeShort(ef.getDataSize(), buf, off);
194             off += 2;
195             ef.writeTo(buf, off);
196             off += ef.getDataSize();
197         }
198     }
199 }