diff --git a/hapsigntool/hap_sign_tool/src/test/java/com/ohos/hapsigntoolcmd/CmdUnitTest.java b/hapsigntool/hap_sign_tool/src/test/java/com/ohos/hapsigntoolcmd/CmdUnitTest.java index aea1fe07e2fe69506772cb421385708d6fac8d4f..9c0762a2430c507f1a61fec7256af570e86f728a 100644 --- a/hapsigntool/hap_sign_tool/src/test/java/com/ohos/hapsigntoolcmd/CmdUnitTest.java +++ b/hapsigntool/hap_sign_tool/src/test/java/com/ohos/hapsigntoolcmd/CmdUnitTest.java @@ -15,12 +15,14 @@ package com.ohos.hapsigntoolcmd; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import com.ohos.hapsigntool.HapSignTool; import com.ohos.hapsigntool.key.KeyPairTools; import com.ohos.hapsigntool.utils.FileUtils; +import com.ohos.hapsigntool.zip.Zip; import org.junit.jupiter.api.MethodOrderer; import org.junit.jupiter.api.Order; @@ -912,6 +914,32 @@ public class CmdUnitTest { } } + + /** + * Test Method: testByteToZip() + * + * @throws IOException read file exception + */ + @Test + public void testByteToZip() throws IOException { + File dir = new File("test"); + dir.mkdir(); + for (int i = 0; i < 10; i++) { + File file = generateHapFile(FileType.FILE_UNCOMPRESSED, FileType.FILE_UNCOMPRESSED, + FileType.FILE_UNCOMPRESSED, FileType.FILE_UNCOMPRESSED); + Zip zip = new Zip(file); + String outFileName = "test/testOut.hap"; + zip.toFile(outFileName); + File outFile = new File(outFileName); + byte[] bytes = FileUtils.readFile(file); + byte[] outBytes = FileUtils.readFile(outFile); + assertArrayEquals(outBytes, bytes); + + deleteFile(file.getCanonicalPath()); + deleteFile(outFileName); + } + } + private boolean generateAppRootCa() { boolean result = HapSignTool.processCmd(new String[]{ CmdUtil.Method.GENERATE_CA, diff --git a/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/codesigning/sign/CentralDirectory.java b/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/codesigning/sign/CentralDirectory.java index 2694fdc4ac764dc6b1e3d210bde410600c2aa945..e664912a75a6ad8abd5bbd0297205dd5db297405 100644 --- a/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/codesigning/sign/CentralDirectory.java +++ b/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/codesigning/sign/CentralDirectory.java @@ -53,7 +53,7 @@ public class CentralDirectory { private final char fileCommentLength; - private final int relativeOffsetOfLocalHeader; + private final long relativeOffsetOfLocalHeader; private final byte[] fileName; @@ -88,7 +88,7 @@ public class CentralDirectory { return Strings.fromByteArray(this.fileName); } - public int getRelativeOffsetOfLocalHeader() { + public long getRelativeOffsetOfLocalHeader() { return relativeOffsetOfLocalHeader; } @@ -130,7 +130,7 @@ public class CentralDirectory { private char fileCommentLength; - private int relativeOffsetOfLocalHeader; + private long relativeOffsetOfLocalHeader; private byte[] fileName; @@ -154,7 +154,7 @@ public class CentralDirectory { return this; } - public Builder setRelativeOffsetOfLocalHeader(int relativeOffsetOfLocalHeader) { + public Builder setRelativeOffsetOfLocalHeader(long relativeOffsetOfLocalHeader) { this.relativeOffsetOfLocalHeader = relativeOffsetOfLocalHeader; return this; } diff --git a/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/codesigning/sign/CodeSigning.java b/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/codesigning/sign/CodeSigning.java index 1b9d16e12d8e5c65dd3eb164d980ccc5d9e19ef8..54d287647341ae53b3e53515c22209a94ab35b67 100644 --- a/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/codesigning/sign/CodeSigning.java +++ b/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/codesigning/sign/CodeSigning.java @@ -35,6 +35,7 @@ import com.ohos.hapsigntool.signer.LocalSigner; import com.ohos.hapsigntool.utils.FileUtils; import com.ohos.hapsigntool.utils.StringUtils; import com.ohos.hapsigntool.zip.RandomAccessFileZipDataInput; +import com.ohos.hapsigntool.zip.UnsignedDecimalUtil; import com.ohos.hapsigntool.zip.ZipDataInput; import com.ohos.hapsigntool.zip.ZipFileInfo; import com.ohos.hapsigntool.zip.ZipUtils; @@ -430,7 +431,7 @@ public class CodeSigning { byte[] attributes = new byte[CentralDirectory.BYTE_SIZE_BETWEEN_FILE_COMMENT_LENGTH_AND_LOCHDR_RELATIVE_OFFSET]; cdBuffer.get(attributes); - int locHdrOffset = cdBuffer.getInt(); + long locHdrOffset = UnsignedDecimalUtil.getUnsignedInt(cdBuffer); builder.setFileNameLength(fileNameLength).setExtraFieldLength(extraFieldLength) .setFileCommentLength(fileCommentLength).setRelativeOffsetOfLocalHeader(locHdrOffset); byte[] fileNameBuffer = new byte[fileNameLength]; diff --git a/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/error/ERROR.java b/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/error/ERROR.java index 286031eef28f9834822ad4c033407b001f43f3fd..2f2627faa8bcc285f2e648dde255108359461151 100644 --- a/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/error/ERROR.java +++ b/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/error/ERROR.java @@ -88,7 +88,12 @@ public enum ERROR { /** * Enum constant IO_CSR_ERROR. */ - IO_CSR_ERROR(118); + IO_CSR_ERROR(118), + + /** + * Enum constant ZIP_ERROR. + */ + ZIP_ERROR(119); /** * Field errorCode. diff --git a/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/error/ZipException.java b/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/error/ZipException.java new file mode 100644 index 0000000000000000000000000000000000000000..6ad0dfb29d8ae3c5ce6463094d3064dcebfc02ed --- /dev/null +++ b/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/error/ZipException.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2023-2023 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.ohos.hapsigntool.error; + +import java.io.IOException; + +/** + * Zip exception for programs. + * + * @since 2023/12/07 + */ +public class ZipException extends IOException { + /** + * new ZipException + * + * @param message exception message + */ + public ZipException(String message) { + super(message); + } + + /** + * new ZipException + * + * @param message exception message + * @param e exception + */ + public ZipException(String message, Exception e) { + super(message, e); + } +} diff --git a/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/hap/provider/SignProvider.java b/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/hap/provider/SignProvider.java index c181bdaf9b50fb31269996830bf6d853f4db1f95..ee5aa984426c2372b917030ac38b3051dbfa9e71 100644 --- a/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/hap/provider/SignProvider.java +++ b/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/hap/provider/SignProvider.java @@ -55,6 +55,7 @@ import com.ohos.hapsigntool.zip.ZipDataInput; import com.ohos.hapsigntool.zip.ZipDataOutput; import com.ohos.hapsigntool.zip.ZipFileInfo; import com.ohos.hapsigntool.zip.ZipUtils; +import com.ohos.hapsigntool.zip.Zip; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -63,7 +64,6 @@ import org.bouncycastle.cms.CMSSignedData; import org.bouncycastle.jce.provider.BouncyCastleProvider; import java.io.File; -import java.io.FileOutputStream; import java.io.IOException; import java.io.RandomAccessFile; import java.nio.ByteBuffer; @@ -83,9 +83,6 @@ import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; -import java.util.TimeZone; -import java.util.jar.JarFile; -import java.util.jar.JarOutputStream; /** * Sign provider super class @@ -329,7 +326,7 @@ public abstract class SignProvider { checkCompatibleVersion(); File input = new File(signParams.get(ParamConstants.PARAM_BASIC_INPUT_FILE)); output = new File(signParams.get(ParamConstants.PARAM_BASIC_OUTPUT_FILE)); - String suffix = getFileSuffix(output); + String suffix = getFileSuffix(input); if (input.getCanonicalPath().equals(output.getCanonicalPath())) { tmpOutput = File.createTempFile("signedHap", "." + suffix); isPathOverlap = true; @@ -504,14 +501,13 @@ public abstract class SignProvider { */ private void copyFileAndAlignment(File input, File tmpOutput, int alignment) throws IOException, HapFormatException { - try (JarFile inputJar = new JarFile(input, false); - FileOutputStream outputFile = new FileOutputStream(tmpOutput); - JarOutputStream outputJar = new JarOutputStream(outputFile)) { - long timestamp = TIMESTAMP; - timestamp -= TimeZone.getDefault().getOffset(timestamp); - outputJar.setLevel(COMPRESSION_MODE); - SignHap.copyFiles(inputJar, outputJar, timestamp, alignment); - } + Zip zip = new Zip(input); + zip.alignment(alignment); + zip.removeSignBlock(); + long start = System.currentTimeMillis(); + zip.toFile(tmpOutput.getCanonicalPath()); + long end = System.currentTimeMillis(); + LOGGER.debug("zip to file use {} ms", end - start); } /** diff --git a/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/utils/FileUtils.java b/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/utils/FileUtils.java index dc1d1b86cc3591077fe6c54525c8fb8fea12bc9e..b61266aeeb79be65d51c146f8f46d65c6e84570f 100644 --- a/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/utils/FileUtils.java +++ b/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/utils/FileUtils.java @@ -18,6 +18,7 @@ package com.ohos.hapsigntool.utils; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.ohos.hapsigntool.error.ERROR; + import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -69,7 +70,7 @@ public final class FileUtils { /** * File reader block size */ - public static final int FILE_BUFFER_BLOCK = 4096; + public static final int FILE_BUFFER_BLOCK = 1024 * 1024; /** * File end @@ -113,7 +114,7 @@ public final class FileUtils { * @throws IOException Read failed */ public static byte[] readFile(File file) throws IOException { - return read(new FileInputStream(file)); + return read(Files.newInputStream(file.toPath())); } /** @@ -136,6 +137,68 @@ public final class FileUtils { } } + /** + * Read byte from input file. + * + * @param file input file + * @param offset offset + * @param length length + * @return data bytes + */ + public static byte[] readFileByOffsetAndLength(File file, long offset, long length) throws IOException { + try (FileInputStream input = new FileInputStream(file)) { + return readInputByOffsetAndLength(input, offset, length); + } + } + + /** + * Read byte from input stream. + * + * @param input input stream + * @param offset offset + * @param length length + * @return data bytes + * @throws IOException read exception + */ + public static byte[] readInputByOffsetAndLength(InputStream input, long offset, long length) throws IOException { + input.skip(offset); + return readInputByLength(input, length); + } + + /** + * Read byte from input stream. + * + * @param input InputStream + * @param length length + * @return data bytes + */ + public static byte[] readInputByLength(InputStream input, long length) throws IOException { + try (ByteArrayOutputStream output = new ByteArrayOutputStream()) { + if (length > Integer.MAX_VALUE) { + throw new IOException("Size cannot be greater than Integer max value: " + length); + } + writeInputToOutPut(input, output, length); + return output.toByteArray(); + } + } + + /** + * write input to output by length + */ + private static void writeInputToOutPut(InputStream input, OutputStream output, long length) throws IOException { + byte[] buffer = new byte[FILE_BUFFER_BLOCK]; + long hasReadLen = 0L; + while (hasReadLen < length) { + int readLen = (int) Math.min(length - hasReadLen, FILE_BUFFER_BLOCK); + int len = input.read(buffer, 0, readLen); + if (len != readLen) { + throw new IOException("read" + hasReadLen + "bytes data less than " + length); + } + output.write(buffer, 0, len); + hasReadLen += len; + } + } + /** * Out put content to file. * @@ -151,6 +214,29 @@ public final class FileUtils { } } + /** + * Write data in file to output stream + * + * @param inFile input file path. + * @param out output file path. + * @param offset file read offset + * @param size file read size + * @return true, if write successfully. + */ + public static boolean appendWriteFileByOffsetToFile(String inFile, FileOutputStream out, long offset, long size) { + File inputFile = new File(inFile); + try (FileInputStream fis = new FileInputStream(inputFile)) { + fis.skip(offset); + writeInputToOutPut(fis, out, size); + return true; + } catch (FileNotFoundException e) { + LOGGER.error("Failed to get input stream object."); + } catch (IOException e) { + LOGGER.error("Failed to read or write data."); + } + return false; + } + /** * Check file exist or not. * @@ -267,14 +353,32 @@ public final class FileUtils { /** * Write byte array data to output file. * - * @param signHeadByte byte array data. + * @param bytes byte array data. * @param outFile output file path. * @return true, if write successfully. */ - public static boolean writeByteToOutFile(byte[] signHeadByte, String outFile) { + public static boolean writeByteToOutFile(byte[] bytes, String outFile) { try (OutputStream ops = new FileOutputStream(outFile, true)) { - ops.write(signHeadByte, 0, signHeadByte.length); - ops.flush(); + return writeByteToOutFile(bytes, ops); + } catch (FileNotFoundException e) { + LOGGER.error("Failed to get output stream object, outfile: " + outFile); + } catch (IOException e) { + LOGGER.error("Failed to write data to ops, outfile: " + outFile); + } + return false; + } + + /** + * Write byte array data to output file. + * + * @param bytes byte array data. + * @param outFile output file path. + * @return true, if write successfully. + */ + public static boolean writeByteToOutFile(byte[] bytes, OutputStream outFile) { + try { + outFile.write(bytes, 0, bytes.length); + outFile.flush(); return true; } catch (FileNotFoundException e) { LOGGER.error("Failed to get output stream object, outfile: " + outFile); diff --git a/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/zip/CentralDirectory.java b/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/zip/CentralDirectory.java new file mode 100644 index 0000000000000000000000000000000000000000..59f1404d9ead0acc84eec602f922a4165491c549 --- /dev/null +++ b/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/zip/CentralDirectory.java @@ -0,0 +1,388 @@ +/* + * Copyright (c) 2023-2023 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.ohos.hapsigntool.zip; + +import com.ohos.hapsigntool.error.ZipException; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.charset.StandardCharsets; + +/** + * resolve zip CentralDirectory data + * + * @since 2023/12/02 + */ +public class CentralDirectory { + /** + * central directory invariable bytes length + */ + public static final int CD_LENGTH = 46; + + /** + * 4 bytes , central directory signature + */ + public static final int SIGNATURE = 0x02014b50; + + /** + * 2 bytes + */ + private short version; + + /** + * 2 bytes + */ + private short versionExtra; + + /** + * 2 bytes + */ + private short flag; + + /** + * 2 bytes + */ + private short method; + + /** + * 2 bytes + */ + private short lastTime; + + /** + * 2 bytes + */ + private short lastDate; + + /** + * 4 bytes + */ + private int crc32; + + /** + * 4 bytes + */ + private long compressedSize; + + /** + * 4 bytes + */ + private long unCompressedSize; + + /** + * 2 bytes + */ + private int fileNameLength; + + /** + * 2 bytes + */ + private int extraLength; + + /** + * 2 bytes + */ + private int commentLength; + + /** + * 2 bytes + */ + private int diskNumStart; + + /** + * 2 bytes + */ + private short internalFile; + + /** + * 4 bytes + */ + private int externalFile; + + /** + * 4 bytes + */ + private long offset; + + /** + * n bytes + */ + private String fileName; + + /** + * n bytes + */ + private byte[] extraData; + + /** + * n bytes + */ + private byte[] comment; + + private int length; + + /** + * get Central Directory + * + * @param bf ByteBuffer + * @return CentralDirectory + * @throws ZipException read Central Directory exception + */ + public static CentralDirectory getCentralDirectory(ByteBuffer bf) throws ZipException { + CentralDirectory cd = new CentralDirectory(); + if (bf.getInt() != SIGNATURE) { + throw new ZipException("find zip central directory failed"); + } + + cd.setVersion(bf.getShort()); + cd.setVersionExtra(bf.getShort()); + cd.setFlag(bf.getShort()); + cd.setMethod(bf.getShort()); + cd.setLastTime(bf.getShort()); + cd.setLastDate(bf.getShort()); + cd.setCrc32(bf.getInt()); + cd.setCompressedSize(UnsignedDecimalUtil.getUnsignedInt(bf)); + cd.setUnCompressedSize(UnsignedDecimalUtil.getUnsignedInt(bf)); + cd.setFileNameLength(UnsignedDecimalUtil.getUnsignedShort(bf)); + cd.setExtraLength(UnsignedDecimalUtil.getUnsignedShort(bf)); + cd.setCommentLength(UnsignedDecimalUtil.getUnsignedShort(bf)); + cd.setDiskNumStart(UnsignedDecimalUtil.getUnsignedShort(bf)); + cd.setInternalFile(bf.getShort()); + cd.setExternalFile(bf.getInt()); + cd.setOffset(UnsignedDecimalUtil.getUnsignedInt(bf)); + if (cd.getFileNameLength() > 0) { + byte[] readFileName = new byte[cd.getFileNameLength()]; + bf.get(readFileName); + cd.setFileName(new String(readFileName, StandardCharsets.UTF_8)); + } + if (cd.getExtraLength() > 0) { + byte[] extra = new byte[cd.getExtraLength()]; + bf.get(extra); + cd.setExtraData(extra); + } + if (cd.getCommentLength() > 0) { + byte[] readComment = new byte[cd.getCommentLength()]; + bf.get(readComment); + cd.setComment(readComment); + } + cd.setLength(CD_LENGTH + cd.getFileNameLength() + cd.getExtraLength() + cd.getCommentLength()); + return cd; + } + + /** + * change Central Directory to bytes + * + * @return bytes + */ + public byte[] toBytes() { + ByteBuffer bf = ByteBuffer.allocate(length).order(ByteOrder.LITTLE_ENDIAN); + bf.putInt(SIGNATURE); + UnsignedDecimalUtil.setUnsignedShort(bf, version); + UnsignedDecimalUtil.setUnsignedShort(bf, versionExtra); + UnsignedDecimalUtil.setUnsignedShort(bf, flag); + UnsignedDecimalUtil.setUnsignedShort(bf, method); + UnsignedDecimalUtil.setUnsignedShort(bf, lastTime); + UnsignedDecimalUtil.setUnsignedShort(bf, lastDate); + UnsignedDecimalUtil.setUnsignedInt(bf, crc32); + UnsignedDecimalUtil.setUnsignedInt(bf, compressedSize); + UnsignedDecimalUtil.setUnsignedInt(bf, unCompressedSize); + UnsignedDecimalUtil.setUnsignedShort(bf, fileNameLength); + UnsignedDecimalUtil.setUnsignedShort(bf, extraLength); + UnsignedDecimalUtil.setUnsignedShort(bf, commentLength); + UnsignedDecimalUtil.setUnsignedShort(bf, diskNumStart); + UnsignedDecimalUtil.setUnsignedShort(bf, internalFile); + UnsignedDecimalUtil.setUnsignedInt(bf, externalFile); + UnsignedDecimalUtil.setUnsignedInt(bf, offset); + if (fileNameLength > 0) { + bf.put(fileName.getBytes(StandardCharsets.UTF_8)); + } + if (extraLength > 0) { + bf.put(extraData); + } + if (commentLength > 0) { + bf.put(extraData); + } + return bf.array(); + } + + public static int getCdLength() { + return CD_LENGTH; + } + + public static int getSIGNATURE() { + return SIGNATURE; + } + + public short getVersion() { + return version; + } + + public void setVersion(short version) { + this.version = version; + } + + public short getVersionExtra() { + return versionExtra; + } + + public void setVersionExtra(short versionExtra) { + this.versionExtra = versionExtra; + } + + public short getFlag() { + return flag; + } + + public void setFlag(short flag) { + this.flag = flag; + } + + public short getMethod() { + return method; + } + + public void setMethod(short method) { + this.method = method; + } + + public short getLastTime() { + return lastTime; + } + + public void setLastTime(short lastTime) { + this.lastTime = lastTime; + } + + public short getLastDate() { + return lastDate; + } + + public void setLastDate(short lastDate) { + this.lastDate = lastDate; + } + + public int getCrc32() { + return crc32; + } + + public void setCrc32(int crc32) { + this.crc32 = crc32; + } + + public long getCompressedSize() { + return compressedSize; + } + + public void setCompressedSize(long compressedSize) { + this.compressedSize = compressedSize; + } + + public long getUnCompressedSize() { + return unCompressedSize; + } + + public void setUnCompressedSize(long unCompressedSize) { + this.unCompressedSize = unCompressedSize; + } + + public int getFileNameLength() { + return fileNameLength; + } + + public void setFileNameLength(int fileNameLength) { + this.fileNameLength = fileNameLength; + } + + public int getExtraLength() { + return extraLength; + } + + public void setExtraLength(int extraLength) { + this.extraLength = extraLength; + } + + public int getCommentLength() { + return commentLength; + } + + public void setCommentLength(int commentLength) { + this.commentLength = commentLength; + } + + public int getDiskNumStart() { + return diskNumStart; + } + + public void setDiskNumStart(int diskNumStart) { + this.diskNumStart = diskNumStart; + } + + public short getInternalFile() { + return internalFile; + } + + public void setInternalFile(short internalFile) { + this.internalFile = internalFile; + } + + public int getExternalFile() { + return externalFile; + } + + public void setExternalFile(int externalFile) { + this.externalFile = externalFile; + } + + public long getOffset() { + return offset; + } + + public void setOffset(long offset) { + this.offset = offset; + } + + public String getFileName() { + return fileName; + } + + public void setFileName(String fileName) { + this.fileName = fileName; + } + + public byte[] getExtraData() { + return extraData; + } + + public void setExtraData(byte[] extraData) { + this.extraData = extraData; + } + + public byte[] getComment() { + return comment; + } + + public void setComment(byte[] comment) { + this.comment = comment; + } + + public int getLength() { + return length; + } + + public void setLength(int length) { + this.length = length; + } +} \ No newline at end of file diff --git a/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/zip/DataDescriptor.java b/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/zip/DataDescriptor.java new file mode 100644 index 0000000000000000000000000000000000000000..aa2d6355c5072c0b330edf50a4abda711654a7bc --- /dev/null +++ b/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/zip/DataDescriptor.java @@ -0,0 +1,124 @@ +/* + * Copyright (c) 2023-2023 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.ohos.hapsigntool.zip; + +import com.ohos.hapsigntool.error.ZipException; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +/** + * resolve zip DataDescriptor data + * File data MAY be followed by a "data descriptor" for the file. Data + * descriptors are used to facilitate ZIP file streaming. + * + * @since 2023/12/02 + */ +public class DataDescriptor { + /** + * DataDescriptor invariable bytes length + */ + public static final int DES_LENGTH = 16; + + /** + * 4 bytes , DataDescriptor signature + */ + public static final int SIGNATURE = 0x08074b50; + + /** + * 4 bytes + */ + private int crc32; + + /** + * 4 bytes + */ + private long compressedSize; + + /** + * 4 bytes + */ + private long unCompressedSize; + + /** + * get Data Descriptor + * + * @param bytes DataDescriptor bytes + * @return DataDescriptor + * @throws ZipException read data descriptor exception + */ + public static DataDescriptor getDataDescriptor(byte[] bytes) throws ZipException { + if (bytes.length != DES_LENGTH) { + throw new ZipException("read Data Descriptor failed"); + } + ByteBuffer bf = ByteBuffer.wrap(bytes); + bf.order(ByteOrder.LITTLE_ENDIAN); + DataDescriptor data = new DataDescriptor(); + if (bf.getInt() != SIGNATURE) { + throw new ZipException("read Data Descriptor failed"); + } + data.setCrc32(bf.getInt()); + data.setCompressedSize(UnsignedDecimalUtil.getUnsignedInt(bf)); + data.setUnCompressedSize(UnsignedDecimalUtil.getUnsignedInt(bf)); + return data; + } + + /** + * change DataDescriptor to bytes + * + * @return bytes + */ + public byte[] toBytes() { + ByteBuffer bf = ByteBuffer.allocate(DES_LENGTH).order(ByteOrder.LITTLE_ENDIAN); + bf.putInt(SIGNATURE); + bf.putInt(crc32); + UnsignedDecimalUtil.setUnsignedInt(bf, compressedSize); + UnsignedDecimalUtil.setUnsignedInt(bf, unCompressedSize); + return bf.array(); + } + + public static int getDesLength() { + return DES_LENGTH; + } + + public static int getSIGNATURE() { + return SIGNATURE; + } + + public int getCrc32() { + return crc32; + } + + public void setCrc32(int crc32) { + this.crc32 = crc32; + } + + public long getCompressedSize() { + return compressedSize; + } + + public void setCompressedSize(long compressedSize) { + this.compressedSize = compressedSize; + } + + public long getUnCompressedSize() { + return unCompressedSize; + } + + public void setUnCompressedSize(long unCompressedSize) { + this.unCompressedSize = unCompressedSize; + } +} diff --git a/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/zip/EndOfCentralDirectory.java b/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/zip/EndOfCentralDirectory.java new file mode 100644 index 0000000000000000000000000000000000000000..5043ef2e0cfb85b105c506765a3aa009c519b606 --- /dev/null +++ b/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/zip/EndOfCentralDirectory.java @@ -0,0 +1,236 @@ +/* + * Copyright (c) 2023-2023 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.ohos.hapsigntool.zip; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.Optional; + +/** + * resolve zip EndOfCentralDirectory data + * A ZIP file MUST contain an "end of central directory record". A ZIP + * file containing only an "end of central directory record" is considered an + * empty ZIP file. Files MAY be added or replaced within a ZIP file, or deleted. + * A ZIP file MUST have only one "end of central directory record". Other + * records defined in this specification MAY be used as needed to support + * storage requirements for individual ZIP files. + * + * @since 2023/12/04 + */ +public class EndOfCentralDirectory { + /** + * EndOfCentralDirectory invariable bytes length + */ + public static final int EOCD_LENGTH = 22; + + /** + * 4 bytes , central directory signature + */ + public static final int SIGNATURE = 0x06054b50; + + /** + * 2 bytes + */ + private int diskNum; + + /** + * 2 bytes + */ + private int cDStartDiskNum; + + /** + * 2 bytes + */ + private int thisDiskCDNum; + + /** + * 2 bytes + */ + private int cDTotal; + + /** + * 4 bytes + */ + private long cDSize; + + /** + * 4 bytes + */ + private long offset; + + /** + * 2 bytes + */ + private int commentLength; + + /** + * n bytes + */ + private byte[] comment; + + private int length; + + /** + * init End Of Central Directory, default offset is 0 + * + * @param bytes End Of Central Directory bytes + * @return End Of Central Directory + */ + public static Optional getEOCDByBytes(byte[] bytes) { + return getEOCDByBytes(bytes, 0); + } + + /** + * init End Of Central Directory + * + * @param bytes End Of Central Directory bytes + * @param offset offset + * @return End Of Central Directory + */ + public static Optional getEOCDByBytes(byte[] bytes, int offset) { + EndOfCentralDirectory eocd = new EndOfCentralDirectory(); + int remainingDataLen = bytes.length - offset; + if (remainingDataLen < EOCD_LENGTH) { + return Optional.empty(); + } + ByteBuffer bf = ByteBuffer.wrap(bytes, offset, remainingDataLen); + bf.order(ByteOrder.LITTLE_ENDIAN); + if (bf.getInt() != SIGNATURE) { + return Optional.empty(); + } + eocd.setDiskNum(UnsignedDecimalUtil.getUnsignedShort(bf)); + eocd.setcDStartDiskNum(UnsignedDecimalUtil.getUnsignedShort(bf)); + eocd.setThisDiskCDNum(UnsignedDecimalUtil.getUnsignedShort(bf)); + eocd.setcDTotal(UnsignedDecimalUtil.getUnsignedShort(bf)); + eocd.setcDSize(UnsignedDecimalUtil.getUnsignedInt(bf)); + eocd.setOffset(UnsignedDecimalUtil.getUnsignedInt(bf)); + eocd.setCommentLength(UnsignedDecimalUtil.getUnsignedShort(bf)); + if (bf.remaining() != eocd.getCommentLength()) { + return Optional.empty(); + } + if (eocd.getCommentLength() > 0) { + byte[] readComment = new byte[eocd.getCommentLength()]; + bf.get(readComment); + eocd.setComment(readComment); + } + eocd.setLength(EOCD_LENGTH + eocd.getCommentLength()); + if (bf.remaining() != 0) { + return Optional.empty(); + } + return Optional.of(eocd); + } + + /** + * change End Of Central Directory to bytes + * + * @return bytes + */ + public byte[] toBytes() { + ByteBuffer bf = ByteBuffer.allocate(length).order(ByteOrder.LITTLE_ENDIAN); + bf.putInt(SIGNATURE); + UnsignedDecimalUtil.setUnsignedShort(bf, diskNum); + UnsignedDecimalUtil.setUnsignedShort(bf, cDStartDiskNum); + UnsignedDecimalUtil.setUnsignedShort(bf, thisDiskCDNum); + UnsignedDecimalUtil.setUnsignedShort(bf, cDTotal); + UnsignedDecimalUtil.setUnsignedInt(bf, cDSize); + UnsignedDecimalUtil.setUnsignedInt(bf, offset); + UnsignedDecimalUtil.setUnsignedShort(bf, commentLength); + if (commentLength > 0) { + bf.put(comment); + } + return bf.array(); + } + + public static int getEocdLength() { + return EOCD_LENGTH; + } + + public static int getSIGNATURE() { + return SIGNATURE; + } + + public int getDiskNum() { + return diskNum; + } + + public void setDiskNum(int diskNum) { + this.diskNum = diskNum; + } + + public int getcDStartDiskNum() { + return cDStartDiskNum; + } + + public void setcDStartDiskNum(int cDStartDiskNum) { + this.cDStartDiskNum = cDStartDiskNum; + } + + public int getThisDiskCDNum() { + return thisDiskCDNum; + } + + public void setThisDiskCDNum(int thisDiskCDNum) { + this.thisDiskCDNum = thisDiskCDNum; + } + + public int getcDTotal() { + return cDTotal; + } + + public void setcDTotal(int cDTotal) { + this.cDTotal = cDTotal; + } + + public long getcDSize() { + return cDSize; + } + + public void setcDSize(long cDSize) { + this.cDSize = cDSize; + } + + public long getOffset() { + return offset; + } + + public void setOffset(long offset) { + this.offset = offset; + } + + public int getCommentLength() { + return commentLength; + } + + public void setCommentLength(int commentLength) { + this.commentLength = commentLength; + } + + public byte[] getComment() { + return comment; + } + + public void setComment(byte[] comment) { + this.comment = comment; + } + + public int getLength() { + return length; + } + + public void setLength(int length) { + this.length = length; + } +} \ No newline at end of file diff --git a/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/zip/UnsignedDecimalUtil.java b/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/zip/UnsignedDecimalUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..f9233ee75b0448c08fedeacba162bb7a2ed25915 --- /dev/null +++ b/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/zip/UnsignedDecimalUtil.java @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2023-2023 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.ohos.hapsigntool.zip; + +import java.nio.ByteBuffer; + +/** + * Unsigned Decimal Util + * + * @since 2023/12/09 + */ +public class UnsignedDecimalUtil { + /** + * max unsigned int value + */ + public static final long MAX_UNSIGNED_INT_VALUE = 0xFFFFFFFFL; + + /** + * max unsigned int value + */ + public static final int MAX_UNSIGNED_SHORT_VALUE = 0xFFFF; + + private static final int BIT_SIZE = 8; + + private static final int DOUBLE_BIT_SIZE = 16; + + private static final int TRIPLE_BIT_SIZE = 24; + + /** + * get unsigned int to long + * + * @param bf byteBuffer + * @return long + */ + public static long getUnsignedInt(ByteBuffer bf) { + int value = bf.getInt(); + if (value >= 0) { + return value; + } + return value & MAX_UNSIGNED_INT_VALUE; + } + + /** + * get unsigned short to int + * + * @param bf byteBuffer + * @return int + */ + public static int getUnsignedShort(ByteBuffer bf) { + short value = bf.getShort(); + if (value >= 0) { + return value; + } + return value & MAX_UNSIGNED_SHORT_VALUE; + } + + /** + * set long to unsigned int + * + * @param bf byteBuffer + * @param l long + */ + public static void setUnsignedInt(ByteBuffer bf, long l) { + byte[] bytes = new byte[] { + (byte) (l & 0xFF), + (byte) ((l >> BIT_SIZE) & 0xFF), + (byte) ((l >> DOUBLE_BIT_SIZE) & 0xFF), + (byte) ((l >> TRIPLE_BIT_SIZE) & 0xFF) + }; + bf.put(bytes); + } + + /** + * set int to unsigned short + * + * @param bf byteBuffer + * @param i int + */ + public static void setUnsignedShort(ByteBuffer bf, int i) { + byte[] bytes = new byte[] { + (byte) (i & 0xFF), + (byte) ((i >> BIT_SIZE) & 0xFF) + }; + bf.put(bytes); + } +} diff --git a/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/zip/Zip.java b/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/zip/Zip.java new file mode 100644 index 0000000000000000000000000000000000000000..3b6ded9636a9dc23240d09b4ade0f18d9aa137a6 --- /dev/null +++ b/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/zip/Zip.java @@ -0,0 +1,355 @@ +/* + * Copyright (c) 2023-2023 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.ohos.hapsigntool.zip; + +import com.ohos.hapsigntool.error.CustomException; +import com.ohos.hapsigntool.error.ERROR; +import com.ohos.hapsigntool.error.ZipException; +import com.ohos.hapsigntool.utils.FileUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +/** + * resolve zip data + * + * @since 2023/12/02 + */ +public class Zip { + private static final Logger LOGGER = LogManager.getLogger(Zip.class); + + /** + * file is uncompress file flag + */ + public static final int FILE_UNCOMPRESS_METHOD_FLAG = 0; + + /** + * max comment length + */ + public static final int MAX_COMMENT_LENGTH = 65535; + + private List zipEntries; + + private long signingOffset; + + private byte[] signingBlock; + + private long cDOffset; + + private long eOCDOffset; + + private EndOfCentralDirectory endOfCentralDirectory; + + private String file; + + /** + * create Zip by file + * + * @param inputFile file + */ + public Zip(File inputFile) { + try { + this.file = inputFile.getPath(); + if (!inputFile.exists()) { + throw new ZipException("read zip file failed"); + } + long start = System.currentTimeMillis(); + // 1. get eocd data + endOfCentralDirectory = getZipEndOfCentralDirectory(inputFile); + cDOffset = endOfCentralDirectory.getOffset(); + long eocdEnd = System.currentTimeMillis(); + LOGGER.debug("getZipEndOfCentralDirectory use {} ms", eocdEnd - start); + // 2. use eocd's cd offset, get cd data + getZipCentralDirectory(inputFile); + long cdEnd = System.currentTimeMillis(); + LOGGER.debug("getZipCentralDirectory use {} ms", cdEnd - start); + // 3. use cd's entry offset and file size, get entry data + getZipEntries(inputFile); + ZipEntry endEntry = zipEntries.get(zipEntries.size() - 1); + CentralDirectory endCD = endEntry.getCentralDirectory(); + ZipEntryData endEntryData = endEntry.getZipEntryData(); + signingOffset = endCD.getOffset() + endEntryData.getLength(); + long entryEnd = System.currentTimeMillis(); + LOGGER.debug("getZipEntries use {} ms", entryEnd - start); + // 4. file all data - eocd - cd - entry = sign block + signingBlock = getSigningBlock(inputFile); + } catch (IOException e) { + CustomException.throwException(ERROR.ZIP_ERROR, e.getMessage()); + } + } + + private EndOfCentralDirectory getZipEndOfCentralDirectory(File file) throws IOException { + if (file.length() < EndOfCentralDirectory.EOCD_LENGTH) { + throw new ZipException("find zip eocd failed"); + } + + // try to read EOCD without comment + int eocdLength = EndOfCentralDirectory.EOCD_LENGTH; + eOCDOffset = file.length() - eocdLength; + byte[] bytes = FileUtils.readFileByOffsetAndLength(file, eOCDOffset, eocdLength); + Optional eocdByBytes = EndOfCentralDirectory.getEOCDByBytes(bytes); + if (eocdByBytes.isPresent()) { + return eocdByBytes.get(); + } + + // try to search EOCD with comment + long eocdMaxLength = Math.min(EndOfCentralDirectory.EOCD_LENGTH + MAX_COMMENT_LENGTH, file.length()); + eOCDOffset = file.length() - eocdMaxLength; + bytes = FileUtils.readFileByOffsetAndLength(file, eOCDOffset, eocdMaxLength); + for (int start = 0; start < eocdMaxLength; start++) { + eocdByBytes = EndOfCentralDirectory.getEOCDByBytes(bytes, start); + if (eocdByBytes.isPresent()) { + eOCDOffset += start; + return eocdByBytes.get(); + } + } + throw new ZipException("read zip failed: can not find eocd in file"); + } + + private void getZipCentralDirectory(File file) throws IOException { + zipEntries = new ArrayList<>(endOfCentralDirectory.getcDTotal()); + // read full central directory bytes + byte[] cdBytes = FileUtils.readFileByOffsetAndLength(file, cDOffset, endOfCentralDirectory.getcDSize()); + if (cdBytes.length < CentralDirectory.CD_LENGTH) { + throw new ZipException("find zip cd failed"); + } + ByteBuffer bf = ByteBuffer.wrap(cdBytes); + bf.order(ByteOrder.LITTLE_ENDIAN); + int offset = 0; + // one by one format central directory + while (offset < cdBytes.length) { + CentralDirectory cd = CentralDirectory.getCentralDirectory(bf); + ZipEntry entry = new ZipEntry(); + entry.setCentralDirectory(cd); + zipEntries.add(entry); + offset += cd.getLength(); + } + } + + private byte[] getSigningBlock(File file) throws IOException { + long size = cDOffset - signingOffset; + if (size < 0) { + throw new ZipException("cd offset in front of entry end"); + } + if (size == 0) { + return new byte[0]; + } + return FileUtils.readFileByOffsetAndLength(file, signingOffset, size); + } + + private void getZipEntries(File file) throws IOException { + // use central directory data, find entry data + for (ZipEntry entry : zipEntries) { + CentralDirectory cd = entry.getCentralDirectory(); + long offset = cd.getOffset(); + long unCompressedSize = cd.getUnCompressedSize(); + long compressedSize = cd.getCompressedSize(); + long fileSize = cd.getMethod() == FILE_UNCOMPRESS_METHOD_FLAG ? unCompressedSize : compressedSize; + + entry.setZipEntryData(ZipEntryData.getZipEntry(file, offset, fileSize)); + } + } + + /** + * output zip to zip file + * + * @param outFile file path + */ + public void toFile(String outFile) { + try (FileOutputStream fos = new FileOutputStream(outFile)) { + for (ZipEntry entry : zipEntries) { + ZipEntryData zipEntryData = entry.getZipEntryData(); + FileUtils.writeByteToOutFile(zipEntryData.getZipEntryHeader().toBytes(), fos); + boolean isSuccess = FileUtils.appendWriteFileByOffsetToFile(file, fos, + zipEntryData.getFileOffset(), zipEntryData.getFileSize()); + if (!isSuccess) { + throw new ZipException("write zip data failed"); + } + if (zipEntryData.getDataDescriptor() != null) { + FileUtils.writeByteToOutFile(zipEntryData.getDataDescriptor().toBytes(), fos); + } + } + if (signingBlock != null) { + FileUtils.writeByteToOutFile(signingBlock, fos); + } + for (ZipEntry entry : zipEntries) { + CentralDirectory cd = entry.getCentralDirectory(); + FileUtils.writeByteToOutFile(cd.toBytes(), fos); + } + FileUtils.writeByteToOutFile(endOfCentralDirectory.toBytes(), fos); + } catch (IOException e) { + CustomException.throwException(ERROR.ZIP_ERROR, e.getMessage()); + } + } + + /** + * alignment uncompress entry + * + * @param alignment int alignment + */ + public void alignment(int alignment) { + try { + sort(); + boolean isFirstUnRunnableFile = true; + for (ZipEntry entry : zipEntries) { + ZipEntryData zipEntryData = entry.getZipEntryData(); + short method = zipEntryData.getZipEntryHeader().getMethod(); + if (method != FILE_UNCOMPRESS_METHOD_FLAG && !isFirstUnRunnableFile) { + // only align uncompressed entry and the first compress entry. + break; + } + int alignBytes; + if (method == FILE_UNCOMPRESS_METHOD_FLAG && FileUtils.isRunnableFile( + zipEntryData.getZipEntryHeader().getFileName())) { + // .abc and .so file align 4096 byte. + alignBytes = 4096; + } else if (isFirstUnRunnableFile) { + // the first file after runnable file, align 4096 byte. + alignBytes = 4096; + isFirstUnRunnableFile = false; + } else { + // normal file align 4 byte. + alignBytes = alignment; + } + int add = entry.alignment(alignBytes); + if (add > 0) { + resetOffset(); + } + } + } catch (ZipException e) { + CustomException.throwException(ERROR.ZIP_ERROR, e.getMessage()); + } + } + + /** + * remove sign block + */ + public void removeSignBlock() { + signingBlock = null; + resetOffset(); + } + + /** + * sort uncompress entry in the front. + */ + private void sort() { + // sort uncompress file (so, abc, an) - other uncompress file - compress file + zipEntries.sort((entry1, entry2) -> { + short entry1Method = entry1.getZipEntryData().getZipEntryHeader().getMethod(); + short entry2Method = entry2.getZipEntryData().getZipEntryHeader().getMethod(); + String entry1FileName = entry1.getZipEntryData().getZipEntryHeader().getFileName(); + String entry2FileName = entry2.getZipEntryData().getZipEntryHeader().getFileName(); + + if (entry1Method == FILE_UNCOMPRESS_METHOD_FLAG && entry2Method == FILE_UNCOMPRESS_METHOD_FLAG) { + if (FileUtils.isRunnableFile(entry1FileName) && FileUtils.isRunnableFile(entry2FileName)) { + return entry1FileName.compareTo(entry2FileName); + } else if (FileUtils.isRunnableFile(entry1FileName)) { + return -1; + } else if (FileUtils.isRunnableFile(entry2FileName)) { + return 1; + } + } else if (entry1Method == FILE_UNCOMPRESS_METHOD_FLAG) { + return -1; + } else if (entry2Method == FILE_UNCOMPRESS_METHOD_FLAG) { + return 1; + } + return entry1FileName.compareTo(entry2FileName); + }); + resetOffset(); + } + + private void resetOffset() { + long offset = 0L; + long cdLength = 0L; + for (ZipEntry entry : zipEntries) { + entry.getCentralDirectory().setOffset(offset); + offset += entry.getZipEntryData().getLength(); + cdLength += entry.getCentralDirectory().getLength(); + } + if (signingBlock != null) { + offset += signingBlock.length; + } + cDOffset = offset; + endOfCentralDirectory.setOffset(offset); + endOfCentralDirectory.setcDSize(cdLength); + offset += cdLength; + eOCDOffset = offset; + } + + public List getZipEntries() { + return zipEntries; + } + + public void setZipEntries(List zipEntries) { + this.zipEntries = zipEntries; + } + + public long getSigningOffset() { + return signingOffset; + } + + public void setSigningOffset(long signingOffset) { + this.signingOffset = signingOffset; + } + + public byte[] getSigningBlock() { + return signingBlock; + } + + public void setSigningBlock(byte[] signingBlock) { + this.signingBlock = signingBlock; + } + + public long getCDOffset() { + return cDOffset; + } + + public void setCDOffset(long cDOffset) { + this.cDOffset = cDOffset; + } + + public long getEOCDOffset() { + return eOCDOffset; + } + + public void setEOCDOffset(long eOCDOffset) { + this.eOCDOffset = eOCDOffset; + } + + public EndOfCentralDirectory getEndOfCentralDirectory() { + return endOfCentralDirectory; + } + + public void setEndOfCentralDirectory(EndOfCentralDirectory endOfCentralDirectory) { + this.endOfCentralDirectory = endOfCentralDirectory; + } + + public String getFile() { + return file; + } + + public void setFile(String file) { + this.file = file; + } +} \ No newline at end of file diff --git a/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/zip/ZipEntry.java b/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/zip/ZipEntry.java new file mode 100644 index 0000000000000000000000000000000000000000..c557f1a1630014b3f7103d1a72f9a71c41112b7f --- /dev/null +++ b/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/zip/ZipEntry.java @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2023-2023 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.ohos.hapsigntool.zip; + +import com.ohos.hapsigntool.error.ZipException; + +import java.util.Arrays; + +/** + * ZipEntry and CentralDirectory data + * + * @since 2023/12/02 + */ +public class ZipEntry { + private ZipEntryData zipEntryData; + + private CentralDirectory centralDirectory; + + /** + * alignment one entry + * + * @param alignNum need align bytes length + * @return add bytes length + * @throws ZipException alignment exception + */ + public int alignment(int alignNum) throws ZipException { + int remainder = (int) (zipEntryData.getZipEntryHeader().getLength() + centralDirectory.getOffset()) % alignNum; + if (remainder == 0) { + return 0; + } + int add = alignNum - remainder; + int newExtraLength = zipEntryData.getZipEntryHeader().getExtraLength() + add; + if (newExtraLength > UnsignedDecimalUtil.MAX_UNSIGNED_SHORT_VALUE) { + throw new ZipException("can not align " + zipEntryData.getZipEntryHeader().getFileName()); + } + zipEntryData.getZipEntryHeader().setExtraLength((short) newExtraLength); + byte[] oldExtraData = zipEntryData.getZipEntryHeader().getExtraData(); + byte[] newExtra; + if (oldExtraData == null) { + newExtra = new byte[newExtraLength]; + } else { + newExtra = Arrays.copyOf(oldExtraData, newExtraLength); + } + zipEntryData.getZipEntryHeader().setExtraData(newExtra); + int newLength = ZipEntryHeader.HEADER_LENGTH + zipEntryData.getZipEntryHeader().getFileNameLength() + + newExtraLength; + if (zipEntryData.getZipEntryHeader().getLength() + add != newLength) { + throw new ZipException("can not align " + zipEntryData.getZipEntryHeader().getFileName()); + } + zipEntryData.getZipEntryHeader().setLength(newLength); + zipEntryData.setLength(zipEntryData.getLength() + add); + + centralDirectory.setExtraData(newExtra); + centralDirectory.setLength(centralDirectory.getLength() - centralDirectory.getExtraLength() + newExtraLength); + centralDirectory.setExtraLength(newExtraLength); + return add; + } + + public ZipEntryData getZipEntryData() { + return zipEntryData; + } + + public void setZipEntryData(ZipEntryData zipEntryData) { + this.zipEntryData = zipEntryData; + } + + public CentralDirectory getCentralDirectory() { + return centralDirectory; + } + + public void setCentralDirectory(CentralDirectory centralDirectory) { + this.centralDirectory = centralDirectory; + } +} \ No newline at end of file diff --git a/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/zip/ZipEntryData.java b/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/zip/ZipEntryData.java new file mode 100644 index 0000000000000000000000000000000000000000..61a7bf211ff586a21a9ca8ee4998e07bfe8e95b3 --- /dev/null +++ b/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/zip/ZipEntryData.java @@ -0,0 +1,143 @@ +/* + * Copyright (c) 2023-2023 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.ohos.hapsigntool.zip; + +import com.ohos.hapsigntool.utils.FileUtils; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; + +/** + * resolve zip ZipEntry data + * + * @since 2023/12/04 + */ +public class ZipEntryData { + /** + * data descriptor has or not mask + */ + public static final short HAS_DATA_DESCRIPTOR_MASK = 0x08; + + /** + * data descriptor has or not flag mask + */ + public static final short NOT_HAS_DATA_DESCRIPTOR_FLAG = 0; + + private ZipEntryHeader zipEntryHeader; + + private long fileOffset; + + private long fileSize; + + private DataDescriptor dataDescriptor; + + private long length; + + public ZipEntryHeader getZipEntryHeader() { + return zipEntryHeader; + } + + /** + * init zip entry by file + * + * @param file zip file + * @param entryOffset entry start offset + * @param fileSize compress file size + * @return zip entry + * @throws IOException read zip exception + */ + public static ZipEntryData getZipEntry(File file, long entryOffset, long fileSize) + throws IOException { + try (FileInputStream input = new FileInputStream(file)) { + long offset = entryOffset; + // read entry header by file and offset. + byte[] headBytes = FileUtils.readInputByOffsetAndLength(input, entryOffset, ZipEntryHeader.HEADER_LENGTH); + ZipEntryHeader entryHeader = ZipEntryHeader.getZipEntryHeader(headBytes); + offset += ZipEntryHeader.HEADER_LENGTH; + + // read entry file name and extra by offset. + if (entryHeader.getFileNameLength() > 0) { + byte[] fileNameBytes = FileUtils.readInputByLength(input, entryHeader.getFileNameLength()); + entryHeader.readFileName(fileNameBytes); + offset += entryHeader.getFileNameLength(); + } + + if (entryHeader.getExtraLength() > 0) { + byte[] fileNameBytes = FileUtils.readInputByLength(input, entryHeader.getExtraLength()); + entryHeader.readExtra(fileNameBytes); + offset += entryHeader.getExtraLength(); + } + + // skip file data , save file offset and size. + ZipEntryData entry = new ZipEntryData(); + entry.setFileOffset(offset); + entry.setFileSize(fileSize); + input.skip(fileSize); + + long entryLength = entryHeader.getLength() + fileSize; + short flag = entryHeader.getFlag(); + // set desc null flag + boolean hasDesc = (flag & HAS_DATA_DESCRIPTOR_MASK) != NOT_HAS_DATA_DESCRIPTOR_FLAG; + if (hasDesc) { + // if entry has data descriptor, read entry data descriptor. + byte[] desBytes = FileUtils.readInputByLength(input, DataDescriptor.DES_LENGTH); + DataDescriptor dataDesc = DataDescriptor.getDataDescriptor(desBytes); + entryLength += DataDescriptor.DES_LENGTH; + entry.setDataDescriptor(dataDesc); + } + entry.setZipEntryHeader(entryHeader); + entry.setLength(entryLength); + return entry; + } + } + + public void setZipEntryHeader(ZipEntryHeader zipEntryHeader) { + this.zipEntryHeader = zipEntryHeader; + } + + public DataDescriptor getDataDescriptor() { + return dataDescriptor; + } + + public void setDataDescriptor(DataDescriptor dataDescriptor) { + this.dataDescriptor = dataDescriptor; + } + + public long getFileOffset() { + return fileOffset; + } + + public void setFileOffset(long fileOffset) { + this.fileOffset = fileOffset; + } + + public long getFileSize() { + return fileSize; + } + + public void setFileSize(long fileSize) { + this.fileSize = fileSize; + } + + public long getLength() { + return length; + } + + public void setLength(long length) { + this.length = length; + } +} \ No newline at end of file diff --git a/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/zip/ZipEntryHeader.java b/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/zip/ZipEntryHeader.java new file mode 100644 index 0000000000000000000000000000000000000000..73808bc29597fea5418cb2d50b826c8b0602cd6f --- /dev/null +++ b/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/zip/ZipEntryHeader.java @@ -0,0 +1,302 @@ +/* + * Copyright (c) 2023-2023 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.ohos.hapsigntool.zip; + +import com.ohos.hapsigntool.error.ZipException; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.charset.StandardCharsets; + +/** + * resolve zip ZipEntryHeader data + * Each file placed into a ZIP file MUST be preceded by a "local + * file header" record for that file. Each "local file header" MUST be + * accompanied by a corresponding "central directory header" record within + * the central directory section of the ZIP file. + * + * @since 2023/12/02 + */ +public class ZipEntryHeader { + /** + * ZipEntryHeader invariable bytes length + */ + public static final int HEADER_LENGTH = 30; + + /** + * 4 bytes , entry header signature + */ + public static final int SIGNATURE = 0x04034b50; + + /** + * 2 bytes + */ + private short version; + + /** + * 2 bytes + */ + private short flag; + + /** + * 2 bytes + */ + private short method; + + /** + * 2 bytes + */ + private short lastTime; + + /** + * 2 bytes + */ + private short lastDate; + + /** + * 4 bytes + */ + private int crc32; + + /** + * 4 bytes + */ + private long compressedSize; + + /** + * 4 bytes + */ + private long unCompressedSize; + + /** + * 2 bytes + */ + private int fileNameLength; + + /** + * 2 bytes + */ + private int extraLength; + + /** + * n bytes + */ + private String fileName; + + /** + * n bytes + */ + private byte[] extraData; + + private int length; + + /** + * get Zip Entry Header + * + * @param bytes ZipEntryHeader bytes + * @return ZipEntryHeader + * @throws ZipException read entry header exception + */ + public static ZipEntryHeader getZipEntryHeader(byte[] bytes) throws ZipException { + ZipEntryHeader entryHeader = new ZipEntryHeader(); + ByteBuffer bf = ByteBuffer.wrap(bytes); + bf.order(ByteOrder.LITTLE_ENDIAN); + if (bf.getInt() != ZipEntryHeader.SIGNATURE) { + throw new ZipException("find zip entry head failed"); + } + entryHeader.setVersion(bf.getShort()); + entryHeader.setFlag(bf.getShort()); + entryHeader.setMethod(bf.getShort()); + entryHeader.setLastTime(bf.getShort()); + entryHeader.setLastDate(bf.getShort()); + entryHeader.setCrc32(bf.getInt()); + entryHeader.setCompressedSize(UnsignedDecimalUtil.getUnsignedInt(bf)); + entryHeader.setUnCompressedSize(UnsignedDecimalUtil.getUnsignedInt(bf)); + entryHeader.setFileNameLength(UnsignedDecimalUtil.getUnsignedShort(bf)); + entryHeader.setExtraLength(UnsignedDecimalUtil.getUnsignedShort(bf)); + entryHeader.setLength(HEADER_LENGTH + entryHeader.getFileNameLength() + entryHeader.getExtraLength()); + return entryHeader; + } + + /** + * set entry header name + * + * @param bytes name bytes + */ + public void readFileName(byte[] bytes) { + ByteBuffer bf = ByteBuffer.wrap(bytes); + bf.order(ByteOrder.LITTLE_ENDIAN); + if (fileNameLength > 0) { + byte[] nameBytes = new byte[fileNameLength]; + bf.get(nameBytes); + this.fileName = new String(nameBytes, StandardCharsets.UTF_8); + } + } + + /** + * set entry header extra + * + * @param bytes extra bytes + */ + public void readExtra(byte[] bytes) { + ByteBuffer bf = ByteBuffer.wrap(bytes); + bf.order(ByteOrder.LITTLE_ENDIAN); + if (extraLength > 0) { + byte[] extra = new byte[extraLength]; + bf.get(extra); + this.extraData = extra; + } + } + + /** + * change Zip Entry Header to bytes + * + * @return bytes + */ + public byte[] toBytes() { + ByteBuffer bf = ByteBuffer.allocate(length).order(ByteOrder.LITTLE_ENDIAN); + bf.putInt(SIGNATURE); + bf.putShort(version); + bf.putShort(flag); + bf.putShort(method); + bf.putShort(lastTime); + bf.putShort(lastDate); + bf.putInt(crc32); + UnsignedDecimalUtil.setUnsignedInt(bf, compressedSize); + UnsignedDecimalUtil.setUnsignedInt(bf, unCompressedSize); + UnsignedDecimalUtil.setUnsignedShort(bf, fileNameLength); + UnsignedDecimalUtil.setUnsignedShort(bf, extraLength); + if (fileNameLength > 0) { + bf.put(fileName.getBytes(StandardCharsets.UTF_8)); + } + if (extraLength > 0) { + bf.put(extraData); + } + return bf.array(); + } + + public static int getHeaderLength() { + return HEADER_LENGTH; + } + + public static int getSIGNATURE() { + return SIGNATURE; + } + + public short getVersion() { + return version; + } + + public void setVersion(short version) { + this.version = version; + } + + public short getFlag() { + return flag; + } + + public void setFlag(short flag) { + this.flag = flag; + } + + public short getMethod() { + return method; + } + + public void setMethod(short method) { + this.method = method; + } + + public short getLastTime() { + return lastTime; + } + + public void setLastTime(short lastTime) { + this.lastTime = lastTime; + } + + public short getLastDate() { + return lastDate; + } + + public void setLastDate(short lastDate) { + this.lastDate = lastDate; + } + + public int getCrc32() { + return crc32; + } + + public void setCrc32(int crc32) { + this.crc32 = crc32; + } + + public long getCompressedSize() { + return compressedSize; + } + + public void setCompressedSize(long compressedSize) { + this.compressedSize = compressedSize; + } + + public long getUnCompressedSize() { + return unCompressedSize; + } + + public void setUnCompressedSize(long unCompressedSize) { + this.unCompressedSize = unCompressedSize; + } + + public int getFileNameLength() { + return fileNameLength; + } + + public void setFileNameLength(int fileNameLength) { + this.fileNameLength = fileNameLength; + } + + public int getExtraLength() { + return extraLength; + } + + public void setExtraLength(int extraLength) { + this.extraLength = extraLength; + } + + public String getFileName() { + return fileName; + } + + public void setFileName(String fileName) { + this.fileName = fileName; + } + + public byte[] getExtraData() { + return extraData; + } + + public void setExtraData(byte[] extraData) { + this.extraData = extraData; + } + + public int getLength() { + return length; + } + + public void setLength(int length) { + this.length = length; + } +} \ No newline at end of file