1 /*
2  * Copyright (C) 2019 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.tradefed.util.zip;
18 
19 import com.android.tradefed.log.LogUtil.CLog;
20 import com.android.tradefed.util.ByteArrayUtil;
21 
22 import java.io.File;
23 import java.io.FileInputStream;
24 import java.io.IOException;
25 import java.util.Arrays;
26 
27 /**
28  * EndCentralDirectoryInfo is a class containing the overall information of a zip file. It's at the
29  * end of the zip file.
30  *
31  * <p>Overall zipfile format: [Local file header + Compressed data [+ Extended local header]?]*
32  * [Central directory]* [End of central directory record]
33  *
34  * <p>Refer to following link for more details: https://en.wikipedia.org/wiki/Zip_(file_format)
35  */
36 public final class EndCentralDirectoryInfo {
37 
38     // End central directory of a zip file is at the end of the file, and its size shouldn't be
39     // larger than 64k.
40     public static final int MAX_LOOKBACK = 64 * 1024;
41 
42     private static final byte[] END_CENTRAL_DIRECTORY_SIGNATURE = {0x50, 0x4b, 0x05, 0x06};
43     private static final byte[] ZIP64_END_CENTRAL_DIRECTORY_SIGNATURE = {0x50, 0x4b, 0x06, 0x06};
44     // Central directory signature is always 4 bytes.
45     private static final int CENTRAL_DIRECTORY_MAGIC_LENGTH = 4;
46 
47     private long mEntryNumber;
48     private long mCentralDirSize;
49     private long mCentralDirOffset;
50 
getEntryNumber()51     public long getEntryNumber() {
52         return mEntryNumber;
53     }
54 
getCentralDirSize()55     public long getCentralDirSize() {
56         return mCentralDirSize;
57     }
58 
getCentralDirOffset()59     public long getCentralDirOffset() {
60         return mCentralDirOffset;
61     }
62 
63     /**
64      * Constructor to collect end central directory information of a zip file.
65      *
66      * @param zipFile a {@link File} contains the end central directory information. It's likely the
67      *     ending part of the zip file.
68      * @throws IOException
69      */
EndCentralDirectoryInfo(File zipFile)70     public EndCentralDirectoryInfo(File zipFile) throws IOException {
71         this(zipFile, false);
72     }
73 
74     /**
75      * Constructor to collect end central directory information of a zip file.
76      *
77      * @param zipFile a {@link File} contains the end central directory information. It's likely the
78      *     ending part of the zip file.
79      * @param useZip64 a boolean to support zip64 format in partial download.
80      * @throws IOException
81      */
EndCentralDirectoryInfo(File zipFile, boolean useZip64)82     public EndCentralDirectoryInfo(File zipFile, boolean useZip64) throws IOException {
83         // End of central directory record:
84         //    Offset   Length   Contents
85         //      0      4 bytes  End of central dir signature (0x06054b50)
86         //      4      2 bytes  Number of this disk
87         //      6      2 bytes  Number of the disk with the start of the central directory
88         //      8      2 bytes  Total number of entries in the central dir on this disk
89         //     10      2 bytes  Total number of entries in the central dir
90         //     12      4 bytes  Size of the central directory
91         //     16      4 bytes  Offset of start of central directory with respect to the starting
92         //                      disk number
93         //     20      2 bytes  zipfile comment length (c)
94         //     22     (c)bytes  zipfile comment
95 
96         // ZIP64 End of central directory record:
97         //    Offset   Length   Contents
98         //      0      4 bytes  ZIPO64 End of central dir signature (0x06064b50)
99         //      4      8 bytes  Size of the ZIP64 central directory
100         //     12      2 bytes  Version made by
101         //     14      2 bytes  Version needed to extract
102         //     16      4 bytes  Number of this disk
103         //     20      4 bytes  Number of the disk with the start of the central directory
104         //     24      8 bytes  Total number of entries in the central dir on this disk
105         //     32      8 bytes  Total number of entries in the central dir
106         //     40      8 bytes  Size of the central directory
107         //     48      8 bytes  Offset of start of central directory with respect to the starting
108         //                      disk number
109 
110         byte[] data = getEndCentralDirectoryInfo(zipFile, END_CENTRAL_DIRECTORY_SIGNATURE);
111         // Get the total number of entries in the central directory
112         mEntryNumber = ByteArrayUtil.getInt(data, 10, 2);
113         // Get the size of the central directory block
114         mCentralDirSize = ByteArrayUtil.getLong(data, 12, 4);
115         // Get the offset of start of central directory
116         mCentralDirOffset = ByteArrayUtil.getLong(data, 16, 4);
117         if (!useZip64) {
118             if (mCentralDirOffset < 0) {
119                 throw new IOException(
120                         "Failed to get offset of EndCentralDirectoryInfo. Partial unzip doesn't "
121                                 + "support zip files larger than 4GB.");
122             }
123             return;
124         }
125         // Get the real data while use-zip64-in-partial-download is set and the 3 corresponding
126         // elements match the condition.
127         if (Long.toHexString(mEntryNumber).equals("ffff") ||
128             Long.toHexString(mCentralDirSize).equals("ffffffff") ||
129             Long.toHexString(mCentralDirOffset).equals("ffffffff")) {
130             CLog.i("Values(total number of entries, central directory size, and the offset of start"
131                     + "of central directory header) in EndCentralDirectoryInfo reach limitation, "
132                     + "getting real data from the ZIP64EndCentralDirectoryInfo.");
133             // Get the ZIP64 End Central Directory Info.
134             data = getEndCentralDirectoryInfo(zipFile, ZIP64_END_CENTRAL_DIRECTORY_SIGNATURE);
135             mEntryNumber = ByteArrayUtil.getLong(data, 32, 8);
136             mCentralDirSize = ByteArrayUtil.getLong(data, 40, 8);
137             mCentralDirOffset = ByteArrayUtil.getLong(data, 48, 8);
138         }
139     }
140 
141     /**
142      * Get the EndCentralDirectoryInfo data in a zip file according to the given signature.
143      *
144      * @param zipFile a {@link File} contains the end central directory information. It's likely the
145      *     ending part of the zip file.
146      * @param signature a {@code byte[]} end central directory info signature.
147      * @return a {@code byte[]} end central directory info data in a zip file.
148      * @throws IOException
149      */
getEndCentralDirectoryInfo(File zipFile, byte[] signature)150     private byte[] getEndCentralDirectoryInfo(File zipFile, byte[] signature) throws IOException {
151         try (FileInputStream stream = new FileInputStream(zipFile)) {
152             long size = stream.getChannel().size();
153             if (size > MAX_LOOKBACK) {
154                 stream.skip(size - MAX_LOOKBACK);
155                 size = MAX_LOOKBACK;
156             }
157             byte[] endCentralDir = new byte[(int) size];
158             stream.read(endCentralDir);
159             int offset = (int) size - CENTRAL_DIRECTORY_MAGIC_LENGTH - 1;
160             // Seek from the end of the file, searching for the end central directory signature.
161             while (offset >= 0) {
162                 if (!java.util.Arrays.equals(
163                         signature,
164                         Arrays.copyOfRange(endCentralDir, offset, offset + 4))) {
165                     offset--;
166                     continue;
167                 }
168                 break;
169             }
170             if (offset < 0) {
171                 throw new RuntimeException(
172                         "Failed to find end central directory info for zip file: "
173                                 + zipFile.getPath());
174             }
175             return Arrays.copyOfRange(endCentralDir, offset, offset + 64);
176         }
177     }
178 }
179