View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   https://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.commons.compress.archivers.zip;
20  
21  import static org.apache.commons.compress.archivers.zip.ZipConstants.DWORD;
22  import static org.apache.commons.compress.archivers.zip.ZipConstants.WORD;
23  
24  import java.util.zip.ZipException;
25  
26  import org.apache.commons.compress.utils.ByteUtils;
27  
28  /**
29   * Holds size and other extended information for entries that use Zip64 features.
30   *
31   * <p>
32   * Currently Commons Compress doesn't support encrypting the central directory so the note in APPNOTE.TXT about masking doesn't apply.
33   * </p>
34   *
35   * <p>
36   * The implementation relies on data being read from the local file header and assumes that both size values are always present.
37   * </p>
38   *
39   * @see <a href="https://www.pkware.com/documents/casestudies/APPNOTE.TXT">PKWARE APPNOTE.TXT, section 4.5.3</a>
40   * @since 1.2
41   * @NotThreadSafe
42   */
43  public class Zip64ExtendedInformationExtraField implements ZipExtraField {
44  
45      static final ZipShort HEADER_ID = new ZipShort(0x0001);
46  
47      private static final String LFH_MUST_HAVE_BOTH_SIZES_MSG = "Zip64 extended information must contain both size values in the local file header.";
48      private ZipEightByteInteger size;
49      private ZipEightByteInteger compressedSize;
50      private ZipEightByteInteger relativeHeaderOffset;
51      private ZipLong diskStart;
52  
53      /**
54       * Stored in {@link #parseFromCentralDirectoryData parseFromCentralDirectoryData} so it can be reused when ZipFile calls {@link #reparseCentralDirectoryData
55       * reparseCentralDirectoryData}.
56       *
57       * <p>
58       * Not used for anything else
59       * </p>
60       *
61       * @since 1.3
62       */
63      private byte[] rawCentralDirectoryData;
64  
65      /**
66       * This constructor should only be used by the code that reads archives inside of Commons Compress.
67       */
68      public Zip64ExtendedInformationExtraField() {
69      }
70  
71      /**
72       * Creates an extra field based on the original and compressed size.
73       *
74       * @param size           the entry's original size
75       * @param compressedSize the entry's compressed size
76       * @throws IllegalArgumentException if size or compressedSize is null
77       */
78      public Zip64ExtendedInformationExtraField(final ZipEightByteInteger size, final ZipEightByteInteger compressedSize) {
79          this(size, compressedSize, null, null);
80      }
81  
82      /**
83       * Creates an extra field based on all four possible values.
84       *
85       * @param size                 the entry's original size
86       * @param compressedSize       the entry's compressed size
87       * @param relativeHeaderOffset the entry's offset
88       * @param diskStart            the disk start
89       * @throws IllegalArgumentException if size or compressedSize is null
90       */
91      public Zip64ExtendedInformationExtraField(final ZipEightByteInteger size, final ZipEightByteInteger compressedSize,
92              final ZipEightByteInteger relativeHeaderOffset, final ZipLong diskStart) {
93          this.size = size;
94          this.compressedSize = compressedSize;
95          this.relativeHeaderOffset = relativeHeaderOffset;
96          this.diskStart = diskStart;
97      }
98  
99      private int addSizes(final byte[] data) {
100         int off = 0;
101         if (size != null) {
102             System.arraycopy(size.getBytes(), 0, data, 0, DWORD);
103             off += DWORD;
104         }
105         if (compressedSize != null) {
106             System.arraycopy(compressedSize.getBytes(), 0, data, off, DWORD);
107             off += DWORD;
108         }
109         return off;
110     }
111 
112     @Override
113     public byte[] getCentralDirectoryData() {
114         final byte[] data = new byte[getCentralDirectoryLength().getValue()];
115         int off = addSizes(data);
116         if (relativeHeaderOffset != null) {
117             System.arraycopy(relativeHeaderOffset.getBytes(), 0, data, off, DWORD);
118             off += DWORD;
119         }
120         if (diskStart != null) {
121             System.arraycopy(diskStart.getBytes(), 0, data, off, WORD);
122             off += WORD; // NOSONAR - assignment as documentation
123         }
124         return data;
125     }
126 
127     @Override
128     public ZipShort getCentralDirectoryLength() {
129         return new ZipShort((size != null ? DWORD : 0) + (compressedSize != null ? DWORD : 0) + (relativeHeaderOffset != null ? DWORD : 0)
130                 + (diskStart != null ? WORD : 0));
131     }
132 
133     /**
134      * The compressed size stored in this extra field.
135      *
136      * @return The compressed size stored in this extra field.
137      */
138     public ZipEightByteInteger getCompressedSize() {
139         return compressedSize;
140     }
141 
142     /**
143      * The disk start number stored in this extra field.
144      *
145      * @return The disk start number stored in this extra field.
146      */
147     public ZipLong getDiskStartNumber() {
148         return diskStart;
149     }
150 
151     @Override
152     public ZipShort getHeaderId() {
153         return HEADER_ID;
154     }
155 
156     @Override
157     public byte[] getLocalFileDataData() {
158         if (size != null || compressedSize != null) {
159             if (size == null || compressedSize == null) {
160                 throw new IllegalArgumentException(LFH_MUST_HAVE_BOTH_SIZES_MSG);
161             }
162             final byte[] data = new byte[2 * DWORD];
163             addSizes(data);
164             return data;
165         }
166         return ByteUtils.EMPTY_BYTE_ARRAY;
167     }
168 
169     @Override
170     public ZipShort getLocalFileDataLength() {
171         return new ZipShort(size != null ? 2 * DWORD : 0);
172     }
173 
174     /**
175      * The relative header offset stored in this extra field.
176      *
177      * @return The relative header offset stored in this extra field.
178      */
179     public ZipEightByteInteger getRelativeHeaderOffset() {
180         return relativeHeaderOffset;
181     }
182 
183     /**
184      * The uncompressed size stored in this extra field.
185      *
186      * @return The uncompressed size stored in this extra field.
187      */
188     public ZipEightByteInteger getSize() {
189         return size;
190     }
191 
192     @Override
193     public void parseFromCentralDirectoryData(final byte[] buffer, int offset, final int length) throws ZipException {
194         // store for processing in reparseCentralDirectoryData
195         rawCentralDirectoryData = new byte[length];
196         System.arraycopy(buffer, offset, rawCentralDirectoryData, 0, length);
197 
198         // if there is no size information in here, we are screwed and
199         // can only hope things will get resolved by LFH data later
200         // But there are some cases that can be detected
201         // * all data is there
202         // * length == 24 -> both sizes and offset
203         // * length % 8 == 4 -> at least we can identify the diskStart field
204         if (length >= 3 * DWORD + WORD) {
205             parseFromLocalFileData(buffer, offset, length);
206         } else if (length == 3 * DWORD) {
207             size = new ZipEightByteInteger(buffer, offset);
208             offset += DWORD;
209             compressedSize = new ZipEightByteInteger(buffer, offset);
210             offset += DWORD;
211             relativeHeaderOffset = new ZipEightByteInteger(buffer, offset);
212         } else if (length % DWORD == WORD) {
213             diskStart = new ZipLong(buffer, offset + length - WORD);
214         }
215     }
216 
217     @Override
218     public void parseFromLocalFileData(final byte[] buffer, int offset, final int length) throws ZipException {
219         if (length == 0) {
220             // no local file data at all, may happen if an archive
221             // only holds a ZIP64 extended information extra field
222             // inside the central directory but not inside the local
223             // file header
224             return;
225         }
226         if (length < 2 * DWORD) {
227             throw new ZipException(LFH_MUST_HAVE_BOTH_SIZES_MSG);
228         }
229         size = new ZipEightByteInteger(buffer, offset);
230         offset += DWORD;
231         compressedSize = new ZipEightByteInteger(buffer, offset);
232         offset += DWORD;
233         int remaining = length - 2 * DWORD;
234         if (remaining >= DWORD) {
235             relativeHeaderOffset = new ZipEightByteInteger(buffer, offset);
236             offset += DWORD;
237             remaining -= DWORD;
238         }
239         if (remaining >= WORD) {
240             diskStart = new ZipLong(buffer, offset);
241             offset += WORD; // NOSONAR - assignment as documentation
242             remaining -= WORD; // NOSONAR - assignment as documentation
243         }
244     }
245 
246     /**
247      * Parses the raw bytes read from the central directory extra field with knowledge which fields are expected to be there.
248      *
249      * <p>
250      * All four fields inside the zip64 extended information extra field are optional and must only be present if their corresponding entry inside the central
251      * directory contains the correct magic value.
252      * </p>
253      *
254      * @param hasUncompressedSize     flag to read from central directory
255      * @param hasCompressedSize       flag to read from central directory
256      * @param hasRelativeHeaderOffset flag to read from central directory
257      * @param hasDiskStart            flag to read from central directory
258      * @throws ZipException on error
259      */
260     public void reparseCentralDirectoryData(final boolean hasUncompressedSize, final boolean hasCompressedSize, final boolean hasRelativeHeaderOffset,
261             final boolean hasDiskStart) throws ZipException {
262         if (rawCentralDirectoryData != null) {
263             final int expectedLength = (hasUncompressedSize ? DWORD : 0) + (hasCompressedSize ? DWORD : 0) + (hasRelativeHeaderOffset ? DWORD : 0)
264                     + (hasDiskStart ? WORD : 0);
265             if (rawCentralDirectoryData.length < expectedLength) {
266                 throw new ZipException("Central directory zip64 extended information extra field's length doesn't match central directory"
267                         + " data.  Expected length " + expectedLength + " but is " + rawCentralDirectoryData.length);
268             }
269             int offset = 0;
270             if (hasUncompressedSize) {
271                 size = new ZipEightByteInteger(rawCentralDirectoryData, offset);
272                 offset += DWORD;
273             }
274             if (hasCompressedSize) {
275                 compressedSize = new ZipEightByteInteger(rawCentralDirectoryData, offset);
276                 offset += DWORD;
277             }
278             if (hasRelativeHeaderOffset) {
279                 relativeHeaderOffset = new ZipEightByteInteger(rawCentralDirectoryData, offset);
280                 offset += DWORD;
281             }
282             if (hasDiskStart) {
283                 diskStart = new ZipLong(rawCentralDirectoryData, offset);
284                 offset += WORD; // NOSONAR - assignment as documentation
285             }
286         }
287     }
288 
289     /**
290      * The uncompressed size stored in this extra field.
291      *
292      * @param compressedSize The uncompressed size stored in this extra field.
293      */
294     public void setCompressedSize(final ZipEightByteInteger compressedSize) {
295         this.compressedSize = compressedSize;
296     }
297 
298     /**
299      * The disk start number stored in this extra field.
300      *
301      * @param ds The disk start number stored in this extra field.
302      */
303     public void setDiskStartNumber(final ZipLong ds) {
304         diskStart = ds;
305     }
306 
307     /**
308      * The relative header offset stored in this extra field.
309      *
310      * @param rho The relative header offset stored in this extra field.
311      */
312     public void setRelativeHeaderOffset(final ZipEightByteInteger rho) {
313         relativeHeaderOffset = rho;
314     }
315 
316     /**
317      * The uncompressed size stored in this extra field.
318      *
319      * @param size The uncompressed size stored in this extra field.
320      */
321     public void setSize(final ZipEightByteInteger size) {
322         this.size = size;
323     }
324 }