• 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.SignToolErrMsg;
21 import com.ohos.hapsigntool.error.ZipException;
22 import com.ohos.hapsigntool.utils.FileUtils;
23 
24 import com.ohos.hapsigntool.utils.LogUtils;
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 LogUtils LOGGER = new LogUtils(Zip.class);
42 
43     /**
44      * file is uncompress file flag
45      */
46     public static final short 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, SignToolErrMsg.READ_ZIP_FAILED.toString(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;
193                 if (entry.getZipEntryData().getData() != null) {
194                     ByteBuffer bf = ByteBuffer.wrap(entry.getZipEntryData().getData());
195                     bf.order(ByteOrder.LITTLE_ENDIAN);
196                     isSuccess = FileUtils.writeByteToOutFile(bf.array(), fos);
197                 } else {
198                     isSuccess = FileUtils.appendWriteFileByOffsetToFile(file, fos,
199                             zipEntryData.getFileOffset(), zipEntryData.getFileSize());
200                 }
201                 if (!isSuccess) {
202                     throw new ZipException("write zip data failed");
203                 }
204                 if (zipEntryData.getDataDescriptor() != null) {
205                     FileUtils.writeByteToOutFile(zipEntryData.getDataDescriptor().toBytes(), fos);
206                 }
207             }
208             if (signingBlock != null) {
209                 FileUtils.writeByteToOutFile(signingBlock, fos);
210             }
211             for (ZipEntry entry : zipEntries) {
212                 CentralDirectory cd = entry.getCentralDirectory();
213                 FileUtils.writeByteToOutFile(cd.toBytes(), fos);
214             }
215             FileUtils.writeByteToOutFile(endOfCentralDirectory.toBytes(), fos);
216         } catch (IOException e) {
217             CustomException.throwException(ERROR.ZIP_ERROR, SignToolErrMsg.WRITE_ZIP_FAILED.toString(e.getMessage()));
218         }
219     }
220 
221     /**
222      * alignment uncompress entry
223      *
224      * @param alignment int alignment
225      */
alignment(int alignment)226     public void alignment(int alignment) {
227         try {
228             sort();
229             boolean isFirstUnRunnableFile = true;
230             for (ZipEntry entry : zipEntries) {
231                 ZipEntryData zipEntryData = entry.getZipEntryData();
232                 short method = zipEntryData.getZipEntryHeader().getMethod();
233                 if (method != FILE_UNCOMPRESS_METHOD_FLAG && !isFirstUnRunnableFile) {
234                     // only align uncompressed entry and the first compress entry.
235                     break;
236                 }
237                 int alignBytes;
238                 EntryType type = entry.getZipEntryData().getType();
239                 if ((type == EntryType.RUNNABLE_FILE && method == FILE_UNCOMPRESS_METHOD_FLAG) ||
240                     type == EntryType.BIT_MAP) {
241                     // .abc and .so file align 4096 byte.
242                     alignBytes = 4096;
243                 } else if (isFirstUnRunnableFile) {
244                     // the first file after runnable file, align 4096 byte.
245                     alignBytes = 4096;
246                     isFirstUnRunnableFile = false;
247                 } else {
248                     // normal file align 4 byte.
249                     alignBytes = alignment;
250                 }
251                 int add = entry.alignment(alignBytes);
252                 if (add > 0) {
253                     resetOffset();
254                 }
255             }
256         } catch (ZipException e) {
257             CustomException.throwException(ERROR.ZIP_ERROR, SignToolErrMsg.ALIGNMENT_ZIP_FAILED
258                     .toString(e.getMessage()));
259         }
260     }
261 
262     /**
263      * add bit map entry
264      *
265      * @param data bitmap data
266      * @throws ZipException ZipException
267      */
addBitMap(byte[] data)268     public void addBitMap(byte[] data) throws ZipException {
269         zipEntries.removeIf(e -> e.getZipEntryData().getType() == EntryType.BIT_MAP);
270         ZipEntry entry = new ZipEntry.Builder().setMethod(FILE_UNCOMPRESS_METHOD_FLAG)
271                 .setUncompressedSize(data.length)
272                 .setCompressedSize(data.length)
273                 .setFileName(FileUtils.BIT_MAP_FILENAME)
274                 .setData(data)
275                 .build();
276         zipEntries.add(entry);
277     }
278 
279     /**
280      * remove sign block
281      */
removeSignBlock()282     public void removeSignBlock() {
283         signingBlock = null;
284         resetOffset();
285     }
286 
287     /**
288      * sort uncompress entry in the front.
289      */
sort()290     private void sort() {
291         // sort uncompress file (so, abc, an) - bitmap - other uncompress file - compress file
292         zipEntries.sort((entry1, entry2) -> {
293             short entry1Method = entry1.getZipEntryData().getZipEntryHeader().getMethod();
294             short entry2Method = entry2.getZipEntryData().getZipEntryHeader().getMethod();
295             String entry1FileName = entry1.getZipEntryData().getZipEntryHeader().getFileName();
296             String entry2FileName = entry2.getZipEntryData().getZipEntryHeader().getFileName();
297             if (entry1Method == FILE_UNCOMPRESS_METHOD_FLAG && entry2Method == FILE_UNCOMPRESS_METHOD_FLAG) {
298                 EntryType entry1Type = entry1.getZipEntryData().getType();
299                 EntryType entry2Type = entry2.getZipEntryData().getType();
300                 if (entry1Type != entry2Type) {
301                     return entry1Type.compareTo(entry2Type);
302                 }
303                 return entry1FileName.compareTo(entry2FileName);
304             } else if (entry1Method == FILE_UNCOMPRESS_METHOD_FLAG) {
305                 return -1;
306             } else if (entry2Method == FILE_UNCOMPRESS_METHOD_FLAG) {
307                 return 1;
308             }
309             return entry1FileName.compareTo(entry2FileName);
310         });
311         resetOffset();
312     }
313 
resetOffset()314     private void resetOffset() {
315         long offset = 0L;
316         long cdLength = 0L;
317         for (ZipEntry entry : zipEntries) {
318             entry.updateLength();
319             entry.getCentralDirectory().setOffset(offset);
320             offset += entry.getZipEntryData().getLength();
321             cdLength += entry.getCentralDirectory().getLength();
322         }
323         if (signingBlock != null) {
324             offset += signingBlock.length;
325         }
326         cDOffset = offset;
327         endOfCentralDirectory.setOffset(offset);
328         endOfCentralDirectory.setCDSize(cdLength);
329         offset += cdLength;
330         eOCDOffset = offset;
331         endOfCentralDirectory.setCDTotal(zipEntries.size());
332         endOfCentralDirectory.setThisDiskCDNum(zipEntries.size());
333     }
334 
getZipEntries()335     public List<ZipEntry> getZipEntries() {
336         return zipEntries;
337     }
338 
setZipEntries(List<ZipEntry> zipEntries)339     public void setZipEntries(List<ZipEntry> zipEntries) {
340         this.zipEntries = zipEntries;
341     }
342 
getSigningOffset()343     public long getSigningOffset() {
344         return signingOffset;
345     }
346 
setSigningOffset(long signingOffset)347     public void setSigningOffset(long signingOffset) {
348         this.signingOffset = signingOffset;
349     }
350 
getSigningBlock()351     public byte[] getSigningBlock() {
352         return signingBlock;
353     }
354 
setSigningBlock(byte[] signingBlock)355     public void setSigningBlock(byte[] signingBlock) {
356         this.signingBlock = signingBlock;
357     }
358 
getCDOffset()359     public long getCDOffset() {
360         return cDOffset;
361     }
362 
setCDOffset(long cDOffset)363     public void setCDOffset(long cDOffset) {
364         this.cDOffset = cDOffset;
365     }
366 
getEOCDOffset()367     public long getEOCDOffset() {
368         return eOCDOffset;
369     }
370 
setEOCDOffset(long eOCDOffset)371     public void setEOCDOffset(long eOCDOffset) {
372         this.eOCDOffset = eOCDOffset;
373     }
374 
getEndOfCentralDirectory()375     public EndOfCentralDirectory getEndOfCentralDirectory() {
376         return endOfCentralDirectory;
377     }
378 
setEndOfCentralDirectory(EndOfCentralDirectory endOfCentralDirectory)379     public void setEndOfCentralDirectory(EndOfCentralDirectory endOfCentralDirectory) {
380         this.endOfCentralDirectory = endOfCentralDirectory;
381     }
382 
getFile()383     public String getFile() {
384         return file;
385     }
386 
setFile(String file)387     public void setFile(String file) {
388         this.file = file;
389     }
390 }