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 }