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   * http://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   *
41   * @since 1.2
42   * @NotThreadSafe
43   */
44  public class Zip64ExtendedInformationExtraField implements ZipExtraField {
45  
46      static final ZipShort HEADER_ID = new ZipShort(0x0001);
47  
48      private static final String LFH_MUST_HAVE_BOTH_SIZES_MSG = "Zip64 extended information must contain" + " both size values in the local file header.";
49      private ZipEightByteInteger size, compressedSize, relativeHeaderOffset;
50      private ZipLong diskStart;
51  
52      /**
53       * Stored in {@link #parseFromCentralDirectoryData parseFromCentralDirectoryData} so it can be reused when ZipFile calls {@link #reparseCentralDirectoryData
54       * reparseCentralDirectoryData}.
55       *
56       * <p>
57       * Not used for anything else
58       * </p>
59       *
60       * @since 1.3
61       */
62      private byte[] rawCentralDirectoryData;
63  
64      /**
65       * This constructor should only be used by the code that reads archives inside of Commons Compress.
66       */
67      public Zip64ExtendedInformationExtraField() {
68      }
69  
70      /**
71       * Creates an extra field based on the original and compressed size.
72       *
73       * @param size           the entry's original size
74       * @param compressedSize the entry's compressed size
75       *
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       *
90       * @throws IllegalArgumentException if size or compressedSize is null
91       */
92      public Zip64ExtendedInformationExtraField(final ZipEightByteInteger size, final ZipEightByteInteger compressedSize,
93              final ZipEightByteInteger relativeHeaderOffset, final ZipLong diskStart) {
94          this.size = size;
95          this.compressedSize = compressedSize;
96          this.relativeHeaderOffset = relativeHeaderOffset;
97          this.diskStart = diskStart;
98      }
99  
100     private int addSizes(final byte[] data) {
101         int off = 0;
102         if (size != null) {
103             System.arraycopy(size.getBytes(), 0, data, 0, DWORD);
104             off += DWORD;
105         }
106         if (compressedSize != null) {
107             System.arraycopy(compressedSize.getBytes(), 0, data, off, DWORD);
108             off += DWORD;
109         }
110         return off;
111     }
112 
113     @Override
114     public byte[] getCentralDirectoryData() {
115         final byte[] data = new byte[getCentralDirectoryLength().getValue()];
116         int off = addSizes(data);
117         if (relativeHeaderOffset != null) {
118             System.arraycopy(relativeHeaderOffset.getBytes(), 0, data, off, DWORD);
119             off += DWORD;
120         }
121         if (diskStart != null) {
122             System.arraycopy(diskStart.getBytes(), 0, data, off, WORD);
123             off += WORD; // NOSONAR - assignment as documentation
124         }
125         return data;
126     }
127 
128     @Override
129     public ZipShort getCentralDirectoryLength() {
130         return new ZipShort((size != null ? DWORD : 0) + (compressedSize != null ? DWORD : 0) + (relativeHeaderOffset != null ? DWORD : 0)
131                 + (diskStart != null ? WORD : 0));
132     }
133 
134     /**
135      * The compressed size stored in this extra field.
136      *
137      * @return The compressed size stored in this extra field.
138      */
139     public ZipEightByteInteger getCompressedSize() {
140         return compressedSize;
141     }
142 
143     /**
144      * The disk start number stored in this extra field.
145      *
146      * @return The disk start number stored in this extra field.
147      */
148     public ZipLong getDiskStartNumber() {
149         return diskStart;
150     }
151 
152     @Override
153     public ZipShort getHeaderId() {
154         return HEADER_ID;
155     }
156 
157     @Override
158     public byte[] getLocalFileDataData() {
159         if (size != null || compressedSize != null) {
160             if (size == null || compressedSize == null) {
161                 throw new IllegalArgumentException(LFH_MUST_HAVE_BOTH_SIZES_MSG);
162             }
163             final byte[] data = new byte[2 * DWORD];
164             addSizes(data);
165             return data;
166         }
167         return ByteUtils.EMPTY_BYTE_ARRAY;
168     }
169 
170     @Override
171     public ZipShort getLocalFileDataLength() {
172         return new ZipShort(size != null ? 2 * DWORD : 0);
173     }
174 
175     /**
176      * The relative header offset stored in this extra field.
177      *
178      * @return The relative header offset stored in this extra field.
179      */
180     public ZipEightByteInteger getRelativeHeaderOffset() {
181         return relativeHeaderOffset;
182     }
183 
184     /**
185      * The uncompressed size stored in this extra field.
186      *
187      * @return The uncompressed size stored in this extra field.
188      */
189     public ZipEightByteInteger getSize() {
190         return size;
191     }
192 
193     @Override
194     public void parseFromCentralDirectoryData(final byte[] buffer, int offset, final int length) throws ZipException {
195         // store for processing in reparseCentralDirectoryData
196         rawCentralDirectoryData = new byte[length];
197         System.arraycopy(buffer, offset, rawCentralDirectoryData, 0, length);
198 
199         // if there is no size information in here, we are screwed and
200         // can only hope things will get resolved by LFH data later
201         // But there are some cases that can be detected
202         // * all data is there
203         // * length == 24 -> both sizes and offset
204         // * length % 8 == 4 -> at least we can identify the diskStart field
205         if (length >= 3 * DWORD + WORD) {
206             parseFromLocalFileData(buffer, offset, length);
207         } else if (length == 3 * DWORD) {
208             size = new ZipEightByteInteger(buffer, offset);
209             offset += DWORD;
210             compressedSize = new ZipEightByteInteger(buffer, offset);
211             offset += DWORD;
212             relativeHeaderOffset = new ZipEightByteInteger(buffer, offset);
213         } else if (length % DWORD == WORD) {
214             diskStart = new ZipLong(buffer, offset + length - WORD);
215         }
216     }
217 
218     @Override
219     public void parseFromLocalFileData(final byte[] buffer, int offset, final int length) throws ZipException {
220         if (length == 0) {
221             // no local file data at all, may happen if an archive
222             // only holds a ZIP64 extended information extra field
223             // inside the central directory but not inside the local
224             // file header
225             return;
226         }
227         if (length < 2 * DWORD) {
228             throw new ZipException(LFH_MUST_HAVE_BOTH_SIZES_MSG);
229         }
230         size = new ZipEightByteInteger(buffer, offset);
231         offset += DWORD;
232         compressedSize = new ZipEightByteInteger(buffer, offset);
233         offset += DWORD;
234         int remaining = length - 2 * DWORD;
235         if (remaining >= DWORD) {
236             relativeHeaderOffset = new ZipEightByteInteger(buffer, offset);
237             offset += DWORD;
238             remaining -= DWORD;
239         }
240         if (remaining >= WORD) {
241             diskStart = new ZipLong(buffer, offset);
242             offset += WORD; // NOSONAR - assignment as documentation
243             remaining -= WORD; // NOSONAR - assignment as documentation
244         }
245     }
246 
247     /**
248      * Parses the raw bytes read from the central directory extra field with knowledge which fields are expected to be there.
249      *
250      * <p>
251      * All four fields inside the zip64 extended information extra field are optional and must only be present if their corresponding entry inside the central
252      * directory contains the correct magic value.
253      * </p>
254      *
255      * @param hasUncompressedSize     flag to read from central directory
256      * @param hasCompressedSize       flag to read from central directory
257      * @param hasRelativeHeaderOffset flag to read from central directory
258      * @param hasDiskStart            flag to read from central directory
259      * @throws ZipException on error
260      */
261     public void reparseCentralDirectoryData(final boolean hasUncompressedSize, final boolean hasCompressedSize, final boolean hasRelativeHeaderOffset,
262             final boolean hasDiskStart) throws ZipException {
263         if (rawCentralDirectoryData != null) {
264             final int expectedLength = (hasUncompressedSize ? DWORD : 0) + (hasCompressedSize ? DWORD : 0) + (hasRelativeHeaderOffset ? DWORD : 0)
265                     + (hasDiskStart ? WORD : 0);
266             if (rawCentralDirectoryData.length < expectedLength) {
267                 throw new ZipException("Central directory zip64 extended" + " information extra field's length" + " doesn't match central directory"
268                         + " data.  Expected length " + expectedLength + " but is " + rawCentralDirectoryData.length);
269             }
270             int offset = 0;
271             if (hasUncompressedSize) {
272                 size = new ZipEightByteInteger(rawCentralDirectoryData, offset);
273                 offset += DWORD;
274             }
275             if (hasCompressedSize) {
276                 compressedSize = new ZipEightByteInteger(rawCentralDirectoryData, offset);
277                 offset += DWORD;
278             }
279             if (hasRelativeHeaderOffset) {
280                 relativeHeaderOffset = new ZipEightByteInteger(rawCentralDirectoryData, offset);
281                 offset += DWORD;
282             }
283             if (hasDiskStart) {
284                 diskStart = new ZipLong(rawCentralDirectoryData, offset);
285                 offset += WORD; // NOSONAR - assignment as documentation
286             }
287         }
288     }
289 
290     /**
291      * The uncompressed size stored in this extra field.
292      *
293      * @param compressedSize The uncompressed size stored in this extra field.
294      */
295     public void setCompressedSize(final ZipEightByteInteger compressedSize) {
296         this.compressedSize = compressedSize;
297     }
298 
299     /**
300      * The disk start number stored in this extra field.
301      *
302      * @param ds The disk start number stored in this extra field.
303      */
304     public void setDiskStartNumber(final ZipLong ds) {
305         diskStart = ds;
306     }
307 
308     /**
309      * The relative header offset stored in this extra field.
310      *
311      * @param rho The relative header offset stored in this extra field.
312      */
313     public void setRelativeHeaderOffset(final ZipEightByteInteger rho) {
314         relativeHeaderOffset = rho;
315     }
316 
317     /**
318      * The uncompressed size stored in this extra field.
319      *
320      * @param size The uncompressed size stored in this extra field.
321      */
322     public void setSize(final ZipEightByteInteger size) {
323         this.size = size;
324     }
325 }