• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (c) 2023-2023 Huawei Device Co., Ltd.
3  * Licensed under the Apache License, Version 2.0 (the "License");
4  * you may not use this file except in compliance with the License.
5  * You may obtain a copy of the License at
6  *
7  *     http://www.apache.org/licenses/LICENSE-2.0
8  *
9  * Unless required by applicable law or agreed to in writing, software
10  * distributed under the License is distributed on an "AS IS" BASIS,
11  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12  * See the License for the specific language governing permissions and
13  * limitations under the License.
14  */
15 
16 package com.ohos.hapsigntool.zip;
17 
18 import com.ohos.hapsigntool.error.CustomException;
19 import com.ohos.hapsigntool.error.ERROR;
20 import com.ohos.hapsigntool.error.ZipException;
21 import com.ohos.hapsigntool.utils.FileUtils;
22 
23 import org.apache.logging.log4j.LogManager;
24 import org.apache.logging.log4j.Logger;
25 
26 import java.io.File;
27 import java.io.FileOutputStream;
28 import java.io.IOException;
29 import java.nio.ByteBuffer;
30 import java.nio.ByteOrder;
31 import java.util.ArrayList;
32 import java.util.List;
33 import java.util.Optional;
34 
35 /**
36  * resolve zip data
37  *
38  * @since 2023/12/02
39  */
40 public class Zip {
41     private static final Logger LOGGER = LogManager.getLogger(Zip.class);
42 
43     /**
44      * file is uncompress file flag
45      */
46     public static final int FILE_UNCOMPRESS_METHOD_FLAG = 0;
47 
48     /**
49      * max comment length
50      */
51     public static final int MAX_COMMENT_LENGTH = 65535;
52 
53     private List<ZipEntry> zipEntries;
54 
55     private long signingOffset;
56 
57     private byte[] signingBlock;
58 
59     private long cDOffset;
60 
61     private long eOCDOffset;
62 
63     private EndOfCentralDirectory endOfCentralDirectory;
64 
65     private String file;
66 
67     /**
68      * create Zip by file
69      *
70      * @param inputFile file
71      */
Zip(File inputFile)72     public Zip(File inputFile) {
73         try {
74             this.file = inputFile.getCanonicalPath();
75             if (!inputFile.exists()) {
76                 throw new ZipException("read zip file failed");
77             }
78             long start = System.currentTimeMillis();
79             // 1. get eocd data
80             endOfCentralDirectory = getZipEndOfCentralDirectory(inputFile);
81             cDOffset = endOfCentralDirectory.getOffset();
82             long eocdEnd = System.currentTimeMillis();
83             LOGGER.debug("getZipEndOfCentralDirectory use {} ms", eocdEnd - start);
84             // 2. use eocd's cd offset, get cd data
85             getZipCentralDirectory(inputFile);
86             long cdEnd = System.currentTimeMillis();
87             LOGGER.debug("getZipCentralDirectory use {} ms", cdEnd - start);
88             // 3. use cd's entry offset and file size, get entry data
89             getZipEntries(inputFile);
90             ZipEntry endEntry = zipEntries.get(zipEntries.size() - 1);
91             CentralDirectory endCD = endEntry.getCentralDirectory();
92             ZipEntryData endEntryData = endEntry.getZipEntryData();
93             signingOffset = endCD.getOffset() + endEntryData.getLength();
94             long entryEnd = System.currentTimeMillis();
95             LOGGER.debug("getZipEntries use {} ms", entryEnd - start);
96             // 4. file all data - eocd - cd - entry = sign block
97             signingBlock = getSigningBlock(inputFile);
98         } catch (IOException e) {
99             CustomException.throwException(ERROR.ZIP_ERROR, e.getMessage());
100         }
101     }
102 
getZipEndOfCentralDirectory(File file)103     private EndOfCentralDirectory getZipEndOfCentralDirectory(File file) throws IOException {
104         if (file.length() < EndOfCentralDirectory.EOCD_LENGTH) {
105             throw new ZipException("find zip eocd failed");
106         }
107 
108         // try to read EOCD without comment
109         int eocdLength = EndOfCentralDirectory.EOCD_LENGTH;
110         eOCDOffset = file.length() - eocdLength;
111         byte[] bytes = FileUtils.readFileByOffsetAndLength(file, eOCDOffset, eocdLength);
112         Optional<EndOfCentralDirectory> eocdByBytes = EndOfCentralDirectory.getEOCDByBytes(bytes);
113         if (eocdByBytes.isPresent()) {
114             return eocdByBytes.get();
115         }
116 
117         // try to search EOCD with comment
118         long eocdMaxLength = Math.min(EndOfCentralDirectory.EOCD_LENGTH + MAX_COMMENT_LENGTH, file.length());
119         eOCDOffset = file.length() - eocdMaxLength;
120         bytes = FileUtils.readFileByOffsetAndLength(file, eOCDOffset, eocdMaxLength);
121         for (int start = 0; start < eocdMaxLength; start++) {
122             eocdByBytes = EndOfCentralDirectory.getEOCDByBytes(bytes, start);
123             if (eocdByBytes.isPresent()) {
124                 eOCDOffset += start;
125                 return eocdByBytes.get();
126             }
127         }
128         throw new ZipException("read zip failed: can not find eocd in file");
129     }
130 
getZipCentralDirectory(File file)131     private void getZipCentralDirectory(File file) throws IOException {
132         zipEntries = new ArrayList<>(endOfCentralDirectory.getcDTotal());
133         // read full central directory bytes
134         byte[] cdBytes = FileUtils.readFileByOffsetAndLength(file, cDOffset, endOfCentralDirectory.getcDSize());
135         if (cdBytes.length < CentralDirectory.CD_LENGTH) {
136             throw new ZipException("find zip cd failed");
137         }
138         ByteBuffer bf = ByteBuffer.wrap(cdBytes);
139         bf.order(ByteOrder.LITTLE_ENDIAN);
140         int offset = 0;
141         // one by one format central directory
142         while (offset < cdBytes.length) {
143             CentralDirectory cd = CentralDirectory.getCentralDirectory(bf);
144             ZipEntry entry = new ZipEntry();
145             entry.setCentralDirectory(cd);
146             zipEntries.add(entry);
147             offset += cd.getLength();
148         }
149         if (offset + cDOffset != eOCDOffset) {
150             throw new ZipException("cd end offset not equals to eocd offset, maybe this is a zip64 file");
151         }
152     }
153 
getSigningBlock(File file)154     private byte[] getSigningBlock(File file) throws IOException {
155         long size = cDOffset - signingOffset;
156         if (size < 0) {
157             throw new ZipException("signing offset in front of entry end");
158         }
159         if (size == 0) {
160             return new byte[0];
161         }
162         return FileUtils.readFileByOffsetAndLength(file, signingOffset, size);
163     }
164 
getZipEntries(File file)165     private void getZipEntries(File file) throws IOException {
166         // use central directory data, find entry data
167         for (ZipEntry entry : zipEntries) {
168             CentralDirectory cd = entry.getCentralDirectory();
169             long offset = cd.getOffset();
170             long unCompressedSize = cd.getUnCompressedSize();
171             long compressedSize = cd.getCompressedSize();
172             long fileSize = cd.getMethod() == FILE_UNCOMPRESS_METHOD_FLAG ? unCompressedSize : compressedSize;
173 
174             ZipEntryData zipEntryData = ZipEntryData.getZipEntry(file, offset, fileSize);
175             if (cDOffset - offset < zipEntryData.getLength()) {
176                 throw new ZipException("cd offset in front of entry end");
177             }
178             entry.setZipEntryData(zipEntryData);
179         }
180     }
181 
182     /**
183      * output zip to zip file
184      *
185      * @param outFile file path
186      */
toFile(String outFile)187     public void toFile(String outFile) {
188         try (FileOutputStream fos = new FileOutputStream(outFile)) {
189             for (ZipEntry entry : zipEntries) {
190                 ZipEntryData zipEntryData = entry.getZipEntryData();
191                 FileUtils.writeByteToOutFile(zipEntryData.getZipEntryHeader().toBytes(), fos);
192                 boolean isSuccess = FileUtils.appendWriteFileByOffsetToFile(file, fos,
193                         zipEntryData.getFileOffset(), zipEntryData.getFileSize());
194                 if (!isSuccess) {
195                     throw new ZipException("write zip data failed");
196                 }
197                 if (zipEntryData.getDataDescriptor() != null) {
198                     FileUtils.writeByteToOutFile(zipEntryData.getDataDescriptor().toBytes(), fos);
199                 }
200             }
201             if (signingBlock != null) {
202                 FileUtils.writeByteToOutFile(signingBlock, fos);
203             }
204             for (ZipEntry entry : zipEntries) {
205                 CentralDirectory cd = entry.getCentralDirectory();
206                 FileUtils.writeByteToOutFile(cd.toBytes(), fos);
207             }
208             FileUtils.writeByteToOutFile(endOfCentralDirectory.toBytes(), fos);
209         } catch (IOException e) {
210             CustomException.throwException(ERROR.ZIP_ERROR, e.getMessage());
211         }
212     }
213 
214     /**
215      * alignment uncompress entry
216      *
217      * @param alignment int alignment
218      */
alignment(int alignment)219     public void alignment(int alignment) {
220         try {
221             sort();
222             boolean isFirstUnRunnableFile = true;
223             for (ZipEntry entry : zipEntries) {
224                 ZipEntryData zipEntryData = entry.getZipEntryData();
225                 short method = zipEntryData.getZipEntryHeader().getMethod();
226                 if (method != FILE_UNCOMPRESS_METHOD_FLAG && !isFirstUnRunnableFile) {
227                     // only align uncompressed entry and the first compress entry.
228                     break;
229                 }
230                 int alignBytes;
231                 if (method == FILE_UNCOMPRESS_METHOD_FLAG && FileUtils.isRunnableFile(
232                         zipEntryData.getZipEntryHeader().getFileName())) {
233                     // .abc and .so file align 4096 byte.
234                     alignBytes = 4096;
235                 } else if (isFirstUnRunnableFile) {
236                     // the first file after runnable file, align 4096 byte.
237                     alignBytes = 4096;
238                     isFirstUnRunnableFile = false;
239                 } else {
240                     // normal file align 4 byte.
241                     alignBytes = alignment;
242                 }
243                 int add = entry.alignment(alignBytes);
244                 if (add > 0) {
245                     resetOffset();
246                 }
247             }
248         } catch (ZipException e) {
249             CustomException.throwException(ERROR.ZIP_ERROR, e.getMessage());
250         }
251     }
252 
253     /**
254      * remove sign block
255      */
removeSignBlock()256     public void removeSignBlock() {
257         signingBlock = null;
258         resetOffset();
259     }
260 
261     /**
262      * sort uncompress entry in the front.
263      */
sort()264     private void sort() {
265         // sort uncompress file (so, abc, an) - other uncompress file - compress file
266         zipEntries.sort((entry1, entry2) -> {
267             short entry1Method = entry1.getZipEntryData().getZipEntryHeader().getMethod();
268             short entry2Method = entry2.getZipEntryData().getZipEntryHeader().getMethod();
269             String entry1FileName = entry1.getZipEntryData().getZipEntryHeader().getFileName();
270             String entry2FileName = entry2.getZipEntryData().getZipEntryHeader().getFileName();
271             if (entry1Method == FILE_UNCOMPRESS_METHOD_FLAG && entry2Method == FILE_UNCOMPRESS_METHOD_FLAG) {
272                 boolean isRunnableFile1 = FileUtils.isRunnableFile(entry1FileName);
273                 boolean isRunnableFile2 = FileUtils.isRunnableFile(entry2FileName);
274                 if (isRunnableFile1 && isRunnableFile2) {
275                     return entry1FileName.compareTo(entry2FileName);
276                 } else if (isRunnableFile1) {
277                     return -1;
278                 } else if (isRunnableFile2) {
279                     return 1;
280                 }
281             } else if (entry1Method == FILE_UNCOMPRESS_METHOD_FLAG) {
282                 return -1;
283             } else if (entry2Method == FILE_UNCOMPRESS_METHOD_FLAG) {
284                 return 1;
285             }
286             return entry1FileName.compareTo(entry2FileName);
287         });
288         resetOffset();
289     }
290 
resetOffset()291     private void resetOffset() {
292         long offset = 0L;
293         long cdLength = 0L;
294         for (ZipEntry entry : zipEntries) {
295             entry.getCentralDirectory().setOffset(offset);
296             offset += entry.getZipEntryData().getLength();
297             cdLength += entry.getCentralDirectory().getLength();
298         }
299         if (signingBlock != null) {
300             offset += signingBlock.length;
301         }
302         cDOffset = offset;
303         endOfCentralDirectory.setOffset(offset);
304         endOfCentralDirectory.setcDSize(cdLength);
305         offset += cdLength;
306         eOCDOffset = offset;
307     }
308 
getZipEntries()309     public List<ZipEntry> getZipEntries() {
310         return zipEntries;
311     }
312 
setZipEntries(List<ZipEntry> zipEntries)313     public void setZipEntries(List<ZipEntry> zipEntries) {
314         this.zipEntries = zipEntries;
315     }
316 
getSigningOffset()317     public long getSigningOffset() {
318         return signingOffset;
319     }
320 
setSigningOffset(long signingOffset)321     public void setSigningOffset(long signingOffset) {
322         this.signingOffset = signingOffset;
323     }
324 
getSigningBlock()325     public byte[] getSigningBlock() {
326         return signingBlock;
327     }
328 
setSigningBlock(byte[] signingBlock)329     public void setSigningBlock(byte[] signingBlock) {
330         this.signingBlock = signingBlock;
331     }
332 
getCDOffset()333     public long getCDOffset() {
334         return cDOffset;
335     }
336 
setCDOffset(long cDOffset)337     public void setCDOffset(long cDOffset) {
338         this.cDOffset = cDOffset;
339     }
340 
getEOCDOffset()341     public long getEOCDOffset() {
342         return eOCDOffset;
343     }
344 
setEOCDOffset(long eOCDOffset)345     public void setEOCDOffset(long eOCDOffset) {
346         this.eOCDOffset = eOCDOffset;
347     }
348 
getEndOfCentralDirectory()349     public EndOfCentralDirectory getEndOfCentralDirectory() {
350         return endOfCentralDirectory;
351     }
352 
setEndOfCentralDirectory(EndOfCentralDirectory endOfCentralDirectory)353     public void setEndOfCentralDirectory(EndOfCentralDirectory endOfCentralDirectory) {
354         this.endOfCentralDirectory = endOfCentralDirectory;
355     }
356 
getFile()357     public String getFile() {
358         return file;
359     }
360 
setFile(String file)361     public void setFile(String file) {
362         this.file = file;
363     }
364 }