From d6148f97a5cef82fca960955f23f1d1917db3944 Mon Sep 17 00:00:00 2001 From: Chris Warrington Date: Wed, 7 Feb 2018 15:03:28 +0000 Subject: Update package to com.android.tools.build.apkzlib. From com.android.apkzlib. Test: existing. Change-Id: Ib3b21722d9052aded3f3071bfac38a07978ef933 --- .../com/android/apkzlib/sign/DigestAlgorithm.java | 84 - .../apkzlib/sign/ManifestGenerationExtension.java | 244 -- .../android/apkzlib/sign/SignatureAlgorithm.java | 103 - .../com/android/apkzlib/sign/SigningExtension.java | 392 --- .../com/android/apkzlib/sign/ZFileDataSource.java | 157 -- .../com/android/apkzlib/sign/package-info.java | 153 -- .../com/android/apkzlib/utils/ApkZLibPair.java | 44 - .../android/apkzlib/utils/CachedFileContents.java | 176 -- .../com/android/apkzlib/utils/CachedSupplier.java | 118 - .../android/apkzlib/utils/IOExceptionConsumer.java | 53 - .../android/apkzlib/utils/IOExceptionFunction.java | 53 - .../android/apkzlib/utils/IOExceptionRunnable.java | 51 - .../android/apkzlib/utils/IOExceptionWrapper.java | 42 - .../com/android/apkzlib/utils/package-info.java | 20 - .../java/com/android/apkzlib/zfile/ApkCreator.java | 71 - .../android/apkzlib/zfile/ApkCreatorFactory.java | 246 -- .../com/android/apkzlib/zfile/ApkZFileCreator.java | 192 -- .../apkzlib/zfile/ApkZFileCreatorFactory.java | 54 - .../android/apkzlib/zfile/ManifestAttributes.java | 43 - .../zfile/NativeLibrariesPackagingMode.java | 36 - .../java/com/android/apkzlib/zfile/ZFiles.java | 132 - .../com/android/apkzlib/zfile/package-info.java | 20 - .../com/android/apkzlib/zip/AlignmentRule.java | 39 - .../com/android/apkzlib/zip/AlignmentRules.java | 77 - .../com/android/apkzlib/zip/CentralDirectory.java | 489 ---- .../apkzlib/zip/CentralDirectoryHeader.java | 434 --- .../zip/CentralDirectoryHeaderCompressInfo.java | 119 - .../com/android/apkzlib/zip/CompressionMethod.java | 65 - .../com/android/apkzlib/zip/CompressionResult.java | 86 - .../java/com/android/apkzlib/zip/Compressor.java | 38 - .../android/apkzlib/zip/DataDescriptorType.java | 58 - .../java/com/android/apkzlib/zip/EncodeUtils.java | 139 - src/main/java/com/android/apkzlib/zip/Eocd.java | 271 -- .../java/com/android/apkzlib/zip/ExtraField.java | 406 --- .../java/com/android/apkzlib/zip/FileUseMap.java | 601 ----- .../com/android/apkzlib/zip/FileUseMapEntry.java | 162 -- src/main/java/com/android/apkzlib/zip/GPFlags.java | 179 -- .../android/apkzlib/zip/InflaterByteSource.java | 64 - .../apkzlib/zip/LazyDelegateByteSource.java | 157 -- .../apkzlib/zip/ProcessedAndRawByteSources.java | 86 - .../java/com/android/apkzlib/zip/StoredEntry.java | 818 ------ .../com/android/apkzlib/zip/StoredEntryType.java | 32 - .../java/com/android/apkzlib/zip/VerifyLog.java | 56 - .../java/com/android/apkzlib/zip/VerifyLogs.java | 77 - src/main/java/com/android/apkzlib/zip/ZFile.java | 2764 -------------------- .../com/android/apkzlib/zip/ZFileExtension.java | 146 -- .../java/com/android/apkzlib/zip/ZFileOptions.java | 214 -- .../java/com/android/apkzlib/zip/ZipField.java | 364 --- .../com/android/apkzlib/zip/ZipFieldInvariant.java | 39 - .../apkzlib/zip/ZipFieldInvariantMaxValue.java | 47 - .../apkzlib/zip/ZipFieldInvariantNonNegative.java | 33 - .../java/com/android/apkzlib/zip/ZipFileState.java | 37 - .../BestAndDefaultDeflateExecutorCompressor.java | 89 - .../zip/compress/DeflateExecutionCompressor.java | 81 - .../apkzlib/zip/compress/ExecutorCompressor.java | 72 - .../zip/compress/Zip64NotSupportedException.java | 27 - .../android/apkzlib/zip/compress/package-info.java | 20 - .../com/android/apkzlib/zip/utils/ByteTracker.java | 120 - .../apkzlib/zip/utils/CloseableByteSource.java | 64 - .../zip/utils/CloseableDelegateByteSource.java | 171 -- .../apkzlib/zip/utils/LittleEndianUtils.java | 129 - .../apkzlib/zip/utils/MsDosDateTimeUtils.java | 111 - .../apkzlib/zip/utils/RandomAccessFileUtils.java | 59 - .../tools/build/apkzlib/sign/DigestAlgorithm.java | 84 + .../apkzlib/sign/ManifestGenerationExtension.java | 244 ++ .../build/apkzlib/sign/SignatureAlgorithm.java | 103 + .../tools/build/apkzlib/sign/SigningExtension.java | 392 +++ .../tools/build/apkzlib/sign/ZFileDataSource.java | 157 ++ .../tools/build/apkzlib/sign/package-info.java | 153 ++ .../tools/build/apkzlib/utils/ApkZLibPair.java | 44 + .../build/apkzlib/utils/CachedFileContents.java | 176 ++ .../tools/build/apkzlib/utils/CachedSupplier.java | 118 + .../build/apkzlib/utils/IOExceptionConsumer.java | 53 + .../build/apkzlib/utils/IOExceptionFunction.java | 53 + .../build/apkzlib/utils/IOExceptionRunnable.java | 51 + .../build/apkzlib/utils/IOExceptionWrapper.java | 42 + .../tools/build/apkzlib/utils/package-info.java | 20 + .../tools/build/apkzlib/zfile/ApkCreator.java | 71 + .../build/apkzlib/zfile/ApkCreatorFactory.java | 246 ++ .../tools/build/apkzlib/zfile/ApkZFileCreator.java | 192 ++ .../apkzlib/zfile/ApkZFileCreatorFactory.java | 54 + .../build/apkzlib/zfile/ManifestAttributes.java | 43 + .../zfile/NativeLibrariesPackagingMode.java | 36 + .../android/tools/build/apkzlib/zfile/ZFiles.java | 132 + .../tools/build/apkzlib/zfile/package-info.java | 18 + .../tools/build/apkzlib/zip/AlignmentRule.java | 39 + .../tools/build/apkzlib/zip/AlignmentRules.java | 77 + .../tools/build/apkzlib/zip/CentralDirectory.java | 489 ++++ .../build/apkzlib/zip/CentralDirectoryHeader.java | 434 +++ .../zip/CentralDirectoryHeaderCompressInfo.java | 119 + .../tools/build/apkzlib/zip/CompressionMethod.java | 65 + .../tools/build/apkzlib/zip/CompressionResult.java | 86 + .../tools/build/apkzlib/zip/Compressor.java | 38 + .../build/apkzlib/zip/DataDescriptorType.java | 58 + .../tools/build/apkzlib/zip/EncodeUtils.java | 139 + .../com/android/tools/build/apkzlib/zip/Eocd.java | 271 ++ .../tools/build/apkzlib/zip/ExtraField.java | 406 +++ .../tools/build/apkzlib/zip/FileUseMap.java | 601 +++++ .../tools/build/apkzlib/zip/FileUseMapEntry.java | 162 ++ .../android/tools/build/apkzlib/zip/GPFlags.java | 179 ++ .../build/apkzlib/zip/InflaterByteSource.java | 64 + .../build/apkzlib/zip/LazyDelegateByteSource.java | 156 ++ .../apkzlib/zip/ProcessedAndRawByteSources.java | 86 + .../tools/build/apkzlib/zip/StoredEntry.java | 818 ++++++ .../tools/build/apkzlib/zip/StoredEntryType.java | 32 + .../android/tools/build/apkzlib/zip/VerifyLog.java | 56 + .../tools/build/apkzlib/zip/VerifyLogs.java | 77 + .../com/android/tools/build/apkzlib/zip/ZFile.java | 2764 ++++++++++++++++++++ .../tools/build/apkzlib/zip/ZFileExtension.java | 146 ++ .../tools/build/apkzlib/zip/ZFileOptions.java | 214 ++ .../android/tools/build/apkzlib/zip/ZipField.java | 364 +++ .../tools/build/apkzlib/zip/ZipFieldInvariant.java | 39 + .../apkzlib/zip/ZipFieldInvariantMaxValue.java | 47 + .../apkzlib/zip/ZipFieldInvariantNonNegative.java | 33 + .../tools/build/apkzlib/zip/ZipFileState.java | 37 + .../BestAndDefaultDeflateExecutorCompressor.java | 89 + .../zip/compress/DeflateExecutionCompressor.java | 81 + .../apkzlib/zip/compress/ExecutorCompressor.java | 72 + .../zip/compress/Zip64NotSupportedException.java | 27 + .../build/apkzlib/zip/compress/package-info.java | 20 + .../tools/build/apkzlib/zip/utils/ByteTracker.java | 120 + .../apkzlib/zip/utils/CloseableByteSource.java | 63 + .../zip/utils/CloseableDelegateByteSource.java | 171 ++ .../build/apkzlib/zip/utils/LittleEndianUtils.java | 129 + .../apkzlib/zip/utils/MsDosDateTimeUtils.java | 110 + .../apkzlib/zip/utils/RandomAccessFileUtils.java | 59 + .../com/android/apkzlib/sign/FullApkSignTest.java | 104 - .../com/android/apkzlib/sign/JarSigningTest.java | 378 --- .../apkzlib/sign/ManifestGenerationTest.java | 180 -- .../android/apkzlib/sign/SignatureTestUtils.java | 133 - .../android/apkzlib/utils/ApkZFileTestUtils.java | 139 - .../apkzlib/utils/CachedFileContentsTest.java | 123 - .../android/apkzlib/utils/CachedSupplierTest.java | 112 - .../android/apkzlib/zfile/ApkAlignmentTest.java | 231 -- .../com/android/apkzlib/zip/AlignmentTest.java | 856 ------ .../com/android/apkzlib/zip/EncodeUtilsTest.java | 72 - .../com/android/apkzlib/zip/ExtraFieldTest.java | 361 --- .../com/android/apkzlib/zip/FileUseMapTest.java | 134 - .../com/android/apkzlib/zip/OldApkReadTest.java | 38 - .../ReadWithDifferentCompressionLevelsTest.java | 47 - .../android/apkzlib/zip/ZFileNotificationTest.java | 420 --- .../com/android/apkzlib/zip/ZFileReadOnlyTest.java | 240 -- .../com/android/apkzlib/zip/ZFileSortTest.java | 218 -- .../java/com/android/apkzlib/zip/ZFileTest.java | 1821 ------------- .../android/apkzlib/zip/ZFileTestConstants.java | 38 - .../java/com/android/apkzlib/zip/ZipMergeTest.java | 205 -- .../java/com/android/apkzlib/zip/ZipTestUtils.java | 90 - .../java/com/android/apkzlib/zip/ZipToolsTest.java | 223 -- .../apkzlib/zip/compress/MultiCompressorTest.java | 155 -- .../apkzlib/zip/utils/LittleEndianUtilsTest.java | 112 - .../apkzlib/zip/utils/MsDosDateTimeUtilsTest.java | 71 - .../tools/build/apkzlib/sign/FullApkSignTest.java | 104 + .../tools/build/apkzlib/sign/JarSigningTest.java | 378 +++ .../build/apkzlib/sign/ManifestGenerationTest.java | 180 ++ .../build/apkzlib/sign/SignatureTestUtils.java | 133 + .../build/apkzlib/utils/ApkZFileTestUtils.java | 139 + .../apkzlib/utils/CachedFileContentsTest.java | 121 + .../build/apkzlib/utils/CachedSupplierTest.java | 111 + .../build/apkzlib/zfile/ApkAlignmentTest.java | 231 ++ .../tools/build/apkzlib/zip/AlignmentTest.java | 856 ++++++ .../tools/build/apkzlib/zip/EncodeUtilsTest.java | 72 + .../tools/build/apkzlib/zip/ExtraFieldTest.java | 359 +++ .../tools/build/apkzlib/zip/FileUseMapTest.java | 132 + .../tools/build/apkzlib/zip/OldApkReadTest.java | 38 + .../ReadWithDifferentCompressionLevelsTest.java | 47 + .../build/apkzlib/zip/ZFileNotificationTest.java | 420 +++ .../tools/build/apkzlib/zip/ZFileReadOnlyTest.java | 240 ++ .../tools/build/apkzlib/zip/ZFileSortTest.java | 218 ++ .../android/tools/build/apkzlib/zip/ZFileTest.java | 1821 +++++++++++++ .../build/apkzlib/zip/ZFileTestConstants.java | 38 + .../tools/build/apkzlib/zip/ZipMergeTest.java | 205 ++ .../tools/build/apkzlib/zip/ZipTestUtils.java | 90 + .../tools/build/apkzlib/zip/ZipToolsTest.java | 223 ++ .../apkzlib/zip/compress/MultiCompressorTest.java | 155 ++ .../apkzlib/zip/utils/LittleEndianUtilsTest.java | 111 + .../apkzlib/zip/utils/MsDosDateTimeUtilsTest.java | 70 + 176 files changed, 18011 insertions(+), 18025 deletions(-) delete mode 100644 src/main/java/com/android/apkzlib/sign/DigestAlgorithm.java delete mode 100644 src/main/java/com/android/apkzlib/sign/ManifestGenerationExtension.java delete mode 100644 src/main/java/com/android/apkzlib/sign/SignatureAlgorithm.java delete mode 100644 src/main/java/com/android/apkzlib/sign/SigningExtension.java delete mode 100644 src/main/java/com/android/apkzlib/sign/ZFileDataSource.java delete mode 100644 src/main/java/com/android/apkzlib/sign/package-info.java delete mode 100644 src/main/java/com/android/apkzlib/utils/ApkZLibPair.java delete mode 100644 src/main/java/com/android/apkzlib/utils/CachedFileContents.java delete mode 100644 src/main/java/com/android/apkzlib/utils/CachedSupplier.java delete mode 100644 src/main/java/com/android/apkzlib/utils/IOExceptionConsumer.java delete mode 100644 src/main/java/com/android/apkzlib/utils/IOExceptionFunction.java delete mode 100644 src/main/java/com/android/apkzlib/utils/IOExceptionRunnable.java delete mode 100644 src/main/java/com/android/apkzlib/utils/IOExceptionWrapper.java delete mode 100644 src/main/java/com/android/apkzlib/utils/package-info.java delete mode 100644 src/main/java/com/android/apkzlib/zfile/ApkCreator.java delete mode 100644 src/main/java/com/android/apkzlib/zfile/ApkCreatorFactory.java delete mode 100644 src/main/java/com/android/apkzlib/zfile/ApkZFileCreator.java delete mode 100644 src/main/java/com/android/apkzlib/zfile/ApkZFileCreatorFactory.java delete mode 100644 src/main/java/com/android/apkzlib/zfile/ManifestAttributes.java delete mode 100644 src/main/java/com/android/apkzlib/zfile/NativeLibrariesPackagingMode.java delete mode 100644 src/main/java/com/android/apkzlib/zfile/ZFiles.java delete mode 100644 src/main/java/com/android/apkzlib/zfile/package-info.java delete mode 100644 src/main/java/com/android/apkzlib/zip/AlignmentRule.java delete mode 100644 src/main/java/com/android/apkzlib/zip/AlignmentRules.java delete mode 100644 src/main/java/com/android/apkzlib/zip/CentralDirectory.java delete mode 100644 src/main/java/com/android/apkzlib/zip/CentralDirectoryHeader.java delete mode 100644 src/main/java/com/android/apkzlib/zip/CentralDirectoryHeaderCompressInfo.java delete mode 100644 src/main/java/com/android/apkzlib/zip/CompressionMethod.java delete mode 100644 src/main/java/com/android/apkzlib/zip/CompressionResult.java delete mode 100644 src/main/java/com/android/apkzlib/zip/Compressor.java delete mode 100644 src/main/java/com/android/apkzlib/zip/DataDescriptorType.java delete mode 100644 src/main/java/com/android/apkzlib/zip/EncodeUtils.java delete mode 100644 src/main/java/com/android/apkzlib/zip/Eocd.java delete mode 100644 src/main/java/com/android/apkzlib/zip/ExtraField.java delete mode 100644 src/main/java/com/android/apkzlib/zip/FileUseMap.java delete mode 100644 src/main/java/com/android/apkzlib/zip/FileUseMapEntry.java delete mode 100644 src/main/java/com/android/apkzlib/zip/GPFlags.java delete mode 100644 src/main/java/com/android/apkzlib/zip/InflaterByteSource.java delete mode 100644 src/main/java/com/android/apkzlib/zip/LazyDelegateByteSource.java delete mode 100644 src/main/java/com/android/apkzlib/zip/ProcessedAndRawByteSources.java delete mode 100644 src/main/java/com/android/apkzlib/zip/StoredEntry.java delete mode 100644 src/main/java/com/android/apkzlib/zip/StoredEntryType.java delete mode 100644 src/main/java/com/android/apkzlib/zip/VerifyLog.java delete mode 100644 src/main/java/com/android/apkzlib/zip/VerifyLogs.java delete mode 100644 src/main/java/com/android/apkzlib/zip/ZFile.java delete mode 100644 src/main/java/com/android/apkzlib/zip/ZFileExtension.java delete mode 100644 src/main/java/com/android/apkzlib/zip/ZFileOptions.java delete mode 100644 src/main/java/com/android/apkzlib/zip/ZipField.java delete mode 100644 src/main/java/com/android/apkzlib/zip/ZipFieldInvariant.java delete mode 100644 src/main/java/com/android/apkzlib/zip/ZipFieldInvariantMaxValue.java delete mode 100644 src/main/java/com/android/apkzlib/zip/ZipFieldInvariantNonNegative.java delete mode 100644 src/main/java/com/android/apkzlib/zip/ZipFileState.java delete mode 100644 src/main/java/com/android/apkzlib/zip/compress/BestAndDefaultDeflateExecutorCompressor.java delete mode 100644 src/main/java/com/android/apkzlib/zip/compress/DeflateExecutionCompressor.java delete mode 100644 src/main/java/com/android/apkzlib/zip/compress/ExecutorCompressor.java delete mode 100644 src/main/java/com/android/apkzlib/zip/compress/Zip64NotSupportedException.java delete mode 100644 src/main/java/com/android/apkzlib/zip/compress/package-info.java delete mode 100644 src/main/java/com/android/apkzlib/zip/utils/ByteTracker.java delete mode 100644 src/main/java/com/android/apkzlib/zip/utils/CloseableByteSource.java delete mode 100644 src/main/java/com/android/apkzlib/zip/utils/CloseableDelegateByteSource.java delete mode 100644 src/main/java/com/android/apkzlib/zip/utils/LittleEndianUtils.java delete mode 100644 src/main/java/com/android/apkzlib/zip/utils/MsDosDateTimeUtils.java delete mode 100644 src/main/java/com/android/apkzlib/zip/utils/RandomAccessFileUtils.java create mode 100644 src/main/java/com/android/tools/build/apkzlib/sign/DigestAlgorithm.java create mode 100644 src/main/java/com/android/tools/build/apkzlib/sign/ManifestGenerationExtension.java create mode 100644 src/main/java/com/android/tools/build/apkzlib/sign/SignatureAlgorithm.java create mode 100644 src/main/java/com/android/tools/build/apkzlib/sign/SigningExtension.java create mode 100644 src/main/java/com/android/tools/build/apkzlib/sign/ZFileDataSource.java create mode 100644 src/main/java/com/android/tools/build/apkzlib/sign/package-info.java create mode 100644 src/main/java/com/android/tools/build/apkzlib/utils/ApkZLibPair.java create mode 100644 src/main/java/com/android/tools/build/apkzlib/utils/CachedFileContents.java create mode 100644 src/main/java/com/android/tools/build/apkzlib/utils/CachedSupplier.java create mode 100644 src/main/java/com/android/tools/build/apkzlib/utils/IOExceptionConsumer.java create mode 100644 src/main/java/com/android/tools/build/apkzlib/utils/IOExceptionFunction.java create mode 100644 src/main/java/com/android/tools/build/apkzlib/utils/IOExceptionRunnable.java create mode 100644 src/main/java/com/android/tools/build/apkzlib/utils/IOExceptionWrapper.java create mode 100644 src/main/java/com/android/tools/build/apkzlib/utils/package-info.java create mode 100644 src/main/java/com/android/tools/build/apkzlib/zfile/ApkCreator.java create mode 100644 src/main/java/com/android/tools/build/apkzlib/zfile/ApkCreatorFactory.java create mode 100644 src/main/java/com/android/tools/build/apkzlib/zfile/ApkZFileCreator.java create mode 100644 src/main/java/com/android/tools/build/apkzlib/zfile/ApkZFileCreatorFactory.java create mode 100644 src/main/java/com/android/tools/build/apkzlib/zfile/ManifestAttributes.java create mode 100644 src/main/java/com/android/tools/build/apkzlib/zfile/NativeLibrariesPackagingMode.java create mode 100644 src/main/java/com/android/tools/build/apkzlib/zfile/ZFiles.java create mode 100644 src/main/java/com/android/tools/build/apkzlib/zfile/package-info.java create mode 100644 src/main/java/com/android/tools/build/apkzlib/zip/AlignmentRule.java create mode 100644 src/main/java/com/android/tools/build/apkzlib/zip/AlignmentRules.java create mode 100644 src/main/java/com/android/tools/build/apkzlib/zip/CentralDirectory.java create mode 100644 src/main/java/com/android/tools/build/apkzlib/zip/CentralDirectoryHeader.java create mode 100644 src/main/java/com/android/tools/build/apkzlib/zip/CentralDirectoryHeaderCompressInfo.java create mode 100644 src/main/java/com/android/tools/build/apkzlib/zip/CompressionMethod.java create mode 100644 src/main/java/com/android/tools/build/apkzlib/zip/CompressionResult.java create mode 100644 src/main/java/com/android/tools/build/apkzlib/zip/Compressor.java create mode 100644 src/main/java/com/android/tools/build/apkzlib/zip/DataDescriptorType.java create mode 100644 src/main/java/com/android/tools/build/apkzlib/zip/EncodeUtils.java create mode 100644 src/main/java/com/android/tools/build/apkzlib/zip/Eocd.java create mode 100644 src/main/java/com/android/tools/build/apkzlib/zip/ExtraField.java create mode 100644 src/main/java/com/android/tools/build/apkzlib/zip/FileUseMap.java create mode 100644 src/main/java/com/android/tools/build/apkzlib/zip/FileUseMapEntry.java create mode 100644 src/main/java/com/android/tools/build/apkzlib/zip/GPFlags.java create mode 100644 src/main/java/com/android/tools/build/apkzlib/zip/InflaterByteSource.java create mode 100644 src/main/java/com/android/tools/build/apkzlib/zip/LazyDelegateByteSource.java create mode 100644 src/main/java/com/android/tools/build/apkzlib/zip/ProcessedAndRawByteSources.java create mode 100644 src/main/java/com/android/tools/build/apkzlib/zip/StoredEntry.java create mode 100644 src/main/java/com/android/tools/build/apkzlib/zip/StoredEntryType.java create mode 100644 src/main/java/com/android/tools/build/apkzlib/zip/VerifyLog.java create mode 100644 src/main/java/com/android/tools/build/apkzlib/zip/VerifyLogs.java create mode 100644 src/main/java/com/android/tools/build/apkzlib/zip/ZFile.java create mode 100644 src/main/java/com/android/tools/build/apkzlib/zip/ZFileExtension.java create mode 100644 src/main/java/com/android/tools/build/apkzlib/zip/ZFileOptions.java create mode 100644 src/main/java/com/android/tools/build/apkzlib/zip/ZipField.java create mode 100644 src/main/java/com/android/tools/build/apkzlib/zip/ZipFieldInvariant.java create mode 100644 src/main/java/com/android/tools/build/apkzlib/zip/ZipFieldInvariantMaxValue.java create mode 100644 src/main/java/com/android/tools/build/apkzlib/zip/ZipFieldInvariantNonNegative.java create mode 100644 src/main/java/com/android/tools/build/apkzlib/zip/ZipFileState.java create mode 100644 src/main/java/com/android/tools/build/apkzlib/zip/compress/BestAndDefaultDeflateExecutorCompressor.java create mode 100644 src/main/java/com/android/tools/build/apkzlib/zip/compress/DeflateExecutionCompressor.java create mode 100644 src/main/java/com/android/tools/build/apkzlib/zip/compress/ExecutorCompressor.java create mode 100644 src/main/java/com/android/tools/build/apkzlib/zip/compress/Zip64NotSupportedException.java create mode 100644 src/main/java/com/android/tools/build/apkzlib/zip/compress/package-info.java create mode 100644 src/main/java/com/android/tools/build/apkzlib/zip/utils/ByteTracker.java create mode 100644 src/main/java/com/android/tools/build/apkzlib/zip/utils/CloseableByteSource.java create mode 100644 src/main/java/com/android/tools/build/apkzlib/zip/utils/CloseableDelegateByteSource.java create mode 100644 src/main/java/com/android/tools/build/apkzlib/zip/utils/LittleEndianUtils.java create mode 100644 src/main/java/com/android/tools/build/apkzlib/zip/utils/MsDosDateTimeUtils.java create mode 100644 src/main/java/com/android/tools/build/apkzlib/zip/utils/RandomAccessFileUtils.java delete mode 100644 src/test/java/com/android/apkzlib/sign/FullApkSignTest.java delete mode 100644 src/test/java/com/android/apkzlib/sign/JarSigningTest.java delete mode 100644 src/test/java/com/android/apkzlib/sign/ManifestGenerationTest.java delete mode 100644 src/test/java/com/android/apkzlib/sign/SignatureTestUtils.java delete mode 100644 src/test/java/com/android/apkzlib/utils/ApkZFileTestUtils.java delete mode 100644 src/test/java/com/android/apkzlib/utils/CachedFileContentsTest.java delete mode 100644 src/test/java/com/android/apkzlib/utils/CachedSupplierTest.java delete mode 100644 src/test/java/com/android/apkzlib/zfile/ApkAlignmentTest.java delete mode 100644 src/test/java/com/android/apkzlib/zip/AlignmentTest.java delete mode 100644 src/test/java/com/android/apkzlib/zip/EncodeUtilsTest.java delete mode 100644 src/test/java/com/android/apkzlib/zip/ExtraFieldTest.java delete mode 100644 src/test/java/com/android/apkzlib/zip/FileUseMapTest.java delete mode 100644 src/test/java/com/android/apkzlib/zip/OldApkReadTest.java delete mode 100644 src/test/java/com/android/apkzlib/zip/ReadWithDifferentCompressionLevelsTest.java delete mode 100644 src/test/java/com/android/apkzlib/zip/ZFileNotificationTest.java delete mode 100644 src/test/java/com/android/apkzlib/zip/ZFileReadOnlyTest.java delete mode 100644 src/test/java/com/android/apkzlib/zip/ZFileSortTest.java delete mode 100644 src/test/java/com/android/apkzlib/zip/ZFileTest.java delete mode 100644 src/test/java/com/android/apkzlib/zip/ZFileTestConstants.java delete mode 100644 src/test/java/com/android/apkzlib/zip/ZipMergeTest.java delete mode 100644 src/test/java/com/android/apkzlib/zip/ZipTestUtils.java delete mode 100644 src/test/java/com/android/apkzlib/zip/ZipToolsTest.java delete mode 100644 src/test/java/com/android/apkzlib/zip/compress/MultiCompressorTest.java delete mode 100644 src/test/java/com/android/apkzlib/zip/utils/LittleEndianUtilsTest.java delete mode 100644 src/test/java/com/android/apkzlib/zip/utils/MsDosDateTimeUtilsTest.java create mode 100644 src/test/java/com/android/tools/build/apkzlib/sign/FullApkSignTest.java create mode 100644 src/test/java/com/android/tools/build/apkzlib/sign/JarSigningTest.java create mode 100644 src/test/java/com/android/tools/build/apkzlib/sign/ManifestGenerationTest.java create mode 100644 src/test/java/com/android/tools/build/apkzlib/sign/SignatureTestUtils.java create mode 100644 src/test/java/com/android/tools/build/apkzlib/utils/ApkZFileTestUtils.java create mode 100644 src/test/java/com/android/tools/build/apkzlib/utils/CachedFileContentsTest.java create mode 100644 src/test/java/com/android/tools/build/apkzlib/utils/CachedSupplierTest.java create mode 100644 src/test/java/com/android/tools/build/apkzlib/zfile/ApkAlignmentTest.java create mode 100644 src/test/java/com/android/tools/build/apkzlib/zip/AlignmentTest.java create mode 100644 src/test/java/com/android/tools/build/apkzlib/zip/EncodeUtilsTest.java create mode 100644 src/test/java/com/android/tools/build/apkzlib/zip/ExtraFieldTest.java create mode 100644 src/test/java/com/android/tools/build/apkzlib/zip/FileUseMapTest.java create mode 100644 src/test/java/com/android/tools/build/apkzlib/zip/OldApkReadTest.java create mode 100644 src/test/java/com/android/tools/build/apkzlib/zip/ReadWithDifferentCompressionLevelsTest.java create mode 100644 src/test/java/com/android/tools/build/apkzlib/zip/ZFileNotificationTest.java create mode 100644 src/test/java/com/android/tools/build/apkzlib/zip/ZFileReadOnlyTest.java create mode 100644 src/test/java/com/android/tools/build/apkzlib/zip/ZFileSortTest.java create mode 100644 src/test/java/com/android/tools/build/apkzlib/zip/ZFileTest.java create mode 100644 src/test/java/com/android/tools/build/apkzlib/zip/ZFileTestConstants.java create mode 100644 src/test/java/com/android/tools/build/apkzlib/zip/ZipMergeTest.java create mode 100644 src/test/java/com/android/tools/build/apkzlib/zip/ZipTestUtils.java create mode 100644 src/test/java/com/android/tools/build/apkzlib/zip/ZipToolsTest.java create mode 100644 src/test/java/com/android/tools/build/apkzlib/zip/compress/MultiCompressorTest.java create mode 100644 src/test/java/com/android/tools/build/apkzlib/zip/utils/LittleEndianUtilsTest.java create mode 100644 src/test/java/com/android/tools/build/apkzlib/zip/utils/MsDosDateTimeUtilsTest.java (limited to 'src') diff --git a/src/main/java/com/android/apkzlib/sign/DigestAlgorithm.java b/src/main/java/com/android/apkzlib/sign/DigestAlgorithm.java deleted file mode 100644 index 64427ae..0000000 --- a/src/main/java/com/android/apkzlib/sign/DigestAlgorithm.java +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * 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.android.apkzlib.sign; - -import javax.annotation.Nonnull; - -/** - * Message digest algorithms. - */ -public enum DigestAlgorithm { - /** - * SHA-1 digest. - *

- * Android 2.3 (API Level 9) to 4.2 (API Level 17) (inclusive) do not support SHA-2 - * JAR signatures. - *

- * Moreover, platforms prior to API Level 18, without the additional - * Digest-Algorithms attribute, only support SHA or SHA1 algorithm names in .SF and - * MANIFEST.MF attributes. - */ - SHA1("SHA1", "SHA-1"), - - /** - * SHA-256 digest. - */ - SHA256("SHA-256", "SHA-256"); - - /** - * API level which supports {@link #SHA256} with {@link SignatureAlgorithm#RSA} and - * {@link SignatureAlgorithm#ECDSA}. - */ - public static final int API_SHA_256_RSA_AND_ECDSA = 18; - - /** - * API level which supports {@link #SHA256} for all {@link SignatureAlgorithm}s. - * - *

Before that, SHA256 can only be used with RSA and ECDSA. - */ - public static final int API_SHA_256_ALL_ALGORITHMS = 21; - - /** - * Name of algorithm for message digest. - */ - @Nonnull - public final String messageDigestName; - - /** - * Name of attribute in signature file with the manifest digest. - */ - @Nonnull - public final String manifestAttributeName; - - /** - * Name of attribute in entry (both manifest and signature file) with the entry's digest. - */ - @Nonnull - public final String entryAttributeName; - - /** - * Creates a digest algorithm. - * - * @param attributeName attribute name in the signature file - * @param messageDigestName name of algorithm for message digest - */ - DigestAlgorithm(@Nonnull String attributeName, @Nonnull String messageDigestName) { - this.messageDigestName = messageDigestName; - this.entryAttributeName = attributeName + "-Digest"; - this.manifestAttributeName = attributeName + "-Digest-Manifest"; - } -} diff --git a/src/main/java/com/android/apkzlib/sign/ManifestGenerationExtension.java b/src/main/java/com/android/apkzlib/sign/ManifestGenerationExtension.java deleted file mode 100644 index 33c47c6..0000000 --- a/src/main/java/com/android/apkzlib/sign/ManifestGenerationExtension.java +++ /dev/null @@ -1,244 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * 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.android.apkzlib.sign; - -import com.android.apkzlib.utils.CachedSupplier; -import com.android.apkzlib.utils.IOExceptionRunnable; -import com.android.apkzlib.zfile.ManifestAttributes; -import com.android.apkzlib.zip.StoredEntry; -import com.android.apkzlib.zip.ZFile; -import com.android.apkzlib.zip.ZFileExtension; -import com.google.common.base.Preconditions; -import com.google.common.base.Verify; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.UncheckedIOException; -import java.util.jar.Attributes; -import java.util.jar.Manifest; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -/** - * Extension to {@link ZFile} that will generate a manifest. The extension will register - * automatically with the {@link ZFile}. - * - *

Creating this extension will ensure a manifest for the zip exists. - * This extension will generate a manifest if one does not exist and will update an existing - * manifest, if one does exist. The extension will also provide access to the manifest so that - * others may update the manifest. - * - *

Apart from standard manifest elements, this extension does not handle any particular manifest - * features such as signing or adding custom attributes. It simply generates a plain manifest and - * provides infrastructure so that other extensions can add data in the manifest. - * - *

The manifest itself will only be written when the {@link ZFileExtension#beforeUpdate()} - * notification is received, meaning all manifest manipulation is done in-memory. - */ -public class ManifestGenerationExtension { - - /** - * Name of META-INF directory. - */ - private static final String META_INF_DIR = "META-INF"; - - /** - * Name of the manifest file. - */ - static final String MANIFEST_NAME = META_INF_DIR + "/MANIFEST.MF"; - - /** - * Who should be reported as the manifest builder. - */ - @Nonnull - private final String builtBy; - - /** - * Who should be reported as the manifest creator. - */ - @Nonnull - private final String createdBy; - - /** - * The file this extension is attached to. {@code null} if not yet registered. - */ - @Nullable - private ZFile zFile; - - /** - * The zip file's manifest. - */ - @Nonnull - private final Manifest manifest; - - /** - * Byte representation of the manifest. There is no guarantee that two writes of the java's - * {@code Manifest} object will yield the same byte array (there is no guaranteed order - * of entries in the manifest). - * - *

Because we need the byte representation of the manifest to be stable if there are - * no changes to the manifest, we cannot rely on {@code Manifest} to generate the byte - * representation every time we need the byte representation. - * - *

This cache will ensure that we will request one byte generation from the {@code Manifest} - * and will cache it. All further requests of the manifest's byte representation will - * receive the same byte array. - */ - @Nonnull - private CachedSupplier manifestBytes; - - /** - * Has the current manifest been changed and not yet flushed? If {@link #dirty} is - * {@code true}, then {@link #manifestBytes} should not be valid. This means that - * marking the manifest as dirty should also invalidate {@link #manifestBytes}. To avoid - * breaking the invariant, instead of setting {@link #dirty}, {@link #markDirty()} should - * be called. - */ - private boolean dirty; - - /** - * The extension to register with the {@link ZFile}. {@code null} if not registered. - */ - @Nullable - private ZFileExtension extension; - - /** - * Creates a new extension. This will not register the extension with the provided - * {@link ZFile}. Until {@link #register(ZFile)} is invoked, this extension is not used. - * - * @param builtBy who built the manifest? - * @param createdBy who created the manifest? - */ - public ManifestGenerationExtension(@Nonnull String builtBy, @Nonnull String createdBy) { - this.builtBy = builtBy; - this.createdBy = createdBy; - manifest = new Manifest(); - dirty = false; - manifestBytes = new CachedSupplier<>(() -> { - ByteArrayOutputStream outBytes = new ByteArrayOutputStream(); - try { - manifest.write(outBytes); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - - return outBytes.toByteArray(); - }); - } - - /** - * Marks the manifest as being dirty, i.e., its data has changed since it was last - * read and/or written. - */ - private void markDirty() { - dirty = true; - manifestBytes.reset(); - } - - /** - * Registers the extension with the {@link ZFile} provided in the constructor. - * - * @param zFile the zip file to add the extension to - * @throws IOException failed to analyze the zip - */ - public void register(@Nonnull ZFile zFile) throws IOException { - Preconditions.checkState(extension == null, "register() has already been invoked."); - this.zFile = zFile; - - rebuildManifest(); - - extension = new ZFileExtension() { - @Nullable - @Override - public IOExceptionRunnable beforeUpdate() { - return ManifestGenerationExtension.this::updateManifest; - } - }; - - this.zFile.addZFileExtension(extension); - } - - /** - * Rebuilds the zip file's manifest, if it needs changes. - */ - private void rebuildManifest() throws IOException { - Verify.verifyNotNull(zFile, "zFile == null"); - - StoredEntry manifestEntry = zFile.get(MANIFEST_NAME); - - if (manifestEntry != null) { - /* - * Read the manifest entry in the zip file. Make sure we store these byte sequence - * because writing the manifest may not generate the same byte sequence, which may - * trigger an unnecessary re-sign of the jar. - */ - manifest.clear(); - byte[] manifestBytes = manifestEntry.read(); - manifest.read(new ByteArrayInputStream(manifestBytes)); - this.manifestBytes.precomputed(manifestBytes); - } - - Attributes mainAttributes = manifest.getMainAttributes(); - String currentVersion = mainAttributes.getValue(ManifestAttributes.MANIFEST_VERSION); - if (currentVersion == null) { - setMainAttribute( - ManifestAttributes.MANIFEST_VERSION, - ManifestAttributes.CURRENT_MANIFEST_VERSION); - } else { - if (!currentVersion.equals(ManifestAttributes.CURRENT_MANIFEST_VERSION)) { - throw new IOException("Unsupported manifest version: " + currentVersion + "."); - } - } - - /* - * We "blindly" override all other main attributes. - */ - setMainAttribute(ManifestAttributes.BUILT_BY, builtBy); - setMainAttribute(ManifestAttributes.CREATED_BY, createdBy); - } - - /** - * Sets the value of a main attribute. - * - * @param attribute the attribute - * @param value the value - */ - private void setMainAttribute(@Nonnull String attribute, @Nonnull String value) { - Attributes mainAttributes = manifest.getMainAttributes(); - String current = mainAttributes.getValue(attribute); - if (!value.equals(current)) { - mainAttributes.putValue(attribute, value); - markDirty(); - } - } - - /** - * Updates the manifest in the zip file, if it has been changed. - * - * @throws IOException failed to update the manifest - */ - private void updateManifest() throws IOException { - Verify.verifyNotNull(zFile, "zFile == null"); - - if (!dirty) { - return; - } - - zFile.add(MANIFEST_NAME, new ByteArrayInputStream(manifestBytes.get())); - dirty = false; - } -} diff --git a/src/main/java/com/android/apkzlib/sign/SignatureAlgorithm.java b/src/main/java/com/android/apkzlib/sign/SignatureAlgorithm.java deleted file mode 100644 index 0667252..0000000 --- a/src/main/java/com/android/apkzlib/sign/SignatureAlgorithm.java +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * 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.android.apkzlib.sign; - -import java.security.NoSuchAlgorithmException; -import javax.annotation.Nonnull; - -/** - * Signature algorithm. - */ -public enum SignatureAlgorithm { - /** RSA algorithm. */ - RSA("RSA", 1, "withRSA"), - - /** ECDSA algorithm. */ - ECDSA("EC", 18, "withECDSA"), - - /** DSA algorithm. */ - DSA("DSA", 1, "withDSA"); - - /** Name of the private key as reported by {@code PrivateKey}. */ - @Nonnull public final String keyAlgorithm; - - /** - * Minimum SDK version that allows this signature. - */ - public final int minSdkVersion; - - /** - * Suffix appended to digest algorithm to obtain signature algorithm. - */ - @Nonnull - public final String signatureAlgorithmSuffix; - - /** - * Creates a new signature algorithm. - * - * @param keyAlgorithm the name as reported by {@code PrivateKey} - * @param minSdkVersion minimum SDK version that allows this signature - * @param signatureAlgorithmSuffix suffix for signature name with used with a digest - */ - SignatureAlgorithm( - @Nonnull String keyAlgorithm, int minSdkVersion, @Nonnull String signatureAlgorithmSuffix) { - this.keyAlgorithm = keyAlgorithm; - this.minSdkVersion = minSdkVersion; - this.signatureAlgorithmSuffix = signatureAlgorithmSuffix; - } - - /** - * Obtains the signature algorithm that corresponds to a private key name applicable to a - * SDK version. - * - * @param keyAlgorithm the named referred in the {@code PrivateKey} - * @param minSdkVersion minimum SDK version to run - * @return the algorithm that has {@link #keyAlgorithm} equal to {@code keyAlgorithm} - * @throws NoSuchAlgorithmException if no algorithm was found for the given private key; an - * algorithm was found but is not applicable to the given SDK version - */ - @Nonnull - public static SignatureAlgorithm fromKeyAlgorithm(@Nonnull String keyAlgorithm, - int minSdkVersion) throws NoSuchAlgorithmException { - for (SignatureAlgorithm alg : values()) { - if (alg.keyAlgorithm.equalsIgnoreCase(keyAlgorithm)) { - if (alg.minSdkVersion > minSdkVersion) { - throw new NoSuchAlgorithmException("Signatures with " + keyAlgorithm - + " keys are not supported on minSdkVersion " + minSdkVersion - + ". They are supported only for minSdkVersion >= " - + alg.minSdkVersion); - } - - return alg; - } - } - - throw new NoSuchAlgorithmException("Signing with " + keyAlgorithm - + " keys is not supported"); - } - - /** - * Obtains the name of the signature algorithm when used with a digest algorithm. - * - * @param digestAlgorithm the digest algorithm to use - * @return the name of the signature algorithm - */ - @Nonnull - public String signatureAlgorithmName(@Nonnull DigestAlgorithm digestAlgorithm) { - return digestAlgorithm.messageDigestName.replace("-", "") + signatureAlgorithmSuffix; - } -} diff --git a/src/main/java/com/android/apkzlib/sign/SigningExtension.java b/src/main/java/com/android/apkzlib/sign/SigningExtension.java deleted file mode 100644 index 2685aa1..0000000 --- a/src/main/java/com/android/apkzlib/sign/SigningExtension.java +++ /dev/null @@ -1,392 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * 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.android.apkzlib.sign; - -import com.android.apksig.ApkSignerEngine; -import com.android.apksig.ApkVerifier; -import com.android.apksig.DefaultApkSignerEngine; -import com.android.apksig.apk.ApkFormatException; -import com.android.apksig.util.DataSource; -import com.android.apksig.util.DataSources; -import com.android.apkzlib.utils.IOExceptionRunnable; -import com.android.apkzlib.zip.StoredEntry; -import com.android.apkzlib.zip.ZFile; -import com.android.apkzlib.zip.ZFileExtension; -import com.google.common.base.Preconditions; -import com.google.common.collect.ImmutableList; -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.nio.ByteBuffer; -import java.security.InvalidKeyException; -import java.security.NoSuchAlgorithmException; -import java.security.PrivateKey; -import java.security.SignatureException; -import java.security.cert.CertificateEncodingException; -import java.security.cert.X509Certificate; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -/** - * {@link ZFile} extension which signs the APK. - * - *

- * This extension is capable of signing the APK using JAR signing (aka v1 scheme) and APK Signature - * Scheme v2 (aka v2 scheme). Which schemes are actually used is specified by parameters to this - * extension's constructor. - */ -public class SigningExtension { - // IMPLEMENTATION NOTE: Most of the heavy lifting is performed by the ApkSignerEngine primitive - // from apksig library. This class is an adapter between ZFile extension and ApkSignerEngine. - // This class takes care of invoking the right methods on ApkSignerEngine in response to ZFile - // extension events/callbacks. - // - // The main issue leading to additional complexity in this class is that the current build - // pipeline does not reuse ApkSignerEngine instances (or ZFile extension instances for that - // matter) for incremental builds. Thus: - // * ZFile extension receives no events for JAR entries already in the APK whereas - // ApkSignerEngine needs to know about all JAR entries to be covered by signature. Thus, this - // class, during "beforeUpdate" ZFile event, notifies ApkSignerEngine about JAR entries - // already in the APK which ApkSignerEngine hasn't yet been told about -- these are the JAR - // entries which the incremental build session did not touch. - // * The build pipeline expects the APK not to change if no JAR entry was added to it or removed - // from it whereas ApkSignerEngine produces no output only if it has already produced a signed - // APK and no changes have since been made to it. This class addresses this issue by checking - // in its "register" method whether the APK is correctly signed and, only if that's the case, - // doesn't modify the APK unless a JAR entry is added to it or removed from it after - // "register". - - /** - * Minimum API Level on which this APK is supposed to run. - */ - private final int minSdkVersion; - - /** - * Whether JAR signing (aka v1 signing) is enabled. - */ - private final boolean v1SigningEnabled; - - /** - * Whether APK Signature Scheme v2 sining (aka v2 signing) is enabled. - */ - private final boolean v2SigningEnabled; - - /** - * Certificate of the signer, to be embedded into the APK's signature. - */ - @Nonnull - private final X509Certificate certificate; - - /** - * APK signer which performs most of the heavy lifting. - */ - @Nonnull - private final ApkSignerEngine signer; - - /** - * Names of APK entries which have been processed by {@link #signer}. - */ - private final Set signerProcessedOutputEntryNames = new HashSet<>(); - - /** - * Cached contents of the most recently output APK Signing Block or {@code null} if the block - * hasn't yet been output. - */ - @Nullable - private byte[] cachedApkSigningBlock; - - /** - * {@code true} if signatures may need to be output, {@code false} if there's no need to output - * signatures. This is used in an optimization where we don't modify the APK if it's already - * signed and if no JAR entries have been added to or removed from the file. - */ - private boolean dirty; - - /** - * The extension registered with the {@link ZFile}. {@code null} if not registered. - */ - @Nullable - private ZFileExtension extension; - - /** - * The file this extension is attached to. {@code null} if not yet registered. - */ - @Nullable - private ZFile zFile; - - public SigningExtension( - int minSdkVersion, - @Nonnull X509Certificate certificate, - @Nonnull PrivateKey privateKey, - boolean v1SigningEnabled, - boolean v2SigningEnabled) throws InvalidKeyException { - DefaultApkSignerEngine.SignerConfig signerConfig = - new DefaultApkSignerEngine.SignerConfig.Builder( - "CERT", privateKey, ImmutableList.of(certificate)).build(); - signer = - new DefaultApkSignerEngine.Builder(ImmutableList.of(signerConfig), minSdkVersion) - .setOtherSignersSignaturesPreserved(false) - .setV1SigningEnabled(v1SigningEnabled) - .setV2SigningEnabled(v2SigningEnabled) - .setCreatedBy("1.0 (Android)") - .build(); - this.minSdkVersion = minSdkVersion; - this.v1SigningEnabled = v1SigningEnabled; - this.v2SigningEnabled = v2SigningEnabled; - this.certificate = certificate; - } - - public void register(@Nonnull ZFile zFile) throws NoSuchAlgorithmException, IOException { - Preconditions.checkState(extension == null, "register() already invoked"); - this.zFile = zFile; - dirty = !isCurrentSignatureAsRequested(); - extension = new ZFileExtension() { - @Override - public IOExceptionRunnable added( - @Nonnull StoredEntry entry, @Nullable StoredEntry replaced) { - return () -> onZipEntryOutput(entry); - } - - @Override - public IOExceptionRunnable removed(@Nonnull StoredEntry entry) { - String entryName = entry.getCentralDirectoryHeader().getName(); - return () -> onZipEntryRemovedFromOutput(entryName); - } - - @Override - public IOExceptionRunnable beforeUpdate() throws IOException { - return () -> onOutputZipReadyForUpdate(); - } - - @Override - public void entriesWritten() throws IOException { - onOutputZipEntriesWritten(); - } - - @Override - public void closed() { - onOutputClosed(); - } - }; - this.zFile.addZFileExtension(extension); - } - - /** - * Returns {@code true} if the APK's signatures are as requested by parameters to this signing - * extension. - */ - private boolean isCurrentSignatureAsRequested() throws IOException, NoSuchAlgorithmException { - ApkVerifier.Result result; - try { - result = - new ApkVerifier.Builder(new ZFileDataSource(zFile)) - .setMinCheckedPlatformVersion(minSdkVersion) - .build() - .verify(); - } catch (ApkFormatException e) { - // Malformed APK - return false; - } - - if (!result.isVerified()) { - // Signature(s) did not verify - return false; - } - - if ((result.isVerifiedUsingV1Scheme() != v1SigningEnabled) - || (result.isVerifiedUsingV2Scheme() != v2SigningEnabled)) { - // APK isn't signed with exactly the schemes we want it to be signed - return false; - } - - List verifiedSignerCerts = result.getSignerCertificates(); - if (verifiedSignerCerts.size() != 1) { - // APK is not signed by exactly one signer - return false; - } - - byte[] expectedEncodedCert; - byte[] actualEncodedCert; - try { - expectedEncodedCert = certificate.getEncoded(); - actualEncodedCert = verifiedSignerCerts.get(0).getEncoded(); - } catch (CertificateEncodingException e) { - // Failed to encode signing certificates - return false; - } - - if (!Arrays.equals(expectedEncodedCert, actualEncodedCert)) { - // APK is signed by a wrong signer - return false; - } - - // APK is signed the way we want it to be signed - return true; - } - - private void onZipEntryOutput(@Nonnull StoredEntry entry) throws IOException { - setDirty(); - String entryName = entry.getCentralDirectoryHeader().getName(); - // This event may arrive after the entry has already been deleted. In that case, we don't - // report the addition of the entry to ApkSignerEngine. - if (entry.isDeleted()) { - return; - } - ApkSignerEngine.InspectJarEntryRequest inspectEntryRequest = - signer.outputJarEntry(entryName); - signerProcessedOutputEntryNames.add(entryName); - if (inspectEntryRequest != null) { - byte[] entryContents = entry.read(); - inspectEntryRequest.getDataSink().consume(entryContents, 0, entryContents.length); - inspectEntryRequest.done(); - } - } - - private void onZipEntryRemovedFromOutput(@Nonnull String entryName) { - setDirty(); - signer.outputJarEntryRemoved(entryName); - signerProcessedOutputEntryNames.remove(entryName); - } - - private void onOutputZipReadyForUpdate() throws IOException { - if (!dirty) { - return; - } - - // Notify signer engine about ZIP entries that have appeared in the output without the - // engine knowing. Also identify ZIP entries which disappeared from the output without the - // engine knowing. - Set unprocessedRemovedEntryNames = new HashSet<>(signerProcessedOutputEntryNames); - for (StoredEntry entry : zFile.entries()) { - String entryName = entry.getCentralDirectoryHeader().getName(); - unprocessedRemovedEntryNames.remove(entryName); - if (!signerProcessedOutputEntryNames.contains(entryName)) { - // Signer engine is not yet aware that this entry is in the output - onZipEntryOutput(entry); - } - } - - // Notify signer engine about entries which disappeared from the output without the engine - // knowing - for (String entryName : unprocessedRemovedEntryNames) { - onZipEntryRemovedFromOutput(entryName); - } - - // Check whether we need to output additional JAR entries which comprise the v1 signature - ApkSignerEngine.OutputJarSignatureRequest addV1SignatureRequest; - try { - addV1SignatureRequest = signer.outputJarEntries(); - } catch (Exception e) { - throw new IOException("Failed to generate v1 signature", e); - } - if (addV1SignatureRequest == null) { - return; - } - - // We need to output additional JAR entries which comprise the v1 signature - List v1SignatureEntries = - new ArrayList<>(addV1SignatureRequest.getAdditionalJarEntries()); - - // Reorder the JAR entries comprising the v1 signature so that MANIFEST.MF is the first - // entry. This ensures that it cleanly overwrites the existing MANIFEST.MF output by - // ManifestGenerationExtension. - for (int i = 0; i < v1SignatureEntries.size(); i++) { - ApkSignerEngine.OutputJarSignatureRequest.JarEntry entry = v1SignatureEntries.get(i); - String name = entry.getName(); - if (!ManifestGenerationExtension.MANIFEST_NAME.equals(name)) { - continue; - } - if (i != 0) { - v1SignatureEntries.remove(i); - v1SignatureEntries.add(0, entry); - } - break; - } - - // Output the JAR entries comprising the v1 signature - for (ApkSignerEngine.OutputJarSignatureRequest.JarEntry entry : v1SignatureEntries) { - String name = entry.getName(); - byte[] data = entry.getData(); - zFile.add(name, new ByteArrayInputStream(data)); - } - - addV1SignatureRequest.done(); - } - - private void onOutputZipEntriesWritten() throws IOException { - if (!dirty) { - return; - } - - // Check whether we should output an APK Signing Block which contains v2 signatures - byte[] apkSigningBlock; - byte[] centralDirBytes = zFile.getCentralDirectoryBytes(); - byte[] eocdBytes = zFile.getEocdBytes(); - ApkSignerEngine.OutputApkSigningBlockRequest addV2SignatureRequest; - // This event may arrive a second time -- after we write out the APK Signing Block. Thus, we - // cache the block to speed things up. The cached block is invalidated by any changes to the - // file (as reported to this extension). - if (cachedApkSigningBlock != null) { - apkSigningBlock = cachedApkSigningBlock; - addV2SignatureRequest = null; - } else { - DataSource centralDir = DataSources.asDataSource(ByteBuffer.wrap(centralDirBytes)); - DataSource eocd = DataSources.asDataSource(ByteBuffer.wrap(eocdBytes)); - long zipEntriesSizeBytes = - zFile.getCentralDirectoryOffset() - zFile.getExtraDirectoryOffset(); - DataSource zipEntries = new ZFileDataSource(zFile, 0, zipEntriesSizeBytes); - try { - addV2SignatureRequest = signer.outputZipSections(zipEntries, centralDir, eocd); - } catch (NoSuchAlgorithmException | InvalidKeyException | SignatureException - | ApkFormatException | IOException e) { - throw new IOException("Failed to generate v2 signature", e); - } - apkSigningBlock = - (addV2SignatureRequest != null) - ? addV2SignatureRequest.getApkSigningBlock() : new byte[0]; - cachedApkSigningBlock = apkSigningBlock; - } - - // Insert the APK Signing Block into the output right before the ZIP Central Directory and - // accordingly update the start offset of ZIP Central Directory in ZIP End of Central - // Directory. - zFile.directWrite( - zFile.getCentralDirectoryOffset() - zFile.getExtraDirectoryOffset(), - apkSigningBlock); - zFile.setExtraDirectoryOffset(apkSigningBlock.length); - - if (addV2SignatureRequest != null) { - addV2SignatureRequest.done(); - } - } - - private void onOutputClosed() { - if (!dirty) { - return; - } - signer.outputDone(); - dirty = false; - } - - private void setDirty() { - dirty = true; - cachedApkSigningBlock = null; - } -} \ No newline at end of file diff --git a/src/main/java/com/android/apkzlib/sign/ZFileDataSource.java b/src/main/java/com/android/apkzlib/sign/ZFileDataSource.java deleted file mode 100644 index 049cf35..0000000 --- a/src/main/java/com/android/apkzlib/sign/ZFileDataSource.java +++ /dev/null @@ -1,157 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * 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.android.apkzlib.sign; - -import com.android.apksig.util.DataSink; -import com.android.apksig.util.DataSource; -import com.android.apkzlib.zip.ZFile; -import com.google.common.base.Preconditions; -import java.io.EOFException; -import java.io.IOException; -import java.nio.ByteBuffer; -import javax.annotation.Nonnull; - -/** - * {@link DataSource} backed by contents of {@link ZFile}. - */ -class ZFileDataSource implements DataSource { - - private static final int MAX_READ_CHUNK_SIZE = 65536; - - @Nonnull - private final ZFile file; - - /** - * Offset (in bytes) relative to the start of file where the region visible in this data source - * starts. - */ - private final long offset; - - /** - * Size (in bytes) of the file region visible in this data source or {@code -1} if the whole - * file is visible in this data source and thus its size may change if the file's size changes. - */ - private final long size; - - /** - * Constructs a new {@code ZFileDataSource} based on the data contained in the file. Changes to - * the contents of the file, including the size of the file, will be visible in this data - * source. - */ - public ZFileDataSource(@Nonnull ZFile file) { - this.file = file; - offset = 0; - size = -1; - } - - /** - * Constructs a new {@code ZFileDataSource} based on the data contained in the specified region - * of the provided file. Changes to the contents of this region of the file will be visible in - * this data source. - */ - public ZFileDataSource(@Nonnull ZFile file, long offset, long size) { - Preconditions.checkArgument(offset >= 0, "offset < 0"); - Preconditions.checkArgument(size >= 0, "size < 0"); - this.file = file; - this.offset = offset; - this.size = size; - } - - @Override - public long size() { - if (size == -1) { - // Data source size is the current size of the file - try { - return file.directSize(); - } catch (IOException e) { - return 0; - } - } else { - // Data source size is fixed - return size; - } - } - - @Override - public DataSource slice(long offset, long size) { - long sourceSize = size(); - checkChunkValid(offset, size, sourceSize); - if ((offset == 0) && (size == sourceSize)) { - return this; - } - - return new ZFileDataSource(file, this.offset + offset, size); - } - - @Override - public void feed(long offset, long size, @Nonnull DataSink sink) throws IOException { - long sourceSize = size(); - checkChunkValid(offset, size, sourceSize); - if (size == 0) { - return; - } - - long chunkOffsetInFile = this.offset + offset; - long remaining = size; - byte[] buf = new byte[(int) Math.min(remaining, MAX_READ_CHUNK_SIZE)]; - while (remaining > 0) { - int chunkSize = (int) Math.min(remaining, buf.length); - int readSize = file.directRead(chunkOffsetInFile, buf, 0, chunkSize); - if (readSize == -1) { - throw new EOFException("Premature EOF"); - } - if (readSize > 0) { - sink.consume(buf, 0, readSize); - chunkOffsetInFile += readSize; - remaining -= readSize; - } - } - } - - @Override - public void copyTo(long offset, int size, @Nonnull ByteBuffer dest) throws IOException { - long sourceSize = size(); - checkChunkValid(offset, size, sourceSize); - if (size == 0) { - return; - } - - int prevLimit = dest.limit(); - try { - file.directFullyRead(this.offset + offset, dest); - } finally { - dest.limit(prevLimit); - } - } - - @Override - public ByteBuffer getByteBuffer(long offset, int size) throws IOException { - ByteBuffer result = ByteBuffer.allocate(size); - copyTo(offset, size, result); - result.flip(); - return result; - } - - private static void checkChunkValid(long offset, long size, long sourceSize) { - Preconditions.checkArgument(offset >= 0, "offset < 0"); - Preconditions.checkArgument(size >= 0, "size < 0"); - Preconditions.checkArgument(offset <= sourceSize, "offset > sourceSize"); - long endOffset = offset + size; - Preconditions.checkArgument(offset <= endOffset, "offset > endOffset"); - Preconditions.checkArgument(endOffset <= sourceSize, "endOffset > sourceSize"); - } -} diff --git a/src/main/java/com/android/apkzlib/sign/package-info.java b/src/main/java/com/android/apkzlib/sign/package-info.java deleted file mode 100644 index 6bb692c..0000000 --- a/src/main/java/com/android/apkzlib/sign/package-info.java +++ /dev/null @@ -1,153 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * 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. - */ - -/** - -The {@code sign} package provides extensions for the {@code zip} package that allow: -

-

-Because the {@code zip} package is completely independent of the {@code sign} package, the -actual coordination between the two is complex. The {@code sign} package works by registering -extensions with the {@code zip} package. These extensions are notified in changes made in the zip -and will change the zip file itself. -

-The {@link com.android.apkzlib.sign.ManifestGenerationExtension} extension will -ensure the zip has a manifest file and is, therefore, a valid jar. -The {@link com.android.apkzlib.sign.SigningExtension} extension will -ensure the jar is signed. -

-The extension mechanism used is the one provided in the {@code zip} package (see -{@link com.android.apkzlib.zip.ZFile} -and {@link com.android.apkzlib.zip.ZFileExtension}. Building the zip and then -operating the extensions is not done sequentially, as we don't want to build a zip and then sign it. -We want to build a zip that is automatically signed. Extension are basically observers that -register on the zip and are notified when things happen in the zip. They will then modify the zip -accordingly. -

-The zip file notifies extensions in 4 critical moments: when a file is added or removed from the -zip, when the zip is about to be flushed to disk and when the zip's entries have been flushed but -the central directory not. At these moments, the extensions can act to update the zip in any way -they need. -

-To see how this works, consider the manifest generation extension: when the extension is created, -it checks the zip file to see if there is a manifest. If a manifest exists and does not need -updating, it does not change anything, otherwise it generates a new manifest for the zip file. At -this point, the extension could write the manifest to the zip, but we opted not to. It would be -irrelevant anyway as the zip will only be written when flushed. -

-Now, when the {@code ZFile} notifies the extension that it is about to start writing the zip file, -the manifest extension, if it has noted that the manifest needs to be rewritten, will -- before the -{@code ZFile} actually writes anything -- modify the zip and add or replace the existing manifest -file. So, process-wise, the zip is written only once with the correct manifest. The flow is as -follows (if only the manifest generation extension was added to the {@code ZFile}): -

    -
  1. {@code ZFile.update()} is called.
  2. -
  3. {@code ZFile} calls {@code beforeUpdate()} for all {@code ZFileExtensions} registered, in - this case, only the instance of the anonymous inner class generated in the - {@code ManifestGenerationExtension} constructor is invoked.
  4. -
  5. {@code ManifestGenerationExtension.updateManifest()} is called.
  6. -
  7. If the manifest does not need to be updated, {@code updateManifest()} returns - immediately.
  8. -
  9. If the manifest needs updating, {@code ZFile.add()} is invoked to add or replace the - manifest.
  10. -
  11. {@code ManifestGenerationExtension.updateManifest()} returns.
  12. -
  13. {@code ZFile.update()} continues and writes the zip file, containing the manifest.
  14. -
  15. The zip is finally written with an updated manifest.
  16. -
-

-To generate a signed apk, we need to add a second extension, the {@code SigningExtension}. -This extension will also register listeners with the {@code ZFile}. -

-In this case the flow would be (starting a bit earlier for clarity and assuming a package task -in the build process): -

    -
  1. Package task creates a {@code ZFile} on the target apk (or non-existing file, if there is - no target apk in the output directory).
  2. -
  3. Package task configures the {@code ZFile} with alignment rules.
  4. -
  5. Package task creates a {@code ManifestGenerationExtension}.
  6. -
  7. Package task registers the {@code ManifestGenerationExtension} with the {@code ZFile}.
  8. -
  9. The {@code ManifestGenerationExtension} looks at the {@code ZFile} to see if there is valid - manifest. No changes are done to the {@code ZFile}.
  10. -
  11. Package task creates a {@code SigningExtension}.
  12. -
  13. Package task registers the {@code SigningExtension} with the {@code ZFile}.
  14. -
  15. The {@code SigningExtension} registers a {@code ZFileExtension} with the {@code ZFile} - and look at the {@code ZFile} to see if there is a valid signature file.
  16. -
  17. If there are changes to the digital signature file needed, these are marked internally in - the extension. If there are changes needed to the digests, the manifest is updated (by calling - {@code ManifestGenerationExtension}.
    - (note that this point, the apk file, if any existed, has not been touched, the manifest is - only updated in memory and the digests of all files in the apk, if any, have been computed and - stored in memory only; the digital signature of the {@code SF} file has not been computed.) -
  18. -
  19. The Package task now adds all files to the {@code ZFile}.
  20. -
  21. For each file that is added (*), {@code ZFile} calls the added {@code ZFileExtension.added} - method of all registered extensions.
  22. -
  23. The {@code ManifestGenerationExtension} ignores added invocations.
  24. -
  25. The {@code SigningExtension} computes the digest for the added file and stores them in - the manifest.
    - (when all files are added to the apk, all digests are computed and the manifest is updated - but only in memory; the apk file has not been touched; also note that {@code ZFile} has not - actually written anything to disk at this point, all files added are kept in memory).
  26. -
  27. Package task calls {@code ZFile.update()} to update the apk.
  28. -
  29. {@code ZFile} calls {@code before()} for all {@code ZFileExtensions} registered. This is - done before anything is written. In this case both the {@code ManifestGenerationExtension} and - {@code SigningExtension} are invoked.
  30. -
  31. The {@code ManifestGenerationExtension} will update the {@code ZFile} with the new manifest, - unless nothing has changed, in which case it does nothing.
  32. -
  33. The {@code SigningExtension} will add the SF file (unless nothing has changed), will - compute the digital signature of the SF file and write it to the {@code ZFile}.
    - (note that the order by which the {@code ManifestGenerationExtension} and - {@code SigningExtension} are called is non-deterministic; however, this is not a problem - because the manifest is already computed by the {@code ManifestGenerationExtension} at this - time and the {@code SigningExtension} will obtain the manifest data from the - {@code ManifestGenerationExtension} and not from the {@code ZFile}; this means that the - {@code SF} file may be added to the {@code ZFile} before the {@code MF} file, but that is - irrelevant.)
  34. -
  35. Once both extensions have finished doing the {@code beforeUpdate()} method, the - {@code ZFile.update()} method continues.
  36. -
  37. {@code ZFile.update()} writes all changes and new entries to the zip file.
  38. -
  39. {@code ZFile.update()} calls {@code ZFileExtension.entriesWritten()} for all - registered extensions. {@code SigningExtension} will kick in at this point, if v2 signature - has changed.
  40. -
  41. {@code ZFile} writes the central directory and EOCD.
  42. -
  43. {@code ZFile.update()} returns control to the package task.
  44. -
  45. The package task finishes.
  46. -
-(*) There is a number of optimizations if we're adding files from another {@code ZFile}, which -is the case when we add the output of aapt to the apk. In particular, files from the aapt are -ignored if they are already in the apk (same name, same CRC32) and also files copied from -the aapt's output are not recompressed (the binary compressed data is directly copied to the -zip). -

-If there are no changes to the {@code ZFile} made by the package task and the file's manifest and v1 -signatures are correct, neither the {@code ManifestGenerationExtension} nor the -{@code SigningExtension} will not do anything on the {@code beforeUpdate()} and the -{@code ZFile} won't even be open for writing. -

-This implementation provides perfect incremental updates. -

-Additionally, by adding/removing extensions we can configure what type of apk we want: -

-So, by configuring which extensions to add, the package task can decide what type of apk we want. -*/ -package com.android.apkzlib.sign; \ No newline at end of file diff --git a/src/main/java/com/android/apkzlib/utils/ApkZLibPair.java b/src/main/java/com/android/apkzlib/utils/ApkZLibPair.java deleted file mode 100644 index 8cf79ee..0000000 --- a/src/main/java/com/android/apkzlib/utils/ApkZLibPair.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * 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.android.apkzlib.utils; - -/** - * Pair implementation to use with the {@code apkzlib} library. - */ -public class ApkZLibPair { - - /** - * First value. - */ - public T1 v1; - - /** - * Second value. - */ - public T2 v2; - - /** - * Creates a new pair. - * - * @param v1 the first value - * @param v2 the second value - */ - public ApkZLibPair(T1 v1, T2 v2) { - this.v1 = v1; - this.v2 = v2; - } -} diff --git a/src/main/java/com/android/apkzlib/utils/CachedFileContents.java b/src/main/java/com/android/apkzlib/utils/CachedFileContents.java deleted file mode 100644 index f2a3331..0000000 --- a/src/main/java/com/android/apkzlib/utils/CachedFileContents.java +++ /dev/null @@ -1,176 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * 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.android.apkzlib.utils; - -import com.google.common.base.Objects; -import com.google.common.hash.HashCode; -import com.google.common.hash.Hashing; -import com.google.common.io.Files; -import java.io.File; -import java.io.IOException; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -/** - * A cache for file contents. The cache allows closing a file and saving in memory its contents (or - * some related information). It can then be used to check if the contents are still valid at some - * later time. Typical usage flow is: - * - *

- * - *

{@code
- * Object fileRepresentation = // ...
- * File toWrite = // ...
- * // Write file contents and update in memory representation
- * CachedFileContents contents = new CachedFileContents(toWrite);
- * contents.closed(fileRepresentation);
- *
- * // Later, when data is needed:
- * if (contents.isValid()) {
- *     fileRepresentation = contents.getCache();
- * } else {
- *     // Re-read the file and recreate the file representation
- * }
- * }
- *
- * @param  the type of cached contents
- */
-public class CachedFileContents {
-
-    /**
-     * The file.
-     */
-    @Nonnull
-    private File file;
-
-    /**
-     * Time when last closed (time when {@link #closed(Object)} was invoked).
-     */
-    private long lastClosed;
-
-    /**
-     * Size of the file when last closed.
-     */
-    private long size;
-
-    /**
-     * Hash of the file when closed. {@code null} if hashing failed for some reason.
-     */
-    @Nullable
-    private HashCode hash;
-
-    /**
-     * Cached data associated with the file.
-     */
-    @Nullable
-    private T cache;
-
-    /**
-     * Creates a new contents. When the file is written, {@link #closed(Object)} should be invoked
-     * to set the cache.
-     *
-     * @param file the file
-     */
-    public CachedFileContents(@Nonnull File file) {
-        this.file = file;
-    }
-
-    /**
-     * Should be called when the file's contents are set and the file closed. This will save the
-     * cache and register the file's timestamp to later detect if it has been modified.
-     * 

- * This method can be called as many times as the file has been written. - * - * @param cache an optional cache to save - */ - public void closed(@Nullable T cache) { - this.cache = cache; - lastClosed = file.lastModified(); - size = file.length(); - hash = hashFile(); - } - - /** - * Are the cached contents still valid? If this method determines that the file has been - * modified since the last time {@link #closed(Object)} was invoked. - * - * @return are the cached contents still valid? If this method returns {@code false}, the - * cache is cleared - */ - public boolean isValid() { - boolean valid = true; - - if (!file.exists()) { - valid = false; - } - - if (valid && file.lastModified() != lastClosed) { - valid = false; - } - - if (valid && file.length() != size) { - valid = false; - } - - if (valid && !Objects.equal(hash, hashFile())) { - valid = false; - } - - if (!valid) { - cache = null; - } - - return valid; - } - - /** - * Obtains the cached data set with {@link #closed(Object)} if the file has not been modified - * since {@link #closed(Object)} was invoked. - * - * @return the last cached data or {@code null} if the file has been modified since - * {@link #closed(Object)} has been invoked - */ - @Nullable - public T getCache() { - return cache; - } - - /** - * Computes the hashcode of the cached file. - * - * @return the hash code - */ - @Nullable - private HashCode hashFile() { - try { - return Files.hash(file, Hashing.crc32()); - } catch (IOException e) { - return null; - } - } - - /** - * Obtains the file used for caching. - * - * @return the file; this file always exists and contains the old (cached) contents of the - * file - */ - @Nonnull - public File getFile() { - return file; - } -} diff --git a/src/main/java/com/android/apkzlib/utils/CachedSupplier.java b/src/main/java/com/android/apkzlib/utils/CachedSupplier.java deleted file mode 100644 index bc5eb69..0000000 --- a/src/main/java/com/android/apkzlib/utils/CachedSupplier.java +++ /dev/null @@ -1,118 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * 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.android.apkzlib.utils; - -import java.util.function.Supplier; -import javax.annotation.Nonnull; - -/** - * Supplier that will cache a computed value and always supply the same value. It can be used to - * lazily compute data. For example: - * - *

{@code
- * CachedSupplier value = new CachedSupplier<>(() -> {
- *     Integer result;
- *     // Do some expensive computation.
- *     return result;
- * });
- *
- * if (a) {
- *     // We need the result of the expensive computation.
- *     Integer r = value.get();
- * }
- *
- * if (b) {
- *     // We also need the result of the expensive computation.
- *     Integer r = value.get();
- * }
- *
- * // If neither a nor b are true, we avoid doing the computation at all.
- * }
- */ -public class CachedSupplier { - - /** - * The cached data, {@code null} if computation resulted in {@code null}. It is also - * {@code null} if the cached data has not yet been computed. - */ - private T cached; - - /** - * Is the current data in {@link #cached} valid? - */ - private boolean valid; - - /** - * Actual supplier of data, if computation is needed. - */ - @Nonnull - private final Supplier supplier; - - /** - * Creates a new supplier. - */ - public CachedSupplier(@Nonnull Supplier supplier) { - valid = false; - this.supplier = supplier; - } - - - /** - * Obtains the value. - * - * @return the value, either cached (if one exists) or computed - */ - public synchronized T get() { - if (!valid) { - cached = supplier.get(); - valid = true; - } - - return cached; - } - - /** - * Resets the cache forcing a {@code get()} on the supplier next time {@link #get()} is invoked. - */ - public synchronized void reset() { - cached = null; - valid = false; - } - - /** - * In some cases, we may be able to precompute the cache value (or load it from somewhere we - * had previously stored it). This method allows the cache value to be loaded. - * - *

If this method is invoked, then an invocation of {@link #get()} will not trigger an - * invocation of the supplier provided in the constructor. - * - * @param t the new cache contents; will replace any currently cache content, if one exists - */ - public synchronized void precomputed(T t) { - cached = t; - valid = true; - } - - /** - * Checks if the contents of the cache are valid. - * - * @return are there valid contents in the cache? - */ - public synchronized boolean isValid() { - return valid; - } -} diff --git a/src/main/java/com/android/apkzlib/utils/IOExceptionConsumer.java b/src/main/java/com/android/apkzlib/utils/IOExceptionConsumer.java deleted file mode 100644 index fcf3ab0..0000000 --- a/src/main/java/com/android/apkzlib/utils/IOExceptionConsumer.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * 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.android.apkzlib.utils; - -import java.io.IOException; -import java.io.UncheckedIOException; -import java.util.function.Consumer; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -/** - * Consumer that can throw an {@link IOException}. - */ -@FunctionalInterface -public interface IOExceptionConsumer { - - /** - * Performs an operation on the given input. - * - * @param input the input - */ - void accept(@Nullable T input) throws IOException; - - /** - * Wraps a consumer that may throw an IO Exception throwing an {@code UncheckedIOException}. - * - * @param c the consumer - */ - @Nonnull - static Consumer asConsumer(@Nonnull IOExceptionConsumer c) { - return i -> { - try { - c.accept(i); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - }; - } -} diff --git a/src/main/java/com/android/apkzlib/utils/IOExceptionFunction.java b/src/main/java/com/android/apkzlib/utils/IOExceptionFunction.java deleted file mode 100644 index 6d84b5b..0000000 --- a/src/main/java/com/android/apkzlib/utils/IOExceptionFunction.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * 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.android.apkzlib.utils; - -import java.io.IOException; -import java.io.UncheckedIOException; -import java.util.function.Function; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -/** - * Function that can throw an I/O Exception - */ -@FunctionalInterface -public interface IOExceptionFunction { - - /** - * Applies the function to the given input. - * @param input the input - * @return the function result - */ - @Nullable T apply(@Nullable F input) throws IOException; - - /** - * Wraps a function that may throw an IO Exception throwing an {@code UncheckedIOException}. - * - * @param f the function - */ - @Nonnull - static Function asFunction(@Nonnull IOExceptionFunction f) { - return i -> { - try { - return f.apply(i); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - }; - } -} diff --git a/src/main/java/com/android/apkzlib/utils/IOExceptionRunnable.java b/src/main/java/com/android/apkzlib/utils/IOExceptionRunnable.java deleted file mode 100644 index 67ed75c..0000000 --- a/src/main/java/com/android/apkzlib/utils/IOExceptionRunnable.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * 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.android.apkzlib.utils; - -import java.io.IOException; -import java.io.UncheckedIOException; -import javax.annotation.Nonnull; - -/** - * Runnable that can throw I/O exceptions. - */ -@FunctionalInterface -public interface IOExceptionRunnable { - - /** - * Runs the runnable. - * - * @throws IOException failed to run - */ - void run() throws IOException; - - /** - * Wraps a runnable that may throw an IO Exception throwing an {@code UncheckedIOException}. - * - * @param r the runnable - */ - @Nonnull - public static Runnable asRunnable(@Nonnull IOExceptionRunnable r) { - return () -> { - try { - r.run(); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - }; - } -} diff --git a/src/main/java/com/android/apkzlib/utils/IOExceptionWrapper.java b/src/main/java/com/android/apkzlib/utils/IOExceptionWrapper.java deleted file mode 100644 index 067b260..0000000 --- a/src/main/java/com/android/apkzlib/utils/IOExceptionWrapper.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * 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.android.apkzlib.utils; - -import java.io.IOException; -import javax.annotation.Nonnull; - -/** - * Runtime exception used to encapsulate an IO Exception. This is used to allow throwing I/O - * exceptions in functional interfaces that do not allow it and catching the exception afterwards. - */ -public class IOExceptionWrapper extends RuntimeException { - - /** - * Creates a new exception. - * - * @param e the I/O exception to encapsulate - */ - public IOExceptionWrapper(@Nonnull IOException e) { - super(e); - } - - @Override - @Nonnull - public IOException getCause() { - return (IOException) super.getCause(); - } -} diff --git a/src/main/java/com/android/apkzlib/utils/package-info.java b/src/main/java/com/android/apkzlib/utils/package-info.java deleted file mode 100644 index 2f17b60..0000000 --- a/src/main/java/com/android/apkzlib/utils/package-info.java +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * 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. - */ - -/** - * Utilities to work with {@code apkzlib}. - */ -package com.android.apkzlib.utils; \ No newline at end of file diff --git a/src/main/java/com/android/apkzlib/zfile/ApkCreator.java b/src/main/java/com/android/apkzlib/zfile/ApkCreator.java deleted file mode 100644 index 3cac7dc..0000000 --- a/src/main/java/com/android/apkzlib/zfile/ApkCreator.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * 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.android.apkzlib.zfile; - -import java.io.Closeable; -import java.io.File; -import java.io.IOException; -import java.util.function.Function; -import java.util.function.Predicate; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -/** - * Creates or updates APKs based on provided entries. - */ -public interface ApkCreator extends Closeable { - - /** - * Copies the content of a Jar/Zip archive into the receiver archive. - * - *

An optional predicate allows to selectively choose which files to copy over and an - * option function allows renaming the files as they are copied. - * - * @param zip the zip to copy data from - * @param transform an optional transform to apply to file names before copying them - * @param isIgnored an optional filter or {@code null} to mark which out files should not be - * added, even through they are on the zip; if {@code transform} is specified, then this - * predicate applies after transformation - * @throws IOException I/O error - */ - void writeZip( - @Nonnull File zip, - @Nullable Function transform, - @Nullable Predicate isIgnored) - throws IOException; - - /** - * Writes a new {@link File} into the archive. If a file already existed with the given - * path, it should be replaced. - * - * @param inputFile the {@link File} to write. - * @param apkPath the filepath inside the archive. - * @throws IOException I/O error - */ - void writeFile(@Nonnull File inputFile, @Nonnull String apkPath) throws IOException; - - /** - * Deletes a file in a given path. - * - * @param apkPath the path to remove - * @throws IOException failed to remove the entry - */ - void deleteFile(@Nonnull String apkPath) throws IOException; - - /** Returns true if the APK will be rewritten on close. */ - boolean hasPendingChangesWithWait() throws IOException; -} diff --git a/src/main/java/com/android/apkzlib/zfile/ApkCreatorFactory.java b/src/main/java/com/android/apkzlib/zfile/ApkCreatorFactory.java deleted file mode 100644 index 503eaf1..0000000 --- a/src/main/java/com/android/apkzlib/zfile/ApkCreatorFactory.java +++ /dev/null @@ -1,246 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * 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.android.apkzlib.zfile; - -import static com.google.common.base.Preconditions.checkNotNull; - -import com.google.common.base.Preconditions; -import java.io.File; -import java.security.PrivateKey; -import java.security.cert.X509Certificate; -import java.util.function.Predicate; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -/** - * Factory that creates instances of {@link ApkCreator}. - */ -public interface ApkCreatorFactory { - - /** - * Creates an {@link ApkCreator} with a given output location, and signing information. - * - * @param creationData the information to create the APK - */ - ApkCreator make(@Nonnull CreationData creationData); - - /** - * Data structure with the required information to initiate the creation of an APK. See - * {@link ApkCreatorFactory#make(CreationData)}. - */ - class CreationData { - - /** - * The path where the APK should be located. May already exist or not (if it does, then - * the APK may be updated instead of created). - */ - @Nonnull - private final File apkPath; - - /** - * Key used to sign the APK. May be {@code null}. - */ - @Nullable - private final PrivateKey key; - - /** - * Certificate used to sign the APK. Is {@code null} if and only if {@link #key} is - * {@code null}. - */ - @Nullable - private final X509Certificate certificate; - - /** - * Whether signing the APK with JAR Signing Scheme (aka v1 signing) is enabled. - */ - private final boolean v1SigningEnabled; - - /** - * Whether signing the APK with APK Signature Scheme v2 (aka v2 signing) is enabled. - */ - private final boolean v2SigningEnabled; - - /** - * Built-by information for the APK, if any. - */ - @Nullable - private final String builtBy; - - /** - * Created-by information for the APK, if any. - */ - @Nullable - private final String createdBy; - - /** - * Minimum SDk version that will run the APK. - */ - private final int minSdkVersion; - - /** - * How should native libraries be packaged? - */ - @Nonnull - private final NativeLibrariesPackagingMode nativeLibrariesPackagingMode; - - /** - * Predicate identifying paths that should not be compressed. - */ - @Nonnull - private final Predicate noCompressPredicate; - - /** - * - * @param apkPath the path where the APK should be located. May already exist or not (if it - * does, then the APK may be updated instead of created) - * @param key key used to sign the APK. May be {@code null} - * @param certificate certificate used to sign the APK. Is {@code null} if and only if - * {@code key} is {@code null} - * @param v1SigningEnabled {@code true} if this APK should be signed with JAR Signature - * Scheme (aka v1 scheme). - * @param v2SigningEnabled {@code true} if this APK should be signed with APK Signature - * Scheme v2 (aka v2 scheme). - * @param builtBy built-by information for the APK, if any; if {@code null} then the default - * should be used - * @param createdBy created-by information for the APK, if any; if {@code null} then the - * default should be used - * @param minSdkVersion minimum SDK version that will run the APK - * @param nativeLibrariesPackagingMode packaging mode for native libraries - * @param noCompressPredicate predicate to decide which file paths should be uncompressed; - * returns {@code true} for files that should not be compressed - */ - public CreationData( - @Nonnull File apkPath, - @Nullable PrivateKey key, - @Nullable X509Certificate certificate, - boolean v1SigningEnabled, - boolean v2SigningEnabled, - @Nullable String builtBy, - @Nullable String createdBy, - int minSdkVersion, - @Nonnull NativeLibrariesPackagingMode nativeLibrariesPackagingMode, - @Nonnull Predicate noCompressPredicate) { - Preconditions.checkArgument((key == null) == (certificate == null), - "(key == null) != (certificate == null)"); - Preconditions.checkArgument(minSdkVersion >= 0, "minSdkVersion < 0"); - - this.apkPath = apkPath; - this.key = key; - this.certificate = certificate; - this.v1SigningEnabled = v1SigningEnabled; - this.v2SigningEnabled = v2SigningEnabled; - this.builtBy = builtBy; - this.createdBy = createdBy; - this.minSdkVersion = minSdkVersion; - this.nativeLibrariesPackagingMode = checkNotNull(nativeLibrariesPackagingMode); - this.noCompressPredicate = checkNotNull(noCompressPredicate); - } - - /** - * Obtains the path where the APK should be located. If the path already exists, then the - * APK may be updated instead of re-created. - * - * @return the path that may already exist or not - */ - @Nonnull - public File getApkPath() { - return apkPath; - } - - /** - * Obtains the private key used to sign the APK. - * - * @return the private key or {@code null} if the APK should not be signed - */ - @Nullable - public PrivateKey getPrivateKey() { - return key; - } - - /** - * Obtains the certificate used to sign the APK. - * - * @return the certificate or {@code null} if the APK should not be signed; this will return - * {@code null} if and only if {@link #getPrivateKey()} returns {@code null} - */ - @Nullable - public X509Certificate getCertificate() { - return certificate; - } - - /** - * Returns {@code true} if this APK should be signed with JAR Signature Scheme (aka v1 - * scheme). - */ - public boolean isV1SigningEnabled() { - return v1SigningEnabled; - } - - /** - * Returns {@code true} if this APK should be signed with APK Signature Scheme v2 (aka v2 - * scheme). - */ - public boolean isV2SigningEnabled() { - return v2SigningEnabled; - } - - /** - * Obtains the "built-by" text for the APK. - * - * @return the text or {@code null} if the default should be used - */ - @Nullable - public String getBuiltBy() { - return builtBy; - } - - /** - * Obtains the "created-by" text for the APK. - * - * @return the text or {@code null} if the default should be used - */ - @Nullable - public String getCreatedBy() { - return createdBy; - } - - /** - * Obtains the minimum SDK version to run the APK. - * - * @return the minimum SDK version - */ - public int getMinSdkVersion() { - return minSdkVersion; - } - - /** - * Returns the packaging policy that the {@link ApkCreator} should use for native libraries. - */ - @Nonnull - public NativeLibrariesPackagingMode getNativeLibrariesPackagingMode() { - return nativeLibrariesPackagingMode; - } - - /** - * Returns the predicate to decide which file paths should be uncompressed. - */ - @Nonnull - public Predicate getNoCompressPredicate() { - return noCompressPredicate; - } - } -} diff --git a/src/main/java/com/android/apkzlib/zfile/ApkZFileCreator.java b/src/main/java/com/android/apkzlib/zfile/ApkZFileCreator.java deleted file mode 100644 index c85ad44..0000000 --- a/src/main/java/com/android/apkzlib/zfile/ApkZFileCreator.java +++ /dev/null @@ -1,192 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * 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.android.apkzlib.zfile; - -import com.android.apkzlib.zip.AlignmentRule; -import com.android.apkzlib.zip.AlignmentRules; -import com.android.apkzlib.zip.StoredEntry; -import com.android.apkzlib.zip.ZFile; -import com.android.apkzlib.zip.ZFileOptions; -import com.google.common.base.Preconditions; -import com.google.common.io.Closer; -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.util.function.Function; -import java.util.function.Predicate; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -/** - * {@link ApkCreator} that uses {@link ZFileOptions} to generate the APK. - */ -class ApkZFileCreator implements ApkCreator { - - /** - * Suffix for native libraries. - */ - private static final String NATIVE_LIBRARIES_SUFFIX = ".so"; - - /** - * Shared libraries are alignment at 4096 boundaries. - */ - private static final AlignmentRule SO_RULE = - AlignmentRules.constantForSuffix(NATIVE_LIBRARIES_SUFFIX, 4096); - - /** - * The zip file. - */ - @Nonnull - private final ZFile zip; - - /** - * Has the zip file been closed? - */ - private boolean closed; - - /** - * Predicate defining which files should not be compressed. - */ - @Nonnull - private final Predicate noCompressPredicate; - - /** - * Creates a new creator. - * - * @param creationData the data needed to create the APK - * @param options zip file options - * @throws IOException failed to create the zip - */ - ApkZFileCreator( - @Nonnull ApkCreatorFactory.CreationData creationData, - @Nonnull ZFileOptions options) - throws IOException { - - switch (creationData.getNativeLibrariesPackagingMode()) { - case COMPRESSED: - noCompressPredicate = creationData.getNoCompressPredicate(); - break; - case UNCOMPRESSED_AND_ALIGNED: - noCompressPredicate = - creationData.getNoCompressPredicate().or( - name -> name.endsWith(NATIVE_LIBRARIES_SUFFIX)); - options.setAlignmentRule( - AlignmentRules.compose(SO_RULE, options.getAlignmentRule())); - break; - default: - throw new AssertionError(); - } - - zip = ZFiles.apk( - creationData.getApkPath(), - options, - creationData.getPrivateKey(), - creationData.getCertificate(), - creationData.isV1SigningEnabled(), - creationData.isV2SigningEnabled(), - creationData.getBuiltBy(), - creationData.getCreatedBy(), - creationData.getMinSdkVersion()); - closed = false; - } - - @Override - public void writeZip(@Nonnull File zip, @Nullable Function transform, - @Nullable Predicate isIgnored) throws IOException { - Preconditions.checkState(!closed, "closed == true"); - Preconditions.checkArgument(zip.isFile(), "!zip.isFile()"); - - Closer closer = Closer.create(); - try { - ZFile toMerge = closer.register(new ZFile(zip)); - - Predicate ignorePredicate; - if (isIgnored == null) { - ignorePredicate = s -> false; - } else { - ignorePredicate = isIgnored; - } - - // Files that *must* be uncompressed in the result should not be merged and should be - // added after. This is just very slightly less efficient than ignoring just the ones - // that were compressed and must be uncompressed, but it is a lot simpler :) - Predicate noMergePredicate = ignorePredicate.or(noCompressPredicate); - - this.zip.mergeFrom(toMerge, noMergePredicate); - - for (StoredEntry toMergeEntry : toMerge.entries()) { - String path = toMergeEntry.getCentralDirectoryHeader().getName(); - if (noCompressPredicate.test(path) && !ignorePredicate.test(path)) { - // This entry *must* be uncompressed so it was ignored in the merge and should - // now be added to the apk. - try (InputStream ignoredData = toMergeEntry.open()) { - this.zip.add(path, ignoredData, false); - } - } - } - } catch (Throwable t) { - throw closer.rethrow(t); - } finally { - closer.close(); - } - } - - @Override - public void writeFile(@Nonnull File inputFile, @Nonnull String apkPath) throws IOException { - Preconditions.checkState(!closed, "closed == true"); - - boolean mayCompress = !noCompressPredicate.test(apkPath); - - Closer closer = Closer.create(); - try { - FileInputStream inputFileStream = closer.register(new FileInputStream(inputFile)); - zip.add(apkPath, inputFileStream, mayCompress); - } catch (IOException e) { - throw closer.rethrow(e, IOException.class); - } catch (Throwable t) { - throw closer.rethrow(t); - } finally { - closer.close(); - } - } - - @Override - public void deleteFile(@Nonnull String apkPath) throws IOException { - Preconditions.checkState(!closed, "closed == true"); - - StoredEntry entry = zip.get(apkPath); - if (entry != null) { - entry.delete(); - } - } - - @Override - public boolean hasPendingChangesWithWait() throws IOException { - return zip.hasPendingChangesWithWait(); - } - - @Override - public void close() throws IOException { - if (closed) { - return; - } - - zip.close(); - closed = true; - } -} diff --git a/src/main/java/com/android/apkzlib/zfile/ApkZFileCreatorFactory.java b/src/main/java/com/android/apkzlib/zfile/ApkZFileCreatorFactory.java deleted file mode 100644 index 2e4c7c9..0000000 --- a/src/main/java/com/android/apkzlib/zfile/ApkZFileCreatorFactory.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * 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.android.apkzlib.zfile; - -import com.android.apkzlib.zip.ZFileOptions; -import java.io.IOException; -import java.io.UncheckedIOException; -import javax.annotation.Nonnull; - -/** - * Creates instances of {@link ApkZFileCreator}. - */ -public class ApkZFileCreatorFactory implements ApkCreatorFactory { - - /** - * Options for the {@link ZFileOptions} to use in all APKs. - */ - @Nonnull - private final ZFileOptions options; - - /** - * Creates a new factory. - * - * @param options the options to use for all instances created - */ - public ApkZFileCreatorFactory(@Nonnull ZFileOptions options) { - this.options = options; - } - - - @Override - @Nonnull - public ApkCreator make(@Nonnull CreationData creationData) { - try { - return new ApkZFileCreator(creationData, options); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - } -} diff --git a/src/main/java/com/android/apkzlib/zfile/ManifestAttributes.java b/src/main/java/com/android/apkzlib/zfile/ManifestAttributes.java deleted file mode 100644 index d8a7d2d..0000000 --- a/src/main/java/com/android/apkzlib/zfile/ManifestAttributes.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * 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.android.apkzlib.zfile; - -/** - * Java manifest attributes and some default values. - */ -public interface ManifestAttributes { - /** - * Manifest attribute with the built by information. - */ - String BUILT_BY = "Built-By"; - - /** - * Manifest attribute with the created by information. - */ - String CREATED_BY = "Created-By"; - - /** - * Manifest attribute with the manifest version. - */ - String MANIFEST_VERSION = "Manifest-Version"; - - /** - * Manifest attribute value with the manifest version. - */ - String CURRENT_MANIFEST_VERSION = "1.0"; - -} diff --git a/src/main/java/com/android/apkzlib/zfile/NativeLibrariesPackagingMode.java b/src/main/java/com/android/apkzlib/zfile/NativeLibrariesPackagingMode.java deleted file mode 100644 index 8916abe..0000000 --- a/src/main/java/com/android/apkzlib/zfile/NativeLibrariesPackagingMode.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * 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.android.apkzlib.zfile; - -/** - * Describes how native libs should be packaged. - */ -public enum NativeLibrariesPackagingMode { - /** - * Native libs are packaged as any other file. - */ - COMPRESSED, - - /** - * Native libs are packaged uncompressed and page-aligned, so they can be mapped into memory - * at runtime. - * - *

Support for this mode was added in Android 23, it only works if the - * {@code extractNativeLibs} attribute is set in the manifest. - */ - UNCOMPRESSED_AND_ALIGNED; -} diff --git a/src/main/java/com/android/apkzlib/zfile/ZFiles.java b/src/main/java/com/android/apkzlib/zfile/ZFiles.java deleted file mode 100644 index d5102a6..0000000 --- a/src/main/java/com/android/apkzlib/zfile/ZFiles.java +++ /dev/null @@ -1,132 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * 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.android.apkzlib.zfile; - -import com.android.apkzlib.sign.ManifestGenerationExtension; -import com.android.apkzlib.sign.SigningExtension; -import com.android.apkzlib.zip.AlignmentRule; -import com.android.apkzlib.zip.AlignmentRules; -import com.android.apkzlib.zip.ZFile; -import com.android.apkzlib.zip.ZFileOptions; -import java.io.File; -import java.io.IOException; -import java.security.InvalidKeyException; -import java.security.NoSuchAlgorithmException; -import java.security.PrivateKey; -import java.security.cert.X509Certificate; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -/** - * Factory for {@link ZFile}s that are specifically configured to be APKs, AARs, ... - */ -public class ZFiles { - - /** - * By default all non-compressed files are alignment at 4 byte boundaries.. - */ - private static final AlignmentRule APK_DEFAULT_RULE = AlignmentRules.constant(4); - - /** - * Default build by string. - */ - private static final String DEFAULT_BUILD_BY = "Generated-by-ADT"; - - /** - * Default created by string. - */ - private static final String DEFAULT_CREATED_BY = "Generated-by-ADT"; - - /** - * Creates a new zip file configured as an apk, based on a given file. - * - * @param f the file, if this path does not represent an existing path, will create a - * {@link ZFile} based on an non-existing path (a zip will be created when - * {@link ZFile#close()} is invoked) - * @param options the options to create the {@link ZFile} - * @return the zip file - * @throws IOException failed to create the zip file - */ - @Nonnull - public static ZFile apk(@Nonnull File f, @Nonnull ZFileOptions options) throws IOException { - options.setAlignmentRule( - AlignmentRules.compose(options.getAlignmentRule(), APK_DEFAULT_RULE)); - return new ZFile(f, options); - } - - /** - * Creates a new zip file configured as an apk, based on a given file. - * - * @param f the file, if this path does not represent an existing path, will create a - * {@link ZFile} based on an non-existing path (a zip will be created when - * {@link ZFile#close()} is invoked) - * @param options the options to create the {@link ZFile} - * @param key the {@link PrivateKey} used to sign the archive, or {@code null}. - * @param certificate the {@link X509Certificate} used to sign the archive, or - * {@code null}. - * @param v1SigningEnabled whether signing with JAR Signature Scheme (aka v1 signing) is - * enabled. - * @param v2SigningEnabled whether signing with APK Signature Scheme v2 (aka v2 signing) is - * enabled. - * @param builtBy who to mark as builder in the manifest - * @param createdBy who to mark as creator in the manifest - * @param minSdkVersion minimum SDK version supported - * @return the zip file - * @throws IOException failed to create the zip file - */ - @Nonnull - public static ZFile apk( - @Nonnull File f, - @Nonnull ZFileOptions options, - @Nullable PrivateKey key, - @Nullable X509Certificate certificate, - boolean v1SigningEnabled, - boolean v2SigningEnabled, - @Nullable String builtBy, - @Nullable String createdBy, - int minSdkVersion) - throws IOException { - ZFile zfile = apk(f, options); - - if (builtBy == null) { - builtBy = DEFAULT_BUILD_BY; - } - - if (createdBy == null) { - createdBy = DEFAULT_CREATED_BY; - } - - ManifestGenerationExtension manifestExt = new ManifestGenerationExtension(builtBy, - createdBy); - manifestExt.register(zfile); - - if (key != null && certificate != null) { - try { - new SigningExtension( - minSdkVersion, - certificate, - key, - v1SigningEnabled, - v2SigningEnabled).register(zfile); - } catch (NoSuchAlgorithmException | InvalidKeyException e) { - throw new IOException("Failed to create signature extensions", e); - } - } - - return zfile; - } -} diff --git a/src/main/java/com/android/apkzlib/zfile/package-info.java b/src/main/java/com/android/apkzlib/zfile/package-info.java deleted file mode 100644 index 0c5ab6d..0000000 --- a/src/main/java/com/android/apkzlib/zfile/package-info.java +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * 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. - */ - -/** - * The {@code zfile} package contains - */ -package com.android.apkzlib.zfile; \ No newline at end of file diff --git a/src/main/java/com/android/apkzlib/zip/AlignmentRule.java b/src/main/java/com/android/apkzlib/zip/AlignmentRule.java deleted file mode 100644 index 4ee6963..0000000 --- a/src/main/java/com/android/apkzlib/zip/AlignmentRule.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * 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.android.apkzlib.zip; - -import javax.annotation.Nonnull; - -/** - * An alignment rule defines how to a file should be aligned in a zip, based on its name. - */ -public interface AlignmentRule { - - /** - * Alignment value of files that do not require alignment. - */ - int NO_ALIGNMENT = 1; - - /** - * Obtains the alignment this rule computes for a given path. - * - * @param path the path in the zip file - * @return the alignment value, always greater than {@code 0}; if this rule places no - * restrictions on the provided path, then {@link AlignmentRule#NO_ALIGNMENT} is returned - */ - int alignment(@Nonnull String path); -} diff --git a/src/main/java/com/android/apkzlib/zip/AlignmentRules.java b/src/main/java/com/android/apkzlib/zip/AlignmentRules.java deleted file mode 100644 index b06a596..0000000 --- a/src/main/java/com/android/apkzlib/zip/AlignmentRules.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * 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.android.apkzlib.zip; - -import com.google.common.base.Preconditions; -import javax.annotation.Nonnull; - -/** - * Factory for instances of {@link AlignmentRule}. - */ -public final class AlignmentRules { - - private AlignmentRules() {} - - /** - * A rule that defines a constant alignment for all files. - * - * @param alignment the alignment - * @return the rule - */ - public static AlignmentRule constant(int alignment) { - Preconditions.checkArgument(alignment > 0, "alignment <= 0"); - - return (String path) -> alignment; - } - - /** - * A rule that defines constant alignment for all files with a certain suffix, placing no - * restrictions on other files. - * - * @param suffix the suffix - * @param alignment the alignment for paths that match the provided suffix - * @return the rule - */ - public static AlignmentRule constantForSuffix(@Nonnull String suffix, int alignment) { - Preconditions.checkArgument(!suffix.isEmpty(), "suffix.isEmpty()"); - Preconditions.checkArgument(alignment > 0, "alignment <= 0"); - - return (String path) -> path.endsWith(suffix) ? alignment : AlignmentRule.NO_ALIGNMENT; - } - - /** - * A rule that applies other rules in order. - * - * @param rules all rules to be tried; the first rule that does not return - * {@link AlignmentRule#NO_ALIGNMENT} will define the alignment for a path; if there are no - * rules that return a value different from {@link AlignmentRule#NO_ALIGNMENT}, then - * {@link AlignmentRule#NO_ALIGNMENT} is returned - * @return the composition rule - */ - public static AlignmentRule compose(@Nonnull AlignmentRule... rules) { - return (String path) -> { - for (AlignmentRule r : rules) { - int align = r.alignment(path); - if (align != AlignmentRule.NO_ALIGNMENT) { - return align; - } - } - - return AlignmentRule.NO_ALIGNMENT; - }; - } -} diff --git a/src/main/java/com/android/apkzlib/zip/CentralDirectory.java b/src/main/java/com/android/apkzlib/zip/CentralDirectory.java deleted file mode 100644 index 44389c1..0000000 --- a/src/main/java/com/android/apkzlib/zip/CentralDirectory.java +++ /dev/null @@ -1,489 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * 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.android.apkzlib.zip; - -import com.android.apkzlib.utils.CachedSupplier; -import com.android.apkzlib.zip.utils.MsDosDateTimeUtils; -import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.Preconditions; -import com.google.common.collect.ImmutableMap; -import com.google.common.collect.Lists; -import com.google.common.collect.Maps; -import com.google.common.primitives.Ints; -import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; -import java.io.IOException; -import java.io.UncheckedIOException; -import java.nio.ByteBuffer; -import java.util.List; -import java.util.Map; -import java.util.Set; -import javax.annotation.Nonnull; - -/** - * Representation of the central directory of a zip archive. - */ -class CentralDirectory { - - /** - * Field in the central directory with the central directory signature. - */ - private static final ZipField.F4 F_SIGNATURE = new ZipField.F4(0, 0x02014b50, "Signature"); - - /** - * Field in the central directory with the "made by" code. - */ - private static final ZipField.F2 F_MADE_BY = new ZipField.F2(F_SIGNATURE.endOffset(), - "Made by", new ZipFieldInvariantNonNegative()); - - /** - * Field in the central directory with the minimum version required to extract the entry. - */ - @VisibleForTesting - static final ZipField.F2 F_VERSION_EXTRACT = new ZipField.F2(F_MADE_BY.endOffset(), - "Version to extract", new ZipFieldInvariantNonNegative()); - - /** - * Field in the central directory with the GP bit flag. - */ - private static final ZipField.F2 F_GP_BIT = new ZipField.F2(F_VERSION_EXTRACT.endOffset(), - "GP bit"); - - /** - * Field in the central directory with the code of the compression method. See - * {@link CompressionMethod#fromCode(long)}. - */ - private static final ZipField.F2 F_METHOD = new ZipField.F2(F_GP_BIT.endOffset(), "Method"); - - /** - * Field in the central directory with the last modification time in MS-DOS format (see - * {@link MsDosDateTimeUtils#packTime(long)}). - */ - private static final ZipField.F2 F_LAST_MOD_TIME = new ZipField.F2(F_METHOD.endOffset(), - "Last modification time"); - - /** - * Field in the central directory with the last modification date in MS-DOS format. See - * {@link MsDosDateTimeUtils#packDate(long)}. - */ - private static final ZipField.F2 F_LAST_MOD_DATE = new ZipField.F2(F_LAST_MOD_TIME.endOffset(), - "Last modification date"); - - /** - * Field in the central directory with the CRC32 checksum of the entry. This will be zero for - * directories and files with no content. - */ - private static final ZipField.F4 F_CRC32 = new ZipField.F4(F_LAST_MOD_DATE.endOffset(), - "CRC32"); - - /** - * Field in the central directory with the entry's compressed size, i.e., the file on - * the archive. This will be the same as the uncompressed size if the method is - * {@link CompressionMethod#STORE}. - */ - private static final ZipField.F4 F_COMPRESSED_SIZE = new ZipField.F4(F_CRC32.endOffset(), - "Compressed size", new ZipFieldInvariantNonNegative()); - - /** - * Field in the central directory with the entry's uncompressed size, i.e., the size - * the file will have when extracted from the zip. This will be zero for directories and empty - * files and will be the same as the compressed size if the method is - * {@link CompressionMethod#STORE}. - */ - private static final ZipField.F4 F_UNCOMPRESSED_SIZE = new ZipField.F4( - F_COMPRESSED_SIZE.endOffset(), "Uncompressed size", new ZipFieldInvariantNonNegative()); - - /** - * Field in the central directory with the length of the file name. The file name is stored - * after the offset field ({@link #F_OFFSET}). The number of characters in the file name are - * stored in this field. - */ - private static final ZipField.F2 F_FILE_NAME_LENGTH = new ZipField.F2( - F_UNCOMPRESSED_SIZE.endOffset(), "File name length", - new ZipFieldInvariantNonNegative()); - - /** - * Field in the central directory with the length of the extra field. The extra field is - * stored after the file name ({@link #F_FILE_NAME_LENGTH}). The contents of this field are - * partially defined in the zip specification but we do not parse it. - */ - private static final ZipField.F2 F_EXTRA_FIELD_LENGTH = new ZipField.F2( - F_FILE_NAME_LENGTH.endOffset(), "Extra field length", - new ZipFieldInvariantNonNegative()); - - /** - * Field in the central directory with the length of the comment. The comment is stored after - * the extra field ({@link #F_EXTRA_FIELD_LENGTH}). We do not parse the comment. - */ - private static final ZipField.F2 F_COMMENT_LENGTH = new ZipField.F2( - F_EXTRA_FIELD_LENGTH.endOffset(), "Comment length", new ZipFieldInvariantNonNegative()); - - /** - * Number of the disk where the central directory starts. Because we do not support multi-file - * archives, this field has to have value {@code 0}. - */ - private static final ZipField.F2 F_DISK_NUMBER_START = new ZipField.F2( - F_COMMENT_LENGTH.endOffset(), 0, "Disk start"); - - /** - * Internal attributes. This field can only contain one bit set, the {@link #ASCII_BIT}. - */ - private static final ZipField.F2 F_INTERNAL_ATTRIBUTES = new ZipField.F2( - F_DISK_NUMBER_START.endOffset(), "Int attributes"); - - /** - * External attributes. This field is ignored. - */ - private static final ZipField.F4 F_EXTERNAL_ATTRIBUTES = new ZipField.F4( - F_INTERNAL_ATTRIBUTES.endOffset(), "Ext attributes"); - - /** - * Offset into the archive where the entry starts. This is the offset to the local header - * (see {@link StoredEntry} for information on the local header), not to the file data itself. - * The file data, if there is any, will be stored after the local header. - */ - private static final ZipField.F4 F_OFFSET = new ZipField.F4(F_EXTERNAL_ATTRIBUTES.endOffset(), - "Offset", new ZipFieldInvariantNonNegative()); - - /** - * Maximum supported version to extract. - */ - private static final int MAX_VERSION_TO_EXTRACT = 20; - - /** - * Bit that can be set on the internal attributes stating that the file is an ASCII file. We - * don't do anything with this information, but we check that nothing unexpected appears in the - * internal attributes. - */ - private static final int ASCII_BIT = 1; - - /** - * Contains all entries in the directory mapped from their names. - */ - @Nonnull - private final Map entries; - - /** - * The file where this directory belongs to. - */ - @Nonnull - private final ZFile file; - - /** - * Supplier that provides a byte representation of the central directory. - */ - @Nonnull - private final CachedSupplier bytesSupplier; - - /** - * Verify log for the central directory. - */ - @Nonnull - private final VerifyLog verifyLog; - - /** - * Creates a new, empty, central directory, for a given zip file. - * - * @param file the file - */ - CentralDirectory(@Nonnull ZFile file) { - entries = Maps.newHashMap(); - this.file = file; - bytesSupplier = new CachedSupplier<>(this::computeByteRepresentation); - verifyLog = file.getVerifyLog(); - } - - /** - * Reads the central directory data from a zip file, parses it, and creates the in-memory - * structure representing the directory. - * - * @param bytes the data of the central directory; the directory is read from the buffer's - * current position; when this method terminates, the buffer's position is the first byte - * after the directory - * @param count the number of entries expected in the central directory (usually read from the - * {@link Eocd}). - * @param file the zip file this central directory belongs to - * @return the central directory - * @throws IOException failed to read data from the zip, or the central directory is corrupted - * or has unsupported features - */ - static CentralDirectory makeFromData(@Nonnull ByteBuffer bytes, int count, @Nonnull ZFile file) - throws IOException { - Preconditions.checkNotNull(bytes, "bytes == null"); - Preconditions.checkArgument(count >= 0, "count < 0"); - - CentralDirectory directory = new CentralDirectory(file); - - for (int i = 0; i < count; i++) { - try { - directory.readEntry(bytes); - } catch (IOException e) { - throw new IOException( - "Failed to read directory entry index " - + i - + " (total " - + "directory bytes read: " - + bytes.position() - + ").", - e); - } - } - - return directory; - } - - /** - * Creates a new central directory from the entries. This is used to build a new central - * directory from entries in the zip file. - * - * @param entries the entries in the zip file - * @param file the zip file itself - * @return the created central directory - */ - static CentralDirectory makeFromEntries( - @Nonnull Set entries, - @Nonnull ZFile file) { - CentralDirectory directory = new CentralDirectory(file); - for (StoredEntry entry : entries) { - CentralDirectoryHeader cdr = entry.getCentralDirectoryHeader(); - Preconditions.checkArgument( - !directory.entries.containsKey(cdr.getName()), - "Duplicate filename"); - directory.entries.put(cdr.getName(), entry); - } - - return directory; - } - - /** - * Reads the next entry from the central directory and adds it to {@link #entries}. - * - * @param bytes the central directory's data, positioned starting at the beginning of the next - * entry to read; when finished, the buffer's position will be at the first byte after the - * entry - * @throws IOException failed to read the directory entry, either because of an I/O error, - * because it is corrupt or contains unsupported features - */ - private void readEntry(@Nonnull ByteBuffer bytes) throws IOException { - F_SIGNATURE.verify(bytes); - long madeBy = F_MADE_BY.read(bytes); - - long versionNeededToExtract = F_VERSION_EXTRACT.read(bytes); - verifyLog.verify( - versionNeededToExtract <= MAX_VERSION_TO_EXTRACT, - "Ignored unknown version needed to extract in zip directory entry: %s.", - versionNeededToExtract); - - long gpBit = F_GP_BIT.read(bytes); - GPFlags flags = GPFlags.from(gpBit); - - long methodCode = F_METHOD.read(bytes); - CompressionMethod method = CompressionMethod.fromCode(methodCode); - verifyLog.verify(method != null, "Unknown method in zip directory entry: %s.", methodCode); - - long lastModTime; - long lastModDate; - if (file.areTimestampsIgnored()) { - lastModTime = 0; - lastModDate = 0; - F_LAST_MOD_TIME.skip(bytes); - F_LAST_MOD_DATE.skip(bytes); - } else { - lastModTime = F_LAST_MOD_TIME.read(bytes); - lastModDate = F_LAST_MOD_DATE.read(bytes); - } - - long crc32 = F_CRC32.read(bytes); - long compressedSize = F_COMPRESSED_SIZE.read(bytes); - long uncompressedSize = F_UNCOMPRESSED_SIZE.read(bytes); - int fileNameLength = Ints.checkedCast(F_FILE_NAME_LENGTH.read(bytes)); - int extraFieldLength = Ints.checkedCast(F_EXTRA_FIELD_LENGTH.read(bytes)); - int fileCommentLength = Ints.checkedCast(F_COMMENT_LENGTH.read(bytes)); - - F_DISK_NUMBER_START.verify(bytes, verifyLog); - long internalAttributes = F_INTERNAL_ATTRIBUTES.read(bytes); - verifyLog.verify( - (internalAttributes & ~ASCII_BIT) == 0, - "Ignored invalid internal attributes: %s.", - internalAttributes); - - long externalAttributes = F_EXTERNAL_ATTRIBUTES.read(bytes); - long entryOffset = F_OFFSET.read(bytes); - - long remainingSize = fileNameLength + extraFieldLength + fileCommentLength; - - if (bytes.remaining() < fileNameLength + extraFieldLength + fileCommentLength) { - throw new IOException( - "Directory entry should have " - + remainingSize - + " bytes remaining (name = " - + fileNameLength - + ", extra = " - + extraFieldLength - + ", comment = " - + fileCommentLength - + "), but it has " - + bytes.remaining() - + "."); - } - - byte[] encodedFileName = new byte[fileNameLength]; - bytes.get(encodedFileName); - String fileName = EncodeUtils.decode(encodedFileName, flags); - - byte[] extraField = new byte[extraFieldLength]; - bytes.get(extraField); - - byte[] fileCommentField = new byte[fileCommentLength]; - bytes.get(fileCommentField); - - /* - * Tricky: to create a CentralDirectoryHeader we need the future that will hold the result - * of the compress information. But, to actually create the result of the compress - * information we need the CentralDirectoryHeader - */ - ListenableFuture compressInfo = - Futures.immediateFuture( - new CentralDirectoryHeaderCompressInfo( - method, - compressedSize, - versionNeededToExtract)); - CentralDirectoryHeader centralDirectoryHeader = - new CentralDirectoryHeader( - fileName, encodedFileName, uncompressedSize, compressInfo, flags, file); - centralDirectoryHeader.setMadeBy(madeBy); - centralDirectoryHeader.setLastModTime(lastModTime); - centralDirectoryHeader.setLastModDate(lastModDate); - centralDirectoryHeader.setCrc32(crc32); - centralDirectoryHeader.setInternalAttributes(internalAttributes); - centralDirectoryHeader.setExternalAttributes(externalAttributes); - centralDirectoryHeader.setOffset(entryOffset); - centralDirectoryHeader.setExtraFieldNoNotify(new ExtraField(extraField)); - centralDirectoryHeader.setComment(fileCommentField); - - StoredEntry entry; - - try { - entry = new StoredEntry(centralDirectoryHeader, file, null); - } catch (IOException e) { - throw new IOException("Failed to read stored entry '" + fileName + "'.", e); - } - - if (entries.containsKey(fileName)) { - verifyLog.log("File file contains duplicate file '" + fileName + "'."); - } - - entries.put(fileName, entry); - } - - /** - * Obtains all the entries in the central directory. - * - * @return all entries on a non-modifiable map - */ - @Nonnull - Map getEntries() { - return ImmutableMap.copyOf(entries); - } - - /** - * Obtains the byte representation of the central directory. - * - * @return a byte array containing the whole central directory - * @throws IOException failed to write the byte array - */ - byte[] toBytes() throws IOException { - return bytesSupplier.get(); - } - - /** - * Computes the byte representation of the central directory. - * - * @return a byte array containing the whole central directory - * @throws UncheckedIOException failed to write the byte array - */ - private byte[] computeByteRepresentation() { - - List sorted = Lists.newArrayList(entries.values()); - sorted.sort(StoredEntry.COMPARE_BY_NAME); - - CentralDirectoryHeader[] cdhs = new CentralDirectoryHeader[entries.size()]; - CentralDirectoryHeaderCompressInfo[] compressInfos = - new CentralDirectoryHeaderCompressInfo[entries.size()]; - byte[][] encodedFileNames = new byte[entries.size()][]; - byte[][] extraFields = new byte[entries.size()][]; - byte[][] comments = new byte[entries.size()][]; - - try { - /* - * First collect all the data and compute the total size of the central directory. - */ - int idx = 0; - int total = 0; - for (StoredEntry entry : sorted) { - cdhs[idx] = entry.getCentralDirectoryHeader(); - compressInfos[idx] = cdhs[idx].getCompressionInfoWithWait(); - encodedFileNames[idx] = cdhs[idx].getEncodedFileName(); - extraFields[idx] = new byte[cdhs[idx].getExtraField().size()]; - cdhs[idx].getExtraField().write(ByteBuffer.wrap(extraFields[idx])); - comments[idx] = cdhs[idx].getComment(); - - total += F_OFFSET.endOffset() + encodedFileNames[idx].length - + extraFields[idx].length + comments[idx].length; - idx++; - } - - ByteBuffer out = ByteBuffer.allocate(total); - - for (idx = 0; idx < entries.size(); idx++) { - F_SIGNATURE.write(out); - F_MADE_BY.write(out, cdhs[idx].getMadeBy()); - F_VERSION_EXTRACT.write(out, compressInfos[idx].getVersionExtract()); - F_GP_BIT.write(out, cdhs[idx].getGpBit().getValue()); - F_METHOD.write(out, compressInfos[idx].getMethod().methodCode); - - if (file.areTimestampsIgnored()) { - F_LAST_MOD_TIME.write(out, 0); - F_LAST_MOD_DATE.write(out, 0); - } else { - F_LAST_MOD_TIME.write(out, cdhs[idx].getLastModTime()); - F_LAST_MOD_DATE.write(out, cdhs[idx].getLastModDate()); - } - - F_CRC32.write(out, cdhs[idx].getCrc32()); - F_COMPRESSED_SIZE.write(out, compressInfos[idx].getCompressedSize()); - F_UNCOMPRESSED_SIZE.write(out, cdhs[idx].getUncompressedSize()); - - F_FILE_NAME_LENGTH.write(out, cdhs[idx].getEncodedFileName().length); - F_EXTRA_FIELD_LENGTH.write(out, cdhs[idx].getExtraField().size()); - F_COMMENT_LENGTH.write(out, cdhs[idx].getComment().length); - F_DISK_NUMBER_START.write(out); - F_INTERNAL_ATTRIBUTES.write(out, cdhs[idx].getInternalAttributes()); - F_EXTERNAL_ATTRIBUTES.write(out, cdhs[idx].getExternalAttributes()); - F_OFFSET.write(out, cdhs[idx].getOffset()); - - out.put(encodedFileNames[idx]); - out.put(extraFields[idx]); - out.put(comments[idx]); - } - - return out.array(); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - } -} diff --git a/src/main/java/com/android/apkzlib/zip/CentralDirectoryHeader.java b/src/main/java/com/android/apkzlib/zip/CentralDirectoryHeader.java deleted file mode 100644 index f10477f..0000000 --- a/src/main/java/com/android/apkzlib/zip/CentralDirectoryHeader.java +++ /dev/null @@ -1,434 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * 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.android.apkzlib.zip; - -import com.android.apkzlib.zip.utils.MsDosDateTimeUtils; -import com.google.common.base.Verify; -import java.io.IOException; -import java.util.Arrays; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Future; -import javax.annotation.Nonnull; - -/** - * The Central Directory Header contains information about files stored in the zip. Instances of - * this class contain information for files that already are in the zip and, for which the data was - * read from the Central Directory. But some instances of this class are used for new files. - * Because instances of this class can refer to files not yet on the zip, some of the fields may - * not be filled in, or may be filled in with default values. - *

- * Because compression decision is done lazily, some data is stored with futures. - */ -public class CentralDirectoryHeader implements Cloneable { - - /** - * Default "version made by" field: upper byte needs to be 0 to set to MS-DOS compatibility. - * Lower byte can be anything, really. We use 18 because aapt uses 17 :) - */ - private static final int DEFAULT_VERSION_MADE_BY = 0x0018; - - /** - * Name of the file. - */ - @Nonnull - private String name; - - /** - * CRC32 of the data. 0 if not yet computed. - */ - private long crc32; - - /** - * Size of the file uncompressed. 0 if the file has no data. - */ - private long uncompressedSize; - - /** - * Code of the program that made the zip. We actually don't care about this. - */ - private long madeBy; - - /** - * General-purpose bit flag. - */ - @Nonnull - private GPFlags gpBit; - - /** - * Last modification time in MS-DOS format (see {@link MsDosDateTimeUtils#packTime(long)}). - */ - private long lastModTime; - - /** - * Last modification time in MS-DOS format (see {@link MsDosDateTimeUtils#packDate(long)}). - */ - private long lastModDate; - - /** - * Extra data field contents. This field follows a specific structure according to the - * specification. - */ - @Nonnull - private ExtraField extraField; - - /** - * File comment. - */ - @Nonnull - private byte[] comment; - - /** - * File internal attributes. - */ - private long internalAttributes; - - /** - * File external attributes. - */ - private long externalAttributes; - - /** - * Offset in the file where the data is located. This will be -1 if the header corresponds to - * a new file that is not yet written in the zip and, therefore, has no written data. - */ - private long offset; - - /** - * Encoded file name. - */ - private byte[] encodedFileName; - - /** - * Compress information that may not have been computed yet due to lazy compression. - */ - @Nonnull - private Future compressInfo; - - /** - * The file this header belongs to. - */ - @Nonnull - private final ZFile file; - - /** - * Creates data for a file. - * - * @param name the file name - * @param encodedFileName the encoded file name, this array will be owned by the header - * @param uncompressedSize the uncompressed file size - * @param compressInfo computation that defines the compression information - * @param flags flags used in the entry - * @param zFile the file this header belongs to - */ - CentralDirectoryHeader( - @Nonnull String name, - @Nonnull byte[] encodedFileName, - long uncompressedSize, - @Nonnull Future compressInfo, - @Nonnull GPFlags flags, - @Nonnull ZFile zFile) { - this.name = name; - this.uncompressedSize = uncompressedSize; - crc32 = 0; - - /* - * Set sensible defaults for the rest. - */ - madeBy = DEFAULT_VERSION_MADE_BY; - - gpBit = flags; - lastModTime = MsDosDateTimeUtils.packCurrentTime(); - lastModDate = MsDosDateTimeUtils.packCurrentDate(); - extraField = new ExtraField(); - comment = new byte[0]; - internalAttributes = 0; - externalAttributes = 0; - offset = -1; - this.encodedFileName = encodedFileName; - this.compressInfo = compressInfo; - file = zFile; - } - - /** - * Obtains the name of the file. - * - * @return the name - */ - @Nonnull - public String getName() { - return name; - } - - /** - * Obtains the size of the uncompressed file. - * - * @return the size of the file - */ - public long getUncompressedSize() { - return uncompressedSize; - } - - /** - * Obtains the CRC32 of the data. - * - * @return the CRC32, 0 if not yet computed - */ - public long getCrc32() { - return crc32; - } - - /** - * Sets the CRC32 of the data. - * - * @param crc32 the CRC 32 - */ - void setCrc32(long crc32) { - this.crc32 = crc32; - } - - /** - * Obtains the code of the program that made the zip. - * - * @return the code - */ - public long getMadeBy() { - return madeBy; - } - - /** - * Sets the code of the progtram that made the zip. - * - * @param madeBy the code - */ - void setMadeBy(long madeBy) { - this.madeBy = madeBy; - } - - /** - * Obtains the general-purpose bit flag. - * - * @return the bit flag - */ - @Nonnull - public GPFlags getGpBit() { - return gpBit; - } - - /** - * Obtains the last modification time of the entry. - * - * @return the last modification time in MS-DOS format (see - * {@link MsDosDateTimeUtils#packTime(long)}) - */ - public long getLastModTime() { - return lastModTime; - } - - /** - * Sets the last modification time of the entry. - * - * @param lastModTime the last modification time in MS-DOS format (see - * {@link MsDosDateTimeUtils#packTime(long)}) - */ - void setLastModTime(long lastModTime) { - this.lastModTime = lastModTime; - } - - /** - * Obtains the last modification date of the entry. - * - * @return the last modification date in MS-DOS format (see - * {@link MsDosDateTimeUtils#packDate(long)}) - */ - public long getLastModDate() { - return lastModDate; - } - - /** - * Sets the last modification date of the entry. - * - * @param lastModDate the last modification date in MS-DOS format (see - * {@link MsDosDateTimeUtils#packDate(long)}) - */ - void setLastModDate(long lastModDate) { - this.lastModDate = lastModDate; - } - - /** - * Obtains the data in the extra field. - * - * @return the data (returns an empty array if there is none) - */ - @Nonnull - public ExtraField getExtraField() { - return extraField; - } - - /** - * Sets the data in the extra field. - * - * @param extraField the data to set - */ - public void setExtraField(@Nonnull ExtraField extraField) { - setExtraFieldNoNotify(extraField); - file.centralDirectoryChanged(); - } - - /** - * Sets the data in the extra field, but does not notify {@link ZFile}. This method is invoked - * when the {@link ZFile} knows the extra field is being set. - * - * @param extraField the data to set - */ - void setExtraFieldNoNotify(@Nonnull ExtraField extraField) { - this.extraField = extraField; - } - - /** - * Obtains the entry's comment. - * - * @return the comment (returns an empty array if there is no comment) - */ - @Nonnull - public byte[] getComment() { - return comment; - } - - /** - * Sets the entry's comment. - * - * @param comment the comment - */ - void setComment(@Nonnull byte[] comment) { - this.comment = comment; - } - - /** - * Obtains the entry's internal attributes. - * - * @return the entry's internal attributes - */ - public long getInternalAttributes() { - return internalAttributes; - } - - /** - * Sets the entry's internal attributes. - * - * @param internalAttributes the entry's internal attributes - */ - void setInternalAttributes(long internalAttributes) { - this.internalAttributes = internalAttributes; - } - - /** - * Obtains the entry's external attributes. - * - * @return the entry's external attributes - */ - public long getExternalAttributes() { - return externalAttributes; - } - - /** - * Sets the entry's external attributes. - * - * @param externalAttributes the entry's external attributes - */ - void setExternalAttributes(long externalAttributes) { - this.externalAttributes = externalAttributes; - } - - /** - * Obtains the offset in the zip file where this entry's data is. - * - * @return the offset or {@code -1} if the file has no data in the zip and, therefore, data - * is stored in memory - */ - public long getOffset() { - return offset; - } - - /** - * Sets the offset in the zip file where this entry's data is. - * - * @param offset the offset or {@code -1} if the file is new and has no data in the zip yet - */ - void setOffset(long offset) { - this.offset = offset; - } - - /** - * Obtains the encoded file name. - * - * @return the encoded file name - */ - public byte[] getEncodedFileName() { - return encodedFileName; - } - - /** - * Resets the deferred CRC flag in the GP flags. - */ - void resetDeferredCrc() { - /* - * We actually create a new set of flags. Since the only information we care about is the - * UTF-8 encoding, we'll just create a brand new object. - */ - gpBit = GPFlags.make(gpBit.isUtf8FileName()); - } - - @Override - protected CentralDirectoryHeader clone() throws CloneNotSupportedException { - CentralDirectoryHeader cdr = (CentralDirectoryHeader) super.clone(); - cdr.extraField = extraField; - cdr.comment = Arrays.copyOf(comment, comment.length); - cdr.encodedFileName = Arrays.copyOf(encodedFileName, encodedFileName.length); - return cdr; - } - - /** - * Obtains the future with the compression information. - * - * @return the information - */ - @Nonnull - public Future getCompressionInfo() { - return compressInfo; - } - - /** - * Equivalent to {@code getCompressionInfo().get()} but masking the possible exceptions and - * guaranteeing non-{@code null} return. - * - * @return the result of the future - * @throws IOException failed to get the information - */ - @Nonnull - public CentralDirectoryHeaderCompressInfo getCompressionInfoWithWait() - throws IOException { - try { - CentralDirectoryHeaderCompressInfo info = getCompressionInfo().get(); - Verify.verifyNotNull(info, "info == null"); - return info; - } catch (InterruptedException e) { - throw new IOException("Interrupted while waiting for compression information.", e); - } catch (ExecutionException e) { - throw new IOException("Execution of compression failed.", e); - } - } -} diff --git a/src/main/java/com/android/apkzlib/zip/CentralDirectoryHeaderCompressInfo.java b/src/main/java/com/android/apkzlib/zip/CentralDirectoryHeaderCompressInfo.java deleted file mode 100644 index 7c3ad63..0000000 --- a/src/main/java/com/android/apkzlib/zip/CentralDirectoryHeaderCompressInfo.java +++ /dev/null @@ -1,119 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * 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.android.apkzlib.zip; - -import javax.annotation.Nonnull; - -/** - * Information stored in the {@link CentralDirectoryHeader} that is related to compression and may - * need to be computed lazily. - */ -public class CentralDirectoryHeaderCompressInfo { - - /** - * Version of zip file that only supports stored files. - */ - public static final long VERSION_WITH_STORE_FILES_ONLY = 10L; - - /** - * Version of zip file that only supports directories and deflated files. - */ - public static final long VERSION_WITH_DIRECTORIES_AND_DEFLATE = 20L; - - /** - * The compression method. - */ - @Nonnull - private final CompressionMethod mMethod; - - /** - * Size of the file compressed. 0 if the file has no data. - */ - private final long compressedSize; - - /** - * Version needed to extract the zip. - */ - private final long versionExtract; - - /** - * Creates new compression information for the central directory header. - * - * @param method the compression method - * @param compressedSize the compressed size - * @param versionToExtract minimum version to extract (typically - * {@link #VERSION_WITH_STORE_FILES_ONLY} or {@link #VERSION_WITH_DIRECTORIES_AND_DEFLATE}) - */ - public CentralDirectoryHeaderCompressInfo( - @Nonnull CompressionMethod method, - long compressedSize, - long versionToExtract) { - mMethod = method; - this.compressedSize = compressedSize; - versionExtract = versionToExtract; - } - - /** - * Creates new compression information for the central directory header. - * - * @param header the header this information relates to - * @param method the compression method - * @param compressedSize the compressed size - */ - public CentralDirectoryHeaderCompressInfo(@Nonnull CentralDirectoryHeader header, - @Nonnull CompressionMethod method, long compressedSize) { - mMethod = method; - this.compressedSize = compressedSize; - - if (header.getName().endsWith("/") || method == CompressionMethod.DEFLATE) { - /* - * Directories and compressed files only in version 2.0. - */ - versionExtract = VERSION_WITH_DIRECTORIES_AND_DEFLATE; - } else { - versionExtract = VERSION_WITH_STORE_FILES_ONLY; - } - } - - /** - * Obtains the compression data size. - * - * @return the compressed data size - */ - public long getCompressedSize() { - return compressedSize; - } - - /** - * Obtains the compression method. - * - * @return the compression method - */ - @Nonnull - public CompressionMethod getMethod() { - return mMethod; - } - - /** - * Obtains the minimum version for extract. - * - * @return the minimum version - */ - public long getVersionExtract() { - return versionExtract; - } -} diff --git a/src/main/java/com/android/apkzlib/zip/CompressionMethod.java b/src/main/java/com/android/apkzlib/zip/CompressionMethod.java deleted file mode 100644 index dd2ee8d..0000000 --- a/src/main/java/com/android/apkzlib/zip/CompressionMethod.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * 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.android.apkzlib.zip; - -import javax.annotation.Nullable; - -/** - * Enumeration with all known compression methods. - */ -public enum CompressionMethod { - /** - * STORE method: data is stored without any compression. - */ - STORE(0), - - /** - * DEFLATE method: data is stored compressed using the DEFLATE algorithm. - */ - DEFLATE(8); - - /** - * Code, within the zip file, that identifies this compression method. - */ - int methodCode; - - /** - * Creates a new compression method. - * - * @param methodCode the code used in the zip file that identifies the compression method - */ - CompressionMethod(int methodCode) { - this.methodCode = methodCode; - } - - /** - * Obtains the compression method that corresponds to the provided code. - * - * @param code the code - * @return the method or {@code null} if no method has the provided code - */ - @Nullable - static CompressionMethod fromCode(long code) { - for (CompressionMethod method : values()) { - if (method.methodCode == code) { - return method; - } - } - - return null; - } -} diff --git a/src/main/java/com/android/apkzlib/zip/CompressionResult.java b/src/main/java/com/android/apkzlib/zip/CompressionResult.java deleted file mode 100644 index 34f5d72..0000000 --- a/src/main/java/com/android/apkzlib/zip/CompressionResult.java +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * 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.android.apkzlib.zip; - -import com.android.apkzlib.zip.utils.CloseableByteSource; -import javax.annotation.Nonnull; - -/** - * Result of compressing data. - */ -public class CompressionResult { - - /** - * The compression method used. - */ - @Nonnull - private final CompressionMethod compressionMethod; - - /** - * The resulting data. - */ - @Nonnull - private final CloseableByteSource source; - - /** - * Size of the compressed source. Kept because {@code source.size()} can throw - * {@code IOException}. - */ - private final long mSize; - - /** - * Creates a new compression result. - * - * @param source the data source - * @param method the compression method - */ - public CompressionResult(@Nonnull CloseableByteSource source, @Nonnull CompressionMethod method, - long size) { - compressionMethod = method; - this.source = source; - mSize = size; - } - - /** - * Obtains the compression method. - * - * @return the compression method - */ - @Nonnull - public CompressionMethod getCompressionMethod() { - return compressionMethod; - } - - /** - * Obtains the compressed data. - * - * @return the data, the resulting array should not be modified - */ - @Nonnull - public CloseableByteSource getSource() { - return source; - } - - /** - * Obtains the size of the compression result. - * - * @return the size - */ - public long getSize() { - return mSize; - } -} diff --git a/src/main/java/com/android/apkzlib/zip/Compressor.java b/src/main/java/com/android/apkzlib/zip/Compressor.java deleted file mode 100644 index a94242e..0000000 --- a/src/main/java/com/android/apkzlib/zip/Compressor.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * 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.android.apkzlib.zip; - -import com.android.apkzlib.zip.utils.CloseableByteSource; -import com.google.common.util.concurrent.ListenableFuture; -import javax.annotation.Nonnull; - -/** - * A compressor is capable of, well, compressing data. Data is read from an {@code ByteSource}. - * Compressors are asynchronous: compressing results in a {@code ListenableFuture} that will contain - * the compression result. - */ -public interface Compressor { - - /** - * Compresses an entry source. - * - * @param source the source to compress - * @return a future that will eventually contain the compression result - */ - @Nonnull - ListenableFuture compress(@Nonnull CloseableByteSource source); -} diff --git a/src/main/java/com/android/apkzlib/zip/DataDescriptorType.java b/src/main/java/com/android/apkzlib/zip/DataDescriptorType.java deleted file mode 100644 index 9b7425e..0000000 --- a/src/main/java/com/android/apkzlib/zip/DataDescriptorType.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * 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.android.apkzlib.zip; - -/** - * Type of data descriptor that an entry has. Data descriptors are used if the CRC and sizing data - * is not known when the data is being written and cannot be placed in the file's local header. - * In those cases, after the file data itself, a data descriptor is placed after the entry's - * contents. - *

- * While the zip specification says the data descriptor should be used but it is optional. We - * record also whether the data descriptor contained the 4-byte signature at the start of the - * block or not. - */ -public enum DataDescriptorType { - /** - * The entry has no data descriptor. - */ - NO_DATA_DESCRIPTOR(0), - - /** - * The entry has a data descriptor that does not contain a signature. - */ - DATA_DESCRIPTOR_WITHOUT_SIGNATURE(12), - - /** - * The entry has a data descriptor that contains a signature. - */ - DATA_DESCRIPTOR_WITH_SIGNATURE(16); - - /** - * The number of bytes the data descriptor spans. - */ - public int size; - - /** - * Creates a new data descriptor. - * - * @param size the number of bytes the data descriptor spans - */ - DataDescriptorType(int size) { - this.size = size; - } -} diff --git a/src/main/java/com/android/apkzlib/zip/EncodeUtils.java b/src/main/java/com/android/apkzlib/zip/EncodeUtils.java deleted file mode 100644 index 259f64e..0000000 --- a/src/main/java/com/android/apkzlib/zip/EncodeUtils.java +++ /dev/null @@ -1,139 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * 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.android.apkzlib.zip; - -import com.google.common.base.Charsets; -import java.io.IOException; -import java.nio.ByteBuffer; -import java.nio.charset.CharacterCodingException; -import java.nio.charset.Charset; -import java.nio.charset.CodingErrorAction; -import javax.annotation.Nonnull; - -/** - * Utilities to encode and decode file names in zips. - */ -public class EncodeUtils { - - /** - * Utility class: no constructor. - */ - private EncodeUtils() { - /* - * Nothing to do. - */ - } - - /** - * Decodes a file name. - * - * @param bytes the raw data buffer to read from - * @param length the number of bytes in the raw data buffer containing the string to decode - * @param flags the zip entry flags - * @return the decode file name - */ - @Nonnull - public static String decode(@Nonnull ByteBuffer bytes, int length, @Nonnull GPFlags flags) - throws IOException { - if (bytes.remaining() < length) { - throw new IOException("Only " + bytes.remaining() + " bytes exist in the buffer, but " - + "length is " + length + "."); - } - - byte[] stringBytes = new byte[length]; - bytes.get(stringBytes); - return decode(stringBytes, flags); - } - - /** - * Decodes a file name. - * - * @param data the raw data - * @param flags the zip entry flags - * @return the decode file name - */ - @Nonnull - public static String decode(@Nonnull byte[] data, @Nonnull GPFlags flags) { - return decode(data, flagsCharset(flags)); - } - - /** - * Decodes a file name. - * - * @param data the raw data - * @param charset the charset to use - * @return the decode file name - */ - @Nonnull - private static String decode(@Nonnull byte[] data, @Nonnull Charset charset) { - try { - return charset.newDecoder() - .onMalformedInput(CodingErrorAction.REPORT) - .decode(ByteBuffer.wrap(data)) - .toString(); - } catch (CharacterCodingException e) { - // If we're trying to decode ASCII, try UTF-8. Otherwise, revert to the default - // behavior (usually replacing invalid characters). - if (charset.equals(Charsets.US_ASCII)) { - return decode(data, Charsets.UTF_8); - } else { - return charset.decode(ByteBuffer.wrap(data)).toString(); - } - } - } - - /** - * Encodes a file name. - * - * @param name the name to encode - * @param flags the zip entry flags - * @return the encoded file name - */ - @Nonnull - public static byte[] encode(@Nonnull String name, @Nonnull GPFlags flags) { - Charset charset = flagsCharset(flags); - ByteBuffer bytes = charset.encode(name); - byte[] result = new byte[bytes.remaining()]; - bytes.get(result); - return result; - } - - /** - * Obtains the charset to encode and decode zip entries, given a set of flags. - * - * @param flags the flags - * @return the charset to use - */ - @Nonnull - private static Charset flagsCharset(@Nonnull GPFlags flags) { - if (flags.isUtf8FileName()) { - return Charsets.UTF_8; - } else { - return Charsets.US_ASCII; - } - } - - /** - * Checks if some text may be encoded using ASCII. - * - * @param text the text to check - * @return can it be encoded using ASCII? - */ - public static boolean canAsciiEncode(String text) { - return Charsets.US_ASCII.newEncoder().canEncode(text); - } -} diff --git a/src/main/java/com/android/apkzlib/zip/Eocd.java b/src/main/java/com/android/apkzlib/zip/Eocd.java deleted file mode 100644 index 1568840..0000000 --- a/src/main/java/com/android/apkzlib/zip/Eocd.java +++ /dev/null @@ -1,271 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * 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.android.apkzlib.zip; - -import com.android.apkzlib.utils.CachedSupplier; -import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.Preconditions; -import com.google.common.base.Verify; -import com.google.common.primitives.Ints; -import java.io.IOException; -import java.io.UncheckedIOException; -import java.nio.ByteBuffer; -import javax.annotation.Nonnull; - -/** - * End Of Central Directory record in a zip file. - */ -class Eocd { - /** - * Field in the record: the record signature, fixed at this value by the specification. - */ - private static final ZipField.F4 F_SIGNATURE = new ZipField.F4(0, 0x06054b50, "EOCD signature"); - - /** - * Field in the record: the number of the disk where the EOCD is located. It has to be zero - * because we do not support multi-file archives. - */ - private static final ZipField.F2 F_NUMBER_OF_DISK = new ZipField.F2(F_SIGNATURE.endOffset(), 0, - "Number of this disk"); - - /** - * Field in the record: the number of the disk where the Central Directory starts. Has to be - * zero because we do not support multi-file archives. - */ - private static final ZipField.F2 F_DISK_CD_START = new ZipField.F2(F_NUMBER_OF_DISK.endOffset(), - 0, "Disk where CD starts"); - - /** - * Field in the record: the number of entries in the Central Directory on this disk. Because - * we do not support multi-file archives, this is the same as {@link #F_RECORDS_TOTAL}. - */ - private static final ZipField.F2 F_RECORDS_DISK = new ZipField.F2(F_DISK_CD_START.endOffset(), - "Record on disk count", new ZipFieldInvariantNonNegative()); - - /** - * Field in the record: the total number of entries in the Central Directory. - */ - private static final ZipField.F2 F_RECORDS_TOTAL = new ZipField.F2(F_RECORDS_DISK.endOffset(), - "Total records", new ZipFieldInvariantNonNegative(), - new ZipFieldInvariantMaxValue(Integer.MAX_VALUE)); - - /** - * Field in the record: number of bytes of the Central Directory. - * This is not private because it is required in unit tests. - */ - @VisibleForTesting - static final ZipField.F4 F_CD_SIZE = new ZipField.F4(F_RECORDS_TOTAL.endOffset(), - "Directory size", new ZipFieldInvariantNonNegative()); - - /** - * Field in the record: offset, from the archive start, where the Central Directory starts. - * This is not private because it is required in unit tests. - */ - @VisibleForTesting - static final ZipField.F4 F_CD_OFFSET = new ZipField.F4(F_CD_SIZE.endOffset(), - "Directory offset", new ZipFieldInvariantNonNegative()); - - /** - * Field in the record: number of bytes of the file comment (located at the end of the EOCD - * record). - */ - private static final ZipField.F2 F_COMMENT_SIZE = new ZipField.F2(F_CD_OFFSET.endOffset(), - "File comment size", new ZipFieldInvariantNonNegative()); - - /** - * Number of entries in the central directory. - */ - private final int totalRecords; - - /** - * Offset from the beginning of the archive where the Central Directory is located. - */ - private final long directoryOffset; - - /** - * Number of bytes of the Central Directory. - */ - private final long directorySize; - - /** - * Contents of the EOCD comment. - */ - @Nonnull - private final byte[] comment; - - /** - * Supplier of the byte representation of the EOCD. - */ - @Nonnull - private final CachedSupplier byteSupplier; - - /** - * Creates a new EOCD, reading it from a byte source. This method will parse the byte source - * and obtain the EOCD. It will check that the byte source starts with the EOCD signature. - * - * @param bytes the byte buffer with the EOCD data; when this method finishes, the byte - * buffer's position will have moved to the end of the EOCD - * @throws IOException failed to read information or the EOCD data is corrupt or invalid - */ - Eocd(@Nonnull ByteBuffer bytes) throws IOException { - - /* - * Read the EOCD record. - */ - F_SIGNATURE.verify(bytes); - F_NUMBER_OF_DISK.verify(bytes); - F_DISK_CD_START.verify(bytes); - long totalRecords1 = F_RECORDS_DISK.read(bytes); - long totalRecords2 = F_RECORDS_TOTAL.read(bytes); - long directorySize = F_CD_SIZE.read(bytes); - long directoryOffset = F_CD_OFFSET.read(bytes); - int commentSize = Ints.checkedCast(F_COMMENT_SIZE.read(bytes)); - - /* - * Some sanity checks. - */ - if (totalRecords1 != totalRecords2) { - throw new IOException("Zip states records split in multiple disks, which is not " - + "supported."); - } - - Verify.verify(totalRecords1 <= Integer.MAX_VALUE); - - totalRecords = Ints.checkedCast(totalRecords1); - this.directorySize = directorySize; - this.directoryOffset = directoryOffset; - - if (bytes.remaining() < commentSize) { - throw new IOException("Corrupt EOCD record: not enough data for comment (comment " - + "size is " + commentSize + ")."); - } - - comment = new byte[commentSize]; - bytes.get(comment); - byteSupplier = new CachedSupplier<>(this::computeByteRepresentation); - } - - /** - * Creates a new EOCD. This is used when generating an EOCD for an Central Directory that has - * just been generated. The EOCD will be generated without any comment. - * - * @param totalRecords total number of records in the directory - * @param directoryOffset offset, since beginning of archive, where the Central Directory is - * located - * @param directorySize number of bytes of the Central Directory - * @param comment the EOCD comment - */ - Eocd(int totalRecords, long directoryOffset, long directorySize, @Nonnull byte[] comment) { - Preconditions.checkArgument(totalRecords >= 0, "totalRecords < 0"); - Preconditions.checkArgument(directoryOffset >= 0, "directoryOffset < 0"); - Preconditions.checkArgument(directorySize >= 0, "directorySize < 0"); - - this.totalRecords = totalRecords; - this.directoryOffset = directoryOffset; - this.directorySize = directorySize; - this.comment = comment; - byteSupplier = new CachedSupplier<>(this::computeByteRepresentation); - } - - /** - * Obtains the number of records in the Central Directory. - * - * @return the number of records - */ - int getTotalRecords() { - return totalRecords; - } - - /** - * Obtains the offset since the beginning of the zip archive where the Central Directory is - * located. - * - * @return the offset where the Central Directory is located - */ - long getDirectoryOffset() { - return directoryOffset; - } - - /** - * Obtains the size of the Central Directory. - * - * @return the number of bytes that make up the Central Directory - */ - long getDirectorySize() { - return directorySize; - } - - /** - * Obtains the size of the EOCD. - * - * @return the size, in bytes, of the EOCD - */ - long getEocdSize() { - return (long) F_COMMENT_SIZE.endOffset() + comment.length; - } - - /** - * Generates the EOCD data. - * - * @return a byte representation of the EOCD that has exactly {@link #getEocdSize()} bytes - * @throws IOException failed to generate the EOCD data - */ - @Nonnull - byte[] toBytes() throws IOException { - return byteSupplier.get(); - } - - /* - * Obtains the comment in the EOCD. - * - * @return the comment exactly as it is represented in the file (no encoding conversion is - * done) - */ - @Nonnull - byte[] getComment() { - byte[] commentCopy = new byte[comment.length]; - System.arraycopy(comment, 0, commentCopy, 0, comment.length); - return commentCopy; - } - - /** - * Computes the byte representation of the EOCD. - * - * @return a byte representation of the EOCD that has exactly {@link #getEocdSize()} bytes - * @throws UncheckedIOException failed to generate the EOCD data - */ - @Nonnull - private byte[] computeByteRepresentation() { - ByteBuffer out = ByteBuffer.allocate(F_COMMENT_SIZE.endOffset() + comment.length); - - try { - F_SIGNATURE.write(out); - F_NUMBER_OF_DISK.write(out); - F_DISK_CD_START.write(out); - F_RECORDS_DISK.write(out, totalRecords); - F_RECORDS_TOTAL.write(out, totalRecords); - F_CD_SIZE.write(out, directorySize); - F_CD_OFFSET.write(out, directoryOffset); - F_COMMENT_SIZE.write(out, comment.length); - out.put(comment); - - return out.array(); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - } -} diff --git a/src/main/java/com/android/apkzlib/zip/ExtraField.java b/src/main/java/com/android/apkzlib/zip/ExtraField.java deleted file mode 100644 index d70fa7f..0000000 --- a/src/main/java/com/android/apkzlib/zip/ExtraField.java +++ /dev/null @@ -1,406 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * 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.android.apkzlib.zip; - -import com.android.apkzlib.zip.utils.LittleEndianUtils; -import com.google.common.base.Preconditions; -import com.google.common.collect.ImmutableList; -import java.io.IOException; -import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.List; -import java.util.stream.Collectors; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -/** - * Contains an extra field. - * - *

According to the zip specification, the extra field is composed of a sequence of fields. - * This class provides a way to access, parse and modify that information. - * - *

The zip specification calls fields to the fields inside the extra field. Because this - * terminology is confusing, we use segment to refer to a part of the extra field. Each - * segment is represented by an instance of {@link Segment} and contains a header ID and data. - * - *

Each instance of {@link ExtraField} is immutable. The extra field of a particular entry can - * be changed by creating a new instanceof {@link ExtraField} and pass it to - * {@link StoredEntry#setLocalExtra(ExtraField)}. - * - *

Instances of {@link ExtraField} can be created directly from the list of segments in it - * or from the raw byte data. If created from the raw byte data, the data will only be parsed - * on demand. So, if neither {@link #getSegments()} nor {@link #getSingleSegment(int)} is - * invoked, the extra field will not be parsed. This guarantees low performance impact of the - * using the extra field unless its contents are needed. - */ -public class ExtraField { - - /** - * Header ID for field with zip alignment. - */ - static final int ALIGNMENT_ZIP_EXTRA_DATA_FIELD_HEADER_ID = 0xd935; - - /** - * The field's raw data, if it is known. Either this variable or {@link #segments} must be - * non-{@code null}. - */ - @Nullable - private final byte[] rawData; - - /** - * The list of field's segments. Will be populated if the extra field is created based on a - * list of segments; will also be populated after parsing if the extra field is created based - * on the raw bytes. - */ - @Nullable - private ImmutableList segments; - - /** - * Creates an extra field based on existing raw data. - * - * @param rawData the raw data; will not be parsed unless needed - */ - public ExtraField(@Nonnull byte[] rawData) { - this.rawData = rawData; - segments = null; - } - - /** - * Creates a new extra field with no segments. - */ - public ExtraField() { - rawData = null; - segments = ImmutableList.of(); - } - - /** - * Creates a new extra field with the given segments. - * - * @param segments the segments - */ - public ExtraField(@Nonnull ImmutableList segments) { - rawData = null; - this.segments = segments; - } - - /** - * Obtains all segments in the extra field. - * - * @return all segments - * @throws IOException failed to parse the extra field - */ - public ImmutableList getSegments() throws IOException { - if (segments == null) { - parseSegments(); - } - - Preconditions.checkNotNull(segments); - return segments; - } - - /** - * Obtains the only segment with the provided header ID. - * - * @param headerId the header ID - * @return the segment found or {@code null} if no segment contains the provided header ID - * @throws IOException there is more than one header with the provided header ID - */ - @Nullable - public Segment getSingleSegment(int headerId) throws IOException { - List found = - getSegments().stream() - .filter(s -> s.getHeaderId() == headerId) - .collect(Collectors.toList()); - if (found.isEmpty()) { - return null; - } else if (found.size() == 1) { - return found.get(0); - } else { - throw new IOException(found.size() + " segments with header ID " + headerId + "found"); - } - } - - /** - * Parses the raw data and generates all segments in {@link #segments}. - * - * @throws IOException failed to parse the data - */ - private void parseSegments() throws IOException { - Preconditions.checkNotNull(rawData); - Preconditions.checkState(segments == null); - - List segments = new ArrayList<>(); - ByteBuffer buffer = ByteBuffer.wrap(rawData); - - while (buffer.remaining() > 0) { - int headerId = LittleEndianUtils.readUnsigned2Le(buffer); - int dataSize = LittleEndianUtils.readUnsigned2Le(buffer); - if (dataSize < 0) { - throw new IOException( - "Invalid data size for extra field segment with header ID " - + headerId - + ": " - + dataSize); - } - - byte[] data = new byte[dataSize]; - if (buffer.remaining() < dataSize) { - throw new IOException( - "Invalid data size for extra field segment with header ID " - + headerId - + ": " - + dataSize - + " (only " - + buffer.remaining() - + " bytes are available)"); - } - buffer.get(data); - - SegmentFactory factory = identifySegmentFactory(headerId); - Segment seg = factory.make(headerId, data); - segments.add(seg); - } - - this.segments = ImmutableList.copyOf(segments); - } - - /** - * Obtains the size of the extra field. - * - * @return the size - */ - public int size() { - if (rawData != null) { - return rawData.length; - } else { - Preconditions.checkNotNull(segments); - int sz = 0; - for (Segment s : segments) { - sz += s.size(); - } - - return sz; - } - } - - /** - * Writes the extra field to the given output buffer. - * - * @param out the output buffer to write the field; exactly {@link #size()} bytes will be - * written - * @throws IOException failed to write the extra fields - */ - public void write(@Nonnull ByteBuffer out) throws IOException { - if (rawData != null) { - out.put(rawData); - } else { - Preconditions.checkNotNull(segments); - for (Segment s : segments) { - s.write(out); - } - } - } - - /** - * Identifies the factory to create the segment with the provided header ID. - * - * @param headerId the header ID - * @return the segmnet factory that creates segments with the given header - */ - @Nonnull - private static SegmentFactory identifySegmentFactory(int headerId) { - if (headerId == ALIGNMENT_ZIP_EXTRA_DATA_FIELD_HEADER_ID) { - return AlignmentSegment::new; - } - - return RawDataSegment::new; - } - - /** - * Field inside the extra field. A segment contains a header ID and data. Specific types of - * segments implement this interface. - */ - public interface Segment { - - /** - * Obtains the segment's header ID. - * - * @return the segment's header ID - */ - int getHeaderId(); - - /** - * Obtains the size of the segment including the header ID. - * - * @return the number of bytes needed to write the segment - */ - int size(); - - /** - * Writes the segment to a buffer. - * - * @param out the buffer where to write the segment to; exactly {@link #size()} bytes will - * be written - * @throws IOException failed to write segment data - */ - void write(@Nonnull ByteBuffer out) throws IOException; - } - - /** - * Factory that creates a segment. - */ - @FunctionalInterface - interface SegmentFactory { - - /** - * Creates a new segment. - * - * @param headerId the header ID - * @param data the segment's data - * @return the created segment - * @throws IOException failed to create the segment from the data - */ - @Nonnull - Segment make(int headerId, @Nonnull byte[] data) throws IOException; - } - - /** - * Segment of raw data: this class represents a general segment containing an array of bytes - * as data. - */ - public static class RawDataSegment implements Segment { - - /** - * Header ID. - */ - private final int headerId; - - /** - * Data in the segment. - */ - @Nonnull - private final byte[] data; - - /** - * Creates a new raw data segment. - * - * @param headerId the header ID - * @param data the segment data - */ - RawDataSegment(int headerId, @Nonnull byte[] data) { - this.headerId = headerId; - this.data = data; - } - - @Override - public int getHeaderId() { - return headerId; - } - - @Override - public void write(@Nonnull ByteBuffer out) throws IOException { - LittleEndianUtils.writeUnsigned2Le(out, headerId); - LittleEndianUtils.writeUnsigned2Le(out, data.length); - out.put(data); - } - - @Override - public int size() { - return 4 + data.length; - } - } - - /** - * Segment with information on an alignment: this segment contains information on how an entry - * should be aligned and contains zero-filled data to force alignment. - * - *

An alignment segment contains the header ID, the size of the data, the alignment value - * and zero bytes to pad - */ - public static class AlignmentSegment implements Segment { - - /** - * Minimum size for an alignment segment. - */ - public static final int MINIMUM_SIZE = 6; - - /** - * The alignment value. - */ - private int alignment; - - /** - * How many bytes of padding are in this segment? - */ - private int padding; - - /** - * Creates a new alignment segment. - * - * @param alignment the alignment value - * @param totalSize how many bytes should this segment take? - */ - public AlignmentSegment(int alignment, int totalSize) { - Preconditions.checkArgument(alignment > 0, "alignment <= 0"); - Preconditions.checkArgument(totalSize >= MINIMUM_SIZE, "totalSize < MINIMUM_SIZE"); - - /* - * We have 6 bytes of fixed data: header ID (2 bytes), data size (2 bytes), alignment - * value (2 bytes). - */ - this.alignment = alignment; - padding = totalSize - MINIMUM_SIZE; - } - - /** - * Creates a new alignment segment from extra data. - * - * @param headerId the header ID - * @param data the segment data - * @throws IOException failed to create the segment from the data - */ - public AlignmentSegment(int headerId, @Nonnull byte[] data) throws IOException { - Preconditions.checkArgument(headerId == ALIGNMENT_ZIP_EXTRA_DATA_FIELD_HEADER_ID); - - ByteBuffer dataBuffer = ByteBuffer.wrap(data); - alignment = LittleEndianUtils.readUnsigned2Le(dataBuffer); - if (alignment <= 0) { - throw new IOException("Invalid alignment in alignment field: " + alignment); - } - - padding = data.length - 2; - } - - @Override - public void write(@Nonnull ByteBuffer out) throws IOException { - LittleEndianUtils.writeUnsigned2Le(out, ALIGNMENT_ZIP_EXTRA_DATA_FIELD_HEADER_ID); - LittleEndianUtils.writeUnsigned2Le(out, padding + 2); - LittleEndianUtils.writeUnsigned2Le(out, alignment); - out.put(new byte[padding]); - } - - @Override - public int size() { - return padding + 6; - } - - @Override - public int getHeaderId() { - return ALIGNMENT_ZIP_EXTRA_DATA_FIELD_HEADER_ID; - } - } -} diff --git a/src/main/java/com/android/apkzlib/zip/FileUseMap.java b/src/main/java/com/android/apkzlib/zip/FileUseMap.java deleted file mode 100644 index 8a76878..0000000 --- a/src/main/java/com/android/apkzlib/zip/FileUseMap.java +++ /dev/null @@ -1,601 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * 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.android.apkzlib.zip; - -import com.google.common.base.Preconditions; -import com.google.common.base.Verify; -import com.google.common.collect.Lists; -import com.google.common.collect.Sets; -import com.google.common.primitives.Ints; -import java.util.List; -import java.util.Set; -import java.util.SortedSet; -import java.util.StringJoiner; -import java.util.TreeSet; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -/** - * The file use map keeps track of which parts of the zip file are used which parts are not. - * It essentially maintains an ordered set of entries ({@link FileUseMapEntry}). Each entry either has - * some data (an entry, the Central Directory, the EOCD) or is a free entry. - * - *

For example: [0-95, "foo/"][95-260, "xpto"][260-310, free][310-360, Central Directory] - * [360-390,EOCD] - * - *

There are a few invariants in this structure: - *

    - *
  • there are no gaps between map entries; - *
  • the map is fully covered up to its size; - *
  • there are no two free entries next to each other; this is guaranteed by coalescing the - * entries upon removal (see {@link #coalesce(FileUseMapEntry)}); - *
  • all free entries have a minimum size defined in the constructor, with the possible exception - * of the last one - *
- */ -class FileUseMap { - /** - * Size of the file according to the map. This should always match the last entry in - * {@code #map}. - */ - private long size; - - /** - * Tree with all intervals ordered by position. Contains coverage from 0 up to {@link #size}. - * If {@link #size} is zero then this set is empty. This is the only situation in which the map - * will be empty. - */ - @Nonnull - private TreeSet> map; - - /** - * Tree with all free blocks ordered by size. This is essentially a view over {@link #map} - * containing only the free blocks, but in a different order. - */ - @Nonnull - private TreeSet> free; - - /** - * If defined, defines the minimum size for a free entry. - */ - private int mMinFreeSize; - - /** - * Creates a new, empty file map. - * - * @param size the size of the file - * @param minFreeSize minimum size of a free entry - */ - FileUseMap(long size, int minFreeSize) { - Preconditions.checkArgument(size >= 0, "size < 0"); - Preconditions.checkArgument(minFreeSize >= 0, "minFreeSize < 0"); - - this.size = size; - map = new TreeSet<>(FileUseMapEntry.COMPARE_BY_START); - free = new TreeSet<>(FileUseMapEntry.COMPARE_BY_SIZE); - mMinFreeSize = minFreeSize; - - if (size > 0) { - internalAdd(FileUseMapEntry.makeFree(0, size)); - } - } - - /** - * Adds an entry to the internal structures. - * - * @param entry the entry to add - */ - private void internalAdd(@Nonnull FileUseMapEntry entry) { - map.add(entry); - - if (entry.isFree()) { - free.add(entry); - } - } - - /** - * Removes an entry from the internal structures. - * - * @param entry the entry to remove - */ - private void internalRemove(@Nonnull FileUseMapEntry entry) { - boolean wasRemoved = map.remove(entry); - Preconditions.checkState(wasRemoved, "entry not in map"); - - if (entry.isFree()) { - free.remove(entry); - } - } - - /** - * Adds a new file to the map. The interval specified by {@code entry} must fit inside an - * empty entry in the map. That entry will be replaced by entry and additional free entries - * will be added before and after if needed to make sure no spaces exist on the map. - * - * @param entry the entry to add - */ - private void add(@Nonnull FileUseMapEntry entry) { - Preconditions.checkArgument(entry.getStart() < size, "entry.getStart() >= size"); - Preconditions.checkArgument(entry.getEnd() <= size, "entry.getEnd() > size"); - Preconditions.checkArgument(!entry.isFree(), "entry.isFree()"); - - FileUseMapEntry container = findContainer(entry); - Verify.verify(container.isFree(), "!container.isFree()"); - - Set> replacements = split(container, entry); - internalRemove(container); - for (FileUseMapEntry r : replacements) { - internalAdd(r); - } - } - - /** - * Removes a file from the map, replacing it with an empty one that is then coalesced with - * neighbors (if the neighbors are free). - * - * @param entry the entry - */ - void remove(@Nonnull FileUseMapEntry entry) { - Preconditions.checkState(map.contains(entry), "!map.contains(entry)"); - Preconditions.checkArgument(!entry.isFree(), "entry.isFree()"); - - internalRemove(entry); - - FileUseMapEntry replacement = FileUseMapEntry.makeFree(entry.getStart(), entry.getEnd()); - internalAdd(replacement); - coalesce(replacement); - } - - /** - * Adds a new file to the map. The interval specified by ({@code start}, {@code end}) must fit - * inside an empty entry in the map. That entry will be replaced by entry and additional free - * entries will be added before and after if needed to make sure no spaces exist on the map. - * - *

The entry cannot extend beyong the end of the map. If necessary, extend the map using - * {@link #extend(long)}. - * - * @param start the start of this entry - * @param end the end of the entry - * @param store extra data to store with the entry - * @param the type of data to store in the entry - * @return the new entry - */ - FileUseMapEntry add(long start, long end, @Nonnull T store) { - Preconditions.checkArgument(start >= 0, "start < 0"); - Preconditions.checkArgument(end > start, "end < start"); - - FileUseMapEntry entry = FileUseMapEntry.makeUsed(start, end, store); - add(entry); - return entry; - } - - /** - * Finds the entry that fully contains the given one. It is assumed that one exists. - * - * @param entry the entry whose container we're looking for - * @return the container - */ - @Nonnull - private FileUseMapEntry findContainer(@Nonnull FileUseMapEntry entry) { - FileUseMapEntry container = map.floor(entry); - Verify.verifyNotNull(container); - Verify.verify(container.getStart() <= entry.getStart()); - Verify.verify(container.getEnd() >= entry.getEnd()); - - return container; - } - - /** - * Splits a container to add an entry, adding new free entries before and after the provided - * entry if needed. - * - * @param container the container entry, a free entry that is in {@link #map} that that - * encloses {@code entry} - * @param entry the entry that will be used to split {@code container} - * @return a set of non-overlapping entries that completely covers {@code container} and that - * includes {@code entry} - */ - @Nonnull - private static Set> split(@Nonnull FileUseMapEntry container, - @Nonnull FileUseMapEntry entry) { - Preconditions.checkArgument(container.isFree(), "!container.isFree()"); - - long farStart = container.getStart(); - long start = entry.getStart(); - long end = entry.getEnd(); - long farEnd = container.getEnd(); - - Verify.verify(farStart <= start, "farStart > start"); - Verify.verify(start < end, "start >= end"); - Verify.verify(farEnd >= end, "farEnd < end"); - - Set> result = Sets.newHashSet(); - if (farStart < start) { - result.add(FileUseMapEntry.makeFree(farStart, start)); - } - - result.add(entry); - - if (end < farEnd) { - result.add(FileUseMapEntry.makeFree(end, farEnd)); - } - - return result; - } - - /** - * Coalesces a free entry replacing it and neighboring free entries with a single, larger - * entry. This method does nothing if {@code entry} does not have free neighbors. - * - * @param entry the free entry to coalesce with neighbors - */ - private void coalesce(@Nonnull FileUseMapEntry entry) { - Preconditions.checkArgument(entry.isFree(), "!entry.isFree()"); - - FileUseMapEntry prevToMerge = null; - long start = entry.getStart(); - if (start > 0) { - /* - * See if we have a previous entry to merge with this one. - */ - prevToMerge = map.floor(FileUseMapEntry.makeFree(start - 1, start)); - Verify.verifyNotNull(prevToMerge); - if (!prevToMerge.isFree()) { - prevToMerge = null; - } - } - - FileUseMapEntry nextToMerge = null; - long end = entry.getEnd(); - if (end < size) { - /* - * See if we have a next entry to merge with this one. - */ - nextToMerge = map.ceiling(FileUseMapEntry.makeFree(end, end + 1)); - Verify.verifyNotNull(nextToMerge); - if (!nextToMerge.isFree()) { - nextToMerge = null; - } - } - - if (prevToMerge == null && nextToMerge == null) { - return; - } - - long newStart = start; - if (prevToMerge != null) { - newStart = prevToMerge.getStart(); - internalRemove(prevToMerge); - } - - long newEnd = end; - if (nextToMerge != null) { - newEnd = nextToMerge.getEnd(); - internalRemove(nextToMerge); - } - - internalRemove(entry); - internalAdd(FileUseMapEntry.makeFree(newStart, newEnd)); - } - - /** - * Truncates map removing the top entry if it is free and reducing the map's size. - */ - void truncate() { - if (size == 0) { - return; - } - - /* - * Find the last entry. - */ - FileUseMapEntry last = map.last(); - Verify.verifyNotNull(last, "last == null"); - if (last.isFree()) { - internalRemove(last); - size = last.getStart(); - } - } - - /** - * Obtains the size of the map. - * - * @return the size - */ - long size() { - return size; - } - - /** - * Obtains the largest used offset in the map. This will be size of the map after truncation. - * - * @return the size of the file discounting the last block if it is empty - */ - long usedSize() { - if (size == 0) { - return 0; - } - - /* - * Find the last entry to see if it is an empty entry. If it is, we need to remove its size - * from the returned value. - */ - FileUseMapEntry last = map.last(); - Verify.verifyNotNull(last, "last == null"); - if (last.isFree()) { - return last.getStart(); - } else { - Verify.verify(last.getEnd() == size); - return size; - } - } - - /** - * Extends the map to guarantee it has at least {@code size} bytes. If the current size is - * as large as {@code size}, this method does nothing. - * - * @param size the new size of the map that cannot be smaller that the current size - */ - void extend(long size) { - Preconditions.checkArgument(size >= this.size, "size < size"); - - if (this.size == size) { - return; - } - - FileUseMapEntry newBlock = FileUseMapEntry.makeFree(this.size, size); - internalAdd(newBlock); - - this.size = size; - - coalesce(newBlock); - } - - /** - * Locates a free area in the map with at least {@code size} bytes such that - * {@code ((start + alignOffset) % align == 0} and such that the free space before {@code start} - * is not smaller than the minimum free entry size. This method will follow the algorithm - * specified by {@code alg}. - * - *

If no free contiguous block exists in the map that can hold the provided - * size then the first free index at the end of the map is provided. This means that the map - * may need to be extended before data can be added. - * - * @param size the size of the contiguous area requested - * @param alignOffset an offset to which alignment needs to be computed (see method description) - * @param align alignment at the offset (see method description) - * @param alg which algorithm to use - * @return the location of the contiguous area; this may be located at the end of the map - */ - long locateFree(long size, long alignOffset, long align, @Nonnull PositionAlgorithm alg) { - Preconditions.checkArgument(size > 0, "size <= 0"); - - FileUseMapEntry minimumSizedEntry = FileUseMapEntry.makeFree(0, size); - SortedSet> matches; - - switch (alg) { - case BEST_FIT: - matches = free.tailSet(minimumSizedEntry); - break; - case FIRST_FIT: - matches = map; - break; - default: - throw new AssertionError(); - } - - FileUseMapEntry best = null; - long bestExtraSize = 0; - for (FileUseMapEntry curr : matches) { - /* - * We don't care about blocks that aren't free. - */ - if (!curr.isFree()) { - continue; - } - - /* - * Compute any extra size we need in this block to make sure we verify the alignment. - * There must be a better to do this... - */ - long extraSize; - if (align == 0) { - extraSize = 0; - } else { - extraSize = (align - ((curr.getStart() + alignOffset) % align)) % align; - } - - /* - * We can't leave than mMinFreeSize before. So if the extraSize is less than - * mMinFreeSize, we have to increase it by 'align' as many times as needed. For - * example, if mMinFreeSize is 20, align 4 and extraSize is 5. We need to increase it - * to 21 (5 + 4 * 4) - */ - if (extraSize > 0 && extraSize < mMinFreeSize) { - int addAlignBlocks = - Ints.checkedCast((mMinFreeSize - extraSize + align - 1) / align); - extraSize += addAlignBlocks * align; - } - - /* - * We don't care about blocks where we don't fit in. - */ - if (curr.getSize() < (size + extraSize)) { - continue; - } - - /* - * We don't care about blocks that leave less than the minimum size after. There are - * two exceptions: (1) this is the last block and (2) the next block is free in which - * case, after coalescing, the free block with have at least the minimum size. - */ - long emptySpaceLeft = curr.getSize() - (size + extraSize); - if (emptySpaceLeft > 0 && emptySpaceLeft < mMinFreeSize) { - FileUseMapEntry next = map.higher(curr); - if (next != null && !next.isFree()) { - continue; - } - } - - /* - * We don't care about blocks that are bigger than the best so far (otherwise this - * wouldn't be a best-fit algorithm). - */ - if (best != null && best.getSize() < curr.getSize()) { - continue; - } - - best = curr; - bestExtraSize = extraSize; - - /* - * If we're doing first fit, we don't want to search for a better one :) - */ - if (alg == PositionAlgorithm.FIRST_FIT) { - break; - } - } - - /* - * If no entry that could hold size is found, get the first free byte. - */ - long firstFree = this.size; - if (best == null && !map.isEmpty()) { - FileUseMapEntry last = map.last(); - if (last.isFree()) { - firstFree = last.getStart(); - } - } - - /* - * We're done: either we found something or we didn't, in which the new entry needs to - * be added to the end of the map. - */ - if (best == null) { - long extra = (align - ((firstFree + alignOffset) % align)) % align; - - /* - * If adding this entry at the end would create a space smaller than the minimum, - * push it for 'align' bytes forward. - */ - if (extra > 0) { - if (extra < mMinFreeSize) { - extra += align * (((mMinFreeSize - extra) + (align - 1)) / align); - } - } - - return firstFree + extra; - } else { - return best.getStart() + bestExtraSize; - } - } - - /** - * Obtains all free areas of the map, excluding any trailing free area. - * - * @return all free areas, an empty set if there are no free areas; the areas are returned - * in file order, that is, if area {@code x} starts before area {@code y}, then area {@code x} - * will be stored before area {@code y} in the list - */ - @Nonnull - List> getFreeAreas() { - List> freeAreas = Lists.newArrayList(); - - for (FileUseMapEntry area : map) { - if (area.isFree() && area.getEnd() != size) { - freeAreas.add(area); - } - } - - return freeAreas; - } - - /** - * Obtains the entry that is located before the one provided. - * - * @param entry the map entry to get the previous one for; must belong to the map - * @return the entry before the provided one, {@code null} if {@code entry} is the first entry - * in the map - */ - @Nullable - FileUseMapEntry before(@Nonnull FileUseMapEntry entry) { - Preconditions.checkNotNull(entry, "entry == null"); - - return map.lower(entry); - } - - /** - * Obtains the entry that is located after the one provided. - * - * @param entry the map entry to get the next one for; must belong to the map - * @return the entry after the provided one, {@code null} if {@code entry} is the last entry in - * the map - */ - @Nullable - FileUseMapEntry after(@Nonnull FileUseMapEntry entry) { - Preconditions.checkNotNull(entry, "entry == null"); - - return map.higher(entry); - } - - /** - * Obtains the entry at the given offset. - * - * @param offset the offset to look for - * @return the entry found or {@code null} if there is no entry (not even a free one) at the - * given offset - */ - @Nullable - FileUseMapEntry at(long offset) { - Preconditions.checkArgument(offset >= 0, "offset < 0"); - Preconditions.checkArgument(offset < size, "offset >= size"); - - FileUseMapEntry entry = map.floor(FileUseMapEntry.makeFree(offset, offset + 1)); - if (entry == null) { - return null; - } - - Verify.verify(entry.getStart() <= offset); - Verify.verify(entry.getEnd() > offset); - - return entry; - } - - @Override - public String toString() { - StringJoiner j = new StringJoiner(", "); - map.stream() - .map(e -> e.getStart() + " - " + e.getEnd() + ": " + e.getStore()) - .forEach(j::add); - return "FileUseMap[" + j.toString() + "]"; - } - - /** - * Algorithms used to position entries in blocks. - */ - public enum PositionAlgorithm { - /** - * Best fit: finds the smallest free block that can receive the entry. - */ - BEST_FIT, - - /** - * First fit: finds the first free block that can receive the entry. - */ - FIRST_FIT - } -} diff --git a/src/main/java/com/android/apkzlib/zip/FileUseMapEntry.java b/src/main/java/com/android/apkzlib/zip/FileUseMapEntry.java deleted file mode 100644 index 3e5aaba..0000000 --- a/src/main/java/com/android/apkzlib/zip/FileUseMapEntry.java +++ /dev/null @@ -1,162 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * 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.android.apkzlib.zip; - -import com.google.common.base.MoreObjects; -import com.google.common.base.Preconditions; -import com.google.common.primitives.Ints; -import java.util.Comparator; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -/** - * Represents an entry in the {@link FileUseMap}. Each entry contains an interval of bytes. The - * end of the interval is exclusive. - *

- * Entries can either be free or used. Used entries must store an object. Free entries - * do not store anything. - *

- * File map entries are used to keep track of which parts of a file map are used and not. - * @param the type of data stored - */ -class FileUseMapEntry { - - /** - * Comparator that compares entries by their start date. - */ - public static final Comparator> COMPARE_BY_START = - (o1, o2) -> Ints.saturatedCast(o1.getStart() - o2.getStart()); - - /** - * Comparator that compares entries by their size. - */ - public static final Comparator> COMPARE_BY_SIZE = - (o1, o2) -> Ints.saturatedCast(o1.getSize() - o2.getSize()); - - /** - * The first byte in the entry. - */ - private final long start; - - /** - * The first byte no longer in the entry. - */ - private final long end; - - /** - * The stored data. If {@code null} then this entry represents a free entry. - */ - @Nullable - private final T store; - - /** - * Creates a new map entry. - * - * @param start the start of the entry - * @param end the end of the entry (first byte no longer in the entry) - * @param store the data to store in the entry or {@code null} if this is a free entry - */ - private FileUseMapEntry(long start, long end, @Nullable T store) { - Preconditions.checkArgument(start >= 0, "start < 0"); - Preconditions.checkArgument(end > start, "end <= start"); - - this.start = start; - this.end = end; - this.store = store; - } - - /** - * Creates a new free entry. - * - * @param start the start of the entry - * @param end the end of the entry (first byte no longer in the entry) - * @return the entry - */ - public static FileUseMapEntry makeFree(long start, long end) { - return new FileUseMapEntry<>(start, end, null); - } - - /** - * Creates a new used entry. - * - * @param start the start of the entry - * @param end the end of the entry (first byte no longer in the entry) - * @param store the data to store in the entry - * @param the type of data to store in the entry - * @return the entry - */ - public static FileUseMapEntry makeUsed(long start, long end, @Nonnull T store) { - Preconditions.checkNotNull(store, "store == null"); - return new FileUseMapEntry<>(start, end, store); - } - - /** - * Obtains the first byte in the entry. - * - * @return the first byte in the entry (if the same value as {@link #getEnd()} then the entry - * is empty and contains no data) - */ - long getStart() { - return start; - } - - /** - * Obtains the first byte no longer in the entry. - * - * @return the first byte no longer in the entry - */ - long getEnd() { - return end; - } - - /** - * Obtains the size of the entry. - * - * @return the number of bytes contained in the entry - */ - long getSize() { - return end - start; - } - - /** - * Determines if this is a free entry. - * - * @return is this entry free? - */ - boolean isFree() { - return store == null; - } - - /** - * Obtains the data stored in the entry. - * - * @return the data stored or {@code null} if this entry is a free entry - */ - @Nullable - T getStore() { - return store; - } - - @Override - public String toString() { - return MoreObjects.toStringHelper(this) - .add("start", start) - .add("end", end) - .add("store", store) - .toString(); - } -} diff --git a/src/main/java/com/android/apkzlib/zip/GPFlags.java b/src/main/java/com/android/apkzlib/zip/GPFlags.java deleted file mode 100644 index fc27c5d..0000000 --- a/src/main/java/com/android/apkzlib/zip/GPFlags.java +++ /dev/null @@ -1,179 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * 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.android.apkzlib.zip; - -import java.io.IOException; -import javax.annotation.Nonnull; - -/** - * General purpose bit flags. Contains the encoding of the zip's general purpose bits. - * - *

We don't really care about the method bit(s). These are bits 1 and 2. Here are the values: - *

    - *
  • 0 (00): Normal (-en) compression option was used. - *
  • 1 (01): Maximum (-exx/-ex) compression option was used. - *
  • 2 (10): Fast (-ef) compression option was used. - *
  • 3 (11): Super Fast (-es) compression option was used. - *
- */ -class GPFlags { - - /** - * Is the entry encrypted? - */ - private static final int BIT_ENCRYPTION = 1; - - /** - * Has CRC computation been deferred and, therefore, does a data description block exist? - */ - private static final int BIT_DEFERRED_CRC = (1 << 3); - - /** - * Is enhanced deflating used? - */ - private static final int BIT_ENHANCED_DEFLATING = (1 << 4); - - /** - * Does the entry contain patched data? - */ - private static final int BIT_PATCHED_DATA = (1 << 5); - - /** - * Is strong encryption used? - */ - private static final int BIT_STRONG_ENCRYPTION = (1 << 6) | (1 << 13); - - /** - * If this bit is set the filename and comment fields for this file must be encoded using UTF-8. - */ - private static final int BIT_EFS = (1 << 11); - - /** - * Unused bits. - */ - private static final int BIT_UNUSED = (1 << 7) | (1 << 8) | (1 << 9) | (1 << 10) - | (1 << 14) | (1 << 15); - - /** - * Bit flag value. - */ - private final long value; - - /** - * Has the CRC computation beeen deferred? - */ - private boolean deferredCrc; - - /** - * Is the file name encoded in UTF-8? - */ - private boolean utf8FileName; - - /** - * Creates a new flags object. - * - * @param value the value of the bit mask - */ - private GPFlags(long value) { - this.value = value; - - deferredCrc = ((value & BIT_DEFERRED_CRC) != 0); - utf8FileName = ((value & BIT_EFS) != 0); - } - - /** - * Obtains the flags value. - * - * @return the value of the bit mask - */ - public long getValue() { - return value; - } - - /** - * Is the CRC computation deferred? - * - * @return is the CRC computation deferred? - */ - public boolean isDeferredCrc() { - return deferredCrc; - } - - /** - * Is the file name encoded in UTF-8? - * - * @return is the file name encoded in UTF-8? - */ - public boolean isUtf8FileName() { - return utf8FileName; - } - - /** - * Creates a new bit mask. - * - * @param utf8Encoding should UTF-8 encoding be used? - * @return the new bit mask - */ - @Nonnull - static GPFlags make(boolean utf8Encoding) { - long flags = 0; - - if (utf8Encoding) { - flags |= BIT_EFS; - } - - return new GPFlags(flags); - } - - /** - * Creates the flag information from a byte. This method will also validate that only - * supported options are defined in the flag. - * - * @param bits the bit mask - * @return the created flag information - * @throws IOException unsupported options are used in the bit mask - */ - @Nonnull - static GPFlags from(long bits) throws IOException { - if ((bits & BIT_ENCRYPTION) != 0) { - throw new IOException("Zip files with encrypted of entries not supported."); - } - - if ((bits & BIT_ENHANCED_DEFLATING) != 0) { - throw new IOException("Enhanced deflating not supported."); - } - - if ((bits & BIT_PATCHED_DATA) != 0) { - throw new IOException("Compressed patched data not supported."); - } - - if ((bits & BIT_STRONG_ENCRYPTION) != 0) { - throw new IOException("Strong encryption not supported."); - } - - if ((bits & BIT_UNUSED) != 0) { - throw new IOException("Unused bits set in directory entry. Weird. I don't know what's " - + "going on."); - } - - if ((bits & 0xffffffff00000000L) != 0) { - throw new IOException("Unsupported bits after 32."); - } - - return new GPFlags(bits); - } -} diff --git a/src/main/java/com/android/apkzlib/zip/InflaterByteSource.java b/src/main/java/com/android/apkzlib/zip/InflaterByteSource.java deleted file mode 100644 index 974d4ac..0000000 --- a/src/main/java/com/android/apkzlib/zip/InflaterByteSource.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * 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.android.apkzlib.zip; - -import com.android.apkzlib.zip.utils.CloseableByteSource; -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.SequenceInputStream; -import java.util.zip.Inflater; -import java.util.zip.InflaterInputStream; -import javax.annotation.Nonnull; - -/** - * Byte source that inflates another byte source. It assumed the inner byte source has deflated - * data. - */ -public class InflaterByteSource extends CloseableByteSource { - - /** - * The stream factory for the deflated data. - */ - @Nonnull - private final CloseableByteSource deflatedSource; - - /** - * Creates a new source. - * @param byteSource the factory for deflated data - */ - public InflaterByteSource(@Nonnull CloseableByteSource byteSource) { - deflatedSource = byteSource; - } - - @Override - public InputStream openStream() throws IOException { - /* - * The extra byte is a dummy byte required by the inflater. Weirdo. - * (see the java.util.Inflater documentation). Looks like a hack... - * "Oh, I need an extra dummy byte to allow for some... err... optimizations..." - */ - ByteArrayInputStream hackByte = new ByteArrayInputStream(new byte[] { 0 }); - return new InflaterInputStream(new SequenceInputStream(deflatedSource.openStream(), - hackByte), new Inflater(true)); - } - - @Override - public void innerClose() throws IOException { - deflatedSource.close(); - } -} diff --git a/src/main/java/com/android/apkzlib/zip/LazyDelegateByteSource.java b/src/main/java/com/android/apkzlib/zip/LazyDelegateByteSource.java deleted file mode 100644 index bdd3e4c..0000000 --- a/src/main/java/com/android/apkzlib/zip/LazyDelegateByteSource.java +++ /dev/null @@ -1,157 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * 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.android.apkzlib.zip; - - -import com.android.apkzlib.zip.utils.CloseableByteSource; -import com.google.common.hash.HashCode; -import com.google.common.hash.HashFunction; -import com.google.common.io.ByteProcessor; -import com.google.common.io.ByteSink; -import com.google.common.io.ByteSource; -import com.google.common.io.CharSource; -import com.google.common.util.concurrent.ListenableFuture; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.nio.charset.Charset; -import java.util.concurrent.ExecutionException; -import javax.annotation.Nonnull; - -/** - * {@code ByteSource} that delegates all operations to another {@code ByteSource}. The other - * byte source, the delegate, may be computed lazily. - */ -public class LazyDelegateByteSource extends CloseableByteSource { - - /** - * Byte source where we delegate operations to. - */ - @Nonnull - private final ListenableFuture delegate; - - /** - * Creates a new byte source that delegates operations to the provided source. - * @param delegate the source that will receive all operations - */ - public LazyDelegateByteSource(@Nonnull ListenableFuture delegate) { - this.delegate = delegate; - } - - /** - * Obtains the delegate future. - * @return the delegate future, that may be computed or not - */ - @Nonnull - public ListenableFuture getDelegate() { - return delegate; - } - - /** - * Obtains the byte source, waiting for the future to be computed. - * @return the byte source - * @throws IOException failed to compute the future :) - */ - @Nonnull - private CloseableByteSource get() throws IOException { - try { - CloseableByteSource r = delegate.get(); - if (r == null) { - throw new IOException("Delegate byte source computation resulted in null."); - } - - return r; - } catch (InterruptedException e) { - throw new IOException("Interrupted while waiting for byte source computation.", e); - } catch (ExecutionException e) { - throw new IOException("Failed to compute byte source.", e); - } - } - - @Override - public CharSource asCharSource(Charset charset) { - try { - return get().asCharSource(charset); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - @Override - public InputStream openBufferedStream() throws IOException { - return get().openBufferedStream(); - } - - @Override - public ByteSource slice(long offset, long length) { - try { - return get().slice(offset, length); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - @Override - public boolean isEmpty() throws IOException { - return get().isEmpty(); - } - - @Override - public long size() throws IOException { - return get().size(); - } - - @Override - public long copyTo(@Nonnull OutputStream output) throws IOException { - return get().copyTo(output); - } - - @Override - public long copyTo(@Nonnull ByteSink sink) throws IOException { - return get().copyTo(sink); - } - - @Override - public byte[] read() throws IOException { - return get().read(); - } - - @Override - public T read(@Nonnull ByteProcessor processor) throws IOException { - return get().read(processor); - } - - @Override - public HashCode hash(HashFunction hashFunction) throws IOException { - return get().hash(hashFunction); - } - - @Override - public boolean contentEquals(@Nonnull ByteSource other) throws IOException { - return get().contentEquals(other); - } - - @Override - public InputStream openStream() throws IOException { - return get().openStream(); - } - - @Override - public void innerClose() throws IOException { - get().close(); - } -} diff --git a/src/main/java/com/android/apkzlib/zip/ProcessedAndRawByteSources.java b/src/main/java/com/android/apkzlib/zip/ProcessedAndRawByteSources.java deleted file mode 100644 index 86c6382..0000000 --- a/src/main/java/com/android/apkzlib/zip/ProcessedAndRawByteSources.java +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * 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.android.apkzlib.zip; - -import com.android.apkzlib.zip.utils.CloseableByteSource; -import com.google.common.io.Closer; -import java.io.Closeable; -import java.io.IOException; -import javax.annotation.Nonnull; - -/** - * Container that has two bytes sources: one representing raw data and another processed data. - * In case of compression, the raw data is the compressed data and the processed data is the - * uncompressed data. It is valid for a RaP ("Raw-and-Processed") to contain the same byte sources - * for both processed and raw data. - */ -public class ProcessedAndRawByteSources implements Closeable { - - /** - * The processed byte source. - */ - @Nonnull - private final CloseableByteSource processedSource; - - /** - * The processed raw source. - */ - @Nonnull - private final CloseableByteSource rawSource; - - /** - * Creates a new container. - * - * @param processedSource the processed source - * @param rawSource the raw source - */ - public ProcessedAndRawByteSources(@Nonnull CloseableByteSource processedSource, - @Nonnull CloseableByteSource rawSource) { - this.processedSource = processedSource; - this.rawSource = rawSource; - } - - /** - * Obtains a byte source that read the processed contents of the entry. - * - * @return a byte source - */ - @Nonnull - public CloseableByteSource getProcessedByteSource() { - return processedSource; - } - - /** - * Obtains a byte source that reads the raw contents of an entry. This is the data that is - * ultimately stored in the file and, in the case of compressed files, is the same data in the - * source returned by {@link #getProcessedByteSource()}. - * - * @return a byte source - */ - @Nonnull - public CloseableByteSource getRawByteSource() { - return rawSource; - } - - @Override - public void close() throws IOException { - Closer closer = Closer.create(); - closer.register(processedSource); - closer.register(rawSource); - closer.close(); - } -} diff --git a/src/main/java/com/android/apkzlib/zip/StoredEntry.java b/src/main/java/com/android/apkzlib/zip/StoredEntry.java deleted file mode 100644 index 854bf3a..0000000 --- a/src/main/java/com/android/apkzlib/zip/StoredEntry.java +++ /dev/null @@ -1,818 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * 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.android.apkzlib.zip; - -import com.android.apkzlib.zip.utils.CloseableByteSource; -import com.android.apkzlib.zip.utils.CloseableDelegateByteSource; -import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.Preconditions; -import com.google.common.base.Verify; -import com.google.common.io.ByteSource; -import com.google.common.io.ByteStreams; -import com.google.common.primitives.Ints; -import java.io.BufferedInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.nio.ByteBuffer; -import java.util.Comparator; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -/** - * A stored entry represents a file in the zip. The entry may or may not be written to the zip - * file. - * - *

Stored entries provide the operations that are related to the files themselves, not to the - * zip. It is through the {@code StoredEntry} class that entries can be deleted ({@link #delete()}, - * open ({@link #open()}) or realigned ({@link #realign()}). - * - *

Entries are not created directly. They are created using - * {@link ZFile#add(String, InputStream, boolean)} and obtained from the zip file - * using {@link ZFile#get(String)} or {@link ZFile#entries()}. - * - *

Most of the data in the an entry is in the Central Directory Header. This includes the name, - * compression method, file compressed and uncompressed sizes, CRC32 checksum, etc. The CDH can - * be obtained using the {@link #getCentralDirectoryHeader()} method. - */ -public class StoredEntry { - - /** - * Comparator that compares instances of {@link StoredEntry} by their names. - */ - static final Comparator COMPARE_BY_NAME = - (o1, o2) -> { - if (o1 == null && o2 == null) { - return 0; - } - - if (o1 == null) { - return -1; - } - - if (o2 == null) { - return 1; - } - - String name1 = o1.getCentralDirectoryHeader().getName(); - String name2 = o2.getCentralDirectoryHeader().getName(); - return name1.compareTo(name2); - }; - - /** - * Signature of the data descriptor. - */ - private static final int DATA_DESC_SIGNATURE = 0x08074b50; - - /** - * Local header field: signature. - */ - private static final ZipField.F4 F_LOCAL_SIGNATURE = new ZipField.F4(0, 0x04034b50, - "Signature"); - - /** - * Local header field: version to extract, should match the CDH's. - */ - @VisibleForTesting - static final ZipField.F2 F_VERSION_EXTRACT = new ZipField.F2( - F_LOCAL_SIGNATURE.endOffset(), "Version to extract", - new ZipFieldInvariantNonNegative()); - - /** - * Local header field: GP bit flag, should match the CDH's. - */ - private static final ZipField.F2 F_GP_BIT = new ZipField.F2(F_VERSION_EXTRACT.endOffset(), - "GP bit flag"); - - /** - * Local header field: compression method, should match the CDH's. - */ - private static final ZipField.F2 F_METHOD = new ZipField.F2(F_GP_BIT.endOffset(), - "Compression method", new ZipFieldInvariantNonNegative()); - - /** - * Local header field: last modification time, should match the CDH's. - */ - private static final ZipField.F2 F_LAST_MOD_TIME = new ZipField.F2(F_METHOD.endOffset(), - "Last modification time"); - - /** - * Local header field: last modification time, should match the CDH's. - */ - private static final ZipField.F2 F_LAST_MOD_DATE = new ZipField.F2(F_LAST_MOD_TIME.endOffset(), - "Last modification date"); - - /** - * Local header field: CRC32 checksum, should match the CDH's. 0 if there is no data. - */ - private static final ZipField.F4 F_CRC32 = new ZipField.F4(F_LAST_MOD_DATE.endOffset(), - "CRC32"); - - /** - * Local header field: compressed size, size the data takes in the zip file. - */ - private static final ZipField.F4 F_COMPRESSED_SIZE = new ZipField.F4(F_CRC32.endOffset(), - "Compressed size", new ZipFieldInvariantNonNegative()); - - /** - * Local header field: uncompressed size, size the data takes after extraction. - */ - private static final ZipField.F4 F_UNCOMPRESSED_SIZE = new ZipField.F4( - F_COMPRESSED_SIZE.endOffset(), "Uncompressed size", new ZipFieldInvariantNonNegative()); - - /** - * Local header field: length of the file name. - */ - private static final ZipField.F2 F_FILE_NAME_LENGTH = new ZipField.F2( - F_UNCOMPRESSED_SIZE.endOffset(), "@File name length", - new ZipFieldInvariantNonNegative()); - - /** - * Local header filed: length of the extra field. - */ - private static final ZipField.F2 F_EXTRA_LENGTH = new ZipField.F2( - F_FILE_NAME_LENGTH.endOffset(), "Extra length", new ZipFieldInvariantNonNegative()); - - /** - * Local header size (fixed part, not counting file name or extra field). - */ - static final int FIXED_LOCAL_FILE_HEADER_SIZE = F_EXTRA_LENGTH.endOffset(); - - /** - * Type of entry. - */ - @Nonnull - private StoredEntryType type; - - /** - * The central directory header with information about the file. - */ - @Nonnull - private CentralDirectoryHeader cdh; - - /** - * The file this entry is associated with - */ - @Nonnull - private ZFile file; - - /** - * Has this entry been deleted? - */ - private boolean deleted; - - /** - * Extra field specified in the local directory. - */ - @Nonnull - private ExtraField localExtra; - - /** - * Type of data descriptor associated with the entry. - */ - @Nonnull - private DataDescriptorType dataDescriptorType; - - /** - * Source for this entry's data. If this entry is a directory, this source has to have zero - * size. - */ - @Nonnull - private ProcessedAndRawByteSources source; - - /** - * Verify log for the entry. - */ - @Nonnull - private final VerifyLog verifyLog; - - /** - * Creates a new stored entry. - * - * @param header the header with the entry information; if the header does not contain an - * offset it means that this entry is not yet written in the zip file - * @param file the zip file containing the entry - * @param source the entry's data source; it can be {@code null} only if the source can be - * read from the zip file, that is, if {@code header.getOffset()} is non-negative - * @throws IOException failed to create the entry - */ - StoredEntry( - @Nonnull CentralDirectoryHeader header, - @Nonnull ZFile file, - @Nullable ProcessedAndRawByteSources source) - throws IOException { - cdh = header; - this.file = file; - deleted = false; - verifyLog = file.makeVerifyLog(); - - if (header.getOffset() >= 0) { - /* - * This will be overwritten during readLocalHeader. However, IJ complains if we don't - * assign a value to localExtra because of the @Nonnull annotation. - */ - localExtra = new ExtraField(); - - readLocalHeader(); - - Preconditions.checkArgument( - source == null, - "Source was defined but contents already exist on file."); - - /* - * Since the file is already in the zip, dynamically create a source that will read - * the file from the zip when needed. The assignment is not really needed, but we - * would get a warning because of the @NotNull otherwise. - */ - this.source = createSourceFromZip(cdh.getOffset()); - } else { - /* - * There is no local extra data for new files. - */ - localExtra = new ExtraField(); - - Preconditions.checkNotNull( - source, - "Source was not defined, but contents are not on file."); - this.source = source; - } - - /* - * It seems that zip utilities store directories as names ending with "/". - * This seems to be respected by all zip utilities although I could not find there anywhere - * in the specification. - */ - if (cdh.getName().endsWith(Character.toString(ZFile.SEPARATOR))) { - type = StoredEntryType.DIRECTORY; - verifyLog.verify( - this.source.getProcessedByteSource().isEmpty(), - "Directory source is not empty."); - verifyLog.verify(cdh.getCrc32() == 0, "Directory has CRC32 = %s.", cdh.getCrc32()); - verifyLog.verify( - cdh.getUncompressedSize() == 0, - "Directory has uncompressed size = %s.", - cdh.getUncompressedSize()); - - /* - * Some clever (OMG!) tools, like jar will actually try to compress the directory - * contents and generate a 2 byte compressed data. Of course, the uncompressed size is - * zero and we're just wasting space. - */ - long compressedSize = cdh.getCompressionInfoWithWait().getCompressedSize(); - verifyLog.verify( - compressedSize == 0 || compressedSize == 2, - "Directory has compressed size = %s.", compressedSize); - } else { - type = StoredEntryType.FILE; - } - - /* - * By default we assume there is no data descriptor unless the CRC is marked as deferred - * in the header's GP Bit. - */ - dataDescriptorType = DataDescriptorType.NO_DATA_DESCRIPTOR; - if (header.getGpBit().isDeferredCrc()) { - /* - * If the deferred CRC bit exists, then we have an extra descriptor field. This extra - * field may have a signature. - */ - Verify.verify(header.getOffset() >= 0, "Files that are not on disk cannot have the " - + "deferred CRC bit set."); - - try { - readDataDescriptorRecord(); - } catch (IOException e) { - throw new IOException("Failed to read data descriptor record.", e); - } - } - } - - /** - * Obtains the size of the local header of this entry. - * - * @return the local header size in bytes - */ - public int getLocalHeaderSize() { - Preconditions.checkState(!deleted, "deleted"); - return FIXED_LOCAL_FILE_HEADER_SIZE + cdh.getEncodedFileName().length + localExtra.size(); - } - - /** - * Obtains the size of the whole entry on disk, including local header and data descriptor. - * This method will wait until compression information is complete, if needed. - * - * @return the number of bytes - * @throws IOException failed to get compression information - */ - long getInFileSize() throws IOException { - Preconditions.checkState(!deleted, "deleted"); - return cdh.getCompressionInfoWithWait().getCompressedSize() + getLocalHeaderSize() - + dataDescriptorType.size; - } - - /** - * Obtains a stream that allows reading from the entry. - * - * @return a stream that will return as many bytes as the uncompressed entry size - * @throws IOException failed to open the stream - */ - @Nonnull - public InputStream open() throws IOException { - return source.getProcessedByteSource().openStream(); - } - - /** - * Obtains the contents of the file. - * - * @return a byte array with the contents of the file (uncompressed if the file was compressed) - * @throws IOException failed to read the file - */ - @Nonnull - public byte[] read() throws IOException { - try (InputStream is = open()) { - return ByteStreams.toByteArray(is); - } - } - - /** - * Obtains the contents of the file in an existing buffer. - * - * @param bytes buffer to read the file contents in. - * @return the number of bytes read - * @throws IOException failed to read the file. - */ - public int read(byte[] bytes) throws IOException { - if (bytes.length < getCentralDirectoryHeader().getUncompressedSize()) { - throw new RuntimeException( - "Buffer to small while reading {}" + getCentralDirectoryHeader().getName()); - } - try (InputStream is = new BufferedInputStream(open())) { - return ByteStreams.read(is, bytes, 0, bytes.length); - } - } - - /** - * Obtains the type of entry. - * - * @return the type of entry - */ - @Nonnull - public StoredEntryType getType() { - Preconditions.checkState(!deleted, "deleted"); - return type; - } - - /** - * Deletes this entry from the zip file. Invoking this method doesn't update the zip itself. - * To eventually write updates to disk, {@link ZFile#update()} must be called. - * - * @throws IOException failed to delete the entry - * @throws IllegalStateException if the zip file was open in read-only mode - */ - public void delete() throws IOException { - delete(true); - } - - /** - * Deletes this entry from the zip file. Invoking this method doesn't update the zip itself. - * To eventually write updates to disk, {@link ZFile#update()} must be called. - * - * @param notify should listeners be notified of the deletion? This will only be - * {@code false} if the entry is being removed as part of a replacement - * @throws IOException failed to delete the entry - * @throws IllegalStateException if the zip file was open in read-only mode - */ - void delete(boolean notify) throws IOException { - Preconditions.checkState(!deleted, "deleted"); - file.delete(this, notify); - deleted = true; - source.close(); - } - - /** - * Returns {@code true} if this entry has been deleted/replaced. - */ - public boolean isDeleted() { - return deleted; - } - - /** - * Obtains the CDH associated with this entry. - * - * @return the CDH - */ - @Nonnull - public CentralDirectoryHeader getCentralDirectoryHeader() { - return cdh; - } - - /** - * Reads the file's local header and verifies that it matches the Central Directory - * Header provided in the constructor. This method should only be called if the entry already - * exists on disk; new entries do not have local headers. - *

- * This method will define the {@link #localExtra} field that is only defined in the - * local descriptor. - * - * @throws IOException failed to read the local header - */ - private void readLocalHeader() throws IOException { - byte[] localHeader = new byte[FIXED_LOCAL_FILE_HEADER_SIZE]; - file.directFullyRead(cdh.getOffset(), localHeader); - - CentralDirectoryHeaderCompressInfo compressInfo = cdh.getCompressionInfoWithWait(); - - ByteBuffer bytes = ByteBuffer.wrap(localHeader); - F_LOCAL_SIGNATURE.verify(bytes); - F_VERSION_EXTRACT.verify(bytes, compressInfo.getVersionExtract(), verifyLog); - F_GP_BIT.verify(bytes, cdh.getGpBit().getValue(), verifyLog); - F_METHOD.verify(bytes, compressInfo.getMethod().methodCode, verifyLog); - - if (file.areTimestampsIgnored()) { - F_LAST_MOD_TIME.skip(bytes); - F_LAST_MOD_DATE.skip(bytes); - } else { - F_LAST_MOD_TIME.verify(bytes, cdh.getLastModTime(), verifyLog); - F_LAST_MOD_DATE.verify(bytes, cdh.getLastModDate(), verifyLog); - } - - /* - * If CRC-32, compressed size and uncompressed size are deferred, their values in Local - * File Header must be ignored and their actual values must be read from the Data - * Descriptor following the contents of this entry. See readDataDescriptorRecord(). - */ - if (cdh.getGpBit().isDeferredCrc()) { - F_CRC32.skip(bytes); - F_COMPRESSED_SIZE.skip(bytes); - F_UNCOMPRESSED_SIZE.skip(bytes); - } else { - F_CRC32.verify(bytes, cdh.getCrc32(), verifyLog); - F_COMPRESSED_SIZE.verify(bytes, compressInfo.getCompressedSize(), verifyLog); - F_UNCOMPRESSED_SIZE.verify(bytes, cdh.getUncompressedSize(), verifyLog); - } - - F_FILE_NAME_LENGTH.verify(bytes, cdh.getEncodedFileName().length); - long extraLength = F_EXTRA_LENGTH.read(bytes); - long fileNameStart = cdh.getOffset() + F_EXTRA_LENGTH.endOffset(); - byte[] fileNameData = new byte[cdh.getEncodedFileName().length]; - file.directFullyRead(fileNameStart, fileNameData); - - String fileName = EncodeUtils.decode(fileNameData, cdh.getGpBit()); - if (!fileName.equals(cdh.getName())) { - verifyLog.log( - String.format( - "Central directory reports file as being named '%s' but local header" - + "reports file being named '%s'.", - cdh.getName(), - fileName)); - } - - long localExtraStart = fileNameStart + cdh.getEncodedFileName().length; - byte[] localExtraRaw = new byte[Ints.checkedCast(extraLength)]; - file.directFullyRead(localExtraStart, localExtraRaw); - localExtra = new ExtraField(localExtraRaw); - } - - /** - * Reads the data descriptor record. This method can only be invoked once it is established - * that a data descriptor does exist. It will read the data descriptor and check that the data - * described there matches the data provided in the Central Directory. - *

- * This method will set the {@link #dataDescriptorType} field to the appropriate type of - * data descriptor record. - * - * @throws IOException failed to read the data descriptor record - */ - private void readDataDescriptorRecord() throws IOException { - CentralDirectoryHeaderCompressInfo compressInfo = cdh.getCompressionInfoWithWait(); - - long ddStart = cdh.getOffset() + FIXED_LOCAL_FILE_HEADER_SIZE - + cdh.getName().length() + localExtra.size() + compressInfo.getCompressedSize(); - byte[] ddData = new byte[DataDescriptorType.DATA_DESCRIPTOR_WITH_SIGNATURE.size]; - file.directFullyRead(ddStart, ddData); - - ByteBuffer ddBytes = ByteBuffer.wrap(ddData); - - ZipField.F4 signatureField = new ZipField.F4(0, "Data descriptor signature"); - int cpos = ddBytes.position(); - long sig = signatureField.read(ddBytes); - if (sig == DATA_DESC_SIGNATURE) { - dataDescriptorType = DataDescriptorType.DATA_DESCRIPTOR_WITH_SIGNATURE; - } else { - dataDescriptorType = DataDescriptorType.DATA_DESCRIPTOR_WITHOUT_SIGNATURE; - ddBytes.position(cpos); - } - - ZipField.F4 crc32Field = new ZipField.F4(0, "CRC32"); - ZipField.F4 compressedField = new ZipField.F4(crc32Field.endOffset(), "Compressed size"); - ZipField.F4 uncompressedField = new ZipField.F4(compressedField.endOffset(), - "Uncompressed size"); - - crc32Field.verify(ddBytes, cdh.getCrc32(), verifyLog); - compressedField.verify(ddBytes, compressInfo.getCompressedSize(), verifyLog); - uncompressedField.verify(ddBytes, cdh.getUncompressedSize(), verifyLog); - } - - /** - * Creates a new source that reads data from the zip. - * - * @param zipOffset the offset into the zip file where the data is, must be non-negative - * @throws IOException failed to close the old source - * @return the created source - */ - @Nonnull - private ProcessedAndRawByteSources createSourceFromZip(final long zipOffset) - throws IOException { - Preconditions.checkArgument(zipOffset >= 0, "zipOffset < 0"); - - final CentralDirectoryHeaderCompressInfo compressInfo; - try { - compressInfo = cdh.getCompressionInfoWithWait(); - } catch (IOException e) { - throw new RuntimeException("IOException should never occur here because compression " - + "information should be immediately available if reading from zip.", e); - } - - /* - * Create a source that will return whatever is on the zip file. - */ - CloseableByteSource rawContents = new CloseableByteSource() { - @Override - public long size() throws IOException { - return compressInfo.getCompressedSize(); - } - - @Nonnull - @Override - public InputStream openStream() throws IOException { - Preconditions.checkState(!deleted, "deleted"); - - long dataStart = zipOffset + getLocalHeaderSize(); - long dataEnd = dataStart + compressInfo.getCompressedSize(); - - file.openReadOnly(); - return file.directOpen(dataStart, dataEnd); - } - - @Override - protected void innerClose() throws IOException { - /* - * Nothing to do here. - */ - } - }; - - return createSourcesFromRawContents(rawContents); - } - - /** - * Creates a {@link ProcessedAndRawByteSources} from the raw data source . The processed source - * will either inflate or do nothing depending on the compression information that, at this - * point, should already be available - * - * @param rawContents the raw data to create the source from - * @return the sources for this entry - */ - @Nonnull - private ProcessedAndRawByteSources createSourcesFromRawContents( - @Nonnull CloseableByteSource rawContents) { - CentralDirectoryHeaderCompressInfo compressInfo; - try { - compressInfo = cdh.getCompressionInfoWithWait(); - } catch (IOException e) { - throw new RuntimeException("IOException should never occur here because compression " - + "information should be immediately available if creating from raw " - + "contents.", e); - } - - CloseableByteSource contents; - - /* - * If the contents are deflated, wrap that source in an inflater source so we get the - * uncompressed data. - */ - if (compressInfo.getMethod() == CompressionMethod.DEFLATE) { - contents = new InflaterByteSource(rawContents); - } else { - contents = rawContents; - } - - return new ProcessedAndRawByteSources(contents, rawContents); - } - - /** - * Replaces {@link #source} with one that reads file data from the zip file. - * - * @param zipFileOffset the offset in the zip file where data is written; must be non-negative - * @throws IOException failed to replace the source - */ - void replaceSourceFromZip(long zipFileOffset) throws IOException { - Preconditions.checkArgument(zipFileOffset >= 0, "zipFileOffset < 0"); - - ProcessedAndRawByteSources oldSource = source; - source = createSourceFromZip(zipFileOffset); - cdh.setOffset(zipFileOffset); - oldSource.close(); - } - - /** - * Loads all data in memory and replaces {@link #source} with one that contains all the data - * in memory. - * - *

If the entry's contents are already in memory, this call does nothing. - * - * @throws IOException failed to replace the source - */ - void loadSourceIntoMemory() throws IOException { - if (cdh.getOffset() == -1) { - /* - * No offset in the CDR means data has not been written to disk which, in turn, - * means data is already loaded into memory. - */ - return; - } - - ProcessedAndRawByteSources oldSource = source; - byte[] rawContents = oldSource.getRawByteSource().read(); - source = createSourcesFromRawContents(new CloseableDelegateByteSource( - ByteSource.wrap(rawContents), rawContents.length)); - cdh.setOffset(-1); - oldSource.close(); - } - - /** - * Obtains the source data for this entry. This method can only be called for files, it - * cannot be called for directories. - * - * @return the entry source - */ - @Nonnull - ProcessedAndRawByteSources getSource() { - return source; - } - - /** - * Obtains the type of data descriptor used in the entry. - * - * @return the type of data descriptor - */ - @Nonnull - public DataDescriptorType getDataDescriptorType() { - return dataDescriptorType; - } - - /** - * Removes the data descriptor, if it has one and resets the data descriptor bit in the - * central directory header. - * - * @return was the data descriptor remove? - */ - boolean removeDataDescriptor() { - if (dataDescriptorType == DataDescriptorType.NO_DATA_DESCRIPTOR) { - return false; - } - - dataDescriptorType = DataDescriptorType.NO_DATA_DESCRIPTOR; - cdh.resetDeferredCrc(); - return true; - } - - /** - * Obtains the local header data. - * - * @return the header data - * @throws IOException failed to get header byte data - */ - @Nonnull - byte[] toHeaderData() throws IOException { - - byte[] encodedFileName = cdh.getEncodedFileName(); - - ByteBuffer out = - ByteBuffer.allocate( - F_EXTRA_LENGTH.endOffset() + encodedFileName.length + localExtra.size()); - - CentralDirectoryHeaderCompressInfo compressInfo = cdh.getCompressionInfoWithWait(); - - F_LOCAL_SIGNATURE.write(out); - F_VERSION_EXTRACT.write(out, compressInfo.getVersionExtract()); - F_GP_BIT.write(out, cdh.getGpBit().getValue()); - F_METHOD.write(out, compressInfo.getMethod().methodCode); - - if (file.areTimestampsIgnored()) { - F_LAST_MOD_TIME.write(out, 0); - F_LAST_MOD_DATE.write(out, 0); - } else { - F_LAST_MOD_TIME.write(out, cdh.getLastModTime()); - F_LAST_MOD_DATE.write(out, cdh.getLastModDate()); - } - - F_CRC32.write(out, cdh.getCrc32()); - F_COMPRESSED_SIZE.write(out, compressInfo.getCompressedSize()); - F_UNCOMPRESSED_SIZE.write(out, cdh.getUncompressedSize()); - F_FILE_NAME_LENGTH.write(out, cdh.getEncodedFileName().length); - F_EXTRA_LENGTH.write(out, localExtra.size()); - - out.put(cdh.getEncodedFileName()); - localExtra.write(out); - - return out.array(); - } - - /** - * Requests that this entry be realigned. If this entry is already aligned according to the - * rules in {@link ZFile} then this method does nothing. Otherwise it will move the file's data - * into memory and place it in a different area of the zip. - * - * @return has this file been changed? Note that if the entry has not yet been written on the - * file, realignment does not count as a change as nothing needs to be updated in the file; - * also, if the entry has been changed, this object may have been marked as deleted and a new - * stored entry may need to be fetched from the file - * @throws IOException failed to realign the entry; the entry may no longer exist in the zip - * file - */ - public boolean realign() throws IOException { - Preconditions.checkState(!deleted, "Entry has been deleted."); - - return file.realign(this); - } - - /** - * Obtains the contents of the local extra field. - * - * @return the contents of the local extra field - */ - @Nonnull - public ExtraField getLocalExtra() { - return localExtra; - } - - /** - * Sets the contents of the local extra field. - * - * @param localExtra the contents of the local extra field - * @throws IOException failed to update the zip file - */ - public void setLocalExtra(@Nonnull ExtraField localExtra) throws IOException { - boolean resized = setLocalExtraNoNotify(localExtra); - file.localHeaderChanged(this, resized); - } - - /** - * Sets the contents of the local extra field, does not notify the {@link ZFile} of the change. - * This is used internally when the {@link ZFile} itself wants to change the local extra and - * doesn't need the callback. - * - * @param localExtra the contents of the local extra field - * @return has the local header size changed? - * @throws IOException failed to load the file - */ - boolean setLocalExtraNoNotify(@Nonnull ExtraField localExtra) throws IOException { - boolean sizeChanged; - - /* - * Make sure we load into memory. - * - * If we change the size of the local header, the actual start of the file changes - * according to our in-memory structures so, if we don't read the file now, we won't be - * able to load it later :) - * - * But, even if the size doesn't change, we need to read it force the entry to be - * rewritten otherwise the changes in the local header aren't written. Of course this case - * may be optimized with some extra complexity added :) - */ - loadSourceIntoMemory(); - - if (this.localExtra.size() != localExtra.size()) { - sizeChanged = true; - } else { - sizeChanged = false; - } - - this.localExtra = localExtra; - return sizeChanged; - } - - /** - * Obtains the verify log for the entry. - * - * @return the verify log - */ - @Nonnull - public VerifyLog getVerifyLog() { - return verifyLog; - } -} diff --git a/src/main/java/com/android/apkzlib/zip/StoredEntryType.java b/src/main/java/com/android/apkzlib/zip/StoredEntryType.java deleted file mode 100644 index 9ce9252..0000000 --- a/src/main/java/com/android/apkzlib/zip/StoredEntryType.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * 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.android.apkzlib.zip; - -/** - * Type of stored entry. - */ -public enum StoredEntryType { - /** - * Entry is a file. - */ - FILE, - - /** - * Entry is a directory. - */ - DIRECTORY -} diff --git a/src/main/java/com/android/apkzlib/zip/VerifyLog.java b/src/main/java/com/android/apkzlib/zip/VerifyLog.java deleted file mode 100644 index 2a7db7c..0000000 --- a/src/main/java/com/android/apkzlib/zip/VerifyLog.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * 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.android.apkzlib.zip; - -import com.google.common.collect.ImmutableList; -import javax.annotation.Nonnull; - -/** - * The verify log contains verification messages. It is used to capture validation issues with a - * zip file or with parts of a zip file. - */ -public interface VerifyLog { - - /** - * Logs a message. - * - * @param message the message to verify - */ - void log(@Nonnull String message); - - /** - * Obtains all save logged messages. - * - * @return the logged messages - */ - @Nonnull - ImmutableList getLogs(); - - /** - * Performs verification of a non-critical condition, logging a message if the condition is - * not verified. - * - * @param condition the condition - * @param message the message to write if {@code condition} is {@code false}. - * @param args arguments for formatting {@code message} using {@code String.format} - */ - default void verify(boolean condition, @Nonnull String message, @Nonnull Object... args) { - if (!condition) { - log(String.format(message, args)); - } - } -} diff --git a/src/main/java/com/android/apkzlib/zip/VerifyLogs.java b/src/main/java/com/android/apkzlib/zip/VerifyLogs.java deleted file mode 100644 index b7acb83..0000000 --- a/src/main/java/com/android/apkzlib/zip/VerifyLogs.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * 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.android.apkzlib.zip; - -import com.google.common.collect.ImmutableList; -import java.util.ArrayList; -import java.util.List; -import javax.annotation.Nonnull; - -/** - * Factory for verification logs. - */ -final class VerifyLogs { - - private VerifyLogs() {} - - /** - * Creates a {@link VerifyLog} that ignores all messages logged. - * - * @return the log - */ - @Nonnull - static VerifyLog devNull() { - return new VerifyLog() { - @Override - public void log(@Nonnull String message) {} - - @Nonnull - @Override - public ImmutableList getLogs() { - return ImmutableList.of(); - } - }; - } - - /** - * Creates a {@link VerifyLog} that stores all log messages. - * - * @return the log - */ - @Nonnull - static VerifyLog unlimited() { - return new VerifyLog() { - - /** - * All saved messages. - */ - @Nonnull - private final List messages = new ArrayList<>(); - - @Override - public void log(@Nonnull String message) { - messages.add(message); - } - - @Nonnull - @Override - public ImmutableList getLogs() { - return ImmutableList.copyOf(messages); - } - }; - } -} diff --git a/src/main/java/com/android/apkzlib/zip/ZFile.java b/src/main/java/com/android/apkzlib/zip/ZFile.java deleted file mode 100644 index 9034f4c..0000000 --- a/src/main/java/com/android/apkzlib/zip/ZFile.java +++ /dev/null @@ -1,2764 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * 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.android.apkzlib.zip; - -import com.android.apkzlib.utils.CachedFileContents; -import com.android.apkzlib.utils.IOExceptionFunction; -import com.android.apkzlib.utils.IOExceptionRunnable; -import com.android.apkzlib.zip.compress.Zip64NotSupportedException; -import com.android.apkzlib.zip.utils.ByteTracker; -import com.android.apkzlib.zip.utils.CloseableByteSource; -import com.android.apkzlib.zip.utils.LittleEndianUtils; -import com.google.common.base.Preconditions; -import com.google.common.base.Verify; -import com.google.common.base.VerifyException; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.Lists; -import com.google.common.collect.Maps; -import com.google.common.collect.Sets; -import com.google.common.hash.Hashing; -import com.google.common.io.ByteSource; -import com.google.common.io.Closer; -import com.google.common.io.Files; -import com.google.common.primitives.Ints; -import com.google.common.util.concurrent.FutureCallback; -import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; -import com.google.common.util.concurrent.MoreExecutors; -import com.google.common.util.concurrent.SettableFuture; -import java.io.ByteArrayInputStream; -import java.io.Closeable; -import java.io.EOFException; -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.RandomAccessFile; -import java.nio.ByteBuffer; -import java.nio.channels.FileChannel; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.SortedSet; -import java.util.TreeMap; -import java.util.TreeSet; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Future; -import java.util.function.Function; -import java.util.function.Predicate; -import java.util.function.Supplier; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -/** - * The {@code ZFile} provides the main interface for interacting with zip files. A {@code ZFile} - * can be created on a new file or in an existing file. Once created, files can be added or removed - * from the zip file. - * - *

Changes in the zip file are always deferred. Any change requested is made in memory and - * written to disk only when {@link #update()} or {@link #close()} is invoked. - * - *

Zip files are open initially in read-only mode and will switch to read-write when needed. This - * is done automatically. Because modifications to the file are done in-memory, the zip file can - * be manipulated when closed. When invoking {@link #update()} or {@link #close()} the zip file - * will be reopen and changes will be written. However, the zip file cannot be modified outside - * the control of {@code ZFile}. So, if a {@code ZFile} is closed, modified outside and then a file - * is added or removed from the zip file, when reopening the zip file, {@link ZFile} will detect - * the outside modification and will fail. - * - *

In memory manipulation means that files added to the zip file are kept in memory until written - * to disk. This provides much faster operation and allows better zip file allocation (see below). - * It may, however, increase the memory footprint of the application. When adding large files, if - * memory consumption is a concern, a call to {@link #update()} will actually write the file to - * disk and discard the memory buffer. Information about allocation can be obtained from a - * {@link ByteTracker} that can be given to the file on creation. - * - *

{@code ZFile} keeps track of allocation inside of the zip file. If a file is deleted, its - * space is marked as freed and will be reused for an added file if it fits in the space. - * Allocation of files to empty areas is done using a best fit algorithm. When adding a - * file, if it doesn't fit in any free area, the zip file will be extended. - * - *

{@code ZFile} provides a fast way to merge data from another zip file - * (see {@link #mergeFrom(ZFile, Predicate)}) avoiding recompression and copying of equal files. - * When merging, patterns of files may be provided that are ignored. This allows handling special - * files in the merging process, such as files in {@code META-INF}. - * - *

When adding files to the zip file, unless files are explicitly required to be stored, files - * will be deflated. However, deflating will not occur if the deflated file is larger then the - * stored file, e.g. if compression would yield a bigger file. See {@link Compressor} for - * details on how compression works. - * - *

Because {@code ZFile} was designed to be used in a build system and not as general-purpose - * zip utility, it is very strict (and unforgiving) about the zip format and unsupported features. - * - *

{@code ZFile} supports alignment. Alignment means that file data (not entries -- the - * local header must be discounted) must start at offsets that are multiple of a number -- the - * alignment. Alignment is defined by an alignment rules ({@link AlignmentRule} in the - * {@link ZFileOptions} object used to create the {@link ZFile}. - * - *

When a file is added to the zip, the alignment rules will be checked and alignment will be - * honored when positioning the file in the zip. This means that unused spaces in the zip may - * be generated as a result. However, alignment of existing entries will not be changed. - * - *

Entries can be realigned individually (see {@link StoredEntry#realign()} or the full zip file - * may be realigned (see {@link #realign()}). When realigning the full zip entries that are already - * aligned will not be affected. - * - *

Because realignment may cause files to move in the zip, realignment is done in-memory meaning - * that files that need to change location will moved to memory and will only be flushed when - * either {@link #update()} or {@link #close()} are called. - * - *

Alignment only applies to filed that are forced to be uncompressed. This is because alignment - * is used to allow mapping files in the archive directly into memory and compressing defeats the - * purpose of alignment. - * - *

Manipulating zip files with {@link ZFile} may yield zip files with empty spaces between files. - * This happens in two situations: (1) if alignment is required, files may be shifted to conform to - * the request alignment leaving an empty space before the previous file, and (2) if a file is - * removed or replaced with a file that does not fit the space it was in. By default, {@link ZFile} - * does not do any special processing in these situations. Files are indexed by their offsets from - * the central directory and empty spaces can exist in the zip file. - * - *

However, it is possible to tell {@link ZFile} to use the extra field in the local header - * to do cover the empty spaces. This is done by setting - * {@link ZFileOptions#setCoverEmptySpaceUsingExtraField(boolean)} to {@code true}. This has the - * advantage of leaving no gaps between entries in the zip, as required by some tools like Oracle's - * {code jar} tool. However, setting this option will destroy the contents of the file's extra - * field. - * - *

Activating {@link ZFileOptions#setCoverEmptySpaceUsingExtraField(boolean)} may lead to - * virtual files being added to the zip file. Since extra field is limited to 64k, it is not - * possible to cover any space bigger than that using the extra field. In those cases, virtual - * files are added to the file. A virtual file is a file that exists in the actual zip data, - * but is not referenced from the central directory. A zip-compliant utility should ignore these - * files. However, zip utilities that expect the zip to be a stream, such as Oracle's jar, will - * find these files instead of considering the zip to be corrupt. - * - *

{@code ZFile} support sorting zip files. Sorting (done through the {@link #sortZipContents()} - * method) is a process by which all files are re-read into memory, if not already in memory, - * removed from the zip and re-added in alphabetical order, respecting alignment rules. So, in - * general, file {@code b} will come after file {@code a} unless file {@code a} is subject to - * alignment that forces an empty space before that can be occupied by {@code b}. Sorting can be - * used to minimize the changes between two zips. - * - *

Sorting in {@code ZFile} can be done manually or automatically. Manual sorting is done by - * invoking {@link #sortZipContents()}. Automatic sorting is done by setting the - * {@link ZFileOptions#getAutoSortFiles()} option when creating the {@code ZFile}. Automatic - * sorting invokes {@link #sortZipContents()} immediately when doing an {@link #update()} after - * all extensions have processed the {@link ZFileExtension#beforeUpdate()}. This has the guarantee - * that files added by extensions will be sorted, something that does not happen if the invocation - * is sequential, i.e., {@link #sortZipContents()} called before {@link #update()}. The - * drawback of automatic sorting is that sorting will happen every time {@link #update()} is - * called and the file is dirty having a possible penalty in performance. - * - *

To allow whole-apk signing, the {@code ZFile} allows the central directory location to be - * offset by a fixed amount. This amount can be set using the {@link #setExtraDirectoryOffset(long)} - * method. Setting a non-zero value will add extra (unused) space in the zip file before the - * central directory. This value can be changed at any time and it will force the central directory - * rewritten when the file is updated or closed. - * - *

{@code ZFile} provides an extension mechanism to allow objects to register with the file - * and be notified when changes to the file happen. This should be used - * to add extra features to the zip file while providing strong decoupling. See - * {@link ZFileExtension}, {@link ZFile#addZFileExtension(ZFileExtension)} and - * {@link ZFile#removeZFileExtension(ZFileExtension)}. - * - *

This class is not thread-safe. Neither are any of the classes associated with - * it in this package, except when otherwise noticed. - */ -public class ZFile implements Closeable { - - /** - * The file separator in paths in the zip file. This is fixed by the zip specification - * (section 4.4.17). - */ - public static final char SEPARATOR = '/'; - - /** - * Minimum size the EOCD can have. - */ - private static final int MIN_EOCD_SIZE = 22; - - /** - * Number of bytes of the Zip64 EOCD locator record. - */ - private static final int ZIP64_EOCD_LOCATOR_SIZE = 20; - - /** - * Maximum size for the EOCD. - */ - private static final int MAX_EOCD_COMMENT_SIZE = 65535; - - /** - * How many bytes to look back from the end of the file to look for the EOCD signature. - */ - private static final int LAST_BYTES_TO_READ = MIN_EOCD_SIZE + MAX_EOCD_COMMENT_SIZE; - - /** - * Signature of the Zip64 EOCD locator record. - */ - private static final int ZIP64_EOCD_LOCATOR_SIGNATURE = 0x07064b50; - - /** - * Signature of the EOCD record. - */ - private static final byte[] EOCD_SIGNATURE = new byte[] { 0x06, 0x05, 0x4b, 0x50 }; - - /** - * Size of buffer for I/O operations. - */ - private static final int IO_BUFFER_SIZE = 1024 * 1024; - - /** - * When extensions request re-runs, we do maximum number of cycles until we decide to stop and - * flag a infinite recursion problem. - */ - private static final int MAXIMUM_EXTENSION_CYCLE_COUNT = 10; - - /** - * Minimum size for the extra field when we have to add one. We rely on the alignment segment - * to do that so the minimum size for the extra field is the minimum size of an alignment - * segment. - */ - private static final int MINIMUM_EXTRA_FIELD_SIZE = ExtraField.AlignmentSegment.MINIMUM_SIZE; - - /** - * Maximum size of the extra field. - * - *

Theoretically, this is (1 << 16) - 1 = 65535 and not (1 < 15) -1 = 32767. However, due to - * http://b.android.com/221703, we need to keep this limited. - */ - private static final int MAX_LOCAL_EXTRA_FIELD_CONTENTS_SIZE = (1 << 15) - 1; - - /** - * File zip file. - */ - @Nonnull - private final File file; - - /** - * The random access file used to access the zip file. This will be {@code null} if and only - * if {@link #state} is {@link ZipFileState#CLOSED}. - */ - @Nullable - private RandomAccessFile raf; - - /** - * The map containing the in-memory contents of the zip file. It keeps track of which parts of - * the zip file are used and which are not. - */ - @Nonnull - private final FileUseMap map; - - /** - * The EOCD entry. Will be {@code null} if there is no EOCD (because the zip is new) or the - * one that exists on disk is no longer valid (because the zip has been changed). - * - *

If the EOCD is deleted because the zip has been changed and the old EOCD was no longer - * valid, then {@link #eocdComment} will contain the comment saved from the EOCD. - */ - @Nullable - private FileUseMapEntry eocdEntry; - - /** - * The Central Directory entry. Will be {@code null} if there is no Central Directory (because - * the zip is new) or because the one that exists on disk is no longer valid (because the zip - * has been changed). - */ - @Nullable - private FileUseMapEntry directoryEntry; - - /** - * All entries in the zip file. It includes in-memory changes and may not reflect what is - * written on disk. Only entries that have been compressed are in this list. - */ - @Nonnull - private final Map> entries; - - /** - * Entries added to the zip file, but that are not yet compressed. When compression is done, - * these entries are eventually moved to {@link #entries}. uncompressedEntries is a list - * because entries need to be kept in the order by which they were added. It allows adding - * multiple files with the same name and getting the right notifications on which files replaced - * which. - * - *

Files are placed in this list in {@link #add(StoredEntry)} method. This method will - * keep files here temporarily and move then to {@link #entries} when the data is - * available. - * - *

Moving files out of this list to {@link #entries} is done by - * {@link #processAllReadyEntries()}. - */ - @Nonnull - private final List uncompressedEntries; - - /** - * Current state of the zip file. - */ - @Nonnull - private ZipFileState state; - - /** - * Are the in-memory changes that have not been written to the zip file? - * - *

This might be false, but will become true after {@link #processAllReadyEntriesWithWait()} - * is called if there are {@link #uncompressedEntries} compressing in the background. - */ - private boolean dirty; - - /** - * Non-{@code null} only if the file is currently closed. Used to detect if the zip is - * modified outside this object's control. If the file has never been written, this will - * be {@code null} even if it is closed. - */ - @Nullable - private CachedFileContents closedControl; - - /** - * The alignment rule. - */ - @Nonnull - private final AlignmentRule alignmentRule; - - /** - * Extensions registered with the file. - */ - @Nonnull - private final List extensions; - - /** - * When notifying extensions, extensions may request that some runnables are executed. This - * list collects all runnables by the order they were requested. Together with - * {@link #isNotifying}, it is used to avoid reordering notifications. - */ - @Nonnull - private final List toRun; - - /** - * {@code true} when {@link #notify(com.android.apkzlib.utils.IOExceptionFunction)} is - * notifying extensions. Used to avoid reordering notifications. - */ - private boolean isNotifying; - - /** - * An extra offset for the central directory location. {@code 0} if the central directory - * should be written in its standard location. - */ - private long extraDirectoryOffset; - - /** - * Should all timestamps be zeroed when reading / writing the zip? - */ - private boolean noTimestamps; - - /** - * Compressor to use. - */ - @Nonnull - private Compressor compressor; - - /** - * Byte tracker to use. - */ - @Nonnull - private final ByteTracker tracker; - - /** - * Use the zip entry's "extra field" field to cover empty space in the zip file? - */ - private boolean coverEmptySpaceUsingExtraField; - - /** - * Should files be automatically sorted when updating? - */ - private boolean autoSortFiles; - - /** - * Verify log factory to use. - */ - @Nonnull - private final Supplier verifyLogFactory; - - /** - * Verify log to use. - */ - @Nonnull - private final VerifyLog verifyLog; - - /** - * This field contains the comment in the zip's EOCD if there is no in-memory EOCD structure. - * This may happen, for example, if the zip has been changed and the Central Directory and - * EOCD have been deleted (in-memory). In that case, this field will save the comment to place - * on the EOCD once it is created. - * - *

This field will only be non-{@code null} if there is no in-memory EOCD structure - * (i.e., {@link #eocdEntry} is {@code null}). If there is an {@link #eocdEntry}, then - * the comment will be there instead of being in this field. - */ - @Nullable - private byte[] eocdComment; - - /** - * Is the file in read-only mode? In read-only mode no changes are allowed. - */ - private boolean readOnly; - - - /** - * Creates a new zip file. If the zip file does not exist, then no file is created at this - * point and {@code ZFile} will contain an empty structure. However, an (empty) zip file will - * be created if either {@link #update()} or {@link #close()} are used. If a zip file exists, - * it will be parsed and read. - * - * @param file the zip file - * @throws IOException some file exists but could not be read - */ - public ZFile(@Nonnull File file) throws IOException { - this(file, new ZFileOptions()); - } - - /** - * Creates a new zip file. If the zip file does not exist, then no file is created at this - * point and {@code ZFile} will contain an empty structure. However, an (empty) zip file will - * be created if either {@link #update()} or {@link #close()} are used. If a zip file exists, - * it will be parsed and read. - * - * @param file the zip file - * @param options configuration options - * @throws IOException some file exists but could not be read - */ - public ZFile(@Nonnull File file, @Nonnull ZFileOptions options) throws IOException { - this(file, options, false); - } - - /** - * Creates a new zip file. If the zip file does not exist, then no file is created at this - * point and {@code ZFile} will contain an empty structure. However, an (empty) zip file will - * be created if either {@link #update()} or {@link #close()} are used. If a zip file exists, - * it will be parsed and read. - * - * @param file the zip file - * @param options configuration options - * @param readOnly should the file be open in read-only mode? If {@code true} then the file must - * exist and no methods can be invoked that could potentially change the file - * @throws IOException some file exists but could not be read - */ - public ZFile(@Nonnull File file, @Nonnull ZFileOptions options, boolean readOnly) - throws IOException { - this.file = file; - map = new FileUseMap( - 0, - options.getCoverEmptySpaceUsingExtraField() - ? MINIMUM_EXTRA_FIELD_SIZE - : 0); - this.readOnly = readOnly; - dirty = false; - closedControl = null; - alignmentRule = options.getAlignmentRule(); - extensions = Lists.newArrayList(); - toRun = Lists.newArrayList(); - noTimestamps = options.getNoTimestamps(); - tracker = options.getTracker(); - compressor = options.getCompressor(); - coverEmptySpaceUsingExtraField = options.getCoverEmptySpaceUsingExtraField(); - autoSortFiles = options.getAutoSortFiles(); - verifyLogFactory = options.getVerifyLogFactory(); - verifyLog = verifyLogFactory.get(); - - /* - * These two values will be overwritten by openReadOnly() below if the file exists. - */ - state = ZipFileState.CLOSED; - raf = null; - - if (file.exists()) { - openReadOnly(); - } else if (readOnly) { - throw new IOException("File does not exist but read-only mode requested"); - } else { - dirty = true; - } - - entries = Maps.newHashMap(); - uncompressedEntries = Lists.newArrayList(); - extraDirectoryOffset = 0; - - try { - if (state != ZipFileState.CLOSED) { - long rafSize = raf.length(); - if (rafSize > Integer.MAX_VALUE) { - throw new IOException("File exceeds size limit of " + Integer.MAX_VALUE + "."); - } - - map.extend(Ints.checkedCast(rafSize)); - readData(); - } - - // If we don't have an EOCD entry, set the comment to empty. - if (eocdEntry == null) { - eocdComment = new byte[0]; - } - - // Notify the extensions if the zip file has been open. - if (state != ZipFileState.CLOSED) { - notify(ZFileExtension::open); - } - } catch (Zip64NotSupportedException e) { - throw e; - } catch (IOException e) { - throw new IOException("Failed to read zip file '" + file.getAbsolutePath() + "'.", e); - } catch (IllegalStateException | IllegalArgumentException | VerifyException e) { - throw new RuntimeException( - "Internal error when trying to read zip file '" + file.getAbsolutePath() + "'.", - e); - } - } - - /** - * Obtains all entries in the file. Entries themselves may be or not written in disk. However, - * all of them can be open for reading. - * - * @return all entries in the zip - */ - @Nonnull - public Set entries() { - Map entries = Maps.newHashMap(); - - for (FileUseMapEntry mapEntry : this.entries.values()) { - StoredEntry entry = mapEntry.getStore(); - assert entry != null; - entries.put(entry.getCentralDirectoryHeader().getName(), entry); - } - - /* - * mUncompressed may override mEntriesReady as we may not have yet processed all - * entries. - */ - for (StoredEntry uncompressed : uncompressedEntries) { - entries.put(uncompressed.getCentralDirectoryHeader().getName(), uncompressed); - } - - return Sets.newHashSet(entries.values()); - } - - /** - * Obtains an entry at a given path in the zip. - * - * @param path the path - * @return the entry at the path or {@code null} if none exists - */ - @Nullable - public StoredEntry get(@Nonnull String path) { - /* - * The latest entries are the last ones in uncompressed and they may eventually override - * files in entries. - */ - for (StoredEntry stillUncompressed : Lists.reverse(uncompressedEntries)) { - if (stillUncompressed.getCentralDirectoryHeader().getName().equals(path)) { - return stillUncompressed; - } - } - - FileUseMapEntry found = entries.get(path); - if (found == null) { - return null; - } - - return found.getStore(); - } - - /** - * Reads all the data in the zip file, except the contents of the entries themselves. This - * method will populate the directory and maps in the instance variables. - * - * @throws IOException failed to read the zip file - */ - private void readData() throws IOException { - Preconditions.checkState(state != ZipFileState.CLOSED, "state == ZipFileState.CLOSED"); - Preconditions.checkState(raf != null, "raf == null"); - - readEocd(); - readCentralDirectory(); - - /* - * Go over all files and create the usage map, verifying there is no overlap in the files. - */ - long entryEndOffset; - long directoryStartOffset; - - if (directoryEntry != null) { - CentralDirectory directory = directoryEntry.getStore(); - assert directory != null; - - entryEndOffset = 0; - - for (StoredEntry entry : directory.getEntries().values()) { - long start = entry.getCentralDirectoryHeader().getOffset(); - long end = start + entry.getInFileSize(); - - /* - * If isExtraAlignmentBlock(entry.getLocalExtra()) is true, we know the entry - * has an extra field that is solely used for alignment. This means the - * actual entry could start at start + extra.length and leave space before. - * - * But, if we did this here, we would be modifying the zip file and that is - * weird because we're just opening it for reading. - * - * The downside is that we will never reuse that space. Maybe one day ZFile - * can be clever enough to remove the local extra when we start modifying the zip - * file. - */ - - Verify.verify(start >= 0, "start < 0"); - Verify.verify(end < map.size(), "end >= map.size()"); - - FileUseMapEntry found = map.at(start); - Verify.verifyNotNull(found); - - // We've got a problem if the found entry is not free or is a free entry but - // doesn't cover the whole file. - if (!found.isFree() || found.getEnd() < end) { - if (found.isFree()) { - found = map.after(found); - Verify.verify(found != null && !found.isFree()); - } - - Object foundEntry = found.getStore(); - Verify.verify(foundEntry != null); - - // Obtains a custom description of an entry. - IOExceptionFunction describe = - e -> - String.format( - "'%s' (offset: %d, size: %d)", - e.getCentralDirectoryHeader().getName(), - e.getCentralDirectoryHeader().getOffset(), - e.getInFileSize()); - - String overlappingEntryDescription; - if (foundEntry instanceof StoredEntry) { - StoredEntry foundStored = (StoredEntry) foundEntry; - overlappingEntryDescription = describe.apply((StoredEntry) foundEntry); - } else { - overlappingEntryDescription = - "Central Directory / EOCD: " - + found.getStart() - + " - " - + found.getEnd(); - } - - throw new IOException( - "Cannot read entry " - + describe.apply(entry) - + " because it overlaps with " - + overlappingEntryDescription); - } - - FileUseMapEntry mapEntry = map.add(start, end, entry); - entries.put(entry.getCentralDirectoryHeader().getName(), mapEntry); - - if (end > entryEndOffset) { - entryEndOffset = end; - } - } - - directoryStartOffset = directoryEntry.getStart(); - } else { - /* - * No directory means an empty zip file. Use the start of the EOCD to compute - * an existing offset. - */ - Verify.verifyNotNull(eocdEntry); - assert eocdEntry != null; - directoryStartOffset = eocdEntry.getStart(); - entryEndOffset = 0; - } - - /* - * Check if there is an extra central directory offset. If there is, save it. Note that - * we can't call extraDirectoryOffset() because that would mark the file as dirty. - */ - long extraOffset = directoryStartOffset - entryEndOffset; - Verify.verify(extraOffset >= 0, "extraOffset (%s) < 0", extraOffset); - extraDirectoryOffset = extraOffset; - } - - /** - * Finds the EOCD marker and reads it. It will populate the {@link #eocdEntry} variable. - * - * @throws IOException failed to read the EOCD - */ - private void readEocd() throws IOException { - Preconditions.checkState(state != ZipFileState.CLOSED, "state == ZipFileState.CLOSED"); - Preconditions.checkState(raf != null, "raf == null"); - - /* - * Read the last part of the zip into memory. If we don't find the EOCD signature by then, - * the file is corrupt. - */ - int lastToRead = LAST_BYTES_TO_READ; - if (lastToRead > raf.length()) { - lastToRead = Ints.checkedCast(raf.length()); - } - - byte[] last = new byte[lastToRead]; - directFullyRead(raf.length() - lastToRead, last); - - - /* - * Start endIdx at the first possible location where the signature can be located and then - * move backwards. Because the EOCD must have at least MIN_EOCD size, the first byte of the - * signature (and first byte of the EOCD) must be located at last.length - MIN_EOCD_SIZE. - * - * Because the EOCD signature may exist in the file comment, when we find a signature we - * will try to read the Eocd. If we fail, we continue searching for the signature. However, - * we will keep the last exception in case we don't find any signature. - */ - Eocd eocd = null; - int foundEocdSignature = -1; - IOException errorFindingSignature = null; - int eocdStart = -1; - - for (int endIdx = last.length - MIN_EOCD_SIZE; endIdx >= 0 && foundEocdSignature == -1; - endIdx--) { - /* - * Remember: little endian... - */ - if (last[endIdx] == EOCD_SIGNATURE[3] - && last[endIdx + 1] == EOCD_SIGNATURE[2] - && last[endIdx + 2] == EOCD_SIGNATURE[1] - && last[endIdx + 3] == EOCD_SIGNATURE[0]) { - - /* - * We found a signature. Try to read the EOCD record. - */ - - foundEocdSignature = endIdx; - ByteBuffer eocdBytes = - ByteBuffer.wrap(last, foundEocdSignature, last.length - foundEocdSignature); - - try { - eocd = new Eocd(eocdBytes); - eocdStart = Ints.checkedCast(raf.length() - lastToRead + foundEocdSignature); - - /* - * Make sure the EOCD takes the whole file up to the end. Log an error if it - * doesn't. - */ - if (eocdStart + eocd.getEocdSize() != raf.length()) { - verifyLog.log("EOCD starts at " - + eocdStart - + " and has " - + eocd.getEocdSize() - + " bytes, but file ends at " - + raf.length() - + "."); - } - } catch (IOException e) { - if (errorFindingSignature != null) { - e.addSuppressed(errorFindingSignature); - } - - errorFindingSignature = e; - foundEocdSignature = -1; - eocd = null; - } - } - } - - if (foundEocdSignature == -1) { - throw new IOException("EOCD signature not found in the last " - + lastToRead + " bytes of the file.", errorFindingSignature); - } - - Verify.verify(eocdStart >= 0); - - /* - * Look for the Zip64 central directory locator. If we find it, then this file is a Zip64 - * file and we do not support it. - */ - int zip64LocatorStart = eocdStart - ZIP64_EOCD_LOCATOR_SIZE; - if (zip64LocatorStart >= 0) { - byte[] possibleZip64Locator = new byte[4]; - directFullyRead(zip64LocatorStart, possibleZip64Locator); - if (LittleEndianUtils.readUnsigned4Le(ByteBuffer.wrap(possibleZip64Locator)) == - ZIP64_EOCD_LOCATOR_SIGNATURE) { - throw new Zip64NotSupportedException( - "Zip64 EOCD locator found but Zip64 format is not supported."); - } - } - - eocdEntry = map.add(eocdStart, eocdStart + eocd.getEocdSize(), eocd); - } - - /** - * Reads the zip's central directory and populates the {@link #directoryEntry} variable. This - * method can only be called after the EOCD has been read. If the central directory is empty - * (if there are no files on the zip archive), then {@link #directoryEntry} will be set to - * {@code null}. - * - * @throws IOException failed to read the central directory - */ - private void readCentralDirectory() throws IOException { - Preconditions.checkNotNull(eocdEntry, "eocdEntry == null"); - Preconditions.checkNotNull(eocdEntry.getStore(), "eocdEntry.getStore() == null"); - Preconditions.checkState(state != ZipFileState.CLOSED, "state == ZipFileState.CLOSED"); - Preconditions.checkState(raf != null, "raf == null"); - Preconditions.checkState(directoryEntry == null, "directoryEntry != null"); - - Eocd eocd = eocdEntry.getStore(); - - long dirSize = eocd.getDirectorySize(); - if (dirSize > Integer.MAX_VALUE) { - throw new IOException("Cannot read central directory with size " + dirSize + "."); - } - - long centralDirectoryEnd = eocd.getDirectoryOffset() + dirSize; - if (centralDirectoryEnd != eocdEntry.getStart()) { - String msg = "Central directory is stored in [" - + eocd.getDirectoryOffset() - + " - " - + (centralDirectoryEnd - 1) - + "] and EOCD starts at " - + eocdEntry.getStart() - + "."; - - /* - * If there is an empty space between the central directory and the EOCD, we proceed - * logging an error. If the central directory ends after the start of the EOCD (and - * therefore, they overlap), throw an exception. - */ - if (centralDirectoryEnd > eocdEntry.getSize()) { - throw new IOException(msg); - } else { - verifyLog.log(msg); - } - } - - byte[] directoryData = new byte[Ints.checkedCast(dirSize)]; - directFullyRead(eocd.getDirectoryOffset(), directoryData); - - CentralDirectory directory = - CentralDirectory.makeFromData( - ByteBuffer.wrap(directoryData), - eocd.getTotalRecords(), - this); - if (eocd.getDirectorySize() > 0) { - directoryEntry = map.add( - eocd.getDirectoryOffset(), - eocd.getDirectoryOffset() + eocd.getDirectorySize(), - directory); - } - } - - /** - * Opens a portion of the zip for reading. The zip must be open for this method to be invoked. - * Note that if the zip has not been updated, the individual zip entries may not have been - * written yet. - * - * @param start the index within the zip file to start reading - * @param end the index within the zip file to end reading (the actual byte pointed by - * end will not be read) - * @return a stream that will read the portion of the file; no decompression is done, data is - * returned as is - * @throws IOException failed to open the zip file - */ - @Nonnull - public InputStream directOpen(final long start, final long end) throws IOException { - Preconditions.checkState(state != ZipFileState.CLOSED, "state == ZipFileState.CLOSED"); - Preconditions.checkState(raf != null, "raf == null"); - Preconditions.checkArgument(start >= 0, "start < 0"); - Preconditions.checkArgument(end >= start, "end < start"); - Preconditions.checkArgument(end <= raf.length(), "end > raf.length()"); - - return new InputStream() { - private long mCurr = start; - - @Override - public int read() throws IOException { - if (mCurr == end) { - return -1; - } - - byte[] b = new byte[1]; - int r = directRead(mCurr, b); - if (r > 0) { - mCurr++; - return b[0]; - } else { - return -1; - } - } - - @Override - public int read(@Nonnull byte[] b, int off, int len) throws IOException { - Preconditions.checkNotNull(b, "b == null"); - Preconditions.checkArgument(off >= 0, "off < 0"); - Preconditions.checkArgument(off <= b.length, "off > b.length"); - Preconditions.checkArgument(len >= 0, "len < 0"); - Preconditions.checkArgument(off + len <= b.length, "off + len > b.length"); - - long availableToRead = end - mCurr; - long toRead = Math.min(len, availableToRead); - - if (toRead == 0) { - return -1; - } - - if (toRead > Integer.MAX_VALUE) { - throw new IOException("Cannot read " + toRead + " bytes."); - } - - int r = directRead(mCurr, b, off, Ints.checkedCast(toRead)); - if (r > 0) { - mCurr += r; - } - - return r; - } - }; - } - - /** - * Deletes an entry from the zip. This method does not actually delete anything on disk. It - * just changes in-memory structures. Use {@link #update()} to update the contents on disk. - * - * @param entry the entry to delete - * @param notify should listeners be notified of the deletion? This will only be - * {@code false} if the entry is being removed as part of a replacement - * @throws IOException failed to delete the entry - * @throws IllegalStateException if open in read-only mode - */ - void delete(@Nonnull final StoredEntry entry, boolean notify) throws IOException { - checkNotInReadOnlyMode(); - - String path = entry.getCentralDirectoryHeader().getName(); - FileUseMapEntry mapEntry = entries.get(path); - Preconditions.checkNotNull(mapEntry, "mapEntry == null"); - Preconditions.checkArgument(entry == mapEntry.getStore(), "entry != mapEntry.getStore()"); - - dirty = true; - - map.remove(mapEntry); - entries.remove(path); - - if (notify) { - notify(ext -> ext.removed(entry)); - } - } - - /** - * Checks that the file is not in read-only mode. - * - * @throws IllegalStateException if the file is in read-only mode - */ - private void checkNotInReadOnlyMode() { - if (readOnly) { - throw new IllegalStateException("Illegal operation in read only model"); - } - } - - /** - * Updates the file writing new entries and removing deleted entries. This will force - * reopening the file as read/write if the file wasn't open in read/write mode. - * - * @throws IOException failed to update the file; this exception may have been thrown by - * the compressor but only reported here - */ - public void update() throws IOException { - checkNotInReadOnlyMode(); - - /* - * Process all background stuff before calling in the extensions. - */ - processAllReadyEntriesWithWait(); - - notify(ZFileExtension::beforeUpdate); - - /* - * Process all background stuff that may be leftover by the extensions. - */ - processAllReadyEntriesWithWait(); - - - if (!dirty) { - return; - } - - reopenRw(); - - /* - * At this point, no more files can be added. We may need to repack to remove extra - * empty spaces or sort. If we sort, we don't need to repack as sorting forces the - * zip file to be as compact as possible. - */ - if (autoSortFiles) { - sortZipContents(); - } else { - packIfNecessary(); - } - - /* - * We're going to change the file so delete the central directory and the EOCD as they - * will have to be rewritten. - */ - deleteDirectoryAndEocd(); - map.truncate(); - - /* - * If we need to use the extra field to cover empty spaces, we do the processing here. - */ - if (coverEmptySpaceUsingExtraField) { - - /* We will go over all files in the zip and check whether there is empty space before - * them. If there is, then we will move the entry to the beginning of the empty space - * (covering it) and extend the extra field with the size of the empty space. - */ - for (FileUseMapEntry entry : new HashSet<>(entries.values())) { - StoredEntry storedEntry = entry.getStore(); - assert storedEntry != null; - - FileUseMapEntry before = map.before(entry); - if (before == null || !before.isFree()) { - continue; - } - - /* - * We have free space before the current entry. However, we do know that it can - * be covered by the extra field, because both sortZipContents() and - * packIfNecessary() guarantee it. - */ - int localExtraSize = - storedEntry.getLocalExtra().size() + Ints.checkedCast(before.getSize()); - Verify.verify(localExtraSize <= MAX_LOCAL_EXTRA_FIELD_CONTENTS_SIZE); - - /* - * Move file back in the zip. - */ - storedEntry.loadSourceIntoMemory(); - - long newStart = before.getStart(); - long newSize = entry.getSize() + before.getSize(); - - /* - * Remove the entry. - */ - String name = storedEntry.getCentralDirectoryHeader().getName(); - map.remove(entry); - Verify.verify(entry == entries.remove(name)); - - /* - * Make a list will all existing segments in the entry's extra field, but remove - * the alignment field, if it exists. Also, sum the size of all kept extra field - * segments. - */ - ImmutableList currentSegments; - try { - currentSegments = storedEntry.getLocalExtra().getSegments(); - } catch (IOException e) { - /* - * Parsing current segments has failed. This means the contents of the extra - * field are not valid. We'll continue discarding the existing segments. - */ - currentSegments = ImmutableList.of(); - } - - List extraFieldSegments = new ArrayList<>(); - int newExtraFieldSize = currentSegments.stream() - .filter(s -> s.getHeaderId() - != ExtraField.ALIGNMENT_ZIP_EXTRA_DATA_FIELD_HEADER_ID) - .peek(extraFieldSegments::add) - .map(ExtraField.Segment::size) - .reduce(0, Integer::sum); - - int spaceToFill = - Ints.checkedCast( - before.getSize() - + storedEntry.getLocalExtra().size() - - newExtraFieldSize); - - extraFieldSegments.add( - new ExtraField.AlignmentSegment(chooseAlignment(storedEntry),spaceToFill)); - - storedEntry.setLocalExtraNoNotify( - new ExtraField(ImmutableList.copyOf(extraFieldSegments))); - entries.put(name, map.add(newStart, newStart + newSize, storedEntry)); - - /* - * Reset the offset to force the file to be rewritten. - */ - storedEntry.getCentralDirectoryHeader().setOffset(-1); - } - } - - /* - * Write new files in the zip. We identify new files because they don't have an offset - * in the zip where they are written although we already know, by their location in the - * file map, where they will be written to. - * - * Before writing the files, we sort them in the order they are written in the file so that - * writes are made in order on disk. - * This is, however, unlikely to optimize anything relevant given the way the Operating - * System does caching, but it certainly won't hurt :) - */ - TreeMap, StoredEntry> toWriteToStore = - new TreeMap<>(FileUseMapEntry.COMPARE_BY_START); - - for (FileUseMapEntry entry : entries.values()) { - StoredEntry entryStore = entry.getStore(); - assert entryStore != null; - if (entryStore.getCentralDirectoryHeader().getOffset() == -1) { - toWriteToStore.put(entry, entryStore); - } - } - - /* - * Add all free entries to the set. - */ - for(FileUseMapEntry freeArea : map.getFreeAreas()) { - toWriteToStore.put(freeArea, null); - } - - /* - * Write everything to file. - */ - for (FileUseMapEntry fileUseMapEntry : toWriteToStore.keySet()) { - StoredEntry entry = toWriteToStore.get(fileUseMapEntry); - if (entry == null) { - int size = Ints.checkedCast(fileUseMapEntry.getSize()); - directWrite(fileUseMapEntry.getStart(), new byte[size]); - } else { - writeEntry(entry, fileUseMapEntry.getStart()); - } - } - - boolean hasCentralDirectory; - int extensionBugDetector = MAXIMUM_EXTENSION_CYCLE_COUNT; - do { - computeCentralDirectory(); - computeEocd(); - - hasCentralDirectory = (directoryEntry != null); - - notify(ext -> { - ext.entriesWritten(); - return null; - }); - - if ((--extensionBugDetector) == 0) { - throw new IOException("Extensions keep resetting the central directory. This is " - + "probably a bug."); - } - } while (hasCentralDirectory && directoryEntry == null); - - appendCentralDirectory(); - appendEocd(); - - Verify.verifyNotNull(raf); - raf.setLength(map.size()); - - dirty = false; - - notify(ext -> { - ext.updated(); - return null; - }); - } - - /** - * Reorganizes the zip so that there are no gaps between files bigger than - * {@link #MAX_LOCAL_EXTRA_FIELD_CONTENTS_SIZE} if {@link #coverEmptySpaceUsingExtraField} - * is set to {@code true}. - * - *

Essentially, this makes sure we can cover any empty space with the extra field, given - * that the local extra field is limited to {@link #MAX_LOCAL_EXTRA_FIELD_CONTENTS_SIZE}. If - * an entry is too far from the previous one, it is removed and re-added. - * - * @throws IOException failed to repack - */ - private void packIfNecessary() throws IOException { - if (!coverEmptySpaceUsingExtraField) { - return; - } - - SortedSet> entriesByLocation = - new TreeSet<>(FileUseMapEntry.COMPARE_BY_START); - entriesByLocation.addAll(entries.values()); - - for (FileUseMapEntry entry : entriesByLocation) { - StoredEntry storedEntry = entry.getStore(); - assert storedEntry != null; - - FileUseMapEntry before = map.before(entry); - if (before == null || !before.isFree()) { - continue; - } - - int localExtraSize = - storedEntry.getLocalExtra().size() + Ints.checkedCast(before.getSize()); - if (localExtraSize > MAX_LOCAL_EXTRA_FIELD_CONTENTS_SIZE) { - /* - * This entry is too far from the previous one. Remove it and re-add it to the - * zip file. - */ - reAdd(storedEntry, PositionHint.LOWEST_OFFSET); - } - } - } - - /** - * Removes a stored entry from the zip and adds it back again. This will force the entry to be - * loaded into memory and repositioned in the zip file. It will also mark the archive as - * being dirty. - * - * @param entry the entry - * @param positionHint hint to where the file should be positioned when re-adding - * @throws IOException failed to load the entry into memory - */ - private void reAdd(@Nonnull StoredEntry entry, @Nonnull PositionHint positionHint) - throws IOException { - String name = entry.getCentralDirectoryHeader().getName(); - FileUseMapEntry mapEntry = entries.get(name); - Preconditions.checkNotNull(mapEntry); - Preconditions.checkState(mapEntry.getStore() == entry); - - entry.loadSourceIntoMemory(); - - map.remove(mapEntry); - entries.remove(name); - FileUseMapEntry positioned = positionInFile(entry, positionHint); - entries.put(name, positioned); - dirty = true; - } - - /** - * Invoked from {@link StoredEntry} when entry has changed in a way that forces the local - * header to be rewritten - * - * @param entry the entry that changed - * @param resized was the local header resized? - * @throws IOException failed to load the entry into memory - */ - void localHeaderChanged(@Nonnull StoredEntry entry, boolean resized) throws IOException { - dirty = true; - - if (resized) { - reAdd(entry, PositionHint.ANYWHERE); - } - } - - /** - * Invoked when the central directory has changed and needs to be rewritten. - */ - void centralDirectoryChanged() { - dirty = true; - deleteDirectoryAndEocd(); - } - - /** - * Updates the file and closes it. - */ - @Override - public void close() throws IOException { - // We need to make sure to release raf, otherwise we end up locking the file on - // Windows. Use try-with-resources to handle exception suppressing. - try (Closeable ignored = this::innerClose) { - if (!readOnly) { - update(); - } - } - - notify(ext -> { - ext.closed(); - return null; - }); - } - - /** - * Removes the Central Directory and EOCD from the file. This will free space for new entries - * as well as allowing the zip file to be truncated if files have been removed. - * - *

This method does not mark the zip as dirty. - */ - private void deleteDirectoryAndEocd() { - if (directoryEntry != null) { - map.remove(directoryEntry); - directoryEntry = null; - } - - if (eocdEntry != null) { - map.remove(eocdEntry); - - Eocd eocd = eocdEntry.getStore(); - Verify.verify(eocd != null); - eocdComment = eocd.getComment(); - eocdEntry = null; - } - } - - /** - * Writes an entry's data in the zip file. This includes everything: the local header and - * the data itself. After writing, the entry is updated with the offset and its source replaced - * with a source that reads from the zip file. - * - * @param entry the entry to write - * @param offset the offset at which the entry should be written - * @throws IOException failed to write the entry - */ - private void writeEntry(@Nonnull StoredEntry entry, long offset) throws IOException { - Preconditions.checkArgument(entry.getDataDescriptorType() - == DataDescriptorType. NO_DATA_DESCRIPTOR, "Cannot write entries with a data " - + "descriptor."); - Preconditions.checkNotNull(raf, "raf == null"); - Preconditions.checkState(state == ZipFileState.OPEN_RW, "state != ZipFileState.OPEN_RW"); - - /* - * Place the cursor and write the local header. - */ - byte[] headerData = entry.toHeaderData(); - directWrite(offset, headerData); - - /* - * Get the raw source data to write. - */ - ProcessedAndRawByteSources source = entry.getSource(); - ByteSource rawContents = source.getRawByteSource(); - - /* - * Write the source data. - */ - byte[] chunk = new byte[IO_BUFFER_SIZE]; - int r; - long writeOffset = offset + headerData.length; - InputStream is = rawContents.openStream(); - while ((r = is.read(chunk)) >= 0) { - directWrite(writeOffset, chunk, 0, r); - writeOffset += r; - } - - is.close(); - - /* - * Set the entry's offset and create the entry source. - */ - entry.replaceSourceFromZip(offset); - } - - /** - * Computes the central directory. The central directory must not have been computed yet. When - * this method finishes, the central directory has been computed {@link #directoryEntry}, - * unless the directory is empty in which case {@link #directoryEntry} - * is left as {@code null}. Nothing is written to disk as a result of this method's invocation. - * - * @throws IOException failed to append the central directory - */ - private void computeCentralDirectory() throws IOException { - Preconditions.checkState(state == ZipFileState.OPEN_RW, "state != ZipFileState.OPEN_RW"); - Preconditions.checkNotNull(raf, "raf == null"); - Preconditions.checkState(directoryEntry == null, "directoryEntry == null"); - - Set newStored = Sets.newHashSet(); - for (FileUseMapEntry mapEntry : entries.values()) { - newStored.add(mapEntry.getStore()); - } - - /* - * Make sure we truncate the map before computing the central directory's location since - * the central directory is the last part of the file. - */ - map.truncate(); - - CentralDirectory newDirectory = CentralDirectory.makeFromEntries(newStored, this); - byte[] newDirectoryBytes = newDirectory.toBytes(); - long directoryOffset = map.size() + extraDirectoryOffset; - - map.extend(directoryOffset + newDirectoryBytes.length); - - if (newDirectoryBytes.length > 0) { - directoryEntry = map.add(directoryOffset, directoryOffset + newDirectoryBytes.length, - newDirectory); - } - } - - /** - * Writes the central directory to the end of the zip file. {@link #directoryEntry} may be - * {@code null} only if there are no files in the archive. - * - * @throws IOException failed to append the central directory - */ - private void appendCentralDirectory() throws IOException { - Preconditions.checkState(state == ZipFileState.OPEN_RW, "state != ZipFileState.OPEN_RW"); - Preconditions.checkNotNull(raf, "raf == null"); - - if (entries.isEmpty()) { - Preconditions.checkState(directoryEntry == null, "directoryEntry != null"); - return; - } - - Preconditions.checkNotNull(directoryEntry, "directoryEntry != null"); - - CentralDirectory newDirectory = directoryEntry.getStore(); - Preconditions.checkNotNull(newDirectory, "newDirectory != null"); - - byte[] newDirectoryBytes = newDirectory.toBytes(); - long directoryOffset = directoryEntry.getStart(); - - /* - * It is fine to seek beyond the end of file. Seeking beyond the end of file will not extend - * the file. Even if we do not have any directory data to write, the extend() call below - * will force the file to be extended leaving exactly extraDirectoryOffset bytes empty at - * the beginning. - */ - directWrite(directoryOffset, newDirectoryBytes); - } - - /** - * Obtains the byte array representation of the central directory. The central directory must - * have been already computed. If there are no entries in the zip, the central directory will be - * empty. - * - * @return the byte representation, or an empty array if there are no entries in the zip - * @throws IOException failed to compute the central directory byte representation - */ - @Nonnull - public byte[] getCentralDirectoryBytes() throws IOException { - if (entries.isEmpty()) { - Preconditions.checkState(directoryEntry == null, "directoryEntry != null"); - return new byte[0]; - } - - Preconditions.checkNotNull(directoryEntry, "directoryEntry == null"); - - CentralDirectory cd = directoryEntry.getStore(); - Preconditions.checkNotNull(cd, "cd == null"); - return cd.toBytes(); - } - - /** - * Computes the EOCD. This creates a new {@link #eocdEntry}. The - * central directory must already be written. If {@link #directoryEntry} is {@code null}, then - * the zip file must not have any entries. - * - * @throws IOException failed to write the EOCD - */ - private void computeEocd() throws IOException { - Preconditions.checkState(state == ZipFileState.OPEN_RW, "state != ZipFileState.OPEN_RW"); - Preconditions.checkNotNull(raf, "raf == null"); - if (directoryEntry == null) { - Preconditions.checkState(entries.isEmpty(), - "directoryEntry == null && !entries.isEmpty()"); - } - - long dirStart; - long dirSize = 0; - - if (directoryEntry != null) { - CentralDirectory directory = directoryEntry.getStore(); - assert directory != null; - - dirStart = directoryEntry.getStart(); - dirSize = directoryEntry.getSize(); - Verify.verify(directory.getEntries().size() == entries.size()); - } else { - /* - * If we do not have a directory, then we must leave any requested offset empty. - */ - dirStart = extraDirectoryOffset; - } - - Verify.verify(eocdComment != null); - Eocd eocd = new Eocd(entries.size(), dirStart, dirSize, eocdComment); - eocdComment = null; - - byte[] eocdBytes = eocd.toBytes(); - long eocdOffset = map.size(); - - map.extend(eocdOffset + eocdBytes.length); - - eocdEntry = map.add(eocdOffset, eocdOffset + eocdBytes.length, eocd); - } - - /** - * Writes the EOCD to the end of the zip file. This creates a new {@link #eocdEntry}. The - * central directory must already be written. If {@link #directoryEntry} is {@code null}, then - * the zip file must not have any entries. - * - * @throws IOException failed to write the EOCD - */ - private void appendEocd() throws IOException { - Preconditions.checkState(state == ZipFileState.OPEN_RW, "state != ZipFileState.OPEN_RW"); - Preconditions.checkNotNull(raf, "raf == null"); - Preconditions.checkNotNull(eocdEntry, "eocdEntry == null"); - - Eocd eocd = eocdEntry.getStore(); - Preconditions.checkNotNull(eocd, "eocd == null"); - - byte[] eocdBytes = eocd.toBytes(); - long eocdOffset = eocdEntry.getStart(); - - directWrite(eocdOffset, eocdBytes); - } - - /** - * Obtains the byte array representation of the EOCD. The EOCD must have already been computed - * for this method to be invoked. - * - * @return the byte representation of the EOCD - * @throws IOException failed to obtain the byte representation of the EOCD - */ - @Nonnull - public byte[] getEocdBytes() throws IOException { - Preconditions.checkNotNull(eocdEntry, "eocdEntry == null"); - - Eocd eocd = eocdEntry.getStore(); - Preconditions.checkNotNull(eocd, "eocd == null"); - return eocd.toBytes(); - } - - /** - * Closes the file, if it is open. - * - * @throws IOException failed to close the file - */ - private void innerClose() throws IOException { - if (state == ZipFileState.CLOSED) { - return; - } - - Verify.verifyNotNull(raf, "raf == null"); - - raf.close(); - raf = null; - state = ZipFileState.CLOSED; - if (closedControl == null) { - closedControl = new CachedFileContents<>(file); - } - - closedControl.closed(null); - } - - /** - * If the zip file is closed, opens it in read-only mode. If it is already open, does nothing. - * In general, it is not necessary to directly invoke this method. However, if directly - * reading the zip file using, for example {@link #directRead(long, byte[])}, then this - * method needs to be called. - * @throws IOException failed to open the file - */ - public void openReadOnly() throws IOException { - if (state != ZipFileState.CLOSED) { - return; - } - - state = ZipFileState.OPEN_RO; - raf = new RandomAccessFile(file, "r"); - } - - /** - * Opens (or reopens) the zip file as read-write. This method will ensure that - * {@link #raf} is not null and open for writing. - * - * @throws IOException failed to open the file, failed to close it or the file was closed and - * has been modified outside the control of this object - */ - private void reopenRw() throws IOException { - // We an never open a file RW in read-only mode. We should never get this far, though. - Verify.verify(!readOnly); - - if (state == ZipFileState.OPEN_RW) { - return; - } - - boolean wasClosed; - if (state == ZipFileState.OPEN_RO) { - /* - * ReadAccessFile does not have a way to reopen as RW so we have to close it and - * open it again. - */ - innerClose(); - wasClosed = false; - } else { - wasClosed = true; - } - - Verify.verify(state == ZipFileState.CLOSED, "state != ZpiFileState.CLOSED"); - Verify.verify(raf == null, "raf != null"); - - if (closedControl != null && !closedControl.isValid()) { - throw new IOException("File '" + file.getAbsolutePath() + "' has been modified " - + "by an external application."); - } - - raf = new RandomAccessFile(file, "rw"); - state = ZipFileState.OPEN_RW; - - /* - * Now that we've open the zip and are ready to write, clear out any data descriptors - * in the zip since we don't need them and they take space in the archive. - */ - for (StoredEntry entry : entries()) { - dirty |= entry.removeDataDescriptor(); - } - - if (wasClosed) { - notify(ZFileExtension::open); - } - } - - /** - * Equivalent to call {@link #add(String, InputStream, boolean)} using - * {@code true} as {@code mayCompress}. - * - * @param name the file name (i.e., path); paths should be defined using slashes - * and the name should not end in slash - * @param stream the source for the file's data - * @throws IOException failed to read the source data - * @throws IllegalStateException if the file is in read-only mode - */ - public void add(@Nonnull String name, @Nonnull InputStream stream) throws IOException { - checkNotInReadOnlyMode(); - add(name, stream, true); - } - - /** - * Creates a stored entry. This does not add the entry to the zip file, it just creates the - * {@link StoredEntry} object. - * - * @param name the name of the entry - * @param stream the input stream with the entry's data - * @param mayCompress can the entry be compressed? - * @return the created entry - * @throws IOException failed to create the entry - */ - @Nonnull - private StoredEntry makeStoredEntry( - @Nonnull String name, - @Nonnull InputStream stream, - boolean mayCompress) - throws IOException { - CloseableByteSource source = tracker.fromStream(stream); - long crc32 = source.hash(Hashing.crc32()).padToLong(); - - boolean encodeWithUtf8 = !EncodeUtils.canAsciiEncode(name); - - SettableFuture compressInfo = - SettableFuture.create(); - GPFlags flags = GPFlags.make(encodeWithUtf8); - CentralDirectoryHeader newFileData = - new CentralDirectoryHeader( - name, - EncodeUtils.encode(name, flags), - source.size(), - compressInfo, - flags, - this); - newFileData.setCrc32(crc32); - - /* - * Create the new entry and sets its data source. Offset should be set to -1 automatically - * because this is a new file. With offset set to -1, StoredEntry does not try to verify the - * local header. Since this is a new file, there is no local header and not checking it is - * what we want to happen. - */ - Verify.verify(newFileData.getOffset() == -1); - return new StoredEntry( - newFileData, - this, - createSources(mayCompress, source, compressInfo, newFileData)); - } - - /** - * Creates the processed and raw sources for an entry. - * - * @param mayCompress can the entry be compressed? - * @param source the entry's data (uncompressed) - * @param compressInfo the compression info future that will be set when the raw entry is - * created and the {@link CentralDirectoryHeaderCompressInfo} object can be created - * @param newFileData the central directory header for the new file - * @return the sources whose data may or may not be already defined - * @throws IOException failed to create the raw sources - */ - @Nonnull - private ProcessedAndRawByteSources createSources( - boolean mayCompress, - @Nonnull CloseableByteSource source, - @Nonnull SettableFuture compressInfo, - @Nonnull CentralDirectoryHeader newFileData) - throws IOException { - if (mayCompress) { - ListenableFuture result = compressor.compress(source); - Futures.addCallback( - result, - new FutureCallback() { - @Override - public void onSuccess(CompressionResult result) { - compressInfo.set( - new CentralDirectoryHeaderCompressInfo( - newFileData, - result.getCompressionMethod(), - result.getSize())); - } - - @Override - public void onFailure(@Nonnull Throwable t) { - compressInfo.setException(t); - } - }, - MoreExecutors.directExecutor()); - - ListenableFuture compressedByteSourceFuture = - Futures.transform( - result, CompressionResult::getSource, MoreExecutors.directExecutor()); - LazyDelegateByteSource compressedByteSource = new LazyDelegateByteSource( - compressedByteSourceFuture); - return new ProcessedAndRawByteSources(source, compressedByteSource); - } else { - compressInfo.set(new CentralDirectoryHeaderCompressInfo(newFileData, - CompressionMethod.STORE, source.size())); - return new ProcessedAndRawByteSources(source, source); - } - } - - /** - * Adds a file to the archive. - * - *

Adding the file will not update the archive immediately. Updating will only happen - * when the {@link #update()} method is invoked. - * - *

Adding a file with the same name as an existing file will replace that file in the - * archive. - * - * @param name the file name (i.e., path); paths should be defined using slashes - * and the name should not end in slash - * @param stream the source for the file's data - * @param mayCompress can the file be compressed? This flag will be ignored if the alignment - * rules force the file to be aligned, in which case the file will not be compressed. - * @throws IOException failed to read the source data - * @throws IllegalStateException if the file is in read-only mode - */ - public void add(@Nonnull String name, @Nonnull InputStream stream, boolean mayCompress) - throws IOException { - checkNotInReadOnlyMode(); - - /* - * Clean pending background work, if needed. - */ - processAllReadyEntries(); - - add(makeStoredEntry(name, stream, mayCompress)); - } - - /** - * Adds a {@link StoredEntry} to the zip. The entry is not immediately added to - * {@link #entries} because data may not yet be available. Instead, it is placed under - * {@link #uncompressedEntries} and later moved to {@link #processAllReadyEntries()} when - * done. - * - *

This method invokes {@link #processAllReadyEntries()} to move the entry if it has already - * been computed so, if there is no delay in compression, and no more files are in waiting - * queue, then the entry is added to {@link #entries} immediately. - * - * @param newEntry the entry to add - * @throws IOException failed to process this entry (or a previous one whose future only - * completed now) - */ - private void add(@Nonnull final StoredEntry newEntry) throws IOException { - uncompressedEntries.add(newEntry); - processAllReadyEntries(); - } - - /** - * Moves all ready entries from {@link #uncompressedEntries} to {@link #entries}. It will - * stop as soon as entry whose future has not been completed is found. - * - * @throws IOException the exception reported in the future computation, if any, or failed - * to add a file to the archive - */ - private void processAllReadyEntries() throws IOException { - /* - * Many things can happen during addToEntries(). Because addToEntries() fires - * notifications to extensions, other files can be added, removed, etc. Ee are *not* - * guaranteed that new stuff does not get into uncompressedEntries: add() will still work - * and will add new entries in there. - * - * However -- important -- processReadyEntries() may be invoked during addToEntries() - * because of the extension mechanism. This means that stuff *can* be removed from - * uncompressedEntries and moved to entries during addToEntries(). - */ - while (!uncompressedEntries.isEmpty()) { - StoredEntry next = uncompressedEntries.get(0); - CentralDirectoryHeader cdh = next.getCentralDirectoryHeader(); - Future compressionInfo = cdh.getCompressionInfo(); - if (!compressionInfo.isDone()) { - /* - * First entry in queue is not yet complete. We can't do anything else. - */ - return; - } - - uncompressedEntries.remove(0); - - try { - compressionInfo.get(); - } catch (InterruptedException e) { - throw new IOException("Impossible I/O exception: get for already computed " - + "future throws InterruptedException", e); - } catch (ExecutionException e) { - throw new IOException("Failed to obtain compression information for entry", e); - } - - addToEntries(next); - } - } - - /** - * Waits until {@link #uncompressedEntries} is empty. - * - * @throws IOException the exception reported in the future computation, if any, or failed - * to add a file to the archive - */ - private void processAllReadyEntriesWithWait() throws IOException { - processAllReadyEntries(); - while (!uncompressedEntries.isEmpty()) { - /* - * Wait for the first future to complete and then try again. Keep looping until we're - * done. - */ - StoredEntry first = uncompressedEntries.get(0); - CentralDirectoryHeader cdh = first.getCentralDirectoryHeader(); - cdh.getCompressionInfoWithWait(); - - processAllReadyEntries(); - } - } - - /** - * Adds a new file to {@link #entries}. This is actually added to the zip and its space - * allocated in the {@link #map}. - * - * @param newEntry the new entry to add - * @throws IOException failed to add the file - */ - private void addToEntries(@Nonnull final StoredEntry newEntry) throws IOException { - Preconditions.checkArgument(newEntry.getDataDescriptorType() == - DataDescriptorType.NO_DATA_DESCRIPTOR, "newEntry has data descriptor"); - - /* - * If there is a file with the same name in the archive, remove it. We remove it by - * calling delete() on the entry (this is the public API to remove a file from the archive). - * StoredEntry.delete() will call {@link ZFile#delete(StoredEntry, boolean)} to perform - * data structure cleanup. - */ - FileUseMapEntry toReplace = entries.get( - newEntry.getCentralDirectoryHeader().getName()); - final StoredEntry replaceStore; - if (toReplace != null) { - replaceStore = toReplace.getStore(); - assert replaceStore != null; - replaceStore.delete(false); - } else { - replaceStore = null; - } - - FileUseMapEntry fileUseMapEntry = - positionInFile(newEntry, PositionHint.ANYWHERE); - entries.put(newEntry.getCentralDirectoryHeader().getName(), fileUseMapEntry); - - dirty = true; - - notify(ext -> ext.added(newEntry, replaceStore)); - } - - /** - * Finds a location in the zip where this entry will be added to and create the map entry. - * This method cannot be called if there is already a map entry for the given entry (if you - * do that, then you're doing something wrong somewhere). - * - *

This may delete the central directory and EOCD (if it deletes one, it deletes the other) - * if there is no space before the central directory. Otherwise, the file would be added - * after the central directory. This would force a new central directory to be written - * when updating the file and would create a hole in the zip. Me no like holes. Holes are evil. - * - * @param entry the entry to place in the zip - * @param positionHint hint to where the file should be positioned - * @return the position in the file where the entry should be placed - */ - @Nonnull - private FileUseMapEntry positionInFile( - @Nonnull StoredEntry entry, - @Nonnull PositionHint positionHint) - throws IOException { - deleteDirectoryAndEocd(); - long size = entry.getInFileSize(); - int localHeaderSize = entry.getLocalHeaderSize(); - int alignment = chooseAlignment(entry); - - FileUseMap.PositionAlgorithm algorithm; - - switch (positionHint) { - case LOWEST_OFFSET: - algorithm = FileUseMap.PositionAlgorithm.FIRST_FIT; - break; - case ANYWHERE: - algorithm = FileUseMap.PositionAlgorithm.BEST_FIT; - break; - default: - throw new AssertionError(); - } - - long newOffset = map.locateFree(size, localHeaderSize, alignment, algorithm); - long newEnd = newOffset + entry.getInFileSize(); - if (newEnd > map.size()) { - map.extend(newEnd); - } - - return map.add(newOffset, newEnd, entry); - } - - /** - * Determines what is the alignment value of an entry. - * - * @param entry the entry - * @return the alignment value, {@link AlignmentRule#NO_ALIGNMENT} if there is no alignment - * required for the entry - * @throws IOException failed to determine the alignment - */ - private int chooseAlignment(@Nonnull StoredEntry entry) throws IOException { - CentralDirectoryHeader cdh = entry.getCentralDirectoryHeader(); - CentralDirectoryHeaderCompressInfo compressionInfo = cdh.getCompressionInfoWithWait(); - - boolean isCompressed = compressionInfo.getMethod() != CompressionMethod.STORE; - if (isCompressed) { - return AlignmentRule.NO_ALIGNMENT; - } else { - return alignmentRule.alignment(cdh.getName()); - } - } - - /** - * Adds all files from another zip file, maintaining their compression. Files specified in - * src that are already on this file will replace the ones in this file. However, if - * their sizes and checksums are equal, they will be ignored. - * - *

This method will not perform any changes in itself, it will only update in-memory data - * structures. To actually write the zip file, invoke either {@link #update()} or - * {@link #close()}. - * - * @param src the source archive - * @param ignoreFilter predicate that, if {@code true}, identifies files in src that - * should be ignored by merging; merging will behave as if these files were not there - * @throws IOException failed to read from src or write on the output - * @throws IllegalStateException if the file is in read-only mode - */ - public void mergeFrom(@Nonnull ZFile src, @Nonnull Predicate ignoreFilter) - throws IOException { - checkNotInReadOnlyMode(); - - for (StoredEntry fromEntry : src.entries()) { - if (ignoreFilter.test(fromEntry.getCentralDirectoryHeader().getName())) { - continue; - } - - boolean replaceCurrent = true; - String path = fromEntry.getCentralDirectoryHeader().getName(); - FileUseMapEntry currentEntry = entries.get(path); - - if (currentEntry != null) { - long fromSize = fromEntry.getCentralDirectoryHeader().getUncompressedSize(); - long fromCrc = fromEntry.getCentralDirectoryHeader().getCrc32(); - - StoredEntry currentStore = currentEntry.getStore(); - assert currentStore != null; - - long currentSize = currentStore.getCentralDirectoryHeader().getUncompressedSize(); - long currentCrc = currentStore.getCentralDirectoryHeader().getCrc32(); - - if (fromSize == currentSize && fromCrc == currentCrc) { - replaceCurrent = false; - } - } - - if (replaceCurrent) { - CentralDirectoryHeader fromCdr = fromEntry.getCentralDirectoryHeader(); - CentralDirectoryHeaderCompressInfo fromCompressInfo = - fromCdr.getCompressionInfoWithWait(); - CentralDirectoryHeader newFileData; - try { - /* - * We make two changes in the central directory from the file to merge: - * we reset the offset to force the entry to be written and we reset the - * deferred CRC bit as we don't need the extra stuff after the file. It takes - * space and is totally useless. - */ - newFileData = fromCdr.clone(); - newFileData.setOffset(-1); - newFileData.resetDeferredCrc(); - } catch (CloneNotSupportedException e) { - throw new IOException("Failed to clone CDR.", e); - } - - /* - * Read the data (read directly the compressed source if there is one). - */ - ProcessedAndRawByteSources fromSource = fromEntry.getSource(); - InputStream fromInput = fromSource.getRawByteSource().openStream(); - long sourceSize = fromSource.getRawByteSource().size(); - if (sourceSize > Integer.MAX_VALUE) { - throw new IOException("Cannot read source with " + sourceSize + " bytes."); - } - - byte[] data = new byte[Ints.checkedCast(sourceSize)]; - int read = 0; - while (read < data.length) { - int r = fromInput.read(data, read, data.length - read); - Verify.verify(r >= 0, "There should be at least 'size' bytes in the stream."); - read += r; - } - - /* - * Build the new source and wrap it around an inflater source if data came from - * a compressed source. - */ - CloseableByteSource rawContents = tracker.fromSource(fromSource.getRawByteSource()); - CloseableByteSource processedContents; - if (fromCompressInfo.getMethod() == CompressionMethod.DEFLATE) { - //noinspection IOResourceOpenedButNotSafelyClosed - processedContents = new InflaterByteSource(rawContents); - } else { - processedContents = rawContents; - } - - ProcessedAndRawByteSources newSource = new ProcessedAndRawByteSources( - processedContents, rawContents); - - /* - * Add will replace any current entry with the same name. - */ - StoredEntry newEntry = new StoredEntry(newFileData, this, newSource); - add(newEntry); - } - } - } - - /** - * Forcibly marks this zip file as touched, forcing it to be updated when {@link #update()} - * or {@link #close()} are invoked. - * - * @throws IllegalStateException if the file is in read-only mode - */ - public void touch() { - checkNotInReadOnlyMode(); - dirty = true; - } - - /** - * Wait for any background tasks to finish and report any errors. In general this method does - * not need to be invoked directly as errors from background tasks are reported during - * {@link #add(String, InputStream, boolean)}, {@link #update()} and {@link #close()}. - * However, if required for some purposes, e.g., ensuring all notifications have been - * done to extensions, then this method may be called. It will wait for all background tasks - * to complete. - * @throws IOException some background work failed - */ - public void finishAllBackgroundTasks() throws IOException { - processAllReadyEntriesWithWait(); - } - - /** - * Realigns all entries in the zip. This is equivalent to call {@link StoredEntry#realign()} - * for all entries in the zip file. - * - * @return has any entry been changed? Note that for entries that have not yet been written on - * the file, realignment does not count as a change as nothing needs to be updated in the file; - * entries that have been updated may have been recreated and the existing references outside - * of {@code ZFile} may refer to {@link StoredEntry}s that are no longer valid - * @throws IOException failed to realign the zip; some entries in the zip may have been lost - * due to the I/O error - * @throws IllegalStateException if the file is in read-only mode - */ - public boolean realign() throws IOException { - checkNotInReadOnlyMode(); - - boolean anyChanges = false; - for (StoredEntry entry : entries()) { - anyChanges |= entry.realign(); - } - - if (anyChanges) { - dirty = true; - } - - return anyChanges; - } - - /** - * Realigns a stored entry, if necessary. Realignment is done by removing and re-adding the file - * if it was not aligned. - * - * @param entry the entry to realign - * @return has the entry been changed? Note that if the entry has not yet been written on the - * file, realignment does not count as a change as nothing needs to be updated in the file - * @throws IOException failed to read/write an entry; the entry may no longer exist in the - * file - */ - boolean realign(@Nonnull StoredEntry entry) throws IOException { - FileUseMapEntry mapEntry = - entries.get(entry.getCentralDirectoryHeader().getName()); - Verify.verify(entry == mapEntry.getStore()); - long currentDataOffset = mapEntry.getStart() + entry.getLocalHeaderSize(); - - int expectedAlignment = chooseAlignment(entry); - long misalignment = currentDataOffset % expectedAlignment; - if (misalignment == 0) { - /* - * Good. File is aligned properly. - */ - return false; - } - - if (entry.getCentralDirectoryHeader().getOffset() == -1) { - /* - * File is not aligned but it is not written. We do not really need to do much other - * than find another place in the map. - */ - map.remove(mapEntry); - long newStart = - map.locateFree( - mapEntry.getSize(), - entry.getLocalHeaderSize(), - expectedAlignment, - FileUseMap.PositionAlgorithm.BEST_FIT); - mapEntry = map.add(newStart, newStart + entry.getInFileSize(), entry); - entries.put(entry.getCentralDirectoryHeader().getName(), mapEntry); - - /* - * Just for safety. We're modifying the in-memory structures but the file should - * already be marked as dirty. - */ - Verify.verify(dirty); - - return false; - - } - - /* - * Get the entry data source, but check if we have a compressed one (we don't want to - * inflate and deflate). - */ - CentralDirectoryHeaderCompressInfo compressInfo = - entry.getCentralDirectoryHeader().getCompressionInfoWithWait(); - - ProcessedAndRawByteSources source = entry.getSource(); - - CentralDirectoryHeader clonedCdh; - try { - clonedCdh = entry.getCentralDirectoryHeader().clone(); - } catch (CloneNotSupportedException e) { - Verify.verify(false); - return false; - } - - /* - * We make two changes in the central directory when realigning: - * we reset the offset to force the entry to be written and we reset the - * deferred CRC bit as we don't need the extra stuff after the file. It takes - * space and is totally useless and we may need the extra space to realign the entry... - */ - clonedCdh.setOffset(-1); - clonedCdh.resetDeferredCrc(); - - CloseableByteSource rawContents = tracker.fromSource(source.getRawByteSource()); - CloseableByteSource processedContents; - - if (compressInfo.getMethod() == CompressionMethod.DEFLATE) { - //noinspection IOResourceOpenedButNotSafelyClosed - processedContents = new InflaterByteSource(rawContents); - } else { - processedContents = rawContents; - } - - ProcessedAndRawByteSources newSource = new ProcessedAndRawByteSources(processedContents, - rawContents); - - /* - * Add the new file. This will replace the existing one. - */ - StoredEntry newEntry = new StoredEntry(clonedCdh, this, newSource); - add(newEntry); - return true; - } - - /** - * Adds an extension to this zip file. - * - * @param extension the listener to add - * @throws IllegalStateException if the file is in read-only mode - */ - public void addZFileExtension(@Nonnull ZFileExtension extension) { - checkNotInReadOnlyMode(); - extensions.add(extension); - } - - /** - * Removes an extension from this zip file. - * - * @param extension the listener to remove - * @throws IllegalStateException if the file is in read-only mode - */ - public void removeZFileExtension(@Nonnull ZFileExtension extension) { - checkNotInReadOnlyMode(); - extensions.remove(extension); - } - - /** - * Notifies all extensions, collecting their execution requests and running them. - * - * @param function the function to apply to all listeners, it will generally invoke the - * notification method on the listener and return the result of that invocation - * @throws IOException failed to process some extensions - */ - private void notify(@Nonnull IOExceptionFunction function) - throws IOException { - for (ZFileExtension fl : Lists.newArrayList(extensions)) { - IOExceptionRunnable r = function.apply(fl); - if (r != null) { - toRun.add(r); - } - } - - if (!isNotifying) { - isNotifying = true; - - try { - while (!toRun.isEmpty()) { - IOExceptionRunnable r = toRun.remove(0); - r.run(); - } - } finally { - isNotifying = false; - } - } - } - - /** - * Directly writes data in the zip file. Incorrect use of this method may corrupt the - * zip file. Invoking this method may force the zip to be reopened in read/write - * mode. - * - * @param offset the offset at which data should be written - * @param data the data to write, may be an empty array - * @param start start offset in {@code data} where data to write is located - * @param count number of bytes of data to write - * @throws IOException failed to write the data - * @throws IllegalStateException if the file is in read-only mode - */ - public void directWrite(long offset, @Nonnull byte[] data, int start, int count) - throws IOException { - checkNotInReadOnlyMode(); - - Preconditions.checkArgument(offset >= 0, "offset < 0"); - Preconditions.checkArgument(start >= 0, "start >= 0"); - Preconditions.checkArgument(count >= 0, "count >= 0"); - - if (data.length == 0) { - return; - } - - Preconditions.checkArgument(start <= data.length, "start > data.length"); - Preconditions.checkArgument(start + count <= data.length, "start + count > data.length"); - - reopenRw(); - assert raf != null; - - raf.seek(offset); - raf.write(data, start, count); - } - - /** - * Same as {@code directWrite(offset, data, 0, data.length)}. - * - * @param offset the offset at which data should be written - * @param data the data to write, may be an empty array - * @throws IOException failed to write the data - * @throws IllegalStateException if the file is in read-only mode - */ - public void directWrite(long offset, @Nonnull byte[] data) throws IOException { - checkNotInReadOnlyMode(); - directWrite(offset, data, 0, data.length); - } - - /** - * Returns the current size (in bytes) of the underlying file. - * - * @throws IOException if an I/O error occurs - */ - public long directSize() throws IOException { - /* - * Only force a reopen if the file is closed. - */ - if (raf == null) { - reopenRw(); - assert raf != null; - } - return raf.length(); - } - - /** - * Directly reads data from the zip file. Invoking this method may force the zip to be reopened - * in read/write mode. - * - * @param offset the offset at which data should be written - * @param data the array where read data should be stored - * @param start start position in the array where to write data to - * @param count how many bytes of data can be written - * @return how many bytes of data have been written or {@code -1} if there are no more bytes - * to be read - * @throws IOException failed to write the data - */ - public int directRead(long offset, @Nonnull byte[] data, int start, int count) - throws IOException { - Preconditions.checkArgument(start >= 0, "start >= 0"); - Preconditions.checkArgument(count >= 0, "count >= 0"); - Preconditions.checkArgument(start <= data.length, "start > data.length"); - Preconditions.checkArgument(start + count <= data.length, "start + count > data.length"); - return directRead(offset, ByteBuffer.wrap(data, start, count)); - } - - /** - * Directly reads data from the zip file. Invoking this method may force the zip to be reopened - * in read/write mode. - * - * @param offset the offset from which data should be read - * @param dest the output buffer to fill with data from the {@code offset}. - * @return how many bytes of data have been written or {@code -1} if there are no more bytes - * to be read - * @throws IOException failed to write the data - */ - public int directRead(long offset, @Nonnull ByteBuffer dest) throws IOException { - Preconditions.checkArgument(offset >= 0, "offset < 0"); - - if (!dest.hasRemaining()) { - return 0; - } - - /* - * Only force a reopen if the file is closed. - */ - if (raf == null) { - reopenRw(); - assert raf != null; - } - - raf.seek(offset); - return raf.getChannel().read(dest); - } - - /** - * Same as {@code directRead(offset, data, 0, data.length)}. - * - * @param offset the offset at which data should be read - * @param data receives the read data, may be an empty array - * @throws IOException failed to read the data - */ - public int directRead(long offset, @Nonnull byte[] data) throws IOException { - return directRead(offset, data, 0, data.length); - } - - /** - * Reads exactly {@code data.length} bytes of data, failing if it was not possible to read all - * the requested data. - * - * @param offset the offset at which to start reading - * @param data the array that receives the data read - * @throws IOException failed to read some data or there is not enough data to read - */ - public void directFullyRead(long offset, @Nonnull byte[] data) throws IOException { - directFullyRead(offset, ByteBuffer.wrap(data)); - } - - /** - * Reads exactly {@code dest.remaining()} bytes of data, failing if it was not possible to read - * all the requested data. - * - * @param offset the offset at which to start reading - * @param dest the output buffer to fill with data - * @throws IOException failed to read some data or there is not enough data to read - */ - public void directFullyRead(long offset, @Nonnull ByteBuffer dest) throws IOException { - Preconditions.checkArgument(offset >= 0, "offset < 0"); - - if (!dest.hasRemaining()) { - return; - } - - /* - * Only force a reopen if the file is closed. - */ - if (raf == null) { - reopenRw(); - assert raf != null; - } - - FileChannel fileChannel = raf.getChannel(); - while (dest.hasRemaining()) { - fileChannel.position(offset); - int chunkSize = fileChannel.read(dest); - if (chunkSize == -1) { - throw new EOFException( - "Failed to read " + dest.remaining() + " more bytes: premature EOF"); - } - offset += chunkSize; - } - } - - /** - * Adds all files and directories recursively. - *

- * Equivalent to calling {@link #addAllRecursively(File, Function)} using a function that - * always returns {@code true} - * - * @param file a file or directory; if it is a directory, all files and directories will be - * added recursively - * @throws IOException failed to some (or all ) of the files - * @throws IllegalStateException if the file is in read-only mode - */ - public void addAllRecursively(@Nonnull File file) throws IOException { - checkNotInReadOnlyMode(); - addAllRecursively(file, f -> true); - } - - /** - * Adds all files and directories recursively. - * - * @param file a file or directory; if it is a directory, all files and directories will be - * added recursively - * @param mayCompress a function that decides whether files may be compressed - * @throws IOException failed to some (or all ) of the files - * @throws IllegalStateException if the file is in read-only mode - */ - public void addAllRecursively( - @Nonnull File file, - @Nonnull Function mayCompress) throws IOException { - checkNotInReadOnlyMode(); - - /* - * The case of file.isFile() is different because if file.isFile() we will add it to the - * zip in the root. However, if file.isDirectory() we won't add it and add its children. - */ - if (file.isFile()) { - boolean mayCompressFile = Verify.verifyNotNull(mayCompress.apply(file), - "mayCompress.apply() returned null"); - - try (Closer closer = Closer.create()) { - FileInputStream fileInput = closer.register(new FileInputStream(file)); - add(file.getName(), fileInput, mayCompressFile); - } - - return; - } - - for (File f : Files.fileTreeTraverser().preOrderTraversal(file).skip(1)) { - String path = file.toURI().relativize(f.toURI()).getPath(); - - InputStream stream; - try (Closer closer = Closer.create()) { - boolean mayCompressFile; - if (f.isDirectory()) { - stream = closer.register(new ByteArrayInputStream(new byte[0])); - mayCompressFile = false; - } else { - stream = closer.register(new FileInputStream(f)); - mayCompressFile = Verify.verifyNotNull(mayCompress.apply(f), - "mayCompress.apply() returned null"); - } - - add(path, stream, mayCompressFile); - } - } - } - - /** - * Obtains the offset at which the central directory exists, or at which it will be written - * if the zip file were to be flushed immediately. - * - * @return the offset, in bytes, where the central directory is or will be written; this value - * includes any extra offset for the central directory - */ - public long getCentralDirectoryOffset() { - if (directoryEntry != null) { - return directoryEntry.getStart(); - } - - /* - * If there are no entries, the central directory is written at the start of the file. - */ - if (entries.isEmpty()) { - return extraDirectoryOffset; - } - - /* - * The Central Directory is written after all entries. This will be at the end of the file - * if the - */ - return map.usedSize() + extraDirectoryOffset; - } - - /** - * Obtains the size of the central directory, if the central directory is written in the zip - * file. - * - * @return the size of the central directory or {@code -1} if the central directory has not - * been computed - */ - public long getCentralDirectorySize() { - if (directoryEntry != null) { - return directoryEntry.getSize(); - } - - if (entries.isEmpty()) { - return 0; - } - - return 1; - } - - /** - * Obtains the offset of the EOCD record, if the EOCD has been written to the file. - * - * @return the offset of the EOCD or {@code -1} if none exists yet - */ - public long getEocdOffset() { - if (eocdEntry == null) { - return -1; - } - - return eocdEntry.getStart(); - } - - /** - * Obtains the size of the EOCD record, if the EOCD has been written to the file. - * - * @return the size of the EOCD of {@code -1} it none exists yet - */ - public long getEocdSize() { - if (eocdEntry == null) { - return -1; - } - - return eocdEntry.getSize(); - } - - /** - * Obtains the comment in the EOCD. - * - * @return the comment exactly as it was encoded in the EOCD, no encoding conversion is done - */ - @Nonnull - public byte[] getEocdComment() { - if (eocdEntry == null) { - Verify.verify(eocdComment != null); - byte[] eocdCommentCopy = new byte[eocdComment.length]; - System.arraycopy(eocdComment, 0, eocdCommentCopy, 0, eocdComment.length); - return eocdCommentCopy; - } - - Eocd eocd = eocdEntry.getStore(); - Verify.verify(eocd != null); - return eocd.getComment(); - } - - /** - * Sets the comment in the EOCD. - * - * @param comment the new comment; no conversion is done, these exact bytes will be placed in - * the EOCD comment - * @throws IllegalStateException if file is in read-only mode - */ - public void setEocdComment(@Nonnull byte[] comment) { - checkNotInReadOnlyMode(); - - if (comment.length > MAX_EOCD_COMMENT_SIZE) { - throw new IllegalArgumentException( - "EOCD comment size (" - + comment.length - + ") is larger than the maximum allowed (" - + MAX_EOCD_COMMENT_SIZE - + ")"); - } - - // Check if the EOCD signature appears anywhere in the comment we need to check if it - // is valid. - for (int i = 0; i < comment.length - MIN_EOCD_SIZE; i++) { - // Remember: little endian... - if (comment[i] == EOCD_SIGNATURE[3] - && comment[i + 1] == EOCD_SIGNATURE[2] - && comment[i + 2] == EOCD_SIGNATURE[1] - && comment[i + 3] == EOCD_SIGNATURE[0]) { - // We found a possible EOCD signature at position i. Try to read it. - ByteBuffer bytes = ByteBuffer.wrap(comment, i, comment.length - i); - try { - new Eocd(bytes); - throw new IllegalArgumentException( - "Position " - + i - + " of the comment contains a valid EOCD record."); - } catch (IOException e) { - // Fine, this is an invalid record. Move along... - } - } - } - - deleteDirectoryAndEocd(); - eocdComment = new byte[comment.length]; - System.arraycopy(comment, 0, eocdComment, 0, comment.length); - dirty = true; - } - - /** - * Sets an extra offset for the central directory. See class description for details. Changing - * this value will mark the file as dirty and force a rewrite of the central directory when - * updated. - * - * @param offset the offset or {@code 0} to write the central directory at its current location - * @throws IllegalStateException if file is in read-only mode - */ - public void setExtraDirectoryOffset(long offset) { - checkNotInReadOnlyMode(); - Preconditions.checkArgument(offset >= 0, "offset < 0"); - - if (extraDirectoryOffset != offset) { - extraDirectoryOffset = offset; - deleteDirectoryAndEocd(); - dirty = true; - } - } - - /** - * Obtains the extra offset for the central directory. See class description for details. - * - * @return the offset or {@code 0} if no offset is set - */ - public long getExtraDirectoryOffset() { - return extraDirectoryOffset; - } - - /** - * Obtains whether this {@code ZFile} is ignoring timestamps. - * - * @return are the timestamps being ignored? - */ - public boolean areTimestampsIgnored() { - return noTimestamps; - } - - /** - * Sorts all files in the zip. This will force all files to be loaded and will wait for all - * background tasks to complete. Sorting files is never done implicitly and will operate in - * memory only (maybe reading files from the zip disk into memory, if needed). It will leave - * the zip in dirty state, requiring a call to {@link #update()} to force the entries to be - * written to disk. - * - * @throws IOException failed to load or move a file in the zip - * @throws IllegalStateException if file is in read-only mode - */ - public void sortZipContents() throws IOException { - checkNotInReadOnlyMode(); - reopenRw(); - - processAllReadyEntriesWithWait(); - - Verify.verify(uncompressedEntries.isEmpty()); - - SortedSet sortedEntries = Sets.newTreeSet(StoredEntry.COMPARE_BY_NAME); - for (FileUseMapEntry fmEntry : entries.values()) { - StoredEntry entry = fmEntry.getStore(); - Preconditions.checkNotNull(entry); - sortedEntries.add(entry); - entry.loadSourceIntoMemory(); - - map.remove(fmEntry); - } - - entries.clear(); - for (StoredEntry entry : sortedEntries) { - String name = entry.getCentralDirectoryHeader().getName(); - FileUseMapEntry positioned = - positionInFile(entry, PositionHint.LOWEST_OFFSET); - - entries.put(name, positioned); - } - - dirty = true; - } - - /** - * Obtains the filesystem path to the zip file. - * - * @return the file that may or may not exist (depending on whether something existed there - * before the zip was created and on whether the zip has been updated or not) - */ - @Nonnull - public File getFile() { - return file; - } - - /** - * Creates a new verify log. - * - * @return the new verify log - */ - @Nonnull - VerifyLog makeVerifyLog() { - VerifyLog log = verifyLogFactory.get(); - assert log != null; - return log; - } - - /** - * Obtains the zip file's verify log. - * - * @return the verify log - */ - @Nonnull - VerifyLog getVerifyLog() { - return verifyLog; - } - - /** - * Are there in-memory changes that have not been written to the zip file? - * - *

Waits for all pending processing which may make changes. - */ - public boolean hasPendingChangesWithWait() throws IOException { - processAllReadyEntriesWithWait(); - return dirty; - } - - /** Hint to where files should be positioned. */ - enum PositionHint { - /** - * File may be positioned anywhere, caller doesn't care. - */ - ANYWHERE, - - /** - * File should be positioned at the lowest offset possible. - */ - LOWEST_OFFSET - } -} diff --git a/src/main/java/com/android/apkzlib/zip/ZFileExtension.java b/src/main/java/com/android/apkzlib/zip/ZFileExtension.java deleted file mode 100644 index fdb6ca4..0000000 --- a/src/main/java/com/android/apkzlib/zip/ZFileExtension.java +++ /dev/null @@ -1,146 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * 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.android.apkzlib.zip; - -import com.android.apkzlib.utils.IOExceptionRunnable; -import java.io.IOException; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -/** - * An extension of a {@link ZFile}. Extensions are notified when files are open, updated, closed and - * when files are added or removed from the zip. These notifications are received after the zip - * has been updated in memory for open, when files are added or removed and when the zip has been - * updated on disk or closed. - *

- * An extension is also notified before the file is updated, allowing it to modify the file before - * the update happens. If it does, then all extensions are notified of the changes on the zip file. - * Because the order of the notifications is preserved, all extensions are notified in the same - * order. For example, if two extensions E1 and E2 are registered and they both add a file at - * update time, this would be the flow: - *

    - *
  • E1 receives {@code beforeUpdate} notification.
  • - *
  • E1 adds file F1 to the zip (notifying the addition is suspended because another - * notification is in progress).
  • - *
  • E2 receives {@code beforeUpdate} notification.
  • - *
  • E2 adds file F2 to the zip (notifying the addition is suspended because another - * notification is in progress).
  • - *
  • E1 is notified F1 was added.
  • - *
  • E2 is notified F1 was added.
  • - *
  • E1 is notified F2 was added.
  • - *
  • E2 is notified F2 was added.
  • - *
  • (zip file is updated on disk)
  • - *
  • E1 is notified the zip was updated.
  • - *
  • E2 is notified the zip was updated.
  • - *
- *

- * An extension should not modify the zip file when notified of changes. If allowed, this would - * break event notification order in case multiple extensions are registered with the zip file. - * To allow performing changes to the zip file, all notification method return a - * {@code IOExceptionRunnable} that is invoked when {@link ZFile} has finished notifying all - * extensions. - */ -public abstract class ZFileExtension { - - /** - * The zip file has been open and the zip's contents have been read. The default implementation - * does nothing and returns {@code null}. - * - * @return an optional runnable to run when notification of all listeners has ended - * @throws IOException failed to process the event - */ - @Nullable - public IOExceptionRunnable open() throws IOException { - return null; - } - - /** - * The zip will be updated. This method allows the extension to register changes to the zip - * file before the file is written. The default implementation does nothing and returns - * {@code null}. - *

- * After this notification is received, the extension will receive further - * {@link #added(StoredEntry, StoredEntry)} and {@link #removed(StoredEntry)} notifications if - * it or other extensions add or remove files before update. - *

- * When no more files are updated, the {@link #entriesWritten()} notification is sent. - * - * @return an optional runnable to run when notification of all listeners has ended - * @throws IOException failed to process the event - */ - @Nullable - public IOExceptionRunnable beforeUpdate() throws IOException { - return null; - } - - /** - * This notification is sent when all entries have been written in the file but the central - * directory and the EOCD have not yet been written. No entries should be added, removed or - * updated during this notification. If this method forces an update of either the central - * directory or EOCD, then this method will be invoked again for all extensions with the new - * central directory and EOCD. - *

- * After this notification, {@link #updated()} is sent. - * - * @throws IOException failed to process the event - */ - public void entriesWritten() throws IOException { - } - - /** - * The zip file has been updated on disk. The default implementation does nothing. - * - * @throws IOException failed to perform update tasks - */ - public void updated() throws IOException { - } - - /** - * The zip file has been closed. Note that if {@link ZFile#close()} requires that the zip file - * be updated (because it had in-memory changes), {@link #updated()} will be called before - * this method. The default implementation does nothing. - */ - public void closed() { - } - - /** - * A new entry has been added to the zip, possibly replacing an entry in there. The - * default implementation does nothing and returns {@code null}. - * - * @param entry the entry that was added - * @param replaced the entry that was replaced, if any - * @return an optional runnable to run when notification of all listeners has ended - */ - @Nullable - public IOExceptionRunnable added(@Nonnull StoredEntry entry, @Nullable StoredEntry replaced) { - return null; - } - - /** - * An entry has been removed from the zip. This method is not invoked for entries that have - * been replaced. Those entries are notified using replaced in - * {@link #added(StoredEntry, StoredEntry)}. The default implementation does nothing and - * returns {@code null}. - * - * @param entry the entry that was deleted - * @return an optional runnable to run when notification of all listeners has ended - */ - @Nullable - public IOExceptionRunnable removed(@Nonnull StoredEntry entry) { - return null; - } -} diff --git a/src/main/java/com/android/apkzlib/zip/ZFileOptions.java b/src/main/java/com/android/apkzlib/zip/ZFileOptions.java deleted file mode 100644 index 08a1d83..0000000 --- a/src/main/java/com/android/apkzlib/zip/ZFileOptions.java +++ /dev/null @@ -1,214 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * 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.android.apkzlib.zip; - -import com.android.apkzlib.zip.compress.DeflateExecutionCompressor; -import com.android.apkzlib.zip.utils.ByteTracker; -import java.util.function.Supplier; -import java.util.zip.Deflater; -import javax.annotation.Nonnull; - -/** - * Options to create a {@link ZFile}. - */ -public class ZFileOptions { - - /** - * The byte tracker. - */ - @Nonnull - private ByteTracker tracker; - - /** - * The compressor to use. - */ - @Nonnull - private Compressor compressor; - - /** - * Should timestamps be zeroed? - */ - private boolean noTimestamps; - - /** - * The alignment rule to use. - */ - @Nonnull - private AlignmentRule alignmentRule; - - /** - * Should the extra field be used to cover empty space? - */ - private boolean coverEmptySpaceUsingExtraField; - - /** - * Should files be automatically sorted before update? - */ - private boolean autoSortFiles; - - /** - * Factory creating verification logs to use. - */ - @Nonnull - private Supplier verifyLogFactory; - - /** - * Creates a new options object. All options are set to their defaults. - */ - public ZFileOptions() { - tracker = new ByteTracker(); - compressor = - new DeflateExecutionCompressor( - Runnable::run, - tracker, - Deflater.DEFAULT_COMPRESSION); - alignmentRule = AlignmentRules.compose(); - verifyLogFactory = VerifyLogs::devNull; - } - - /** - * Obtains the ZFile's byte tracker. - * - * @return the byte tracker - */ - @Nonnull - public ByteTracker getTracker() { - return tracker; - } - - /** - * Obtains the compressor to use. - * - * @return the compressor - */ - @Nonnull - public Compressor getCompressor() { - return compressor; - } - - /** - * Sets the compressor to use. - * - * @param compressor the compressor - */ - public ZFileOptions setCompressor(@Nonnull Compressor compressor) { - this.compressor = compressor; - return this; - } - - /** - * Obtains whether timestamps should be zeroed. - * - * @return should timestamps be zeroed? - */ - public boolean getNoTimestamps() { - return noTimestamps; - } - - /** - * Sets whether timestamps should be zeroed. - * - * @param noTimestamps should timestamps be zeroed? - */ - public ZFileOptions setNoTimestamps(boolean noTimestamps) { - this.noTimestamps = noTimestamps; - return this; - } - - /** - * Obtains the alignment rule. - * - * @return the alignment rule - */ - @Nonnull - public AlignmentRule getAlignmentRule() { - return alignmentRule; - } - - /** - * Sets the alignment rule. - * - * @param alignmentRule the alignment rule - */ - public ZFileOptions setAlignmentRule(@Nonnull AlignmentRule alignmentRule) { - this.alignmentRule = alignmentRule; - return this; - } - - /** - * Obtains whether the extra field should be used to cover empty spaces. See {@link ZFile} for - * an explanation on using the extra field for covering empty spaces. - * - * @return should the extra field be used to cover empty spaces? - */ - public boolean getCoverEmptySpaceUsingExtraField() { - return coverEmptySpaceUsingExtraField; - } - - /** - * Sets whether the extra field should be used to cover empty spaces. See {@link ZFile} for an - * explanation on using the extra field for covering empty spaces. - * - * @param coverEmptySpaceUsingExtraField should the extra field be used to cover empty spaces? - */ - public ZFileOptions setCoverEmptySpaceUsingExtraField(boolean coverEmptySpaceUsingExtraField) { - this.coverEmptySpaceUsingExtraField = coverEmptySpaceUsingExtraField; - return this; - } - - /** - * Obtains whether files should be automatically sorted before updating the zip file. See - * {@link ZFile} for an explanation on automatic sorting. - * - * @return should the file be automatically sorted? - */ - public boolean getAutoSortFiles() { - return autoSortFiles; - } - - /** - * Sets whether files should be automatically sorted before updating the zip file. See {@link - * ZFile} for an explanation on automatic sorting. - * - * @param autoSortFiles should the file be automatically sorted? - */ - public ZFileOptions setAutoSortFiles(boolean autoSortFiles) { - this.autoSortFiles = autoSortFiles; - return this; - } - - /** - * Sets the verification log factory. - * - * @param verifyLogFactory verification log factory - */ - public ZFileOptions setVerifyLogFactory(@Nonnull Supplier verifyLogFactory) { - this.verifyLogFactory = verifyLogFactory; - return this; - } - - /** - * Obtains the verification log factory. By default, the verification log doesn't store - * anything and will always return an empty log. - * - * @return the verification log factory - */ - @Nonnull - public Supplier getVerifyLogFactory() { - return verifyLogFactory; - } -} diff --git a/src/main/java/com/android/apkzlib/zip/ZipField.java b/src/main/java/com/android/apkzlib/zip/ZipField.java deleted file mode 100644 index 4b0b675..0000000 --- a/src/main/java/com/android/apkzlib/zip/ZipField.java +++ /dev/null @@ -1,364 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * 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.android.apkzlib.zip; - -import com.android.apkzlib.zip.utils.LittleEndianUtils; -import com.google.common.base.Preconditions; -import com.google.common.base.Verify; -import com.google.common.collect.Sets; -import com.google.common.primitives.Ints; -import java.io.IOException; -import java.nio.ByteBuffer; -import java.nio.ByteOrder; -import java.util.Set; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -/** - * The ZipField class represents a field in a record in a zip file. Zip files are made with records - * that have fields. This class makes it easy to read, write and verify field values. - *

- * There are two main types of fields: 2-byte fields and 4-byte fields. We represent each one as - * a subclass of {@code ZipField}, {@code F2} for the 2-byte field and {@code F4} for the 4-byte - * field. Because Java's {@code int} data type is guaranteed to be 4-byte, all methods use Java's - * native {@link int} as data type. - *

- * For each field we can either read, write or verify. Verification is used for fields whose value - * we know. Some fields, e.g. signature fields, have fixed value. Other fields have - * variable values, but in some situations we know which value they have. For example, the last - * modification time of a file's local header will have to match the value of the file's - * modification time as stored in the central directory. - *

- * Because records are compact, i.e. fields are stored sequentially with no empty spaces, - * fields are generally created in the sequence they exist and the end offset of a field is used - * as the offset of the next one. The end of a field can be obtained by invoking - * {@link #endOffset()}. This allows creating fields in sequence without doing offset computation: - *

- * ZipField.F2 firstField = new ZipField.F2(0, "First field");
- * ZipField.F4 secondField = new ZipField(firstField.endOffset(), "Second field");
- * 
- */ -abstract class ZipField { - - /** - * Field name. Used for providing (more) useful error messages. - */ - @Nonnull - private final String name; - - /** - * Offset of the file in the record. - */ - protected final int offset; - - /** - * Size of the field. Only 2 or 4 allowed. - */ - private final int size; - - /** - * If a fixed value exists for the field, then this attribute will contain that value. - */ - @Nullable - private final Long expected; - - /** - * All invariants that this field must verify. - */ - @Nonnull - private Set invariants; - - /** - * Creates a new field that does not contain a fixed value. - * - * @param offset the field's offset in the record - * @param size the field size - * @param name the field's name - * @param invariants the invariants that must be verified by the field - */ - ZipField(int offset, int size, @Nonnull String name, ZipFieldInvariant... invariants) { - Preconditions.checkArgument(offset >= 0, "offset >= 0"); - Preconditions.checkArgument(size == 2 || size == 4, "size != 2 && size != 4"); - - this.name = name; - this.offset = offset; - this.size = size; - expected = null; - this.invariants = Sets.newHashSet(invariants); - } - - /** - * Creates a new field that contains a fixed value. - * - * @param offset the field's offset in the record - * @param size the field size - * @param expected the expected field value - * @param name the field's name - */ - ZipField(int offset, int size, long expected, @Nonnull String name) { - Preconditions.checkArgument(offset >= 0, "offset >= 0"); - Preconditions.checkArgument(size == 2 || size == 4, "size != 2 && size != 4"); - - this.name = name; - this.offset = offset; - this.size = size; - this.expected = expected; - invariants = Sets.newHashSet(); - } - - /** - * Checks whether a value verifies the field's invariants. Nothing happens if the value verifies - * the invariants. - * - * @param value the value - * @throws IOException the invariants are not verified - */ - private void checkVerifiesInvariants(long value) throws IOException { - for (ZipFieldInvariant invariant : invariants) { - if (!invariant.isValid(value)) { - throw new IOException("Value " + value + " of field " + name + " is invalid " - + "(fails '" + invariant.getName() + "')."); - } - } - } - - /** - * Advances the position in the provided byte buffer by the size of this field. - * - * @param bytes the byte buffer; at the end of the method its position will be greater by - * the size of this field - * @throws IOException failed to advance the buffer - */ - void skip(@Nonnull ByteBuffer bytes) throws IOException { - if (bytes.remaining() < size) { - throw new IOException("Cannot skip field " + name + " because only " - + bytes.remaining() + " remain in the buffer."); - } - - bytes.position(bytes.position() + size); - } - - /** - * Reads a field value. - * - * @param bytes the byte buffer with the record data; after this method finishes, the buffer - * will be positioned at the first byte after the field - * @return the value of the field - * @throws IOException failed to read the field - */ - long read(@Nonnull ByteBuffer bytes) throws IOException { - if (bytes.remaining() < size) { - throw new IOException("Cannot skip field " + name + " because only " - + bytes.remaining() + " remain in the buffer."); - } - - bytes.order(ByteOrder.LITTLE_ENDIAN); - - long r; - if (size == 2) { - r = LittleEndianUtils.readUnsigned2Le(bytes); - } else { - r = LittleEndianUtils.readUnsigned4Le(bytes); - } - - checkVerifiesInvariants(r); - return r; - } - - /** - * Verifies that the field at the current buffer position has the expected value. The field - * must have been created with the constructor that defines the expected value. - * - * @param bytes the byte buffer with the record data; after this method finishes, the buffer - * will be positioned at the first byte after the field - * @throws IOException failed to read the field or the field does not have the expected value - */ - void verify(@Nonnull ByteBuffer bytes) throws IOException { - verify(bytes, null); - } - - /** - * Verifies that the field at the current buffer position has the expected value. The field - * must have been created with the constructor that defines the expected value. - * - * @param bytes the byte buffer with the record data; after this method finishes, the buffer - * will be positioned at the first byte after the field - * @param verifyLog if non-{@code null}, will log the verification error - * @throws IOException failed to read the data or the field does not have the expected value; - * only thrown if {@code verifyLog} is {@code null} - */ - void verify(@Nonnull ByteBuffer bytes, @Nullable VerifyLog verifyLog) throws IOException { - Preconditions.checkState(expected != null, "expected == null"); - verify(bytes, expected, verifyLog); - } - - /** - * Verifies that the field has an expected value. - * - * @param bytes the byte buffer with the record data; after this method finishes, the buffer - * will be positioned at the first byte after the field - * @param expected the value we expect the field to have; if this field has invariants, the - * value must verify them - * @throws IOException failed to read the data or the field does not have the expected value - */ - void verify(@Nonnull ByteBuffer bytes, long expected) throws IOException { - verify(bytes, expected, null); - } - - /** - * Verifies that the field has an expected value. - * - * @param bytes the byte buffer with the record data; after this method finishes, the buffer - * will be positioned at the first byte after the field - * @param expected the value we expect the field to have; if this field has invariants, the - * value must verify them - * @param verifyLog if non-{@code null}, will log the verification error - * @throws IOException failed to read the data or the field does not have the expected value; - * only thrown if {@code verifyLog} is {@code null} - */ - void verify( - @Nonnull ByteBuffer bytes, - long expected, - @Nullable VerifyLog verifyLog) throws IOException { - checkVerifiesInvariants(expected); - long r = read(bytes); - if (r != expected) { - String error = - String.format( - "Incorrect value for field '%s': value is %s but %s expected.", - name, - r, - expected); - - if (verifyLog == null) { - throw new IOException(error); - } else { - verifyLog.log(error); - } - } - } - - /** - * Writes the value of the field. - * - * @param output where to write the field; the field will be written at the current position - * of the buffer - * @param value the value to write - * @throws IOException failed to write the value in the stream - */ - void write(@Nonnull ByteBuffer output, long value) throws IOException { - checkVerifiesInvariants(value); - - Preconditions.checkArgument(value >= 0, "value (%s) < 0", value); - - if (size == 2) { - Preconditions.checkArgument(value <= 0x0000ffff, "value (%s) > 0x0000ffff", value); - LittleEndianUtils.writeUnsigned2Le(output, Ints.checkedCast(value)); - } else { - Verify.verify(size == 4); - Preconditions.checkArgument(value <= 0x00000000ffffffffL, - "value (%s) > 0x00000000ffffffffL", value); - LittleEndianUtils.writeUnsigned4Le(output, value); - } - } - - /** - * Writes the value of the field. The field must have an expected value set in the constructor. - * - * @param output where to write the field; the field will be written at the current position - * of the buffer - * @throws IOException failed to write the value in the stream - */ - void write(@Nonnull ByteBuffer output) throws IOException { - Preconditions.checkState(expected != null, "expected == null"); - write(output, expected); - } - - /** - * Obtains the offset at which the field starts. - * - * @return the start offset - */ - int offset() { - return offset; - } - - /** - * Obtains the offset at which the field ends. This is the exact offset at which the next - * field starts. - * - * @return the end offset - */ - int endOffset() { - return offset + size; - } - - /** - * Concrete implementation of {@link ZipField} that represents a 2-byte field. - */ - static class F2 extends ZipField { - - /** - * Creates a new field. - * - * @param offset the field's offset in the record - * @param name the field's name - * @param invariants the invariants that must be verified by the field - */ - F2(int offset, @Nonnull String name, ZipFieldInvariant... invariants) { - super(offset, 2, name, invariants); - } - - /** - * Creates a new field that contains a fixed value. - * - * @param offset the field's offset in the record - * @param expected the expected field value - * @param name the field's name - */ - F2(int offset, long expected, @Nonnull String name) { - super(offset, 2, expected, name); - } - } - - /** - * Concrete implementation of {@link ZipField} that represents a 4-byte field. - */ - static class F4 extends ZipField { - /** - * Creates a new field. - * - * @param offset the field's offset in the record - * @param name the field's name - * @param invariants the invariants that must be verified by the field - */ - F4(int offset, @Nonnull String name, ZipFieldInvariant... invariants) { - super(offset, 4, name, invariants); - } - - /** - * Creates a new field that contains a fixed value. - * - * @param offset the field's offset in the record - * @param expected the expected field value - * @param name the field's name - */ - F4(int offset, long expected, @Nonnull String name) { - super(offset, 4, expected, name); - } - } -} diff --git a/src/main/java/com/android/apkzlib/zip/ZipFieldInvariant.java b/src/main/java/com/android/apkzlib/zip/ZipFieldInvariant.java deleted file mode 100644 index 87fc46c..0000000 --- a/src/main/java/com/android/apkzlib/zip/ZipFieldInvariant.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * 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.android.apkzlib.zip; - -/** - * A field rule defines an invariant (i.e., a constraint) that has to be verified by a - * field value. - */ -interface ZipFieldInvariant { - - /** - * Evalutes the invariant against a value. - * - * @param value the value to check the invariant - * @return is the invariant valid? - */ - boolean isValid(long value); - - /** - * Obtains the name of the invariant. Used for information purposes. - * - * @return the name of the invariant - */ - String getName(); -} diff --git a/src/main/java/com/android/apkzlib/zip/ZipFieldInvariantMaxValue.java b/src/main/java/com/android/apkzlib/zip/ZipFieldInvariantMaxValue.java deleted file mode 100644 index 5905e1a..0000000 --- a/src/main/java/com/android/apkzlib/zip/ZipFieldInvariantMaxValue.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * 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.android.apkzlib.zip; - -/** - * Invariant checking a zip field does not exceed a threshold. - */ -class ZipFieldInvariantMaxValue implements ZipFieldInvariant { - - /** - * The maximum value allowed. - */ - private long max; - - /** - * Creates a new invariant. - * - * @param max the maximum value allowed for the field - */ - ZipFieldInvariantMaxValue(int max) { - this.max = max; - } - - @Override - public boolean isValid(long value) { - return value <= max; - } - - @Override - public String getName() { - return "Maximum value " + max; - } -} diff --git a/src/main/java/com/android/apkzlib/zip/ZipFieldInvariantNonNegative.java b/src/main/java/com/android/apkzlib/zip/ZipFieldInvariantNonNegative.java deleted file mode 100644 index 4d1770b..0000000 --- a/src/main/java/com/android/apkzlib/zip/ZipFieldInvariantNonNegative.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * 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.android.apkzlib.zip; - -/** - * Invariant that verifies a field's value is not negative. - */ -class ZipFieldInvariantNonNegative implements ZipFieldInvariant { - - @Override - public boolean isValid(long value) { - return value >= 0; - } - - @Override - public String getName() { - return "Is positive"; - } -} diff --git a/src/main/java/com/android/apkzlib/zip/ZipFileState.java b/src/main/java/com/android/apkzlib/zip/ZipFileState.java deleted file mode 100644 index 7ecf2d5..0000000 --- a/src/main/java/com/android/apkzlib/zip/ZipFileState.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * 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.android.apkzlib.zip; - -/** - * The {@code ZipFileState} enumeration holds the state of a {@link ZFile}. - */ -enum ZipFileState { - /** - * Zip file is closed. - */ - CLOSED, - - /** - * File file is open in read-only mode. - */ - OPEN_RO, - - /** - * File file is open in read-write mode. - */ - OPEN_RW -} diff --git a/src/main/java/com/android/apkzlib/zip/compress/BestAndDefaultDeflateExecutorCompressor.java b/src/main/java/com/android/apkzlib/zip/compress/BestAndDefaultDeflateExecutorCompressor.java deleted file mode 100644 index 8948a1c..0000000 --- a/src/main/java/com/android/apkzlib/zip/compress/BestAndDefaultDeflateExecutorCompressor.java +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * 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.android.apkzlib.zip.compress; - -import com.android.apkzlib.zip.CompressionResult; -import com.android.apkzlib.zip.utils.ByteTracker; -import com.android.apkzlib.zip.utils.CloseableByteSource; -import com.google.common.base.Preconditions; -import java.util.concurrent.Executor; -import java.util.zip.Deflater; -import javax.annotation.Nonnull; - -/** - * Compressor that tries both the best and default compression algorithms and picks the default - * unless the best is at least a given percentage smaller. - */ -public class BestAndDefaultDeflateExecutorCompressor extends ExecutorCompressor { - - /** - * Deflater using the default compression level. - */ - @Nonnull - private final DeflateExecutionCompressor defaultDeflater; - - /** - * Deflater using the best compression level. - */ - @Nonnull - private final DeflateExecutionCompressor bestDeflater; - - /** - * Minimum best compression size / default compression size ratio needed to pick the default - * compression size. - */ - private final double minRatio; - - /** - * Creates a new compressor. - * - * @param executor the executor used to perform compression activities. - * @param tracker the byte tracker to keep track of allocated bytes - * @param minRatio the minimum best compression size / default compression size needed to pick - * the default compression size; if {@code 0.0} then the default compression is always picked, - * if {@code 1.0} then the best compression is always picked unless it produces the exact same - * size as the default compression. - */ - public BestAndDefaultDeflateExecutorCompressor(@Nonnull Executor executor, - @Nonnull ByteTracker tracker, double minRatio) { - super(executor); - - Preconditions.checkArgument(minRatio >= 0.0, "minRatio < 0.0"); - Preconditions.checkArgument(minRatio <= 1.0, "minRatio > 1.0"); - - defaultDeflater = - new DeflateExecutionCompressor(executor, tracker, Deflater.DEFAULT_COMPRESSION); - bestDeflater = - new DeflateExecutionCompressor(executor, tracker, Deflater.BEST_COMPRESSION); - this.minRatio = minRatio; - } - - @Nonnull - @Override - protected CompressionResult immediateCompress(@Nonnull CloseableByteSource source) - throws Exception { - CompressionResult defaultResult = defaultDeflater.immediateCompress(source); - CompressionResult bestResult = bestDeflater.immediateCompress(source); - - double sizeRatio = bestResult.getSize() / (double) defaultResult.getSize(); - if (sizeRatio >= minRatio) { - return defaultResult; - } else { - return bestResult; - } - } -} diff --git a/src/main/java/com/android/apkzlib/zip/compress/DeflateExecutionCompressor.java b/src/main/java/com/android/apkzlib/zip/compress/DeflateExecutionCompressor.java deleted file mode 100644 index 309e356..0000000 --- a/src/main/java/com/android/apkzlib/zip/compress/DeflateExecutionCompressor.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * 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.android.apkzlib.zip.compress; - -import com.android.apkzlib.zip.CompressionMethod; -import com.android.apkzlib.zip.CompressionResult; -import com.android.apkzlib.zip.utils.ByteTracker; -import com.android.apkzlib.zip.utils.CloseableByteSource; -import java.io.ByteArrayOutputStream; -import java.util.concurrent.Executor; -import java.util.zip.Deflater; -import java.util.zip.DeflaterOutputStream; -import javax.annotation.Nonnull; - -/** - * Compressor that uses deflate with an executor. - */ -public class DeflateExecutionCompressor extends ExecutorCompressor { - - - /** - * Deflate compression level. - */ - private final int level; - - /** - * Byte tracker to use to create byte sources. - */ - @Nonnull - private final ByteTracker tracker; - - /** - * Creates a new compressor. - * - * @param executor the executor to run deflation tasks - * @param tracker the byte tracker to use to keep track of memory usage - * @param level the compression level - */ - public DeflateExecutionCompressor( - @Nonnull Executor executor, - @Nonnull ByteTracker tracker, - int level) { - super(executor); - - this.level = level; - this.tracker = tracker; - } - - @Nonnull - @Override - protected CompressionResult immediateCompress(@Nonnull CloseableByteSource source) - throws Exception { - ByteArrayOutputStream output = new ByteArrayOutputStream(); - Deflater deflater = new Deflater(level, true); - - try (DeflaterOutputStream dos = new DeflaterOutputStream(output, deflater)) { - dos.write(source.read()); - } - - CloseableByteSource result = tracker.fromStream(output); - if (result.size() >= source.size()) { - return new CompressionResult(source, CompressionMethod.STORE, source.size()); - } else { - return new CompressionResult(result, CompressionMethod.DEFLATE, result.size()); - } - } -} diff --git a/src/main/java/com/android/apkzlib/zip/compress/ExecutorCompressor.java b/src/main/java/com/android/apkzlib/zip/compress/ExecutorCompressor.java deleted file mode 100644 index 54be20c..0000000 --- a/src/main/java/com/android/apkzlib/zip/compress/ExecutorCompressor.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * 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.android.apkzlib.zip.compress; - -import com.android.apkzlib.zip.CompressionResult; -import com.android.apkzlib.zip.Compressor; -import com.android.apkzlib.zip.utils.CloseableByteSource; -import com.google.common.util.concurrent.ListenableFuture; -import com.google.common.util.concurrent.SettableFuture; -import java.util.concurrent.Executor; -import javax.annotation.Nonnull; - -/** - * A synchronous compressor is a compressor that computes the result of compression immediately - * and never returns an uncomputed future object. - */ -public abstract class ExecutorCompressor implements Compressor { - - /** - * The executor that does the work. - */ - @Nonnull - private final Executor executor; - - /** - * Compressor that delegates execution into the given executor. - * @param executor the executor that will do the compress - */ - public ExecutorCompressor(@Nonnull Executor executor) { - this.executor = executor; - } - - @Nonnull - @Override - public ListenableFuture compress( - @Nonnull final CloseableByteSource source) { - final SettableFuture future = SettableFuture.create(); - executor.execute(() -> { - try { - future.set(immediateCompress(source)); - } catch (Throwable e) { - future.setException(e); - } - }); - - return future; - } - - /** - * Immediately compresses a source. - * @param source the source to compress - * @return the result of compression - * @throws Exception failed to compress - */ - @Nonnull - protected abstract CompressionResult immediateCompress(@Nonnull CloseableByteSource source) - throws Exception; -} diff --git a/src/main/java/com/android/apkzlib/zip/compress/Zip64NotSupportedException.java b/src/main/java/com/android/apkzlib/zip/compress/Zip64NotSupportedException.java deleted file mode 100644 index 3b7411e..0000000 --- a/src/main/java/com/android/apkzlib/zip/compress/Zip64NotSupportedException.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * 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.android.apkzlib.zip.compress; - -import java.io.IOException; - -/** Exception raised by ZFile when encountering unsupported Zip64 format jar files. */ -public class Zip64NotSupportedException extends IOException { - - public Zip64NotSupportedException(String message) { - super(message); - } -} diff --git a/src/main/java/com/android/apkzlib/zip/compress/package-info.java b/src/main/java/com/android/apkzlib/zip/compress/package-info.java deleted file mode 100644 index e2fcbd6..0000000 --- a/src/main/java/com/android/apkzlib/zip/compress/package-info.java +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * 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. - */ - -/** - * Compressors to use with the {@code zip} package. - */ -package com.android.apkzlib.zip.compress; \ No newline at end of file diff --git a/src/main/java/com/android/apkzlib/zip/utils/ByteTracker.java b/src/main/java/com/android/apkzlib/zip/utils/ByteTracker.java deleted file mode 100644 index 88ed939..0000000 --- a/src/main/java/com/android/apkzlib/zip/utils/ByteTracker.java +++ /dev/null @@ -1,120 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * 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.android.apkzlib.zip.utils; - -import com.google.common.io.ByteSource; -import com.google.common.io.ByteStreams; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import javax.annotation.Nonnull; - -/** - * Keeps track of used bytes allowing gauging memory usage. - */ -public class ByteTracker { - - /** - * Number of bytes currently in use. - */ - private long bytesUsed; - - /** - * Maximum number of bytes used. - */ - private long maxBytesUsed; - - /** - * Creates a new byte source by fully reading an input stream. - * - * @param stream the input stream - * @return a byte source containing the cached data from the given stream - * @throws IOException failed to read the stream - */ - public CloseableDelegateByteSource fromStream(@Nonnull InputStream stream) throws IOException { - byte[] data = ByteStreams.toByteArray(stream); - updateUsage(data.length); - return new CloseableDelegateByteSource(ByteSource.wrap(data), data.length) { - @Override - public synchronized void innerClose() throws IOException { - super.innerClose(); - updateUsage(-sizeNoException()); - } - }; - } - - /** - * Creates a new byte source by snapshotting the provided stream. - * - * @param stream the stream with the data - * @return a byte source containing the cached data from the given stream - * @throws IOException failed to read the stream - */ - public CloseableDelegateByteSource fromStream(@Nonnull ByteArrayOutputStream stream) - throws IOException { - byte[] data = stream.toByteArray(); - updateUsage(data.length); - return new CloseableDelegateByteSource(ByteSource.wrap(data), data.length) { - @Override - public synchronized void innerClose() throws IOException { - super.innerClose(); - updateUsage(-sizeNoException()); - } - }; - } - - /** - * Creates a new byte source from another byte source. - * - * @param source the byte source to copy data from - * @return the tracked byte source - * @throws IOException failed to read data from the byte source - */ - public CloseableDelegateByteSource fromSource(@Nonnull ByteSource source) throws IOException { - return fromStream(source.openStream()); - } - - /** - * Updates the memory used by this tracker. - * - * @param delta the number of bytes to add or remove, if negative - */ - private synchronized void updateUsage(long delta) { - bytesUsed += delta; - if (maxBytesUsed < bytesUsed) { - maxBytesUsed = bytesUsed; - } - } - - /** - * Obtains the number of bytes currently used. - * - * @return the number of bytes - */ - public synchronized long getBytesUsed() { - return bytesUsed; - } - - /** - * Obtains the maximum number of bytes ever used by this tracker. - * - * @return the number of bytes - */ - public synchronized long getMaxBytesUsed() { - return maxBytesUsed; - } -} diff --git a/src/main/java/com/android/apkzlib/zip/utils/CloseableByteSource.java b/src/main/java/com/android/apkzlib/zip/utils/CloseableByteSource.java deleted file mode 100644 index 1479a03..0000000 --- a/src/main/java/com/android/apkzlib/zip/utils/CloseableByteSource.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * 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.android.apkzlib.zip.utils; - -import com.google.common.io.ByteSource; - -import java.io.Closeable; -import java.io.IOException; - -/** - * Byte source that can be closed. Closing a byte source allows releasing any resources associated - * with it. This should not be confused with closing streams. For example, {@link ByteTracker} uses - * {@code CloseableByteSources} to know when the data associated with the byte source can be - * released. - */ -public abstract class CloseableByteSource extends ByteSource implements Closeable { - - /** - * Has the source been closed? - */ - private boolean closed; - - /** - * Creates a new byte source. - */ - public CloseableByteSource() { - closed = false; - } - - @Override - public final synchronized void close() throws IOException { - if (closed) { - return; - } - - try { - innerClose(); - } finally { - closed = true; - } - } - - /** - * Closes the by source. This method is only invoked once, even if {@link #close()} is - * called multiple times. - * - * @throws IOException failed to close - */ - protected abstract void innerClose() throws IOException; -} diff --git a/src/main/java/com/android/apkzlib/zip/utils/CloseableDelegateByteSource.java b/src/main/java/com/android/apkzlib/zip/utils/CloseableDelegateByteSource.java deleted file mode 100644 index aebb29a..0000000 --- a/src/main/java/com/android/apkzlib/zip/utils/CloseableDelegateByteSource.java +++ /dev/null @@ -1,171 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * 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.android.apkzlib.zip.utils; - -import com.google.common.hash.HashCode; -import com.google.common.hash.HashFunction; -import com.google.common.io.ByteProcessor; -import com.google.common.io.ByteSink; -import com.google.common.io.ByteSource; -import com.google.common.io.CharSource; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.nio.charset.Charset; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -/** - * Closeable byte source that delegates to another byte source. - */ -public class CloseableDelegateByteSource extends CloseableByteSource { - - /** - * The byte source we delegate all operations to. {@code null} if disposed. - */ - @Nullable - private ByteSource inner; - - /** - * Size of the byte source. This is the same as {@code inner.size()} (when {@code inner} - * is not {@code null}), but we keep it separate to avoid calling {@code inner.size()} - * because it might throw {@code IOException}. - */ - private final long mSize; - - /** - * Creates a new byte source. - * - * @param inner the inner byte source - * @param size the size of the source - */ - public CloseableDelegateByteSource(@Nonnull ByteSource inner, long size) { - this.inner = inner; - mSize = size; - } - - /** - * Obtains the inner byte source. Will throw an exception if the inner by byte source has - * been disposed of. - * - * @return the inner byte source - */ - @Nonnull - private synchronized ByteSource get() { - if (inner == null) { - throw new ByteSourceDisposedException(); - } - - return inner; - } - - /** - * Mark the byte source as disposed. - */ - @Override - protected synchronized void innerClose() throws IOException { - if (inner == null) { - return; - } - - inner = null; - } - - /** - * Obtains the size of this byte source. Equivalent to {@link #size()} but not throwing - * {@code IOException}. - * - * @return the size of the byte source - */ - public long sizeNoException() { - return mSize; - } - - @Override - public CharSource asCharSource(Charset charset) { - return get().asCharSource(charset); - } - - @Override - public InputStream openBufferedStream() throws IOException { - return get().openBufferedStream(); - } - - @Override - public ByteSource slice(long offset, long length) { - return get().slice(offset, length); - } - - @Override - public boolean isEmpty() throws IOException { - return get().isEmpty(); - } - - @Override - public long size() throws IOException { - return get().size(); - } - - @Override - public long copyTo(@Nonnull OutputStream output) throws IOException { - return get().copyTo(output); - } - - @Override - public long copyTo(@Nonnull ByteSink sink) throws IOException { - return get().copyTo(sink); - } - - @Override - public byte[] read() throws IOException { - return get().read(); - } - - @Override - public T read(@Nonnull ByteProcessor processor) throws IOException { - return get().read(processor); - } - - @Override - public HashCode hash(HashFunction hashFunction) throws IOException { - return get().hash(hashFunction); - } - - @Override - public boolean contentEquals(@Nonnull ByteSource other) throws IOException { - return get().contentEquals(other); - } - - @Override - public InputStream openStream() throws IOException { - return get().openStream(); - } - - /** - * Exception thrown when trying to use a byte source that has been disposed. - */ - private static class ByteSourceDisposedException extends RuntimeException { - - /** - * Creates a new exception. - */ - private ByteSourceDisposedException() { - super("Byte source was created by a ByteTracker and is now disposed. If you see " - + "this message, then there is a bug."); - } - } -} diff --git a/src/main/java/com/android/apkzlib/zip/utils/LittleEndianUtils.java b/src/main/java/com/android/apkzlib/zip/utils/LittleEndianUtils.java deleted file mode 100644 index c257d39..0000000 --- a/src/main/java/com/android/apkzlib/zip/utils/LittleEndianUtils.java +++ /dev/null @@ -1,129 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * 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.android.apkzlib.zip.utils; - -import com.google.common.base.Preconditions; -import com.google.common.base.Verify; -import java.io.EOFException; -import java.io.IOException; -import java.nio.ByteBuffer; -import javax.annotation.Nonnull; - -/** - * Utilities to read and write 16 and 32 bit integers with support for little-endian - * encoding, as used in zip files. Zip files actually use unsigned data types. We use Java's native - * (signed) data types but will use long (64 bit) to ensure we can fit the whole range. - */ -public class LittleEndianUtils { - /** - * Utility class, no constructor. - */ - private LittleEndianUtils() { - } - - /** - * Reads 4 bytes in little-endian format and converts them into a 32-bit value. - * - * @param bytes from where should the bytes be read; the first 4 bytes of the source will be - * read - * @return the 32-bit value - * @throws IOException failed to read the value - */ - public static long readUnsigned4Le(@Nonnull ByteBuffer bytes) throws IOException { - Preconditions.checkNotNull(bytes, "bytes == null"); - - if (bytes.remaining() < 4) { - throw new EOFException("Not enough data: 4 bytes expected, " + bytes.remaining() - + " available."); - } - - byte b0 = bytes.get(); - byte b1 = bytes.get(); - byte b2 = bytes.get(); - byte b3 = bytes.get(); - long r = (b0 & 0xff) | ((b1 & 0xff) << 8) | ((b2 & 0xff) << 16) | ((b3 & 0xffL) << 24); - Verify.verify(r >= 0); - Verify.verify(r <= 0x00000000ffffffffL); - return r; - } - - /** - * Reads 2 bytes in little-endian format and converts them into a 16-bit value. - * - * @param bytes from where should the bytes be read; the first 2 bytes of the source will be - * read - * @return the 16-bit value - * @throws IOException failed to read the value - */ - public static int readUnsigned2Le(@Nonnull ByteBuffer bytes) throws IOException { - Preconditions.checkNotNull(bytes, "bytes == null"); - - if (bytes.remaining() < 2) { - throw new EOFException( - "Not enough data: 2 bytes expected, " - + bytes.remaining() - + " available."); - } - - byte b0 = bytes.get(); - byte b1 = bytes.get(); - int r = (b0 & 0xff) | ((b1 & 0xff) << 8); - - Verify.verify(r >= 0); - Verify.verify(r <= 0x0000ffff); - return r; - } - - /** - * Writes 4 bytes in little-endian format, converting them from a 32-bit value. - * - * @param output the output stream where the bytes will be written - * @param value the 32-bit value to convert - * @throws IOException failed to write the value data - */ - public static void writeUnsigned4Le(@Nonnull ByteBuffer output, long value) - throws IOException { - Preconditions.checkNotNull(output, "output == null"); - Preconditions.checkArgument(value >= 0, "value (%s) < 0", value); - Preconditions.checkArgument( - value <= 0x00000000ffffffffL, - "value (%s) > 0x00000000ffffffffL", - value); - - output.put((byte) (value & 0xff)); - output.put((byte) ((value >> 8) & 0xff)); - output.put((byte) ((value >> 16) & 0xff)); - output.put((byte) ((value >> 24) & 0xff)); - } - - /** - * Writes 2 bytes in little-endian format, converting them from a 16-bit value. - * - * @param output the output stream where the bytes will be written - * @param value the 16-bit value to convert - * @throws IOException failed to write the value data - */ - public static void writeUnsigned2Le(@Nonnull ByteBuffer output, int value) - throws IOException { - Preconditions.checkNotNull(output, "output == null"); - Preconditions.checkArgument(value >= 0, "value (%s) < 0", value); - Preconditions.checkArgument(value <= 0x0000ffff, "value (%s) > 0x0000ffff", value); - - output.put((byte) (value & 0xff)); - output.put((byte) ((value >> 8) & 0xff)); - } -} diff --git a/src/main/java/com/android/apkzlib/zip/utils/MsDosDateTimeUtils.java b/src/main/java/com/android/apkzlib/zip/utils/MsDosDateTimeUtils.java deleted file mode 100644 index 2b9f365..0000000 --- a/src/main/java/com/android/apkzlib/zip/utils/MsDosDateTimeUtils.java +++ /dev/null @@ -1,111 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * 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.android.apkzlib.zip.utils; - -import com.google.common.base.Verify; - -import java.util.Calendar; -import java.util.Date; - -/** - * Yes. This actually refers to MS-DOS in 2015. That's all I have to say about legacy stuff. - */ -public class MsDosDateTimeUtils { - /** - * Utility class: no constructor. - */ - private MsDosDateTimeUtils() { - } - - /** - * Packs java time value into an MS-DOS time value. - * - * @param time the time value - * @return the MS-DOS packed time - */ - public static int packTime(long time) { - Calendar c = Calendar.getInstance(); - c.setTime(new Date(time)); - - int seconds = c.get(Calendar.SECOND); - int minutes = c.get(Calendar.MINUTE); - int hours = c.get(Calendar.HOUR_OF_DAY); - - /* - * Here is how MS-DOS packs a time value: - * 0-4: seconds (divided by 2 because we only have 5 bits = 32 different numbers) - * 5-10: minutes (6 bits = 64 possible values) - * 11-15: hours (5 bits = 32 possible values) - * - * source: https://msdn.microsoft.com/en-us/library/windows/desktop/ms724247(v=vs.85).aspx - */ - return (hours << 11) | (minutes << 5) | (seconds / 2); - } - - /** - * Packs the current time value into an MS-DOS time value. - * - * @return the MS-DOS packed time - */ - public static int packCurrentTime() { - return packTime(new Date().getTime()); - } - - /** - * Packs java time value into an MS-DOS date value. - * - * @param time the time value - * @return the MS-DOS packed date - */ - public static int packDate(long time) { - Calendar c = Calendar.getInstance(); - c.setTime(new Date(time)); - - /* - * Even MS-DOS used 1 for January. Someone wasn't really thinking when they decided on Java - * it would start at 0... - */ - int day = c.get(Calendar.DAY_OF_MONTH); - int month = c.get(Calendar.MONTH) + 1; - - /* - * MS-DOS counts years starting from 1980. Since its launch date was in 81, it was obviously - * not necessary to talk about dates earlier than that. - */ - int year = c.get(Calendar.YEAR) - 1980; - Verify.verify(year >= 0 && year < 128); - - /* - * Here is how MS-DOS packs a date value: - * 0-4: day (5 bits = 32 values) - * 5-8: month (4 bits = 16 values) - * 9-15: year (7 bits = 128 values) - * - * source: https://msdn.microsoft.com/en-us/library/windows/desktop/ms724247(v=vs.85).aspx - */ - return (year << 9) | (month << 5) | day; - } - - /** - * Packs the current time value into an MS-DOS date value. - * - * @return the MS-DOS packed date - */ - public static int packCurrentDate() { - return packDate(new Date().getTime()); - } -} diff --git a/src/main/java/com/android/apkzlib/zip/utils/RandomAccessFileUtils.java b/src/main/java/com/android/apkzlib/zip/utils/RandomAccessFileUtils.java deleted file mode 100644 index 3bfb55c..0000000 --- a/src/main/java/com/android/apkzlib/zip/utils/RandomAccessFileUtils.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * 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.android.apkzlib.zip.utils; - -import java.io.IOException; -import java.io.RandomAccessFile; -import javax.annotation.Nonnull; - -/** - * Utility class with utility methods for random access files. - */ -public final class RandomAccessFileUtils { - - private RandomAccessFileUtils() {} - - /** - * Reads from an random access file until the provided array is filled. Data is read from the - * current position in the file. - * - * @param raf the file to read data from - * @param data the array that will receive the data - * @throws IOException failed to read the data - */ - public static void fullyRead(@Nonnull RandomAccessFile raf, @Nonnull byte[] data) - throws IOException { - int r; - int p = 0; - - while ((r = raf.read(data, p, data.length - p)) > 0) { - p += r; - if (p == data.length) { - break; - } - } - - if (p < data.length) { - throw new IOException( - "Failed to read " - + data.length - + " bytes from file. Only " - + p - + " bytes could be read."); - } - } -} diff --git a/src/main/java/com/android/tools/build/apkzlib/sign/DigestAlgorithm.java b/src/main/java/com/android/tools/build/apkzlib/sign/DigestAlgorithm.java new file mode 100644 index 0000000..29ccdc8 --- /dev/null +++ b/src/main/java/com/android/tools/build/apkzlib/sign/DigestAlgorithm.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * 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.android.tools.build.apkzlib.sign; + +import javax.annotation.Nonnull; + +/** + * Message digest algorithms. + */ +public enum DigestAlgorithm { + /** + * SHA-1 digest. + *

+ * Android 2.3 (API Level 9) to 4.2 (API Level 17) (inclusive) do not support SHA-2 + * JAR signatures. + *

+ * Moreover, platforms prior to API Level 18, without the additional + * Digest-Algorithms attribute, only support SHA or SHA1 algorithm names in .SF and + * MANIFEST.MF attributes. + */ + SHA1("SHA1", "SHA-1"), + + /** + * SHA-256 digest. + */ + SHA256("SHA-256", "SHA-256"); + + /** + * API level which supports {@link #SHA256} with {@link SignatureAlgorithm#RSA} and + * {@link SignatureAlgorithm#ECDSA}. + */ + public static final int API_SHA_256_RSA_AND_ECDSA = 18; + + /** + * API level which supports {@link #SHA256} for all {@link SignatureAlgorithm}s. + * + *

Before that, SHA256 can only be used with RSA and ECDSA. + */ + public static final int API_SHA_256_ALL_ALGORITHMS = 21; + + /** + * Name of algorithm for message digest. + */ + @Nonnull + public final String messageDigestName; + + /** + * Name of attribute in signature file with the manifest digest. + */ + @Nonnull + public final String manifestAttributeName; + + /** + * Name of attribute in entry (both manifest and signature file) with the entry's digest. + */ + @Nonnull + public final String entryAttributeName; + + /** + * Creates a digest algorithm. + * + * @param attributeName attribute name in the signature file + * @param messageDigestName name of algorithm for message digest + */ + DigestAlgorithm(@Nonnull String attributeName, @Nonnull String messageDigestName) { + this.messageDigestName = messageDigestName; + this.entryAttributeName = attributeName + "-Digest"; + this.manifestAttributeName = attributeName + "-Digest-Manifest"; + } +} diff --git a/src/main/java/com/android/tools/build/apkzlib/sign/ManifestGenerationExtension.java b/src/main/java/com/android/tools/build/apkzlib/sign/ManifestGenerationExtension.java new file mode 100644 index 0000000..b8df2a9 --- /dev/null +++ b/src/main/java/com/android/tools/build/apkzlib/sign/ManifestGenerationExtension.java @@ -0,0 +1,244 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * 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.android.tools.build.apkzlib.sign; + +import com.android.tools.build.apkzlib.utils.CachedSupplier; +import com.android.tools.build.apkzlib.utils.IOExceptionRunnable; +import com.android.tools.build.apkzlib.zfile.ManifestAttributes; +import com.android.tools.build.apkzlib.zip.StoredEntry; +import com.android.tools.build.apkzlib.zip.ZFile; +import com.android.tools.build.apkzlib.zip.ZFileExtension; +import com.google.common.base.Preconditions; +import com.google.common.base.Verify; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.util.jar.Attributes; +import java.util.jar.Manifest; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * Extension to {@link ZFile} that will generate a manifest. The extension will register + * automatically with the {@link ZFile}. + * + *

Creating this extension will ensure a manifest for the zip exists. + * This extension will generate a manifest if one does not exist and will update an existing + * manifest, if one does exist. The extension will also provide access to the manifest so that + * others may update the manifest. + * + *

Apart from standard manifest elements, this extension does not handle any particular manifest + * features such as signing or adding custom attributes. It simply generates a plain manifest and + * provides infrastructure so that other extensions can add data in the manifest. + * + *

The manifest itself will only be written when the {@link ZFileExtension#beforeUpdate()} + * notification is received, meaning all manifest manipulation is done in-memory. + */ +public class ManifestGenerationExtension { + + /** + * Name of META-INF directory. + */ + private static final String META_INF_DIR = "META-INF"; + + /** + * Name of the manifest file. + */ + static final String MANIFEST_NAME = META_INF_DIR + "/MANIFEST.MF"; + + /** + * Who should be reported as the manifest builder. + */ + @Nonnull + private final String builtBy; + + /** + * Who should be reported as the manifest creator. + */ + @Nonnull + private final String createdBy; + + /** + * The file this extension is attached to. {@code null} if not yet registered. + */ + @Nullable + private ZFile zFile; + + /** + * The zip file's manifest. + */ + @Nonnull + private final Manifest manifest; + + /** + * Byte representation of the manifest. There is no guarantee that two writes of the java's + * {@code Manifest} object will yield the same byte array (there is no guaranteed order + * of entries in the manifest). + * + *

Because we need the byte representation of the manifest to be stable if there are + * no changes to the manifest, we cannot rely on {@code Manifest} to generate the byte + * representation every time we need the byte representation. + * + *

This cache will ensure that we will request one byte generation from the {@code Manifest} + * and will cache it. All further requests of the manifest's byte representation will + * receive the same byte array. + */ + @Nonnull + private CachedSupplier manifestBytes; + + /** + * Has the current manifest been changed and not yet flushed? If {@link #dirty} is + * {@code true}, then {@link #manifestBytes} should not be valid. This means that + * marking the manifest as dirty should also invalidate {@link #manifestBytes}. To avoid + * breaking the invariant, instead of setting {@link #dirty}, {@link #markDirty()} should + * be called. + */ + private boolean dirty; + + /** + * The extension to register with the {@link ZFile}. {@code null} if not registered. + */ + @Nullable + private ZFileExtension extension; + + /** + * Creates a new extension. This will not register the extension with the provided + * {@link ZFile}. Until {@link #register(ZFile)} is invoked, this extension is not used. + * + * @param builtBy who built the manifest? + * @param createdBy who created the manifest? + */ + public ManifestGenerationExtension(@Nonnull String builtBy, @Nonnull String createdBy) { + this.builtBy = builtBy; + this.createdBy = createdBy; + manifest = new Manifest(); + dirty = false; + manifestBytes = new CachedSupplier<>(() -> { + ByteArrayOutputStream outBytes = new ByteArrayOutputStream(); + try { + manifest.write(outBytes); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + + return outBytes.toByteArray(); + }); + } + + /** + * Marks the manifest as being dirty, i.e., its data has changed since it was last + * read and/or written. + */ + private void markDirty() { + dirty = true; + manifestBytes.reset(); + } + + /** + * Registers the extension with the {@link ZFile} provided in the constructor. + * + * @param zFile the zip file to add the extension to + * @throws IOException failed to analyze the zip + */ + public void register(@Nonnull ZFile zFile) throws IOException { + Preconditions.checkState(extension == null, "register() has already been invoked."); + this.zFile = zFile; + + rebuildManifest(); + + extension = new ZFileExtension() { + @Nullable + @Override + public IOExceptionRunnable beforeUpdate() { + return ManifestGenerationExtension.this::updateManifest; + } + }; + + this.zFile.addZFileExtension(extension); + } + + /** + * Rebuilds the zip file's manifest, if it needs changes. + */ + private void rebuildManifest() throws IOException { + Verify.verifyNotNull(zFile, "zFile == null"); + + StoredEntry manifestEntry = zFile.get(MANIFEST_NAME); + + if (manifestEntry != null) { + /* + * Read the manifest entry in the zip file. Make sure we store these byte sequence + * because writing the manifest may not generate the same byte sequence, which may + * trigger an unnecessary re-sign of the jar. + */ + manifest.clear(); + byte[] manifestBytes = manifestEntry.read(); + manifest.read(new ByteArrayInputStream(manifestBytes)); + this.manifestBytes.precomputed(manifestBytes); + } + + Attributes mainAttributes = manifest.getMainAttributes(); + String currentVersion = mainAttributes.getValue(ManifestAttributes.MANIFEST_VERSION); + if (currentVersion == null) { + setMainAttribute( + ManifestAttributes.MANIFEST_VERSION, + ManifestAttributes.CURRENT_MANIFEST_VERSION); + } else { + if (!currentVersion.equals(ManifestAttributes.CURRENT_MANIFEST_VERSION)) { + throw new IOException("Unsupported manifest version: " + currentVersion + "."); + } + } + + /* + * We "blindly" override all other main attributes. + */ + setMainAttribute(ManifestAttributes.BUILT_BY, builtBy); + setMainAttribute(ManifestAttributes.CREATED_BY, createdBy); + } + + /** + * Sets the value of a main attribute. + * + * @param attribute the attribute + * @param value the value + */ + private void setMainAttribute(@Nonnull String attribute, @Nonnull String value) { + Attributes mainAttributes = manifest.getMainAttributes(); + String current = mainAttributes.getValue(attribute); + if (!value.equals(current)) { + mainAttributes.putValue(attribute, value); + markDirty(); + } + } + + /** + * Updates the manifest in the zip file, if it has been changed. + * + * @throws IOException failed to update the manifest + */ + private void updateManifest() throws IOException { + Verify.verifyNotNull(zFile, "zFile == null"); + + if (!dirty) { + return; + } + + zFile.add(MANIFEST_NAME, new ByteArrayInputStream(manifestBytes.get())); + dirty = false; + } +} diff --git a/src/main/java/com/android/tools/build/apkzlib/sign/SignatureAlgorithm.java b/src/main/java/com/android/tools/build/apkzlib/sign/SignatureAlgorithm.java new file mode 100644 index 0000000..ef7c71d --- /dev/null +++ b/src/main/java/com/android/tools/build/apkzlib/sign/SignatureAlgorithm.java @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * 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.android.tools.build.apkzlib.sign; + +import java.security.NoSuchAlgorithmException; +import javax.annotation.Nonnull; + +/** + * Signature algorithm. + */ +public enum SignatureAlgorithm { + /** RSA algorithm. */ + RSA("RSA", 1, "withRSA"), + + /** ECDSA algorithm. */ + ECDSA("EC", 18, "withECDSA"), + + /** DSA algorithm. */ + DSA("DSA", 1, "withDSA"); + + /** Name of the private key as reported by {@code PrivateKey}. */ + @Nonnull public final String keyAlgorithm; + + /** + * Minimum SDK version that allows this signature. + */ + public final int minSdkVersion; + + /** + * Suffix appended to digest algorithm to obtain signature algorithm. + */ + @Nonnull + public final String signatureAlgorithmSuffix; + + /** + * Creates a new signature algorithm. + * + * @param keyAlgorithm the name as reported by {@code PrivateKey} + * @param minSdkVersion minimum SDK version that allows this signature + * @param signatureAlgorithmSuffix suffix for signature name with used with a digest + */ + SignatureAlgorithm( + @Nonnull String keyAlgorithm, int minSdkVersion, @Nonnull String signatureAlgorithmSuffix) { + this.keyAlgorithm = keyAlgorithm; + this.minSdkVersion = minSdkVersion; + this.signatureAlgorithmSuffix = signatureAlgorithmSuffix; + } + + /** + * Obtains the signature algorithm that corresponds to a private key name applicable to a + * SDK version. + * + * @param keyAlgorithm the named referred in the {@code PrivateKey} + * @param minSdkVersion minimum SDK version to run + * @return the algorithm that has {@link #keyAlgorithm} equal to {@code keyAlgorithm} + * @throws NoSuchAlgorithmException if no algorithm was found for the given private key; an + * algorithm was found but is not applicable to the given SDK version + */ + @Nonnull + public static SignatureAlgorithm fromKeyAlgorithm(@Nonnull String keyAlgorithm, + int minSdkVersion) throws NoSuchAlgorithmException { + for (SignatureAlgorithm alg : values()) { + if (alg.keyAlgorithm.equalsIgnoreCase(keyAlgorithm)) { + if (alg.minSdkVersion > minSdkVersion) { + throw new NoSuchAlgorithmException("Signatures with " + keyAlgorithm + + " keys are not supported on minSdkVersion " + minSdkVersion + + ". They are supported only for minSdkVersion >= " + + alg.minSdkVersion); + } + + return alg; + } + } + + throw new NoSuchAlgorithmException("Signing with " + keyAlgorithm + + " keys is not supported"); + } + + /** + * Obtains the name of the signature algorithm when used with a digest algorithm. + * + * @param digestAlgorithm the digest algorithm to use + * @return the name of the signature algorithm + */ + @Nonnull + public String signatureAlgorithmName(@Nonnull DigestAlgorithm digestAlgorithm) { + return digestAlgorithm.messageDigestName.replace("-", "") + signatureAlgorithmSuffix; + } +} diff --git a/src/main/java/com/android/tools/build/apkzlib/sign/SigningExtension.java b/src/main/java/com/android/tools/build/apkzlib/sign/SigningExtension.java new file mode 100644 index 0000000..e72de69 --- /dev/null +++ b/src/main/java/com/android/tools/build/apkzlib/sign/SigningExtension.java @@ -0,0 +1,392 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * 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.android.tools.build.apkzlib.sign; + +import com.android.apksig.ApkSignerEngine; +import com.android.apksig.ApkVerifier; +import com.android.apksig.DefaultApkSignerEngine; +import com.android.apksig.apk.ApkFormatException; +import com.android.apksig.util.DataSource; +import com.android.apksig.util.DataSources; +import com.android.tools.build.apkzlib.utils.IOExceptionRunnable; +import com.android.tools.build.apkzlib.zip.StoredEntry; +import com.android.tools.build.apkzlib.zip.ZFile; +import com.android.tools.build.apkzlib.zip.ZFileExtension; +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.SignatureException; +import java.security.cert.CertificateEncodingException; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * {@link ZFile} extension which signs the APK. + * + *

+ * This extension is capable of signing the APK using JAR signing (aka v1 scheme) and APK Signature + * Scheme v2 (aka v2 scheme). Which schemes are actually used is specified by parameters to this + * extension's constructor. + */ +public class SigningExtension { + // IMPLEMENTATION NOTE: Most of the heavy lifting is performed by the ApkSignerEngine primitive + // from apksig library. This class is an adapter between ZFile extension and ApkSignerEngine. + // This class takes care of invoking the right methods on ApkSignerEngine in response to ZFile + // extension events/callbacks. + // + // The main issue leading to additional complexity in this class is that the current build + // pipeline does not reuse ApkSignerEngine instances (or ZFile extension instances for that + // matter) for incremental builds. Thus: + // * ZFile extension receives no events for JAR entries already in the APK whereas + // ApkSignerEngine needs to know about all JAR entries to be covered by signature. Thus, this + // class, during "beforeUpdate" ZFile event, notifies ApkSignerEngine about JAR entries + // already in the APK which ApkSignerEngine hasn't yet been told about -- these are the JAR + // entries which the incremental build session did not touch. + // * The build pipeline expects the APK not to change if no JAR entry was added to it or removed + // from it whereas ApkSignerEngine produces no output only if it has already produced a signed + // APK and no changes have since been made to it. This class addresses this issue by checking + // in its "register" method whether the APK is correctly signed and, only if that's the case, + // doesn't modify the APK unless a JAR entry is added to it or removed from it after + // "register". + + /** + * Minimum API Level on which this APK is supposed to run. + */ + private final int minSdkVersion; + + /** + * Whether JAR signing (aka v1 signing) is enabled. + */ + private final boolean v1SigningEnabled; + + /** + * Whether APK Signature Scheme v2 sining (aka v2 signing) is enabled. + */ + private final boolean v2SigningEnabled; + + /** + * Certificate of the signer, to be embedded into the APK's signature. + */ + @Nonnull + private final X509Certificate certificate; + + /** + * APK signer which performs most of the heavy lifting. + */ + @Nonnull + private final ApkSignerEngine signer; + + /** + * Names of APK entries which have been processed by {@link #signer}. + */ + private final Set signerProcessedOutputEntryNames = new HashSet<>(); + + /** + * Cached contents of the most recently output APK Signing Block or {@code null} if the block + * hasn't yet been output. + */ + @Nullable + private byte[] cachedApkSigningBlock; + + /** + * {@code true} if signatures may need to be output, {@code false} if there's no need to output + * signatures. This is used in an optimization where we don't modify the APK if it's already + * signed and if no JAR entries have been added to or removed from the file. + */ + private boolean dirty; + + /** + * The extension registered with the {@link ZFile}. {@code null} if not registered. + */ + @Nullable + private ZFileExtension extension; + + /** + * The file this extension is attached to. {@code null} if not yet registered. + */ + @Nullable + private ZFile zFile; + + public SigningExtension( + int minSdkVersion, + @Nonnull X509Certificate certificate, + @Nonnull PrivateKey privateKey, + boolean v1SigningEnabled, + boolean v2SigningEnabled) throws InvalidKeyException { + DefaultApkSignerEngine.SignerConfig signerConfig = + new DefaultApkSignerEngine.SignerConfig.Builder( + "CERT", privateKey, ImmutableList.of(certificate)).build(); + signer = + new DefaultApkSignerEngine.Builder(ImmutableList.of(signerConfig), minSdkVersion) + .setOtherSignersSignaturesPreserved(false) + .setV1SigningEnabled(v1SigningEnabled) + .setV2SigningEnabled(v2SigningEnabled) + .setCreatedBy("1.0 (Android)") + .build(); + this.minSdkVersion = minSdkVersion; + this.v1SigningEnabled = v1SigningEnabled; + this.v2SigningEnabled = v2SigningEnabled; + this.certificate = certificate; + } + + public void register(@Nonnull ZFile zFile) throws NoSuchAlgorithmException, IOException { + Preconditions.checkState(extension == null, "register() already invoked"); + this.zFile = zFile; + dirty = !isCurrentSignatureAsRequested(); + extension = new ZFileExtension() { + @Override + public IOExceptionRunnable added( + @Nonnull StoredEntry entry, @Nullable StoredEntry replaced) { + return () -> onZipEntryOutput(entry); + } + + @Override + public IOExceptionRunnable removed(@Nonnull StoredEntry entry) { + String entryName = entry.getCentralDirectoryHeader().getName(); + return () -> onZipEntryRemovedFromOutput(entryName); + } + + @Override + public IOExceptionRunnable beforeUpdate() throws IOException { + return () -> onOutputZipReadyForUpdate(); + } + + @Override + public void entriesWritten() throws IOException { + onOutputZipEntriesWritten(); + } + + @Override + public void closed() { + onOutputClosed(); + } + }; + this.zFile.addZFileExtension(extension); + } + + /** + * Returns {@code true} if the APK's signatures are as requested by parameters to this signing + * extension. + */ + private boolean isCurrentSignatureAsRequested() throws IOException, NoSuchAlgorithmException { + ApkVerifier.Result result; + try { + result = + new ApkVerifier.Builder(new ZFileDataSource(zFile)) + .setMinCheckedPlatformVersion(minSdkVersion) + .build() + .verify(); + } catch (ApkFormatException e) { + // Malformed APK + return false; + } + + if (!result.isVerified()) { + // Signature(s) did not verify + return false; + } + + if ((result.isVerifiedUsingV1Scheme() != v1SigningEnabled) + || (result.isVerifiedUsingV2Scheme() != v2SigningEnabled)) { + // APK isn't signed with exactly the schemes we want it to be signed + return false; + } + + List verifiedSignerCerts = result.getSignerCertificates(); + if (verifiedSignerCerts.size() != 1) { + // APK is not signed by exactly one signer + return false; + } + + byte[] expectedEncodedCert; + byte[] actualEncodedCert; + try { + expectedEncodedCert = certificate.getEncoded(); + actualEncodedCert = verifiedSignerCerts.get(0).getEncoded(); + } catch (CertificateEncodingException e) { + // Failed to encode signing certificates + return false; + } + + if (!Arrays.equals(expectedEncodedCert, actualEncodedCert)) { + // APK is signed by a wrong signer + return false; + } + + // APK is signed the way we want it to be signed + return true; + } + + private void onZipEntryOutput(@Nonnull StoredEntry entry) throws IOException { + setDirty(); + String entryName = entry.getCentralDirectoryHeader().getName(); + // This event may arrive after the entry has already been deleted. In that case, we don't + // report the addition of the entry to ApkSignerEngine. + if (entry.isDeleted()) { + return; + } + ApkSignerEngine.InspectJarEntryRequest inspectEntryRequest = + signer.outputJarEntry(entryName); + signerProcessedOutputEntryNames.add(entryName); + if (inspectEntryRequest != null) { + byte[] entryContents = entry.read(); + inspectEntryRequest.getDataSink().consume(entryContents, 0, entryContents.length); + inspectEntryRequest.done(); + } + } + + private void onZipEntryRemovedFromOutput(@Nonnull String entryName) { + setDirty(); + signer.outputJarEntryRemoved(entryName); + signerProcessedOutputEntryNames.remove(entryName); + } + + private void onOutputZipReadyForUpdate() throws IOException { + if (!dirty) { + return; + } + + // Notify signer engine about ZIP entries that have appeared in the output without the + // engine knowing. Also identify ZIP entries which disappeared from the output without the + // engine knowing. + Set unprocessedRemovedEntryNames = new HashSet<>(signerProcessedOutputEntryNames); + for (StoredEntry entry : zFile.entries()) { + String entryName = entry.getCentralDirectoryHeader().getName(); + unprocessedRemovedEntryNames.remove(entryName); + if (!signerProcessedOutputEntryNames.contains(entryName)) { + // Signer engine is not yet aware that this entry is in the output + onZipEntryOutput(entry); + } + } + + // Notify signer engine about entries which disappeared from the output without the engine + // knowing + for (String entryName : unprocessedRemovedEntryNames) { + onZipEntryRemovedFromOutput(entryName); + } + + // Check whether we need to output additional JAR entries which comprise the v1 signature + ApkSignerEngine.OutputJarSignatureRequest addV1SignatureRequest; + try { + addV1SignatureRequest = signer.outputJarEntries(); + } catch (Exception e) { + throw new IOException("Failed to generate v1 signature", e); + } + if (addV1SignatureRequest == null) { + return; + } + + // We need to output additional JAR entries which comprise the v1 signature + List v1SignatureEntries = + new ArrayList<>(addV1SignatureRequest.getAdditionalJarEntries()); + + // Reorder the JAR entries comprising the v1 signature so that MANIFEST.MF is the first + // entry. This ensures that it cleanly overwrites the existing MANIFEST.MF output by + // ManifestGenerationExtension. + for (int i = 0; i < v1SignatureEntries.size(); i++) { + ApkSignerEngine.OutputJarSignatureRequest.JarEntry entry = v1SignatureEntries.get(i); + String name = entry.getName(); + if (!ManifestGenerationExtension.MANIFEST_NAME.equals(name)) { + continue; + } + if (i != 0) { + v1SignatureEntries.remove(i); + v1SignatureEntries.add(0, entry); + } + break; + } + + // Output the JAR entries comprising the v1 signature + for (ApkSignerEngine.OutputJarSignatureRequest.JarEntry entry : v1SignatureEntries) { + String name = entry.getName(); + byte[] data = entry.getData(); + zFile.add(name, new ByteArrayInputStream(data)); + } + + addV1SignatureRequest.done(); + } + + private void onOutputZipEntriesWritten() throws IOException { + if (!dirty) { + return; + } + + // Check whether we should output an APK Signing Block which contains v2 signatures + byte[] apkSigningBlock; + byte[] centralDirBytes = zFile.getCentralDirectoryBytes(); + byte[] eocdBytes = zFile.getEocdBytes(); + ApkSignerEngine.OutputApkSigningBlockRequest addV2SignatureRequest; + // This event may arrive a second time -- after we write out the APK Signing Block. Thus, we + // cache the block to speed things up. The cached block is invalidated by any changes to the + // file (as reported to this extension). + if (cachedApkSigningBlock != null) { + apkSigningBlock = cachedApkSigningBlock; + addV2SignatureRequest = null; + } else { + DataSource centralDir = DataSources.asDataSource(ByteBuffer.wrap(centralDirBytes)); + DataSource eocd = DataSources.asDataSource(ByteBuffer.wrap(eocdBytes)); + long zipEntriesSizeBytes = + zFile.getCentralDirectoryOffset() - zFile.getExtraDirectoryOffset(); + DataSource zipEntries = new ZFileDataSource(zFile, 0, zipEntriesSizeBytes); + try { + addV2SignatureRequest = signer.outputZipSections(zipEntries, centralDir, eocd); + } catch (NoSuchAlgorithmException | InvalidKeyException | SignatureException + | ApkFormatException | IOException e) { + throw new IOException("Failed to generate v2 signature", e); + } + apkSigningBlock = + (addV2SignatureRequest != null) + ? addV2SignatureRequest.getApkSigningBlock() : new byte[0]; + cachedApkSigningBlock = apkSigningBlock; + } + + // Insert the APK Signing Block into the output right before the ZIP Central Directory and + // accordingly update the start offset of ZIP Central Directory in ZIP End of Central + // Directory. + zFile.directWrite( + zFile.getCentralDirectoryOffset() - zFile.getExtraDirectoryOffset(), + apkSigningBlock); + zFile.setExtraDirectoryOffset(apkSigningBlock.length); + + if (addV2SignatureRequest != null) { + addV2SignatureRequest.done(); + } + } + + private void onOutputClosed() { + if (!dirty) { + return; + } + signer.outputDone(); + dirty = false; + } + + private void setDirty() { + dirty = true; + cachedApkSigningBlock = null; + } +} \ No newline at end of file diff --git a/src/main/java/com/android/tools/build/apkzlib/sign/ZFileDataSource.java b/src/main/java/com/android/tools/build/apkzlib/sign/ZFileDataSource.java new file mode 100644 index 0000000..3a1fc3c --- /dev/null +++ b/src/main/java/com/android/tools/build/apkzlib/sign/ZFileDataSource.java @@ -0,0 +1,157 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * 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.android.tools.build.apkzlib.sign; + +import com.android.apksig.util.DataSink; +import com.android.apksig.util.DataSource; +import com.android.tools.build.apkzlib.zip.ZFile; +import com.google.common.base.Preconditions; +import java.io.EOFException; +import java.io.IOException; +import java.nio.ByteBuffer; +import javax.annotation.Nonnull; + +/** + * {@link DataSource} backed by contents of {@link ZFile}. + */ +class ZFileDataSource implements DataSource { + + private static final int MAX_READ_CHUNK_SIZE = 65536; + + @Nonnull + private final ZFile file; + + /** + * Offset (in bytes) relative to the start of file where the region visible in this data source + * starts. + */ + private final long offset; + + /** + * Size (in bytes) of the file region visible in this data source or {@code -1} if the whole + * file is visible in this data source and thus its size may change if the file's size changes. + */ + private final long size; + + /** + * Constructs a new {@code ZFileDataSource} based on the data contained in the file. Changes to + * the contents of the file, including the size of the file, will be visible in this data + * source. + */ + public ZFileDataSource(@Nonnull ZFile file) { + this.file = file; + offset = 0; + size = -1; + } + + /** + * Constructs a new {@code ZFileDataSource} based on the data contained in the specified region + * of the provided file. Changes to the contents of this region of the file will be visible in + * this data source. + */ + public ZFileDataSource(@Nonnull ZFile file, long offset, long size) { + Preconditions.checkArgument(offset >= 0, "offset < 0"); + Preconditions.checkArgument(size >= 0, "size < 0"); + this.file = file; + this.offset = offset; + this.size = size; + } + + @Override + public long size() { + if (size == -1) { + // Data source size is the current size of the file + try { + return file.directSize(); + } catch (IOException e) { + return 0; + } + } else { + // Data source size is fixed + return size; + } + } + + @Override + public DataSource slice(long offset, long size) { + long sourceSize = size(); + checkChunkValid(offset, size, sourceSize); + if ((offset == 0) && (size == sourceSize)) { + return this; + } + + return new ZFileDataSource(file, this.offset + offset, size); + } + + @Override + public void feed(long offset, long size, @Nonnull DataSink sink) throws IOException { + long sourceSize = size(); + checkChunkValid(offset, size, sourceSize); + if (size == 0) { + return; + } + + long chunkOffsetInFile = this.offset + offset; + long remaining = size; + byte[] buf = new byte[(int) Math.min(remaining, MAX_READ_CHUNK_SIZE)]; + while (remaining > 0) { + int chunkSize = (int) Math.min(remaining, buf.length); + int readSize = file.directRead(chunkOffsetInFile, buf, 0, chunkSize); + if (readSize == -1) { + throw new EOFException("Premature EOF"); + } + if (readSize > 0) { + sink.consume(buf, 0, readSize); + chunkOffsetInFile += readSize; + remaining -= readSize; + } + } + } + + @Override + public void copyTo(long offset, int size, @Nonnull ByteBuffer dest) throws IOException { + long sourceSize = size(); + checkChunkValid(offset, size, sourceSize); + if (size == 0) { + return; + } + + int prevLimit = dest.limit(); + try { + file.directFullyRead(this.offset + offset, dest); + } finally { + dest.limit(prevLimit); + } + } + + @Override + public ByteBuffer getByteBuffer(long offset, int size) throws IOException { + ByteBuffer result = ByteBuffer.allocate(size); + copyTo(offset, size, result); + result.flip(); + return result; + } + + private static void checkChunkValid(long offset, long size, long sourceSize) { + Preconditions.checkArgument(offset >= 0, "offset < 0"); + Preconditions.checkArgument(size >= 0, "size < 0"); + Preconditions.checkArgument(offset <= sourceSize, "offset > sourceSize"); + long endOffset = offset + size; + Preconditions.checkArgument(offset <= endOffset, "offset > endOffset"); + Preconditions.checkArgument(endOffset <= sourceSize, "endOffset > sourceSize"); + } +} diff --git a/src/main/java/com/android/tools/build/apkzlib/sign/package-info.java b/src/main/java/com/android/tools/build/apkzlib/sign/package-info.java new file mode 100644 index 0000000..c1b0829 --- /dev/null +++ b/src/main/java/com/android/tools/build/apkzlib/sign/package-info.java @@ -0,0 +1,153 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * 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. + */ + +/** + +The {@code sign} package provides extensions for the {@code zip} package that allow: +

    +
  • Adding a {@code MANIFEST.MF} file to a zip making a jar.
  • +
  • Signing a jar.
  • +
  • Fully signing a jar using v2 apk signature.
  • +
+

+Because the {@code zip} package is completely independent of the {@code sign} package, the +actual coordination between the two is complex. The {@code sign} package works by registering +extensions with the {@code zip} package. These extensions are notified in changes made in the zip +and will change the zip file itself. +

+The {@link com.android.apkzlib.sign.ManifestGenerationExtension} extension will +ensure the zip has a manifest file and is, therefore, a valid jar. +The {@link com.android.apkzlib.sign.SigningExtension} extension will +ensure the jar is signed. +

+The extension mechanism used is the one provided in the {@code zip} package (see +{@link com.android.apkzlib.zip.ZFile} +and {@link com.android.apkzlib.zip.ZFileExtension}. Building the zip and then +operating the extensions is not done sequentially, as we don't want to build a zip and then sign it. +We want to build a zip that is automatically signed. Extension are basically observers that +register on the zip and are notified when things happen in the zip. They will then modify the zip +accordingly. +

+The zip file notifies extensions in 4 critical moments: when a file is added or removed from the +zip, when the zip is about to be flushed to disk and when the zip's entries have been flushed but +the central directory not. At these moments, the extensions can act to update the zip in any way +they need. +

+To see how this works, consider the manifest generation extension: when the extension is created, +it checks the zip file to see if there is a manifest. If a manifest exists and does not need +updating, it does not change anything, otherwise it generates a new manifest for the zip file. At +this point, the extension could write the manifest to the zip, but we opted not to. It would be +irrelevant anyway as the zip will only be written when flushed. +

+Now, when the {@code ZFile} notifies the extension that it is about to start writing the zip file, +the manifest extension, if it has noted that the manifest needs to be rewritten, will -- before the +{@code ZFile} actually writes anything -- modify the zip and add or replace the existing manifest +file. So, process-wise, the zip is written only once with the correct manifest. The flow is as +follows (if only the manifest generation extension was added to the {@code ZFile}): +

    +
  1. {@code ZFile.update()} is called.
  2. +
  3. {@code ZFile} calls {@code beforeUpdate()} for all {@code ZFileExtensions} registered, in + this case, only the instance of the anonymous inner class generated in the + {@code ManifestGenerationExtension} constructor is invoked.
  4. +
  5. {@code ManifestGenerationExtension.updateManifest()} is called.
  6. +
  7. If the manifest does not need to be updated, {@code updateManifest()} returns + immediately.
  8. +
  9. If the manifest needs updating, {@code ZFile.add()} is invoked to add or replace the + manifest.
  10. +
  11. {@code ManifestGenerationExtension.updateManifest()} returns.
  12. +
  13. {@code ZFile.update()} continues and writes the zip file, containing the manifest.
  14. +
  15. The zip is finally written with an updated manifest.
  16. +
+

+To generate a signed apk, we need to add a second extension, the {@code SigningExtension}. +This extension will also register listeners with the {@code ZFile}. +

+In this case the flow would be (starting a bit earlier for clarity and assuming a package task +in the build process): +

    +
  1. Package task creates a {@code ZFile} on the target apk (or non-existing file, if there is + no target apk in the output directory).
  2. +
  3. Package task configures the {@code ZFile} with alignment rules.
  4. +
  5. Package task creates a {@code ManifestGenerationExtension}.
  6. +
  7. Package task registers the {@code ManifestGenerationExtension} with the {@code ZFile}.
  8. +
  9. The {@code ManifestGenerationExtension} looks at the {@code ZFile} to see if there is valid + manifest. No changes are done to the {@code ZFile}.
  10. +
  11. Package task creates a {@code SigningExtension}.
  12. +
  13. Package task registers the {@code SigningExtension} with the {@code ZFile}.
  14. +
  15. The {@code SigningExtension} registers a {@code ZFileExtension} with the {@code ZFile} + and look at the {@code ZFile} to see if there is a valid signature file.
  16. +
  17. If there are changes to the digital signature file needed, these are marked internally in + the extension. If there are changes needed to the digests, the manifest is updated (by calling + {@code ManifestGenerationExtension}.
    + (note that this point, the apk file, if any existed, has not been touched, the manifest is + only updated in memory and the digests of all files in the apk, if any, have been computed and + stored in memory only; the digital signature of the {@code SF} file has not been computed.) +
  18. +
  19. The Package task now adds all files to the {@code ZFile}.
  20. +
  21. For each file that is added (*), {@code ZFile} calls the added {@code ZFileExtension.added} + method of all registered extensions.
  22. +
  23. The {@code ManifestGenerationExtension} ignores added invocations.
  24. +
  25. The {@code SigningExtension} computes the digest for the added file and stores them in + the manifest.
    + (when all files are added to the apk, all digests are computed and the manifest is updated + but only in memory; the apk file has not been touched; also note that {@code ZFile} has not + actually written anything to disk at this point, all files added are kept in memory).
  26. +
  27. Package task calls {@code ZFile.update()} to update the apk.
  28. +
  29. {@code ZFile} calls {@code before()} for all {@code ZFileExtensions} registered. This is + done before anything is written. In this case both the {@code ManifestGenerationExtension} and + {@code SigningExtension} are invoked.
  30. +
  31. The {@code ManifestGenerationExtension} will update the {@code ZFile} with the new manifest, + unless nothing has changed, in which case it does nothing.
  32. +
  33. The {@code SigningExtension} will add the SF file (unless nothing has changed), will + compute the digital signature of the SF file and write it to the {@code ZFile}.
    + (note that the order by which the {@code ManifestGenerationExtension} and + {@code SigningExtension} are called is non-deterministic; however, this is not a problem + because the manifest is already computed by the {@code ManifestGenerationExtension} at this + time and the {@code SigningExtension} will obtain the manifest data from the + {@code ManifestGenerationExtension} and not from the {@code ZFile}; this means that the + {@code SF} file may be added to the {@code ZFile} before the {@code MF} file, but that is + irrelevant.)
  34. +
  35. Once both extensions have finished doing the {@code beforeUpdate()} method, the + {@code ZFile.update()} method continues.
  36. +
  37. {@code ZFile.update()} writes all changes and new entries to the zip file.
  38. +
  39. {@code ZFile.update()} calls {@code ZFileExtension.entriesWritten()} for all + registered extensions. {@code SigningExtension} will kick in at this point, if v2 signature + has changed.
  40. +
  41. {@code ZFile} writes the central directory and EOCD.
  42. +
  43. {@code ZFile.update()} returns control to the package task.
  44. +
  45. The package task finishes.
  46. +
+(*) There is a number of optimizations if we're adding files from another {@code ZFile}, which +is the case when we add the output of aapt to the apk. In particular, files from the aapt are +ignored if they are already in the apk (same name, same CRC32) and also files copied from +the aapt's output are not recompressed (the binary compressed data is directly copied to the +zip). +

+If there are no changes to the {@code ZFile} made by the package task and the file's manifest and v1 +signatures are correct, neither the {@code ManifestGenerationExtension} nor the +{@code SigningExtension} will not do anything on the {@code beforeUpdate()} and the +{@code ZFile} won't even be open for writing. +

+This implementation provides perfect incremental updates. +

+Additionally, by adding/removing extensions we can configure what type of apk we want: +

    +
  • No SigningExtension ⇒ Aligned, unsigned apk.
  • +
  • SigningExtension ⇒ Aligned, signed apk. +
+So, by configuring which extensions to add, the package task can decide what type of apk we want. +*/ +package com.android.apkzlib.sign; diff --git a/src/main/java/com/android/tools/build/apkzlib/utils/ApkZLibPair.java b/src/main/java/com/android/tools/build/apkzlib/utils/ApkZLibPair.java new file mode 100644 index 0000000..f9f4177 --- /dev/null +++ b/src/main/java/com/android/tools/build/apkzlib/utils/ApkZLibPair.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * 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.android.tools.build.apkzlib.utils; + +/** + * Pair implementation to use with the {@code apkzlib} library. + */ +public class ApkZLibPair { + + /** + * First value. + */ + public T1 v1; + + /** + * Second value. + */ + public T2 v2; + + /** + * Creates a new pair. + * + * @param v1 the first value + * @param v2 the second value + */ + public ApkZLibPair(T1 v1, T2 v2) { + this.v1 = v1; + this.v2 = v2; + } +} diff --git a/src/main/java/com/android/tools/build/apkzlib/utils/CachedFileContents.java b/src/main/java/com/android/tools/build/apkzlib/utils/CachedFileContents.java new file mode 100644 index 0000000..3600800 --- /dev/null +++ b/src/main/java/com/android/tools/build/apkzlib/utils/CachedFileContents.java @@ -0,0 +1,176 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * 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.android.tools.build.apkzlib.utils; + +import com.google.common.base.Objects; +import com.google.common.hash.HashCode; +import com.google.common.hash.Hashing; +import com.google.common.io.Files; +import java.io.File; +import java.io.IOException; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * A cache for file contents. The cache allows closing a file and saving in memory its contents (or + * some related information). It can then be used to check if the contents are still valid at some + * later time. Typical usage flow is: + * + *

+ * + *

{@code
+ * Object fileRepresentation = // ...
+ * File toWrite = // ...
+ * // Write file contents and update in memory representation
+ * CachedFileContents contents = new CachedFileContents(toWrite);
+ * contents.closed(fileRepresentation);
+ *
+ * // Later, when data is needed:
+ * if (contents.isValid()) {
+ *     fileRepresentation = contents.getCache();
+ * } else {
+ *     // Re-read the file and recreate the file representation
+ * }
+ * }
+ *
+ * @param  the type of cached contents
+ */
+public class CachedFileContents {
+
+    /**
+     * The file.
+     */
+    @Nonnull
+    private File file;
+
+    /**
+     * Time when last closed (time when {@link #closed(Object)} was invoked).
+     */
+    private long lastClosed;
+
+    /**
+     * Size of the file when last closed.
+     */
+    private long size;
+
+    /**
+     * Hash of the file when closed. {@code null} if hashing failed for some reason.
+     */
+    @Nullable
+    private HashCode hash;
+
+    /**
+     * Cached data associated with the file.
+     */
+    @Nullable
+    private T cache;
+
+    /**
+     * Creates a new contents. When the file is written, {@link #closed(Object)} should be invoked
+     * to set the cache.
+     *
+     * @param file the file
+     */
+    public CachedFileContents(@Nonnull File file) {
+        this.file = file;
+    }
+
+    /**
+     * Should be called when the file's contents are set and the file closed. This will save the
+     * cache and register the file's timestamp to later detect if it has been modified.
+     * 

+ * This method can be called as many times as the file has been written. + * + * @param cache an optional cache to save + */ + public void closed(@Nullable T cache) { + this.cache = cache; + lastClosed = file.lastModified(); + size = file.length(); + hash = hashFile(); + } + + /** + * Are the cached contents still valid? If this method determines that the file has been + * modified since the last time {@link #closed(Object)} was invoked. + * + * @return are the cached contents still valid? If this method returns {@code false}, the + * cache is cleared + */ + public boolean isValid() { + boolean valid = true; + + if (!file.exists()) { + valid = false; + } + + if (valid && file.lastModified() != lastClosed) { + valid = false; + } + + if (valid && file.length() != size) { + valid = false; + } + + if (valid && !Objects.equal(hash, hashFile())) { + valid = false; + } + + if (!valid) { + cache = null; + } + + return valid; + } + + /** + * Obtains the cached data set with {@link #closed(Object)} if the file has not been modified + * since {@link #closed(Object)} was invoked. + * + * @return the last cached data or {@code null} if the file has been modified since + * {@link #closed(Object)} has been invoked + */ + @Nullable + public T getCache() { + return cache; + } + + /** + * Computes the hashcode of the cached file. + * + * @return the hash code + */ + @Nullable + private HashCode hashFile() { + try { + return Files.hash(file, Hashing.crc32()); + } catch (IOException e) { + return null; + } + } + + /** + * Obtains the file used for caching. + * + * @return the file; this file always exists and contains the old (cached) contents of the + * file + */ + @Nonnull + public File getFile() { + return file; + } +} diff --git a/src/main/java/com/android/tools/build/apkzlib/utils/CachedSupplier.java b/src/main/java/com/android/tools/build/apkzlib/utils/CachedSupplier.java new file mode 100644 index 0000000..84505bc --- /dev/null +++ b/src/main/java/com/android/tools/build/apkzlib/utils/CachedSupplier.java @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * 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.android.tools.build.apkzlib.utils; + +import java.util.function.Supplier; +import javax.annotation.Nonnull; + +/** + * Supplier that will cache a computed value and always supply the same value. It can be used to + * lazily compute data. For example: + * + *

{@code
+ * CachedSupplier value = new CachedSupplier<>(() -> {
+ *     Integer result;
+ *     // Do some expensive computation.
+ *     return result;
+ * });
+ *
+ * if (a) {
+ *     // We need the result of the expensive computation.
+ *     Integer r = value.get();
+ * }
+ *
+ * if (b) {
+ *     // We also need the result of the expensive computation.
+ *     Integer r = value.get();
+ * }
+ *
+ * // If neither a nor b are true, we avoid doing the computation at all.
+ * }
+ */ +public class CachedSupplier { + + /** + * The cached data, {@code null} if computation resulted in {@code null}. It is also + * {@code null} if the cached data has not yet been computed. + */ + private T cached; + + /** + * Is the current data in {@link #cached} valid? + */ + private boolean valid; + + /** + * Actual supplier of data, if computation is needed. + */ + @Nonnull + private final Supplier supplier; + + /** + * Creates a new supplier. + */ + public CachedSupplier(@Nonnull Supplier supplier) { + valid = false; + this.supplier = supplier; + } + + + /** + * Obtains the value. + * + * @return the value, either cached (if one exists) or computed + */ + public synchronized T get() { + if (!valid) { + cached = supplier.get(); + valid = true; + } + + return cached; + } + + /** + * Resets the cache forcing a {@code get()} on the supplier next time {@link #get()} is invoked. + */ + public synchronized void reset() { + cached = null; + valid = false; + } + + /** + * In some cases, we may be able to precompute the cache value (or load it from somewhere we + * had previously stored it). This method allows the cache value to be loaded. + * + *

If this method is invoked, then an invocation of {@link #get()} will not trigger an + * invocation of the supplier provided in the constructor. + * + * @param t the new cache contents; will replace any currently cache content, if one exists + */ + public synchronized void precomputed(T t) { + cached = t; + valid = true; + } + + /** + * Checks if the contents of the cache are valid. + * + * @return are there valid contents in the cache? + */ + public synchronized boolean isValid() { + return valid; + } +} diff --git a/src/main/java/com/android/tools/build/apkzlib/utils/IOExceptionConsumer.java b/src/main/java/com/android/tools/build/apkzlib/utils/IOExceptionConsumer.java new file mode 100644 index 0000000..98aefe7 --- /dev/null +++ b/src/main/java/com/android/tools/build/apkzlib/utils/IOExceptionConsumer.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * 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.android.tools.build.apkzlib.utils; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.util.function.Consumer; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * Consumer that can throw an {@link IOException}. + */ +@FunctionalInterface +public interface IOExceptionConsumer { + + /** + * Performs an operation on the given input. + * + * @param input the input + */ + void accept(@Nullable T input) throws IOException; + + /** + * Wraps a consumer that may throw an IO Exception throwing an {@code UncheckedIOException}. + * + * @param c the consumer + */ + @Nonnull + static Consumer asConsumer(@Nonnull IOExceptionConsumer c) { + return i -> { + try { + c.accept(i); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }; + } +} diff --git a/src/main/java/com/android/tools/build/apkzlib/utils/IOExceptionFunction.java b/src/main/java/com/android/tools/build/apkzlib/utils/IOExceptionFunction.java new file mode 100644 index 0000000..4ccce5f --- /dev/null +++ b/src/main/java/com/android/tools/build/apkzlib/utils/IOExceptionFunction.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * 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.android.tools.build.apkzlib.utils; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.util.function.Function; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * Function that can throw an I/O Exception + */ +@FunctionalInterface +public interface IOExceptionFunction { + + /** + * Applies the function to the given input. + * @param input the input + * @return the function result + */ + @Nullable T apply(@Nullable F input) throws IOException; + + /** + * Wraps a function that may throw an IO Exception throwing an {@code UncheckedIOException}. + * + * @param f the function + */ + @Nonnull + static Function asFunction(@Nonnull IOExceptionFunction f) { + return i -> { + try { + return f.apply(i); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }; + } +} diff --git a/src/main/java/com/android/tools/build/apkzlib/utils/IOExceptionRunnable.java b/src/main/java/com/android/tools/build/apkzlib/utils/IOExceptionRunnable.java new file mode 100644 index 0000000..40de80b --- /dev/null +++ b/src/main/java/com/android/tools/build/apkzlib/utils/IOExceptionRunnable.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * 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.android.tools.build.apkzlib.utils; + +import java.io.IOException; +import java.io.UncheckedIOException; +import javax.annotation.Nonnull; + +/** + * Runnable that can throw I/O exceptions. + */ +@FunctionalInterface +public interface IOExceptionRunnable { + + /** + * Runs the runnable. + * + * @throws IOException failed to run + */ + void run() throws IOException; + + /** + * Wraps a runnable that may throw an IO Exception throwing an {@code UncheckedIOException}. + * + * @param r the runnable + */ + @Nonnull + public static Runnable asRunnable(@Nonnull IOExceptionRunnable r) { + return () -> { + try { + r.run(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }; + } +} diff --git a/src/main/java/com/android/tools/build/apkzlib/utils/IOExceptionWrapper.java b/src/main/java/com/android/tools/build/apkzlib/utils/IOExceptionWrapper.java new file mode 100644 index 0000000..d6f4d8b --- /dev/null +++ b/src/main/java/com/android/tools/build/apkzlib/utils/IOExceptionWrapper.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * 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.android.tools.build.apkzlib.utils; + +import java.io.IOException; +import javax.annotation.Nonnull; + +/** + * Runtime exception used to encapsulate an IO Exception. This is used to allow throwing I/O + * exceptions in functional interfaces that do not allow it and catching the exception afterwards. + */ +public class IOExceptionWrapper extends RuntimeException { + + /** + * Creates a new exception. + * + * @param e the I/O exception to encapsulate + */ + public IOExceptionWrapper(@Nonnull IOException e) { + super(e); + } + + @Override + @Nonnull + public IOException getCause() { + return (IOException) super.getCause(); + } +} diff --git a/src/main/java/com/android/tools/build/apkzlib/utils/package-info.java b/src/main/java/com/android/tools/build/apkzlib/utils/package-info.java new file mode 100644 index 0000000..64d8a62 --- /dev/null +++ b/src/main/java/com/android/tools/build/apkzlib/utils/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * 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. + */ + +/** + * Utilities to work with {@code apkzlib}. + */ +package com.android.tools.build.apkzlib.utils; diff --git a/src/main/java/com/android/tools/build/apkzlib/zfile/ApkCreator.java b/src/main/java/com/android/tools/build/apkzlib/zfile/ApkCreator.java new file mode 100644 index 0000000..e47b04e --- /dev/null +++ b/src/main/java/com/android/tools/build/apkzlib/zfile/ApkCreator.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * 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.android.tools.build.apkzlib.zfile; + +import java.io.Closeable; +import java.io.File; +import java.io.IOException; +import java.util.function.Function; +import java.util.function.Predicate; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * Creates or updates APKs based on provided entries. + */ +public interface ApkCreator extends Closeable { + + /** + * Copies the content of a Jar/Zip archive into the receiver archive. + * + *

An optional predicate allows to selectively choose which files to copy over and an + * option function allows renaming the files as they are copied. + * + * @param zip the zip to copy data from + * @param transform an optional transform to apply to file names before copying them + * @param isIgnored an optional filter or {@code null} to mark which out files should not be + * added, even through they are on the zip; if {@code transform} is specified, then this + * predicate applies after transformation + * @throws IOException I/O error + */ + void writeZip( + @Nonnull File zip, + @Nullable Function transform, + @Nullable Predicate isIgnored) + throws IOException; + + /** + * Writes a new {@link File} into the archive. If a file already existed with the given + * path, it should be replaced. + * + * @param inputFile the {@link File} to write. + * @param apkPath the filepath inside the archive. + * @throws IOException I/O error + */ + void writeFile(@Nonnull File inputFile, @Nonnull String apkPath) throws IOException; + + /** + * Deletes a file in a given path. + * + * @param apkPath the path to remove + * @throws IOException failed to remove the entry + */ + void deleteFile(@Nonnull String apkPath) throws IOException; + + /** Returns true if the APK will be rewritten on close. */ + boolean hasPendingChangesWithWait() throws IOException; +} diff --git a/src/main/java/com/android/tools/build/apkzlib/zfile/ApkCreatorFactory.java b/src/main/java/com/android/tools/build/apkzlib/zfile/ApkCreatorFactory.java new file mode 100644 index 0000000..e782206 --- /dev/null +++ b/src/main/java/com/android/tools/build/apkzlib/zfile/ApkCreatorFactory.java @@ -0,0 +1,246 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * 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.android.tools.build.apkzlib.zfile; + +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.common.base.Preconditions; +import java.io.File; +import java.security.PrivateKey; +import java.security.cert.X509Certificate; +import java.util.function.Predicate; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * Factory that creates instances of {@link ApkCreator}. + */ +public interface ApkCreatorFactory { + + /** + * Creates an {@link ApkCreator} with a given output location, and signing information. + * + * @param creationData the information to create the APK + */ + ApkCreator make(@Nonnull CreationData creationData); + + /** + * Data structure with the required information to initiate the creation of an APK. See + * {@link ApkCreatorFactory#make(CreationData)}. + */ + class CreationData { + + /** + * The path where the APK should be located. May already exist or not (if it does, then + * the APK may be updated instead of created). + */ + @Nonnull + private final File apkPath; + + /** + * Key used to sign the APK. May be {@code null}. + */ + @Nullable + private final PrivateKey key; + + /** + * Certificate used to sign the APK. Is {@code null} if and only if {@link #key} is + * {@code null}. + */ + @Nullable + private final X509Certificate certificate; + + /** + * Whether signing the APK with JAR Signing Scheme (aka v1 signing) is enabled. + */ + private final boolean v1SigningEnabled; + + /** + * Whether signing the APK with APK Signature Scheme v2 (aka v2 signing) is enabled. + */ + private final boolean v2SigningEnabled; + + /** + * Built-by information for the APK, if any. + */ + @Nullable + private final String builtBy; + + /** + * Created-by information for the APK, if any. + */ + @Nullable + private final String createdBy; + + /** + * Minimum SDk version that will run the APK. + */ + private final int minSdkVersion; + + /** + * How should native libraries be packaged? + */ + @Nonnull + private final NativeLibrariesPackagingMode nativeLibrariesPackagingMode; + + /** + * Predicate identifying paths that should not be compressed. + */ + @Nonnull + private final Predicate noCompressPredicate; + + /** + * + * @param apkPath the path where the APK should be located. May already exist or not (if it + * does, then the APK may be updated instead of created) + * @param key key used to sign the APK. May be {@code null} + * @param certificate certificate used to sign the APK. Is {@code null} if and only if + * {@code key} is {@code null} + * @param v1SigningEnabled {@code true} if this APK should be signed with JAR Signature + * Scheme (aka v1 scheme). + * @param v2SigningEnabled {@code true} if this APK should be signed with APK Signature + * Scheme v2 (aka v2 scheme). + * @param builtBy built-by information for the APK, if any; if {@code null} then the default + * should be used + * @param createdBy created-by information for the APK, if any; if {@code null} then the + * default should be used + * @param minSdkVersion minimum SDK version that will run the APK + * @param nativeLibrariesPackagingMode packaging mode for native libraries + * @param noCompressPredicate predicate to decide which file paths should be uncompressed; + * returns {@code true} for files that should not be compressed + */ + public CreationData( + @Nonnull File apkPath, + @Nullable PrivateKey key, + @Nullable X509Certificate certificate, + boolean v1SigningEnabled, + boolean v2SigningEnabled, + @Nullable String builtBy, + @Nullable String createdBy, + int minSdkVersion, + @Nonnull NativeLibrariesPackagingMode nativeLibrariesPackagingMode, + @Nonnull Predicate noCompressPredicate) { + Preconditions.checkArgument((key == null) == (certificate == null), + "(key == null) != (certificate == null)"); + Preconditions.checkArgument(minSdkVersion >= 0, "minSdkVersion < 0"); + + this.apkPath = apkPath; + this.key = key; + this.certificate = certificate; + this.v1SigningEnabled = v1SigningEnabled; + this.v2SigningEnabled = v2SigningEnabled; + this.builtBy = builtBy; + this.createdBy = createdBy; + this.minSdkVersion = minSdkVersion; + this.nativeLibrariesPackagingMode = checkNotNull(nativeLibrariesPackagingMode); + this.noCompressPredicate = checkNotNull(noCompressPredicate); + } + + /** + * Obtains the path where the APK should be located. If the path already exists, then the + * APK may be updated instead of re-created. + * + * @return the path that may already exist or not + */ + @Nonnull + public File getApkPath() { + return apkPath; + } + + /** + * Obtains the private key used to sign the APK. + * + * @return the private key or {@code null} if the APK should not be signed + */ + @Nullable + public PrivateKey getPrivateKey() { + return key; + } + + /** + * Obtains the certificate used to sign the APK. + * + * @return the certificate or {@code null} if the APK should not be signed; this will return + * {@code null} if and only if {@link #getPrivateKey()} returns {@code null} + */ + @Nullable + public X509Certificate getCertificate() { + return certificate; + } + + /** + * Returns {@code true} if this APK should be signed with JAR Signature Scheme (aka v1 + * scheme). + */ + public boolean isV1SigningEnabled() { + return v1SigningEnabled; + } + + /** + * Returns {@code true} if this APK should be signed with APK Signature Scheme v2 (aka v2 + * scheme). + */ + public boolean isV2SigningEnabled() { + return v2SigningEnabled; + } + + /** + * Obtains the "built-by" text for the APK. + * + * @return the text or {@code null} if the default should be used + */ + @Nullable + public String getBuiltBy() { + return builtBy; + } + + /** + * Obtains the "created-by" text for the APK. + * + * @return the text or {@code null} if the default should be used + */ + @Nullable + public String getCreatedBy() { + return createdBy; + } + + /** + * Obtains the minimum SDK version to run the APK. + * + * @return the minimum SDK version + */ + public int getMinSdkVersion() { + return minSdkVersion; + } + + /** + * Returns the packaging policy that the {@link ApkCreator} should use for native libraries. + */ + @Nonnull + public NativeLibrariesPackagingMode getNativeLibrariesPackagingMode() { + return nativeLibrariesPackagingMode; + } + + /** + * Returns the predicate to decide which file paths should be uncompressed. + */ + @Nonnull + public Predicate getNoCompressPredicate() { + return noCompressPredicate; + } + } +} diff --git a/src/main/java/com/android/tools/build/apkzlib/zfile/ApkZFileCreator.java b/src/main/java/com/android/tools/build/apkzlib/zfile/ApkZFileCreator.java new file mode 100644 index 0000000..0a0b90d --- /dev/null +++ b/src/main/java/com/android/tools/build/apkzlib/zfile/ApkZFileCreator.java @@ -0,0 +1,192 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * 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.android.tools.build.apkzlib.zfile; + +import com.android.tools.build.apkzlib.zip.AlignmentRule; +import com.android.tools.build.apkzlib.zip.AlignmentRules; +import com.android.tools.build.apkzlib.zip.StoredEntry; +import com.android.tools.build.apkzlib.zip.ZFile; +import com.android.tools.build.apkzlib.zip.ZFileOptions; +import com.google.common.base.Preconditions; +import com.google.common.io.Closer; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.function.Function; +import java.util.function.Predicate; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * {@link ApkCreator} that uses {@link ZFileOptions} to generate the APK. + */ +class ApkZFileCreator implements ApkCreator { + + /** + * Suffix for native libraries. + */ + private static final String NATIVE_LIBRARIES_SUFFIX = ".so"; + + /** + * Shared libraries are alignment at 4096 boundaries. + */ + private static final AlignmentRule SO_RULE = + AlignmentRules.constantForSuffix(NATIVE_LIBRARIES_SUFFIX, 4096); + + /** + * The zip file. + */ + @Nonnull + private final ZFile zip; + + /** + * Has the zip file been closed? + */ + private boolean closed; + + /** + * Predicate defining which files should not be compressed. + */ + @Nonnull + private final Predicate noCompressPredicate; + + /** + * Creates a new creator. + * + * @param creationData the data needed to create the APK + * @param options zip file options + * @throws IOException failed to create the zip + */ + ApkZFileCreator( + @Nonnull ApkCreatorFactory.CreationData creationData, + @Nonnull ZFileOptions options) + throws IOException { + + switch (creationData.getNativeLibrariesPackagingMode()) { + case COMPRESSED: + noCompressPredicate = creationData.getNoCompressPredicate(); + break; + case UNCOMPRESSED_AND_ALIGNED: + noCompressPredicate = + creationData.getNoCompressPredicate().or( + name -> name.endsWith(NATIVE_LIBRARIES_SUFFIX)); + options.setAlignmentRule( + AlignmentRules.compose(SO_RULE, options.getAlignmentRule())); + break; + default: + throw new AssertionError(); + } + + zip = ZFiles.apk( + creationData.getApkPath(), + options, + creationData.getPrivateKey(), + creationData.getCertificate(), + creationData.isV1SigningEnabled(), + creationData.isV2SigningEnabled(), + creationData.getBuiltBy(), + creationData.getCreatedBy(), + creationData.getMinSdkVersion()); + closed = false; + } + + @Override + public void writeZip(@Nonnull File zip, @Nullable Function transform, + @Nullable Predicate isIgnored) throws IOException { + Preconditions.checkState(!closed, "closed == true"); + Preconditions.checkArgument(zip.isFile(), "!zip.isFile()"); + + Closer closer = Closer.create(); + try { + ZFile toMerge = closer.register(new ZFile(zip)); + + Predicate ignorePredicate; + if (isIgnored == null) { + ignorePredicate = s -> false; + } else { + ignorePredicate = isIgnored; + } + + // Files that *must* be uncompressed in the result should not be merged and should be + // added after. This is just very slightly less efficient than ignoring just the ones + // that were compressed and must be uncompressed, but it is a lot simpler :) + Predicate noMergePredicate = ignorePredicate.or(noCompressPredicate); + + this.zip.mergeFrom(toMerge, noMergePredicate); + + for (StoredEntry toMergeEntry : toMerge.entries()) { + String path = toMergeEntry.getCentralDirectoryHeader().getName(); + if (noCompressPredicate.test(path) && !ignorePredicate.test(path)) { + // This entry *must* be uncompressed so it was ignored in the merge and should + // now be added to the apk. + try (InputStream ignoredData = toMergeEntry.open()) { + this.zip.add(path, ignoredData, false); + } + } + } + } catch (Throwable t) { + throw closer.rethrow(t); + } finally { + closer.close(); + } + } + + @Override + public void writeFile(@Nonnull File inputFile, @Nonnull String apkPath) throws IOException { + Preconditions.checkState(!closed, "closed == true"); + + boolean mayCompress = !noCompressPredicate.test(apkPath); + + Closer closer = Closer.create(); + try { + FileInputStream inputFileStream = closer.register(new FileInputStream(inputFile)); + zip.add(apkPath, inputFileStream, mayCompress); + } catch (IOException e) { + throw closer.rethrow(e, IOException.class); + } catch (Throwable t) { + throw closer.rethrow(t); + } finally { + closer.close(); + } + } + + @Override + public void deleteFile(@Nonnull String apkPath) throws IOException { + Preconditions.checkState(!closed, "closed == true"); + + StoredEntry entry = zip.get(apkPath); + if (entry != null) { + entry.delete(); + } + } + + @Override + public boolean hasPendingChangesWithWait() throws IOException { + return zip.hasPendingChangesWithWait(); + } + + @Override + public void close() throws IOException { + if (closed) { + return; + } + + zip.close(); + closed = true; + } +} diff --git a/src/main/java/com/android/tools/build/apkzlib/zfile/ApkZFileCreatorFactory.java b/src/main/java/com/android/tools/build/apkzlib/zfile/ApkZFileCreatorFactory.java new file mode 100644 index 0000000..b19885d --- /dev/null +++ b/src/main/java/com/android/tools/build/apkzlib/zfile/ApkZFileCreatorFactory.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * 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.android.tools.build.apkzlib.zfile; + +import com.android.tools.build.apkzlib.zip.ZFileOptions; +import java.io.IOException; +import java.io.UncheckedIOException; +import javax.annotation.Nonnull; + +/** + * Creates instances of {@link ApkZFileCreator}. + */ +public class ApkZFileCreatorFactory implements ApkCreatorFactory { + + /** + * Options for the {@link ZFileOptions} to use in all APKs. + */ + @Nonnull + private final ZFileOptions options; + + /** + * Creates a new factory. + * + * @param options the options to use for all instances created + */ + public ApkZFileCreatorFactory(@Nonnull ZFileOptions options) { + this.options = options; + } + + + @Override + @Nonnull + public ApkCreator make(@Nonnull CreationData creationData) { + try { + return new ApkZFileCreator(creationData, options); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } +} diff --git a/src/main/java/com/android/tools/build/apkzlib/zfile/ManifestAttributes.java b/src/main/java/com/android/tools/build/apkzlib/zfile/ManifestAttributes.java new file mode 100644 index 0000000..e8e6c2d --- /dev/null +++ b/src/main/java/com/android/tools/build/apkzlib/zfile/ManifestAttributes.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * 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.android.tools.build.apkzlib.zfile; + +/** + * Java manifest attributes and some default values. + */ +public interface ManifestAttributes { + /** + * Manifest attribute with the built by information. + */ + String BUILT_BY = "Built-By"; + + /** + * Manifest attribute with the created by information. + */ + String CREATED_BY = "Created-By"; + + /** + * Manifest attribute with the manifest version. + */ + String MANIFEST_VERSION = "Manifest-Version"; + + /** + * Manifest attribute value with the manifest version. + */ + String CURRENT_MANIFEST_VERSION = "1.0"; + +} diff --git a/src/main/java/com/android/tools/build/apkzlib/zfile/NativeLibrariesPackagingMode.java b/src/main/java/com/android/tools/build/apkzlib/zfile/NativeLibrariesPackagingMode.java new file mode 100644 index 0000000..0dab9b1 --- /dev/null +++ b/src/main/java/com/android/tools/build/apkzlib/zfile/NativeLibrariesPackagingMode.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * 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.android.tools.build.apkzlib.zfile; + +/** + * Describes how native libs should be packaged. + */ +public enum NativeLibrariesPackagingMode { + /** + * Native libs are packaged as any other file. + */ + COMPRESSED, + + /** + * Native libs are packaged uncompressed and page-aligned, so they can be mapped into memory + * at runtime. + * + *

Support for this mode was added in Android 23, it only works if the + * {@code extractNativeLibs} attribute is set in the manifest. + */ + UNCOMPRESSED_AND_ALIGNED; +} diff --git a/src/main/java/com/android/tools/build/apkzlib/zfile/ZFiles.java b/src/main/java/com/android/tools/build/apkzlib/zfile/ZFiles.java new file mode 100644 index 0000000..a54c50d --- /dev/null +++ b/src/main/java/com/android/tools/build/apkzlib/zfile/ZFiles.java @@ -0,0 +1,132 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * 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.android.tools.build.apkzlib.zfile; + +import com.android.tools.build.apkzlib.sign.ManifestGenerationExtension; +import com.android.tools.build.apkzlib.sign.SigningExtension; +import com.android.tools.build.apkzlib.zip.AlignmentRule; +import com.android.tools.build.apkzlib.zip.AlignmentRules; +import com.android.tools.build.apkzlib.zip.ZFile; +import com.android.tools.build.apkzlib.zip.ZFileOptions; +import java.io.File; +import java.io.IOException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.cert.X509Certificate; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * Factory for {@link ZFile}s that are specifically configured to be APKs, AARs, ... + */ +public class ZFiles { + + /** + * By default all non-compressed files are alignment at 4 byte boundaries.. + */ + private static final AlignmentRule APK_DEFAULT_RULE = AlignmentRules.constant(4); + + /** + * Default build by string. + */ + private static final String DEFAULT_BUILD_BY = "Generated-by-ADT"; + + /** + * Default created by string. + */ + private static final String DEFAULT_CREATED_BY = "Generated-by-ADT"; + + /** + * Creates a new zip file configured as an apk, based on a given file. + * + * @param f the file, if this path does not represent an existing path, will create a + * {@link ZFile} based on an non-existing path (a zip will be created when + * {@link ZFile#close()} is invoked) + * @param options the options to create the {@link ZFile} + * @return the zip file + * @throws IOException failed to create the zip file + */ + @Nonnull + public static ZFile apk(@Nonnull File f, @Nonnull ZFileOptions options) throws IOException { + options.setAlignmentRule( + AlignmentRules.compose(options.getAlignmentRule(), APK_DEFAULT_RULE)); + return new ZFile(f, options); + } + + /** + * Creates a new zip file configured as an apk, based on a given file. + * + * @param f the file, if this path does not represent an existing path, will create a + * {@link ZFile} based on an non-existing path (a zip will be created when + * {@link ZFile#close()} is invoked) + * @param options the options to create the {@link ZFile} + * @param key the {@link PrivateKey} used to sign the archive, or {@code null}. + * @param certificate the {@link X509Certificate} used to sign the archive, or + * {@code null}. + * @param v1SigningEnabled whether signing with JAR Signature Scheme (aka v1 signing) is + * enabled. + * @param v2SigningEnabled whether signing with APK Signature Scheme v2 (aka v2 signing) is + * enabled. + * @param builtBy who to mark as builder in the manifest + * @param createdBy who to mark as creator in the manifest + * @param minSdkVersion minimum SDK version supported + * @return the zip file + * @throws IOException failed to create the zip file + */ + @Nonnull + public static ZFile apk( + @Nonnull File f, + @Nonnull ZFileOptions options, + @Nullable PrivateKey key, + @Nullable X509Certificate certificate, + boolean v1SigningEnabled, + boolean v2SigningEnabled, + @Nullable String builtBy, + @Nullable String createdBy, + int minSdkVersion) + throws IOException { + ZFile zfile = apk(f, options); + + if (builtBy == null) { + builtBy = DEFAULT_BUILD_BY; + } + + if (createdBy == null) { + createdBy = DEFAULT_CREATED_BY; + } + + ManifestGenerationExtension manifestExt = new ManifestGenerationExtension(builtBy, + createdBy); + manifestExt.register(zfile); + + if (key != null && certificate != null) { + try { + new SigningExtension( + minSdkVersion, + certificate, + key, + v1SigningEnabled, + v2SigningEnabled).register(zfile); + } catch (NoSuchAlgorithmException | InvalidKeyException e) { + throw new IOException("Failed to create signature extensions", e); + } + } + + return zfile; + } +} diff --git a/src/main/java/com/android/tools/build/apkzlib/zfile/package-info.java b/src/main/java/com/android/tools/build/apkzlib/zfile/package-info.java new file mode 100644 index 0000000..703b209 --- /dev/null +++ b/src/main/java/com/android/tools/build/apkzlib/zfile/package-info.java @@ -0,0 +1,18 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * 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. + */ + +/** The {@code zfile} package contains */ +package com.android.tools.build.apkzlib.zfile; diff --git a/src/main/java/com/android/tools/build/apkzlib/zip/AlignmentRule.java b/src/main/java/com/android/tools/build/apkzlib/zip/AlignmentRule.java new file mode 100644 index 0000000..d599a03 --- /dev/null +++ b/src/main/java/com/android/tools/build/apkzlib/zip/AlignmentRule.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * 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.android.tools.build.apkzlib.zip; + +import javax.annotation.Nonnull; + +/** + * An alignment rule defines how to a file should be aligned in a zip, based on its name. + */ +public interface AlignmentRule { + + /** + * Alignment value of files that do not require alignment. + */ + int NO_ALIGNMENT = 1; + + /** + * Obtains the alignment this rule computes for a given path. + * + * @param path the path in the zip file + * @return the alignment value, always greater than {@code 0}; if this rule places no + * restrictions on the provided path, then {@link AlignmentRule#NO_ALIGNMENT} is returned + */ + int alignment(@Nonnull String path); +} diff --git a/src/main/java/com/android/tools/build/apkzlib/zip/AlignmentRules.java b/src/main/java/com/android/tools/build/apkzlib/zip/AlignmentRules.java new file mode 100644 index 0000000..f654708 --- /dev/null +++ b/src/main/java/com/android/tools/build/apkzlib/zip/AlignmentRules.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * 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.android.tools.build.apkzlib.zip; + +import com.google.common.base.Preconditions; +import javax.annotation.Nonnull; + +/** + * Factory for instances of {@link AlignmentRule}. + */ +public final class AlignmentRules { + + private AlignmentRules() {} + + /** + * A rule that defines a constant alignment for all files. + * + * @param alignment the alignment + * @return the rule + */ + public static AlignmentRule constant(int alignment) { + Preconditions.checkArgument(alignment > 0, "alignment <= 0"); + + return (String path) -> alignment; + } + + /** + * A rule that defines constant alignment for all files with a certain suffix, placing no + * restrictions on other files. + * + * @param suffix the suffix + * @param alignment the alignment for paths that match the provided suffix + * @return the rule + */ + public static AlignmentRule constantForSuffix(@Nonnull String suffix, int alignment) { + Preconditions.checkArgument(!suffix.isEmpty(), "suffix.isEmpty()"); + Preconditions.checkArgument(alignment > 0, "alignment <= 0"); + + return (String path) -> path.endsWith(suffix) ? alignment : AlignmentRule.NO_ALIGNMENT; + } + + /** + * A rule that applies other rules in order. + * + * @param rules all rules to be tried; the first rule that does not return + * {@link AlignmentRule#NO_ALIGNMENT} will define the alignment for a path; if there are no + * rules that return a value different from {@link AlignmentRule#NO_ALIGNMENT}, then + * {@link AlignmentRule#NO_ALIGNMENT} is returned + * @return the composition rule + */ + public static AlignmentRule compose(@Nonnull AlignmentRule... rules) { + return (String path) -> { + for (AlignmentRule r : rules) { + int align = r.alignment(path); + if (align != AlignmentRule.NO_ALIGNMENT) { + return align; + } + } + + return AlignmentRule.NO_ALIGNMENT; + }; + } +} diff --git a/src/main/java/com/android/tools/build/apkzlib/zip/CentralDirectory.java b/src/main/java/com/android/tools/build/apkzlib/zip/CentralDirectory.java new file mode 100644 index 0000000..909f2d0 --- /dev/null +++ b/src/main/java/com/android/tools/build/apkzlib/zip/CentralDirectory.java @@ -0,0 +1,489 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * 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.android.tools.build.apkzlib.zip; + +import com.android.tools.build.apkzlib.utils.CachedSupplier; +import com.android.tools.build.apkzlib.zip.utils.MsDosDateTimeUtils; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.google.common.primitives.Ints; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.ByteBuffer; +import java.util.List; +import java.util.Map; +import java.util.Set; +import javax.annotation.Nonnull; + +/** + * Representation of the central directory of a zip archive. + */ +class CentralDirectory { + + /** + * Field in the central directory with the central directory signature. + */ + private static final ZipField.F4 F_SIGNATURE = new ZipField.F4(0, 0x02014b50, "Signature"); + + /** + * Field in the central directory with the "made by" code. + */ + private static final ZipField.F2 F_MADE_BY = new ZipField.F2(F_SIGNATURE.endOffset(), + "Made by", new ZipFieldInvariantNonNegative()); + + /** + * Field in the central directory with the minimum version required to extract the entry. + */ + @VisibleForTesting + static final ZipField.F2 F_VERSION_EXTRACT = new ZipField.F2(F_MADE_BY.endOffset(), + "Version to extract", new ZipFieldInvariantNonNegative()); + + /** + * Field in the central directory with the GP bit flag. + */ + private static final ZipField.F2 F_GP_BIT = new ZipField.F2(F_VERSION_EXTRACT.endOffset(), + "GP bit"); + + /** + * Field in the central directory with the code of the compression method. See + * {@link CompressionMethod#fromCode(long)}. + */ + private static final ZipField.F2 F_METHOD = new ZipField.F2(F_GP_BIT.endOffset(), "Method"); + + /** + * Field in the central directory with the last modification time in MS-DOS format (see + * {@link MsDosDateTimeUtils#packTime(long)}). + */ + private static final ZipField.F2 F_LAST_MOD_TIME = new ZipField.F2(F_METHOD.endOffset(), + "Last modification time"); + + /** + * Field in the central directory with the last modification date in MS-DOS format. See + * {@link MsDosDateTimeUtils#packDate(long)}. + */ + private static final ZipField.F2 F_LAST_MOD_DATE = new ZipField.F2(F_LAST_MOD_TIME.endOffset(), + "Last modification date"); + + /** + * Field in the central directory with the CRC32 checksum of the entry. This will be zero for + * directories and files with no content. + */ + private static final ZipField.F4 F_CRC32 = new ZipField.F4(F_LAST_MOD_DATE.endOffset(), + "CRC32"); + + /** + * Field in the central directory with the entry's compressed size, i.e., the file on + * the archive. This will be the same as the uncompressed size if the method is + * {@link CompressionMethod#STORE}. + */ + private static final ZipField.F4 F_COMPRESSED_SIZE = new ZipField.F4(F_CRC32.endOffset(), + "Compressed size", new ZipFieldInvariantNonNegative()); + + /** + * Field in the central directory with the entry's uncompressed size, i.e., the size + * the file will have when extracted from the zip. This will be zero for directories and empty + * files and will be the same as the compressed size if the method is + * {@link CompressionMethod#STORE}. + */ + private static final ZipField.F4 F_UNCOMPRESSED_SIZE = new ZipField.F4( + F_COMPRESSED_SIZE.endOffset(), "Uncompressed size", new ZipFieldInvariantNonNegative()); + + /** + * Field in the central directory with the length of the file name. The file name is stored + * after the offset field ({@link #F_OFFSET}). The number of characters in the file name are + * stored in this field. + */ + private static final ZipField.F2 F_FILE_NAME_LENGTH = new ZipField.F2( + F_UNCOMPRESSED_SIZE.endOffset(), "File name length", + new ZipFieldInvariantNonNegative()); + + /** + * Field in the central directory with the length of the extra field. The extra field is + * stored after the file name ({@link #F_FILE_NAME_LENGTH}). The contents of this field are + * partially defined in the zip specification but we do not parse it. + */ + private static final ZipField.F2 F_EXTRA_FIELD_LENGTH = new ZipField.F2( + F_FILE_NAME_LENGTH.endOffset(), "Extra field length", + new ZipFieldInvariantNonNegative()); + + /** + * Field in the central directory with the length of the comment. The comment is stored after + * the extra field ({@link #F_EXTRA_FIELD_LENGTH}). We do not parse the comment. + */ + private static final ZipField.F2 F_COMMENT_LENGTH = new ZipField.F2( + F_EXTRA_FIELD_LENGTH.endOffset(), "Comment length", new ZipFieldInvariantNonNegative()); + + /** + * Number of the disk where the central directory starts. Because we do not support multi-file + * archives, this field has to have value {@code 0}. + */ + private static final ZipField.F2 F_DISK_NUMBER_START = new ZipField.F2( + F_COMMENT_LENGTH.endOffset(), 0, "Disk start"); + + /** + * Internal attributes. This field can only contain one bit set, the {@link #ASCII_BIT}. + */ + private static final ZipField.F2 F_INTERNAL_ATTRIBUTES = new ZipField.F2( + F_DISK_NUMBER_START.endOffset(), "Int attributes"); + + /** + * External attributes. This field is ignored. + */ + private static final ZipField.F4 F_EXTERNAL_ATTRIBUTES = new ZipField.F4( + F_INTERNAL_ATTRIBUTES.endOffset(), "Ext attributes"); + + /** + * Offset into the archive where the entry starts. This is the offset to the local header + * (see {@link StoredEntry} for information on the local header), not to the file data itself. + * The file data, if there is any, will be stored after the local header. + */ + private static final ZipField.F4 F_OFFSET = new ZipField.F4(F_EXTERNAL_ATTRIBUTES.endOffset(), + "Offset", new ZipFieldInvariantNonNegative()); + + /** + * Maximum supported version to extract. + */ + private static final int MAX_VERSION_TO_EXTRACT = 20; + + /** + * Bit that can be set on the internal attributes stating that the file is an ASCII file. We + * don't do anything with this information, but we check that nothing unexpected appears in the + * internal attributes. + */ + private static final int ASCII_BIT = 1; + + /** + * Contains all entries in the directory mapped from their names. + */ + @Nonnull + private final Map entries; + + /** + * The file where this directory belongs to. + */ + @Nonnull + private final ZFile file; + + /** + * Supplier that provides a byte representation of the central directory. + */ + @Nonnull + private final CachedSupplier bytesSupplier; + + /** + * Verify log for the central directory. + */ + @Nonnull + private final VerifyLog verifyLog; + + /** + * Creates a new, empty, central directory, for a given zip file. + * + * @param file the file + */ + CentralDirectory(@Nonnull ZFile file) { + entries = Maps.newHashMap(); + this.file = file; + bytesSupplier = new CachedSupplier<>(this::computeByteRepresentation); + verifyLog = file.getVerifyLog(); + } + + /** + * Reads the central directory data from a zip file, parses it, and creates the in-memory + * structure representing the directory. + * + * @param bytes the data of the central directory; the directory is read from the buffer's + * current position; when this method terminates, the buffer's position is the first byte + * after the directory + * @param count the number of entries expected in the central directory (usually read from the + * {@link Eocd}). + * @param file the zip file this central directory belongs to + * @return the central directory + * @throws IOException failed to read data from the zip, or the central directory is corrupted + * or has unsupported features + */ + static CentralDirectory makeFromData(@Nonnull ByteBuffer bytes, int count, @Nonnull ZFile file) + throws IOException { + Preconditions.checkNotNull(bytes, "bytes == null"); + Preconditions.checkArgument(count >= 0, "count < 0"); + + CentralDirectory directory = new CentralDirectory(file); + + for (int i = 0; i < count; i++) { + try { + directory.readEntry(bytes); + } catch (IOException e) { + throw new IOException( + "Failed to read directory entry index " + + i + + " (total " + + "directory bytes read: " + + bytes.position() + + ").", + e); + } + } + + return directory; + } + + /** + * Creates a new central directory from the entries. This is used to build a new central + * directory from entries in the zip file. + * + * @param entries the entries in the zip file + * @param file the zip file itself + * @return the created central directory + */ + static CentralDirectory makeFromEntries( + @Nonnull Set entries, + @Nonnull ZFile file) { + CentralDirectory directory = new CentralDirectory(file); + for (StoredEntry entry : entries) { + CentralDirectoryHeader cdr = entry.getCentralDirectoryHeader(); + Preconditions.checkArgument( + !directory.entries.containsKey(cdr.getName()), + "Duplicate filename"); + directory.entries.put(cdr.getName(), entry); + } + + return directory; + } + + /** + * Reads the next entry from the central directory and adds it to {@link #entries}. + * + * @param bytes the central directory's data, positioned starting at the beginning of the next + * entry to read; when finished, the buffer's position will be at the first byte after the + * entry + * @throws IOException failed to read the directory entry, either because of an I/O error, + * because it is corrupt or contains unsupported features + */ + private void readEntry(@Nonnull ByteBuffer bytes) throws IOException { + F_SIGNATURE.verify(bytes); + long madeBy = F_MADE_BY.read(bytes); + + long versionNeededToExtract = F_VERSION_EXTRACT.read(bytes); + verifyLog.verify( + versionNeededToExtract <= MAX_VERSION_TO_EXTRACT, + "Ignored unknown version needed to extract in zip directory entry: %s.", + versionNeededToExtract); + + long gpBit = F_GP_BIT.read(bytes); + GPFlags flags = GPFlags.from(gpBit); + + long methodCode = F_METHOD.read(bytes); + CompressionMethod method = CompressionMethod.fromCode(methodCode); + verifyLog.verify(method != null, "Unknown method in zip directory entry: %s.", methodCode); + + long lastModTime; + long lastModDate; + if (file.areTimestampsIgnored()) { + lastModTime = 0; + lastModDate = 0; + F_LAST_MOD_TIME.skip(bytes); + F_LAST_MOD_DATE.skip(bytes); + } else { + lastModTime = F_LAST_MOD_TIME.read(bytes); + lastModDate = F_LAST_MOD_DATE.read(bytes); + } + + long crc32 = F_CRC32.read(bytes); + long compressedSize = F_COMPRESSED_SIZE.read(bytes); + long uncompressedSize = F_UNCOMPRESSED_SIZE.read(bytes); + int fileNameLength = Ints.checkedCast(F_FILE_NAME_LENGTH.read(bytes)); + int extraFieldLength = Ints.checkedCast(F_EXTRA_FIELD_LENGTH.read(bytes)); + int fileCommentLength = Ints.checkedCast(F_COMMENT_LENGTH.read(bytes)); + + F_DISK_NUMBER_START.verify(bytes, verifyLog); + long internalAttributes = F_INTERNAL_ATTRIBUTES.read(bytes); + verifyLog.verify( + (internalAttributes & ~ASCII_BIT) == 0, + "Ignored invalid internal attributes: %s.", + internalAttributes); + + long externalAttributes = F_EXTERNAL_ATTRIBUTES.read(bytes); + long entryOffset = F_OFFSET.read(bytes); + + long remainingSize = fileNameLength + extraFieldLength + fileCommentLength; + + if (bytes.remaining() < fileNameLength + extraFieldLength + fileCommentLength) { + throw new IOException( + "Directory entry should have " + + remainingSize + + " bytes remaining (name = " + + fileNameLength + + ", extra = " + + extraFieldLength + + ", comment = " + + fileCommentLength + + "), but it has " + + bytes.remaining() + + "."); + } + + byte[] encodedFileName = new byte[fileNameLength]; + bytes.get(encodedFileName); + String fileName = EncodeUtils.decode(encodedFileName, flags); + + byte[] extraField = new byte[extraFieldLength]; + bytes.get(extraField); + + byte[] fileCommentField = new byte[fileCommentLength]; + bytes.get(fileCommentField); + + /* + * Tricky: to create a CentralDirectoryHeader we need the future that will hold the result + * of the compress information. But, to actually create the result of the compress + * information we need the CentralDirectoryHeader + */ + ListenableFuture compressInfo = + Futures.immediateFuture( + new CentralDirectoryHeaderCompressInfo( + method, + compressedSize, + versionNeededToExtract)); + CentralDirectoryHeader centralDirectoryHeader = + new CentralDirectoryHeader( + fileName, encodedFileName, uncompressedSize, compressInfo, flags, file); + centralDirectoryHeader.setMadeBy(madeBy); + centralDirectoryHeader.setLastModTime(lastModTime); + centralDirectoryHeader.setLastModDate(lastModDate); + centralDirectoryHeader.setCrc32(crc32); + centralDirectoryHeader.setInternalAttributes(internalAttributes); + centralDirectoryHeader.setExternalAttributes(externalAttributes); + centralDirectoryHeader.setOffset(entryOffset); + centralDirectoryHeader.setExtraFieldNoNotify(new ExtraField(extraField)); + centralDirectoryHeader.setComment(fileCommentField); + + StoredEntry entry; + + try { + entry = new StoredEntry(centralDirectoryHeader, file, null); + } catch (IOException e) { + throw new IOException("Failed to read stored entry '" + fileName + "'.", e); + } + + if (entries.containsKey(fileName)) { + verifyLog.log("File file contains duplicate file '" + fileName + "'."); + } + + entries.put(fileName, entry); + } + + /** + * Obtains all the entries in the central directory. + * + * @return all entries on a non-modifiable map + */ + @Nonnull + Map getEntries() { + return ImmutableMap.copyOf(entries); + } + + /** + * Obtains the byte representation of the central directory. + * + * @return a byte array containing the whole central directory + * @throws IOException failed to write the byte array + */ + byte[] toBytes() throws IOException { + return bytesSupplier.get(); + } + + /** + * Computes the byte representation of the central directory. + * + * @return a byte array containing the whole central directory + * @throws UncheckedIOException failed to write the byte array + */ + private byte[] computeByteRepresentation() { + + List sorted = Lists.newArrayList(entries.values()); + sorted.sort(StoredEntry.COMPARE_BY_NAME); + + CentralDirectoryHeader[] cdhs = new CentralDirectoryHeader[entries.size()]; + CentralDirectoryHeaderCompressInfo[] compressInfos = + new CentralDirectoryHeaderCompressInfo[entries.size()]; + byte[][] encodedFileNames = new byte[entries.size()][]; + byte[][] extraFields = new byte[entries.size()][]; + byte[][] comments = new byte[entries.size()][]; + + try { + /* + * First collect all the data and compute the total size of the central directory. + */ + int idx = 0; + int total = 0; + for (StoredEntry entry : sorted) { + cdhs[idx] = entry.getCentralDirectoryHeader(); + compressInfos[idx] = cdhs[idx].getCompressionInfoWithWait(); + encodedFileNames[idx] = cdhs[idx].getEncodedFileName(); + extraFields[idx] = new byte[cdhs[idx].getExtraField().size()]; + cdhs[idx].getExtraField().write(ByteBuffer.wrap(extraFields[idx])); + comments[idx] = cdhs[idx].getComment(); + + total += F_OFFSET.endOffset() + encodedFileNames[idx].length + + extraFields[idx].length + comments[idx].length; + idx++; + } + + ByteBuffer out = ByteBuffer.allocate(total); + + for (idx = 0; idx < entries.size(); idx++) { + F_SIGNATURE.write(out); + F_MADE_BY.write(out, cdhs[idx].getMadeBy()); + F_VERSION_EXTRACT.write(out, compressInfos[idx].getVersionExtract()); + F_GP_BIT.write(out, cdhs[idx].getGpBit().getValue()); + F_METHOD.write(out, compressInfos[idx].getMethod().methodCode); + + if (file.areTimestampsIgnored()) { + F_LAST_MOD_TIME.write(out, 0); + F_LAST_MOD_DATE.write(out, 0); + } else { + F_LAST_MOD_TIME.write(out, cdhs[idx].getLastModTime()); + F_LAST_MOD_DATE.write(out, cdhs[idx].getLastModDate()); + } + + F_CRC32.write(out, cdhs[idx].getCrc32()); + F_COMPRESSED_SIZE.write(out, compressInfos[idx].getCompressedSize()); + F_UNCOMPRESSED_SIZE.write(out, cdhs[idx].getUncompressedSize()); + + F_FILE_NAME_LENGTH.write(out, cdhs[idx].getEncodedFileName().length); + F_EXTRA_FIELD_LENGTH.write(out, cdhs[idx].getExtraField().size()); + F_COMMENT_LENGTH.write(out, cdhs[idx].getComment().length); + F_DISK_NUMBER_START.write(out); + F_INTERNAL_ATTRIBUTES.write(out, cdhs[idx].getInternalAttributes()); + F_EXTERNAL_ATTRIBUTES.write(out, cdhs[idx].getExternalAttributes()); + F_OFFSET.write(out, cdhs[idx].getOffset()); + + out.put(encodedFileNames[idx]); + out.put(extraFields[idx]); + out.put(comments[idx]); + } + + return out.array(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } +} diff --git a/src/main/java/com/android/tools/build/apkzlib/zip/CentralDirectoryHeader.java b/src/main/java/com/android/tools/build/apkzlib/zip/CentralDirectoryHeader.java new file mode 100644 index 0000000..353ed3d --- /dev/null +++ b/src/main/java/com/android/tools/build/apkzlib/zip/CentralDirectoryHeader.java @@ -0,0 +1,434 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * 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.android.tools.build.apkzlib.zip; + +import com.android.tools.build.apkzlib.zip.utils.MsDosDateTimeUtils; +import com.google.common.base.Verify; +import java.io.IOException; +import java.util.Arrays; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import javax.annotation.Nonnull; + +/** + * The Central Directory Header contains information about files stored in the zip. Instances of + * this class contain information for files that already are in the zip and, for which the data was + * read from the Central Directory. But some instances of this class are used for new files. + * Because instances of this class can refer to files not yet on the zip, some of the fields may + * not be filled in, or may be filled in with default values. + *

+ * Because compression decision is done lazily, some data is stored with futures. + */ +public class CentralDirectoryHeader implements Cloneable { + + /** + * Default "version made by" field: upper byte needs to be 0 to set to MS-DOS compatibility. + * Lower byte can be anything, really. We use 18 because aapt uses 17 :) + */ + private static final int DEFAULT_VERSION_MADE_BY = 0x0018; + + /** + * Name of the file. + */ + @Nonnull + private String name; + + /** + * CRC32 of the data. 0 if not yet computed. + */ + private long crc32; + + /** + * Size of the file uncompressed. 0 if the file has no data. + */ + private long uncompressedSize; + + /** + * Code of the program that made the zip. We actually don't care about this. + */ + private long madeBy; + + /** + * General-purpose bit flag. + */ + @Nonnull + private GPFlags gpBit; + + /** + * Last modification time in MS-DOS format (see {@link MsDosDateTimeUtils#packTime(long)}). + */ + private long lastModTime; + + /** + * Last modification time in MS-DOS format (see {@link MsDosDateTimeUtils#packDate(long)}). + */ + private long lastModDate; + + /** + * Extra data field contents. This field follows a specific structure according to the + * specification. + */ + @Nonnull + private ExtraField extraField; + + /** + * File comment. + */ + @Nonnull + private byte[] comment; + + /** + * File internal attributes. + */ + private long internalAttributes; + + /** + * File external attributes. + */ + private long externalAttributes; + + /** + * Offset in the file where the data is located. This will be -1 if the header corresponds to + * a new file that is not yet written in the zip and, therefore, has no written data. + */ + private long offset; + + /** + * Encoded file name. + */ + private byte[] encodedFileName; + + /** + * Compress information that may not have been computed yet due to lazy compression. + */ + @Nonnull + private Future compressInfo; + + /** + * The file this header belongs to. + */ + @Nonnull + private final ZFile file; + + /** + * Creates data for a file. + * + * @param name the file name + * @param encodedFileName the encoded file name, this array will be owned by the header + * @param uncompressedSize the uncompressed file size + * @param compressInfo computation that defines the compression information + * @param flags flags used in the entry + * @param zFile the file this header belongs to + */ + CentralDirectoryHeader( + @Nonnull String name, + @Nonnull byte[] encodedFileName, + long uncompressedSize, + @Nonnull Future compressInfo, + @Nonnull GPFlags flags, + @Nonnull ZFile zFile) { + this.name = name; + this.uncompressedSize = uncompressedSize; + crc32 = 0; + + /* + * Set sensible defaults for the rest. + */ + madeBy = DEFAULT_VERSION_MADE_BY; + + gpBit = flags; + lastModTime = MsDosDateTimeUtils.packCurrentTime(); + lastModDate = MsDosDateTimeUtils.packCurrentDate(); + extraField = new ExtraField(); + comment = new byte[0]; + internalAttributes = 0; + externalAttributes = 0; + offset = -1; + this.encodedFileName = encodedFileName; + this.compressInfo = compressInfo; + file = zFile; + } + + /** + * Obtains the name of the file. + * + * @return the name + */ + @Nonnull + public String getName() { + return name; + } + + /** + * Obtains the size of the uncompressed file. + * + * @return the size of the file + */ + public long getUncompressedSize() { + return uncompressedSize; + } + + /** + * Obtains the CRC32 of the data. + * + * @return the CRC32, 0 if not yet computed + */ + public long getCrc32() { + return crc32; + } + + /** + * Sets the CRC32 of the data. + * + * @param crc32 the CRC 32 + */ + void setCrc32(long crc32) { + this.crc32 = crc32; + } + + /** + * Obtains the code of the program that made the zip. + * + * @return the code + */ + public long getMadeBy() { + return madeBy; + } + + /** + * Sets the code of the progtram that made the zip. + * + * @param madeBy the code + */ + void setMadeBy(long madeBy) { + this.madeBy = madeBy; + } + + /** + * Obtains the general-purpose bit flag. + * + * @return the bit flag + */ + @Nonnull + public GPFlags getGpBit() { + return gpBit; + } + + /** + * Obtains the last modification time of the entry. + * + * @return the last modification time in MS-DOS format (see + * {@link MsDosDateTimeUtils#packTime(long)}) + */ + public long getLastModTime() { + return lastModTime; + } + + /** + * Sets the last modification time of the entry. + * + * @param lastModTime the last modification time in MS-DOS format (see + * {@link MsDosDateTimeUtils#packTime(long)}) + */ + void setLastModTime(long lastModTime) { + this.lastModTime = lastModTime; + } + + /** + * Obtains the last modification date of the entry. + * + * @return the last modification date in MS-DOS format (see + * {@link MsDosDateTimeUtils#packDate(long)}) + */ + public long getLastModDate() { + return lastModDate; + } + + /** + * Sets the last modification date of the entry. + * + * @param lastModDate the last modification date in MS-DOS format (see + * {@link MsDosDateTimeUtils#packDate(long)}) + */ + void setLastModDate(long lastModDate) { + this.lastModDate = lastModDate; + } + + /** + * Obtains the data in the extra field. + * + * @return the data (returns an empty array if there is none) + */ + @Nonnull + public ExtraField getExtraField() { + return extraField; + } + + /** + * Sets the data in the extra field. + * + * @param extraField the data to set + */ + public void setExtraField(@Nonnull ExtraField extraField) { + setExtraFieldNoNotify(extraField); + file.centralDirectoryChanged(); + } + + /** + * Sets the data in the extra field, but does not notify {@link ZFile}. This method is invoked + * when the {@link ZFile} knows the extra field is being set. + * + * @param extraField the data to set + */ + void setExtraFieldNoNotify(@Nonnull ExtraField extraField) { + this.extraField = extraField; + } + + /** + * Obtains the entry's comment. + * + * @return the comment (returns an empty array if there is no comment) + */ + @Nonnull + public byte[] getComment() { + return comment; + } + + /** + * Sets the entry's comment. + * + * @param comment the comment + */ + void setComment(@Nonnull byte[] comment) { + this.comment = comment; + } + + /** + * Obtains the entry's internal attributes. + * + * @return the entry's internal attributes + */ + public long getInternalAttributes() { + return internalAttributes; + } + + /** + * Sets the entry's internal attributes. + * + * @param internalAttributes the entry's internal attributes + */ + void setInternalAttributes(long internalAttributes) { + this.internalAttributes = internalAttributes; + } + + /** + * Obtains the entry's external attributes. + * + * @return the entry's external attributes + */ + public long getExternalAttributes() { + return externalAttributes; + } + + /** + * Sets the entry's external attributes. + * + * @param externalAttributes the entry's external attributes + */ + void setExternalAttributes(long externalAttributes) { + this.externalAttributes = externalAttributes; + } + + /** + * Obtains the offset in the zip file where this entry's data is. + * + * @return the offset or {@code -1} if the file has no data in the zip and, therefore, data + * is stored in memory + */ + public long getOffset() { + return offset; + } + + /** + * Sets the offset in the zip file where this entry's data is. + * + * @param offset the offset or {@code -1} if the file is new and has no data in the zip yet + */ + void setOffset(long offset) { + this.offset = offset; + } + + /** + * Obtains the encoded file name. + * + * @return the encoded file name + */ + public byte[] getEncodedFileName() { + return encodedFileName; + } + + /** + * Resets the deferred CRC flag in the GP flags. + */ + void resetDeferredCrc() { + /* + * We actually create a new set of flags. Since the only information we care about is the + * UTF-8 encoding, we'll just create a brand new object. + */ + gpBit = GPFlags.make(gpBit.isUtf8FileName()); + } + + @Override + protected CentralDirectoryHeader clone() throws CloneNotSupportedException { + CentralDirectoryHeader cdr = (CentralDirectoryHeader) super.clone(); + cdr.extraField = extraField; + cdr.comment = Arrays.copyOf(comment, comment.length); + cdr.encodedFileName = Arrays.copyOf(encodedFileName, encodedFileName.length); + return cdr; + } + + /** + * Obtains the future with the compression information. + * + * @return the information + */ + @Nonnull + public Future getCompressionInfo() { + return compressInfo; + } + + /** + * Equivalent to {@code getCompressionInfo().get()} but masking the possible exceptions and + * guaranteeing non-{@code null} return. + * + * @return the result of the future + * @throws IOException failed to get the information + */ + @Nonnull + public CentralDirectoryHeaderCompressInfo getCompressionInfoWithWait() + throws IOException { + try { + CentralDirectoryHeaderCompressInfo info = getCompressionInfo().get(); + Verify.verifyNotNull(info, "info == null"); + return info; + } catch (InterruptedException e) { + throw new IOException("Interrupted while waiting for compression information.", e); + } catch (ExecutionException e) { + throw new IOException("Execution of compression failed.", e); + } + } +} diff --git a/src/main/java/com/android/tools/build/apkzlib/zip/CentralDirectoryHeaderCompressInfo.java b/src/main/java/com/android/tools/build/apkzlib/zip/CentralDirectoryHeaderCompressInfo.java new file mode 100644 index 0000000..f6fd749 --- /dev/null +++ b/src/main/java/com/android/tools/build/apkzlib/zip/CentralDirectoryHeaderCompressInfo.java @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * 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.android.tools.build.apkzlib.zip; + +import javax.annotation.Nonnull; + +/** + * Information stored in the {@link CentralDirectoryHeader} that is related to compression and may + * need to be computed lazily. + */ +public class CentralDirectoryHeaderCompressInfo { + + /** + * Version of zip file that only supports stored files. + */ + public static final long VERSION_WITH_STORE_FILES_ONLY = 10L; + + /** + * Version of zip file that only supports directories and deflated files. + */ + public static final long VERSION_WITH_DIRECTORIES_AND_DEFLATE = 20L; + + /** + * The compression method. + */ + @Nonnull + private final CompressionMethod mMethod; + + /** + * Size of the file compressed. 0 if the file has no data. + */ + private final long compressedSize; + + /** + * Version needed to extract the zip. + */ + private final long versionExtract; + + /** + * Creates new compression information for the central directory header. + * + * @param method the compression method + * @param compressedSize the compressed size + * @param versionToExtract minimum version to extract (typically + * {@link #VERSION_WITH_STORE_FILES_ONLY} or {@link #VERSION_WITH_DIRECTORIES_AND_DEFLATE}) + */ + public CentralDirectoryHeaderCompressInfo( + @Nonnull CompressionMethod method, + long compressedSize, + long versionToExtract) { + mMethod = method; + this.compressedSize = compressedSize; + versionExtract = versionToExtract; + } + + /** + * Creates new compression information for the central directory header. + * + * @param header the header this information relates to + * @param method the compression method + * @param compressedSize the compressed size + */ + public CentralDirectoryHeaderCompressInfo(@Nonnull CentralDirectoryHeader header, + @Nonnull CompressionMethod method, long compressedSize) { + mMethod = method; + this.compressedSize = compressedSize; + + if (header.getName().endsWith("/") || method == CompressionMethod.DEFLATE) { + /* + * Directories and compressed files only in version 2.0. + */ + versionExtract = VERSION_WITH_DIRECTORIES_AND_DEFLATE; + } else { + versionExtract = VERSION_WITH_STORE_FILES_ONLY; + } + } + + /** + * Obtains the compression data size. + * + * @return the compressed data size + */ + public long getCompressedSize() { + return compressedSize; + } + + /** + * Obtains the compression method. + * + * @return the compression method + */ + @Nonnull + public CompressionMethod getMethod() { + return mMethod; + } + + /** + * Obtains the minimum version for extract. + * + * @return the minimum version + */ + public long getVersionExtract() { + return versionExtract; + } +} diff --git a/src/main/java/com/android/tools/build/apkzlib/zip/CompressionMethod.java b/src/main/java/com/android/tools/build/apkzlib/zip/CompressionMethod.java new file mode 100644 index 0000000..82f374b --- /dev/null +++ b/src/main/java/com/android/tools/build/apkzlib/zip/CompressionMethod.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * 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.android.tools.build.apkzlib.zip; + +import javax.annotation.Nullable; + +/** + * Enumeration with all known compression methods. + */ +public enum CompressionMethod { + /** + * STORE method: data is stored without any compression. + */ + STORE(0), + + /** + * DEFLATE method: data is stored compressed using the DEFLATE algorithm. + */ + DEFLATE(8); + + /** + * Code, within the zip file, that identifies this compression method. + */ + int methodCode; + + /** + * Creates a new compression method. + * + * @param methodCode the code used in the zip file that identifies the compression method + */ + CompressionMethod(int methodCode) { + this.methodCode = methodCode; + } + + /** + * Obtains the compression method that corresponds to the provided code. + * + * @param code the code + * @return the method or {@code null} if no method has the provided code + */ + @Nullable + static CompressionMethod fromCode(long code) { + for (CompressionMethod method : values()) { + if (method.methodCode == code) { + return method; + } + } + + return null; + } +} diff --git a/src/main/java/com/android/tools/build/apkzlib/zip/CompressionResult.java b/src/main/java/com/android/tools/build/apkzlib/zip/CompressionResult.java new file mode 100644 index 0000000..1688248 --- /dev/null +++ b/src/main/java/com/android/tools/build/apkzlib/zip/CompressionResult.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * 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.android.tools.build.apkzlib.zip; + +import com.android.tools.build.apkzlib.zip.utils.CloseableByteSource; +import javax.annotation.Nonnull; + +/** + * Result of compressing data. + */ +public class CompressionResult { + + /** + * The compression method used. + */ + @Nonnull + private final CompressionMethod compressionMethod; + + /** + * The resulting data. + */ + @Nonnull + private final CloseableByteSource source; + + /** + * Size of the compressed source. Kept because {@code source.size()} can throw + * {@code IOException}. + */ + private final long mSize; + + /** + * Creates a new compression result. + * + * @param source the data source + * @param method the compression method + */ + public CompressionResult(@Nonnull CloseableByteSource source, @Nonnull CompressionMethod method, + long size) { + compressionMethod = method; + this.source = source; + mSize = size; + } + + /** + * Obtains the compression method. + * + * @return the compression method + */ + @Nonnull + public CompressionMethod getCompressionMethod() { + return compressionMethod; + } + + /** + * Obtains the compressed data. + * + * @return the data, the resulting array should not be modified + */ + @Nonnull + public CloseableByteSource getSource() { + return source; + } + + /** + * Obtains the size of the compression result. + * + * @return the size + */ + public long getSize() { + return mSize; + } +} diff --git a/src/main/java/com/android/tools/build/apkzlib/zip/Compressor.java b/src/main/java/com/android/tools/build/apkzlib/zip/Compressor.java new file mode 100644 index 0000000..9c70129 --- /dev/null +++ b/src/main/java/com/android/tools/build/apkzlib/zip/Compressor.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * 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.android.tools.build.apkzlib.zip; + +import com.android.tools.build.apkzlib.zip.utils.CloseableByteSource; +import com.google.common.util.concurrent.ListenableFuture; +import javax.annotation.Nonnull; + +/** + * A compressor is capable of, well, compressing data. Data is read from an {@code ByteSource}. + * Compressors are asynchronous: compressing results in a {@code ListenableFuture} that will contain + * the compression result. + */ +public interface Compressor { + + /** + * Compresses an entry source. + * + * @param source the source to compress + * @return a future that will eventually contain the compression result + */ + @Nonnull + ListenableFuture compress(@Nonnull CloseableByteSource source); +} diff --git a/src/main/java/com/android/tools/build/apkzlib/zip/DataDescriptorType.java b/src/main/java/com/android/tools/build/apkzlib/zip/DataDescriptorType.java new file mode 100644 index 0000000..d7d9086 --- /dev/null +++ b/src/main/java/com/android/tools/build/apkzlib/zip/DataDescriptorType.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * 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.android.tools.build.apkzlib.zip; + +/** + * Type of data descriptor that an entry has. Data descriptors are used if the CRC and sizing data + * is not known when the data is being written and cannot be placed in the file's local header. + * In those cases, after the file data itself, a data descriptor is placed after the entry's + * contents. + *

+ * While the zip specification says the data descriptor should be used but it is optional. We + * record also whether the data descriptor contained the 4-byte signature at the start of the + * block or not. + */ +public enum DataDescriptorType { + /** + * The entry has no data descriptor. + */ + NO_DATA_DESCRIPTOR(0), + + /** + * The entry has a data descriptor that does not contain a signature. + */ + DATA_DESCRIPTOR_WITHOUT_SIGNATURE(12), + + /** + * The entry has a data descriptor that contains a signature. + */ + DATA_DESCRIPTOR_WITH_SIGNATURE(16); + + /** + * The number of bytes the data descriptor spans. + */ + public int size; + + /** + * Creates a new data descriptor. + * + * @param size the number of bytes the data descriptor spans + */ + DataDescriptorType(int size) { + this.size = size; + } +} diff --git a/src/main/java/com/android/tools/build/apkzlib/zip/EncodeUtils.java b/src/main/java/com/android/tools/build/apkzlib/zip/EncodeUtils.java new file mode 100644 index 0000000..34111e6 --- /dev/null +++ b/src/main/java/com/android/tools/build/apkzlib/zip/EncodeUtils.java @@ -0,0 +1,139 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * 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.android.tools.build.apkzlib.zip; + +import com.google.common.base.Charsets; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.charset.CharacterCodingException; +import java.nio.charset.Charset; +import java.nio.charset.CodingErrorAction; +import javax.annotation.Nonnull; + +/** + * Utilities to encode and decode file names in zips. + */ +public class EncodeUtils { + + /** + * Utility class: no constructor. + */ + private EncodeUtils() { + /* + * Nothing to do. + */ + } + + /** + * Decodes a file name. + * + * @param bytes the raw data buffer to read from + * @param length the number of bytes in the raw data buffer containing the string to decode + * @param flags the zip entry flags + * @return the decode file name + */ + @Nonnull + public static String decode(@Nonnull ByteBuffer bytes, int length, @Nonnull GPFlags flags) + throws IOException { + if (bytes.remaining() < length) { + throw new IOException("Only " + bytes.remaining() + " bytes exist in the buffer, but " + + "length is " + length + "."); + } + + byte[] stringBytes = new byte[length]; + bytes.get(stringBytes); + return decode(stringBytes, flags); + } + + /** + * Decodes a file name. + * + * @param data the raw data + * @param flags the zip entry flags + * @return the decode file name + */ + @Nonnull + public static String decode(@Nonnull byte[] data, @Nonnull GPFlags flags) { + return decode(data, flagsCharset(flags)); + } + + /** + * Decodes a file name. + * + * @param data the raw data + * @param charset the charset to use + * @return the decode file name + */ + @Nonnull + private static String decode(@Nonnull byte[] data, @Nonnull Charset charset) { + try { + return charset.newDecoder() + .onMalformedInput(CodingErrorAction.REPORT) + .decode(ByteBuffer.wrap(data)) + .toString(); + } catch (CharacterCodingException e) { + // If we're trying to decode ASCII, try UTF-8. Otherwise, revert to the default + // behavior (usually replacing invalid characters). + if (charset.equals(Charsets.US_ASCII)) { + return decode(data, Charsets.UTF_8); + } else { + return charset.decode(ByteBuffer.wrap(data)).toString(); + } + } + } + + /** + * Encodes a file name. + * + * @param name the name to encode + * @param flags the zip entry flags + * @return the encoded file name + */ + @Nonnull + public static byte[] encode(@Nonnull String name, @Nonnull GPFlags flags) { + Charset charset = flagsCharset(flags); + ByteBuffer bytes = charset.encode(name); + byte[] result = new byte[bytes.remaining()]; + bytes.get(result); + return result; + } + + /** + * Obtains the charset to encode and decode zip entries, given a set of flags. + * + * @param flags the flags + * @return the charset to use + */ + @Nonnull + private static Charset flagsCharset(@Nonnull GPFlags flags) { + if (flags.isUtf8FileName()) { + return Charsets.UTF_8; + } else { + return Charsets.US_ASCII; + } + } + + /** + * Checks if some text may be encoded using ASCII. + * + * @param text the text to check + * @return can it be encoded using ASCII? + */ + public static boolean canAsciiEncode(String text) { + return Charsets.US_ASCII.newEncoder().canEncode(text); + } +} diff --git a/src/main/java/com/android/tools/build/apkzlib/zip/Eocd.java b/src/main/java/com/android/tools/build/apkzlib/zip/Eocd.java new file mode 100644 index 0000000..a9da14b --- /dev/null +++ b/src/main/java/com/android/tools/build/apkzlib/zip/Eocd.java @@ -0,0 +1,271 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * 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.android.tools.build.apkzlib.zip; + +import com.android.tools.build.apkzlib.utils.CachedSupplier; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; +import com.google.common.base.Verify; +import com.google.common.primitives.Ints; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.ByteBuffer; +import javax.annotation.Nonnull; + +/** + * End Of Central Directory record in a zip file. + */ +class Eocd { + /** + * Field in the record: the record signature, fixed at this value by the specification. + */ + private static final ZipField.F4 F_SIGNATURE = new ZipField.F4(0, 0x06054b50, "EOCD signature"); + + /** + * Field in the record: the number of the disk where the EOCD is located. It has to be zero + * because we do not support multi-file archives. + */ + private static final ZipField.F2 F_NUMBER_OF_DISK = new ZipField.F2(F_SIGNATURE.endOffset(), 0, + "Number of this disk"); + + /** + * Field in the record: the number of the disk where the Central Directory starts. Has to be + * zero because we do not support multi-file archives. + */ + private static final ZipField.F2 F_DISK_CD_START = new ZipField.F2(F_NUMBER_OF_DISK.endOffset(), + 0, "Disk where CD starts"); + + /** + * Field in the record: the number of entries in the Central Directory on this disk. Because + * we do not support multi-file archives, this is the same as {@link #F_RECORDS_TOTAL}. + */ + private static final ZipField.F2 F_RECORDS_DISK = new ZipField.F2(F_DISK_CD_START.endOffset(), + "Record on disk count", new ZipFieldInvariantNonNegative()); + + /** + * Field in the record: the total number of entries in the Central Directory. + */ + private static final ZipField.F2 F_RECORDS_TOTAL = new ZipField.F2(F_RECORDS_DISK.endOffset(), + "Total records", new ZipFieldInvariantNonNegative(), + new ZipFieldInvariantMaxValue(Integer.MAX_VALUE)); + + /** + * Field in the record: number of bytes of the Central Directory. + * This is not private because it is required in unit tests. + */ + @VisibleForTesting + static final ZipField.F4 F_CD_SIZE = new ZipField.F4(F_RECORDS_TOTAL.endOffset(), + "Directory size", new ZipFieldInvariantNonNegative()); + + /** + * Field in the record: offset, from the archive start, where the Central Directory starts. + * This is not private because it is required in unit tests. + */ + @VisibleForTesting + static final ZipField.F4 F_CD_OFFSET = new ZipField.F4(F_CD_SIZE.endOffset(), + "Directory offset", new ZipFieldInvariantNonNegative()); + + /** + * Field in the record: number of bytes of the file comment (located at the end of the EOCD + * record). + */ + private static final ZipField.F2 F_COMMENT_SIZE = new ZipField.F2(F_CD_OFFSET.endOffset(), + "File comment size", new ZipFieldInvariantNonNegative()); + + /** + * Number of entries in the central directory. + */ + private final int totalRecords; + + /** + * Offset from the beginning of the archive where the Central Directory is located. + */ + private final long directoryOffset; + + /** + * Number of bytes of the Central Directory. + */ + private final long directorySize; + + /** + * Contents of the EOCD comment. + */ + @Nonnull + private final byte[] comment; + + /** + * Supplier of the byte representation of the EOCD. + */ + @Nonnull + private final CachedSupplier byteSupplier; + + /** + * Creates a new EOCD, reading it from a byte source. This method will parse the byte source + * and obtain the EOCD. It will check that the byte source starts with the EOCD signature. + * + * @param bytes the byte buffer with the EOCD data; when this method finishes, the byte + * buffer's position will have moved to the end of the EOCD + * @throws IOException failed to read information or the EOCD data is corrupt or invalid + */ + Eocd(@Nonnull ByteBuffer bytes) throws IOException { + + /* + * Read the EOCD record. + */ + F_SIGNATURE.verify(bytes); + F_NUMBER_OF_DISK.verify(bytes); + F_DISK_CD_START.verify(bytes); + long totalRecords1 = F_RECORDS_DISK.read(bytes); + long totalRecords2 = F_RECORDS_TOTAL.read(bytes); + long directorySize = F_CD_SIZE.read(bytes); + long directoryOffset = F_CD_OFFSET.read(bytes); + int commentSize = Ints.checkedCast(F_COMMENT_SIZE.read(bytes)); + + /* + * Some sanity checks. + */ + if (totalRecords1 != totalRecords2) { + throw new IOException("Zip states records split in multiple disks, which is not " + + "supported."); + } + + Verify.verify(totalRecords1 <= Integer.MAX_VALUE); + + totalRecords = Ints.checkedCast(totalRecords1); + this.directorySize = directorySize; + this.directoryOffset = directoryOffset; + + if (bytes.remaining() < commentSize) { + throw new IOException("Corrupt EOCD record: not enough data for comment (comment " + + "size is " + commentSize + ")."); + } + + comment = new byte[commentSize]; + bytes.get(comment); + byteSupplier = new CachedSupplier<>(this::computeByteRepresentation); + } + + /** + * Creates a new EOCD. This is used when generating an EOCD for an Central Directory that has + * just been generated. The EOCD will be generated without any comment. + * + * @param totalRecords total number of records in the directory + * @param directoryOffset offset, since beginning of archive, where the Central Directory is + * located + * @param directorySize number of bytes of the Central Directory + * @param comment the EOCD comment + */ + Eocd(int totalRecords, long directoryOffset, long directorySize, @Nonnull byte[] comment) { + Preconditions.checkArgument(totalRecords >= 0, "totalRecords < 0"); + Preconditions.checkArgument(directoryOffset >= 0, "directoryOffset < 0"); + Preconditions.checkArgument(directorySize >= 0, "directorySize < 0"); + + this.totalRecords = totalRecords; + this.directoryOffset = directoryOffset; + this.directorySize = directorySize; + this.comment = comment; + byteSupplier = new CachedSupplier<>(this::computeByteRepresentation); + } + + /** + * Obtains the number of records in the Central Directory. + * + * @return the number of records + */ + int getTotalRecords() { + return totalRecords; + } + + /** + * Obtains the offset since the beginning of the zip archive where the Central Directory is + * located. + * + * @return the offset where the Central Directory is located + */ + long getDirectoryOffset() { + return directoryOffset; + } + + /** + * Obtains the size of the Central Directory. + * + * @return the number of bytes that make up the Central Directory + */ + long getDirectorySize() { + return directorySize; + } + + /** + * Obtains the size of the EOCD. + * + * @return the size, in bytes, of the EOCD + */ + long getEocdSize() { + return (long) F_COMMENT_SIZE.endOffset() + comment.length; + } + + /** + * Generates the EOCD data. + * + * @return a byte representation of the EOCD that has exactly {@link #getEocdSize()} bytes + * @throws IOException failed to generate the EOCD data + */ + @Nonnull + byte[] toBytes() throws IOException { + return byteSupplier.get(); + } + + /* + * Obtains the comment in the EOCD. + * + * @return the comment exactly as it is represented in the file (no encoding conversion is + * done) + */ + @Nonnull + byte[] getComment() { + byte[] commentCopy = new byte[comment.length]; + System.arraycopy(comment, 0, commentCopy, 0, comment.length); + return commentCopy; + } + + /** + * Computes the byte representation of the EOCD. + * + * @return a byte representation of the EOCD that has exactly {@link #getEocdSize()} bytes + * @throws UncheckedIOException failed to generate the EOCD data + */ + @Nonnull + private byte[] computeByteRepresentation() { + ByteBuffer out = ByteBuffer.allocate(F_COMMENT_SIZE.endOffset() + comment.length); + + try { + F_SIGNATURE.write(out); + F_NUMBER_OF_DISK.write(out); + F_DISK_CD_START.write(out); + F_RECORDS_DISK.write(out, totalRecords); + F_RECORDS_TOTAL.write(out, totalRecords); + F_CD_SIZE.write(out, directorySize); + F_CD_OFFSET.write(out, directoryOffset); + F_COMMENT_SIZE.write(out, comment.length); + out.put(comment); + + return out.array(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } +} diff --git a/src/main/java/com/android/tools/build/apkzlib/zip/ExtraField.java b/src/main/java/com/android/tools/build/apkzlib/zip/ExtraField.java new file mode 100644 index 0000000..aa41491 --- /dev/null +++ b/src/main/java/com/android/tools/build/apkzlib/zip/ExtraField.java @@ -0,0 +1,406 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * 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.android.tools.build.apkzlib.zip; + +import com.android.tools.build.apkzlib.zip.utils.LittleEndianUtils; +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * Contains an extra field. + * + *

According to the zip specification, the extra field is composed of a sequence of fields. + * This class provides a way to access, parse and modify that information. + * + *

The zip specification calls fields to the fields inside the extra field. Because this + * terminology is confusing, we use segment to refer to a part of the extra field. Each + * segment is represented by an instance of {@link Segment} and contains a header ID and data. + * + *

Each instance of {@link ExtraField} is immutable. The extra field of a particular entry can + * be changed by creating a new instanceof {@link ExtraField} and pass it to + * {@link StoredEntry#setLocalExtra(ExtraField)}. + * + *

Instances of {@link ExtraField} can be created directly from the list of segments in it + * or from the raw byte data. If created from the raw byte data, the data will only be parsed + * on demand. So, if neither {@link #getSegments()} nor {@link #getSingleSegment(int)} is + * invoked, the extra field will not be parsed. This guarantees low performance impact of the + * using the extra field unless its contents are needed. + */ +public class ExtraField { + + /** + * Header ID for field with zip alignment. + */ + static final int ALIGNMENT_ZIP_EXTRA_DATA_FIELD_HEADER_ID = 0xd935; + + /** + * The field's raw data, if it is known. Either this variable or {@link #segments} must be + * non-{@code null}. + */ + @Nullable + private final byte[] rawData; + + /** + * The list of field's segments. Will be populated if the extra field is created based on a + * list of segments; will also be populated after parsing if the extra field is created based + * on the raw bytes. + */ + @Nullable + private ImmutableList segments; + + /** + * Creates an extra field based on existing raw data. + * + * @param rawData the raw data; will not be parsed unless needed + */ + public ExtraField(@Nonnull byte[] rawData) { + this.rawData = rawData; + segments = null; + } + + /** + * Creates a new extra field with no segments. + */ + public ExtraField() { + rawData = null; + segments = ImmutableList.of(); + } + + /** + * Creates a new extra field with the given segments. + * + * @param segments the segments + */ + public ExtraField(@Nonnull ImmutableList segments) { + rawData = null; + this.segments = segments; + } + + /** + * Obtains all segments in the extra field. + * + * @return all segments + * @throws IOException failed to parse the extra field + */ + public ImmutableList getSegments() throws IOException { + if (segments == null) { + parseSegments(); + } + + Preconditions.checkNotNull(segments); + return segments; + } + + /** + * Obtains the only segment with the provided header ID. + * + * @param headerId the header ID + * @return the segment found or {@code null} if no segment contains the provided header ID + * @throws IOException there is more than one header with the provided header ID + */ + @Nullable + public Segment getSingleSegment(int headerId) throws IOException { + List found = + getSegments().stream() + .filter(s -> s.getHeaderId() == headerId) + .collect(Collectors.toList()); + if (found.isEmpty()) { + return null; + } else if (found.size() == 1) { + return found.get(0); + } else { + throw new IOException(found.size() + " segments with header ID " + headerId + "found"); + } + } + + /** + * Parses the raw data and generates all segments in {@link #segments}. + * + * @throws IOException failed to parse the data + */ + private void parseSegments() throws IOException { + Preconditions.checkNotNull(rawData); + Preconditions.checkState(segments == null); + + List segments = new ArrayList<>(); + ByteBuffer buffer = ByteBuffer.wrap(rawData); + + while (buffer.remaining() > 0) { + int headerId = LittleEndianUtils.readUnsigned2Le(buffer); + int dataSize = LittleEndianUtils.readUnsigned2Le(buffer); + if (dataSize < 0) { + throw new IOException( + "Invalid data size for extra field segment with header ID " + + headerId + + ": " + + dataSize); + } + + byte[] data = new byte[dataSize]; + if (buffer.remaining() < dataSize) { + throw new IOException( + "Invalid data size for extra field segment with header ID " + + headerId + + ": " + + dataSize + + " (only " + + buffer.remaining() + + " bytes are available)"); + } + buffer.get(data); + + SegmentFactory factory = identifySegmentFactory(headerId); + Segment seg = factory.make(headerId, data); + segments.add(seg); + } + + this.segments = ImmutableList.copyOf(segments); + } + + /** + * Obtains the size of the extra field. + * + * @return the size + */ + public int size() { + if (rawData != null) { + return rawData.length; + } else { + Preconditions.checkNotNull(segments); + int sz = 0; + for (Segment s : segments) { + sz += s.size(); + } + + return sz; + } + } + + /** + * Writes the extra field to the given output buffer. + * + * @param out the output buffer to write the field; exactly {@link #size()} bytes will be + * written + * @throws IOException failed to write the extra fields + */ + public void write(@Nonnull ByteBuffer out) throws IOException { + if (rawData != null) { + out.put(rawData); + } else { + Preconditions.checkNotNull(segments); + for (Segment s : segments) { + s.write(out); + } + } + } + + /** + * Identifies the factory to create the segment with the provided header ID. + * + * @param headerId the header ID + * @return the segmnet factory that creates segments with the given header + */ + @Nonnull + private static SegmentFactory identifySegmentFactory(int headerId) { + if (headerId == ALIGNMENT_ZIP_EXTRA_DATA_FIELD_HEADER_ID) { + return AlignmentSegment::new; + } + + return RawDataSegment::new; + } + + /** + * Field inside the extra field. A segment contains a header ID and data. Specific types of + * segments implement this interface. + */ + public interface Segment { + + /** + * Obtains the segment's header ID. + * + * @return the segment's header ID + */ + int getHeaderId(); + + /** + * Obtains the size of the segment including the header ID. + * + * @return the number of bytes needed to write the segment + */ + int size(); + + /** + * Writes the segment to a buffer. + * + * @param out the buffer where to write the segment to; exactly {@link #size()} bytes will + * be written + * @throws IOException failed to write segment data + */ + void write(@Nonnull ByteBuffer out) throws IOException; + } + + /** + * Factory that creates a segment. + */ + @FunctionalInterface + interface SegmentFactory { + + /** + * Creates a new segment. + * + * @param headerId the header ID + * @param data the segment's data + * @return the created segment + * @throws IOException failed to create the segment from the data + */ + @Nonnull + Segment make(int headerId, @Nonnull byte[] data) throws IOException; + } + + /** + * Segment of raw data: this class represents a general segment containing an array of bytes + * as data. + */ + public static class RawDataSegment implements Segment { + + /** + * Header ID. + */ + private final int headerId; + + /** + * Data in the segment. + */ + @Nonnull + private final byte[] data; + + /** + * Creates a new raw data segment. + * + * @param headerId the header ID + * @param data the segment data + */ + RawDataSegment(int headerId, @Nonnull byte[] data) { + this.headerId = headerId; + this.data = data; + } + + @Override + public int getHeaderId() { + return headerId; + } + + @Override + public void write(@Nonnull ByteBuffer out) throws IOException { + LittleEndianUtils.writeUnsigned2Le(out, headerId); + LittleEndianUtils.writeUnsigned2Le(out, data.length); + out.put(data); + } + + @Override + public int size() { + return 4 + data.length; + } + } + + /** + * Segment with information on an alignment: this segment contains information on how an entry + * should be aligned and contains zero-filled data to force alignment. + * + *

An alignment segment contains the header ID, the size of the data, the alignment value + * and zero bytes to pad + */ + public static class AlignmentSegment implements Segment { + + /** + * Minimum size for an alignment segment. + */ + public static final int MINIMUM_SIZE = 6; + + /** + * The alignment value. + */ + private int alignment; + + /** + * How many bytes of padding are in this segment? + */ + private int padding; + + /** + * Creates a new alignment segment. + * + * @param alignment the alignment value + * @param totalSize how many bytes should this segment take? + */ + public AlignmentSegment(int alignment, int totalSize) { + Preconditions.checkArgument(alignment > 0, "alignment <= 0"); + Preconditions.checkArgument(totalSize >= MINIMUM_SIZE, "totalSize < MINIMUM_SIZE"); + + /* + * We have 6 bytes of fixed data: header ID (2 bytes), data size (2 bytes), alignment + * value (2 bytes). + */ + this.alignment = alignment; + padding = totalSize - MINIMUM_SIZE; + } + + /** + * Creates a new alignment segment from extra data. + * + * @param headerId the header ID + * @param data the segment data + * @throws IOException failed to create the segment from the data + */ + public AlignmentSegment(int headerId, @Nonnull byte[] data) throws IOException { + Preconditions.checkArgument(headerId == ALIGNMENT_ZIP_EXTRA_DATA_FIELD_HEADER_ID); + + ByteBuffer dataBuffer = ByteBuffer.wrap(data); + alignment = LittleEndianUtils.readUnsigned2Le(dataBuffer); + if (alignment <= 0) { + throw new IOException("Invalid alignment in alignment field: " + alignment); + } + + padding = data.length - 2; + } + + @Override + public void write(@Nonnull ByteBuffer out) throws IOException { + LittleEndianUtils.writeUnsigned2Le(out, ALIGNMENT_ZIP_EXTRA_DATA_FIELD_HEADER_ID); + LittleEndianUtils.writeUnsigned2Le(out, padding + 2); + LittleEndianUtils.writeUnsigned2Le(out, alignment); + out.put(new byte[padding]); + } + + @Override + public int size() { + return padding + 6; + } + + @Override + public int getHeaderId() { + return ALIGNMENT_ZIP_EXTRA_DATA_FIELD_HEADER_ID; + } + } +} diff --git a/src/main/java/com/android/tools/build/apkzlib/zip/FileUseMap.java b/src/main/java/com/android/tools/build/apkzlib/zip/FileUseMap.java new file mode 100644 index 0000000..7e8e9d9 --- /dev/null +++ b/src/main/java/com/android/tools/build/apkzlib/zip/FileUseMap.java @@ -0,0 +1,601 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * 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.android.tools.build.apkzlib.zip; + +import com.google.common.base.Preconditions; +import com.google.common.base.Verify; +import com.google.common.collect.Lists; +import com.google.common.collect.Sets; +import com.google.common.primitives.Ints; +import java.util.List; +import java.util.Set; +import java.util.SortedSet; +import java.util.StringJoiner; +import java.util.TreeSet; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * The file use map keeps track of which parts of the zip file are used which parts are not. + * It essentially maintains an ordered set of entries ({@link FileUseMapEntry}). Each entry either has + * some data (an entry, the Central Directory, the EOCD) or is a free entry. + * + *

For example: [0-95, "foo/"][95-260, "xpto"][260-310, free][310-360, Central Directory] + * [360-390,EOCD] + * + *

There are a few invariants in this structure: + *

    + *
  • there are no gaps between map entries; + *
  • the map is fully covered up to its size; + *
  • there are no two free entries next to each other; this is guaranteed by coalescing the + * entries upon removal (see {@link #coalesce(FileUseMapEntry)}); + *
  • all free entries have a minimum size defined in the constructor, with the possible exception + * of the last one + *
+ */ +class FileUseMap { + /** + * Size of the file according to the map. This should always match the last entry in + * {@code #map}. + */ + private long size; + + /** + * Tree with all intervals ordered by position. Contains coverage from 0 up to {@link #size}. + * If {@link #size} is zero then this set is empty. This is the only situation in which the map + * will be empty. + */ + @Nonnull + private TreeSet> map; + + /** + * Tree with all free blocks ordered by size. This is essentially a view over {@link #map} + * containing only the free blocks, but in a different order. + */ + @Nonnull + private TreeSet> free; + + /** + * If defined, defines the minimum size for a free entry. + */ + private int mMinFreeSize; + + /** + * Creates a new, empty file map. + * + * @param size the size of the file + * @param minFreeSize minimum size of a free entry + */ + FileUseMap(long size, int minFreeSize) { + Preconditions.checkArgument(size >= 0, "size < 0"); + Preconditions.checkArgument(minFreeSize >= 0, "minFreeSize < 0"); + + this.size = size; + map = new TreeSet<>(FileUseMapEntry.COMPARE_BY_START); + free = new TreeSet<>(FileUseMapEntry.COMPARE_BY_SIZE); + mMinFreeSize = minFreeSize; + + if (size > 0) { + internalAdd(FileUseMapEntry.makeFree(0, size)); + } + } + + /** + * Adds an entry to the internal structures. + * + * @param entry the entry to add + */ + private void internalAdd(@Nonnull FileUseMapEntry entry) { + map.add(entry); + + if (entry.isFree()) { + free.add(entry); + } + } + + /** + * Removes an entry from the internal structures. + * + * @param entry the entry to remove + */ + private void internalRemove(@Nonnull FileUseMapEntry entry) { + boolean wasRemoved = map.remove(entry); + Preconditions.checkState(wasRemoved, "entry not in map"); + + if (entry.isFree()) { + free.remove(entry); + } + } + + /** + * Adds a new file to the map. The interval specified by {@code entry} must fit inside an + * empty entry in the map. That entry will be replaced by entry and additional free entries + * will be added before and after if needed to make sure no spaces exist on the map. + * + * @param entry the entry to add + */ + private void add(@Nonnull FileUseMapEntry entry) { + Preconditions.checkArgument(entry.getStart() < size, "entry.getStart() >= size"); + Preconditions.checkArgument(entry.getEnd() <= size, "entry.getEnd() > size"); + Preconditions.checkArgument(!entry.isFree(), "entry.isFree()"); + + FileUseMapEntry container = findContainer(entry); + Verify.verify(container.isFree(), "!container.isFree()"); + + Set> replacements = split(container, entry); + internalRemove(container); + for (FileUseMapEntry r : replacements) { + internalAdd(r); + } + } + + /** + * Removes a file from the map, replacing it with an empty one that is then coalesced with + * neighbors (if the neighbors are free). + * + * @param entry the entry + */ + void remove(@Nonnull FileUseMapEntry entry) { + Preconditions.checkState(map.contains(entry), "!map.contains(entry)"); + Preconditions.checkArgument(!entry.isFree(), "entry.isFree()"); + + internalRemove(entry); + + FileUseMapEntry replacement = FileUseMapEntry.makeFree(entry.getStart(), entry.getEnd()); + internalAdd(replacement); + coalesce(replacement); + } + + /** + * Adds a new file to the map. The interval specified by ({@code start}, {@code end}) must fit + * inside an empty entry in the map. That entry will be replaced by entry and additional free + * entries will be added before and after if needed to make sure no spaces exist on the map. + * + *

The entry cannot extend beyong the end of the map. If necessary, extend the map using + * {@link #extend(long)}. + * + * @param start the start of this entry + * @param end the end of the entry + * @param store extra data to store with the entry + * @param the type of data to store in the entry + * @return the new entry + */ + FileUseMapEntry add(long start, long end, @Nonnull T store) { + Preconditions.checkArgument(start >= 0, "start < 0"); + Preconditions.checkArgument(end > start, "end < start"); + + FileUseMapEntry entry = FileUseMapEntry.makeUsed(start, end, store); + add(entry); + return entry; + } + + /** + * Finds the entry that fully contains the given one. It is assumed that one exists. + * + * @param entry the entry whose container we're looking for + * @return the container + */ + @Nonnull + private FileUseMapEntry findContainer(@Nonnull FileUseMapEntry entry) { + FileUseMapEntry container = map.floor(entry); + Verify.verifyNotNull(container); + Verify.verify(container.getStart() <= entry.getStart()); + Verify.verify(container.getEnd() >= entry.getEnd()); + + return container; + } + + /** + * Splits a container to add an entry, adding new free entries before and after the provided + * entry if needed. + * + * @param container the container entry, a free entry that is in {@link #map} that that + * encloses {@code entry} + * @param entry the entry that will be used to split {@code container} + * @return a set of non-overlapping entries that completely covers {@code container} and that + * includes {@code entry} + */ + @Nonnull + private static Set> split(@Nonnull FileUseMapEntry container, + @Nonnull FileUseMapEntry entry) { + Preconditions.checkArgument(container.isFree(), "!container.isFree()"); + + long farStart = container.getStart(); + long start = entry.getStart(); + long end = entry.getEnd(); + long farEnd = container.getEnd(); + + Verify.verify(farStart <= start, "farStart > start"); + Verify.verify(start < end, "start >= end"); + Verify.verify(farEnd >= end, "farEnd < end"); + + Set> result = Sets.newHashSet(); + if (farStart < start) { + result.add(FileUseMapEntry.makeFree(farStart, start)); + } + + result.add(entry); + + if (end < farEnd) { + result.add(FileUseMapEntry.makeFree(end, farEnd)); + } + + return result; + } + + /** + * Coalesces a free entry replacing it and neighboring free entries with a single, larger + * entry. This method does nothing if {@code entry} does not have free neighbors. + * + * @param entry the free entry to coalesce with neighbors + */ + private void coalesce(@Nonnull FileUseMapEntry entry) { + Preconditions.checkArgument(entry.isFree(), "!entry.isFree()"); + + FileUseMapEntry prevToMerge = null; + long start = entry.getStart(); + if (start > 0) { + /* + * See if we have a previous entry to merge with this one. + */ + prevToMerge = map.floor(FileUseMapEntry.makeFree(start - 1, start)); + Verify.verifyNotNull(prevToMerge); + if (!prevToMerge.isFree()) { + prevToMerge = null; + } + } + + FileUseMapEntry nextToMerge = null; + long end = entry.getEnd(); + if (end < size) { + /* + * See if we have a next entry to merge with this one. + */ + nextToMerge = map.ceiling(FileUseMapEntry.makeFree(end, end + 1)); + Verify.verifyNotNull(nextToMerge); + if (!nextToMerge.isFree()) { + nextToMerge = null; + } + } + + if (prevToMerge == null && nextToMerge == null) { + return; + } + + long newStart = start; + if (prevToMerge != null) { + newStart = prevToMerge.getStart(); + internalRemove(prevToMerge); + } + + long newEnd = end; + if (nextToMerge != null) { + newEnd = nextToMerge.getEnd(); + internalRemove(nextToMerge); + } + + internalRemove(entry); + internalAdd(FileUseMapEntry.makeFree(newStart, newEnd)); + } + + /** + * Truncates map removing the top entry if it is free and reducing the map's size. + */ + void truncate() { + if (size == 0) { + return; + } + + /* + * Find the last entry. + */ + FileUseMapEntry last = map.last(); + Verify.verifyNotNull(last, "last == null"); + if (last.isFree()) { + internalRemove(last); + size = last.getStart(); + } + } + + /** + * Obtains the size of the map. + * + * @return the size + */ + long size() { + return size; + } + + /** + * Obtains the largest used offset in the map. This will be size of the map after truncation. + * + * @return the size of the file discounting the last block if it is empty + */ + long usedSize() { + if (size == 0) { + return 0; + } + + /* + * Find the last entry to see if it is an empty entry. If it is, we need to remove its size + * from the returned value. + */ + FileUseMapEntry last = map.last(); + Verify.verifyNotNull(last, "last == null"); + if (last.isFree()) { + return last.getStart(); + } else { + Verify.verify(last.getEnd() == size); + return size; + } + } + + /** + * Extends the map to guarantee it has at least {@code size} bytes. If the current size is + * as large as {@code size}, this method does nothing. + * + * @param size the new size of the map that cannot be smaller that the current size + */ + void extend(long size) { + Preconditions.checkArgument(size >= this.size, "size < size"); + + if (this.size == size) { + return; + } + + FileUseMapEntry newBlock = FileUseMapEntry.makeFree(this.size, size); + internalAdd(newBlock); + + this.size = size; + + coalesce(newBlock); + } + + /** + * Locates a free area in the map with at least {@code size} bytes such that + * {@code ((start + alignOffset) % align == 0} and such that the free space before {@code start} + * is not smaller than the minimum free entry size. This method will follow the algorithm + * specified by {@code alg}. + * + *

If no free contiguous block exists in the map that can hold the provided + * size then the first free index at the end of the map is provided. This means that the map + * may need to be extended before data can be added. + * + * @param size the size of the contiguous area requested + * @param alignOffset an offset to which alignment needs to be computed (see method description) + * @param align alignment at the offset (see method description) + * @param alg which algorithm to use + * @return the location of the contiguous area; this may be located at the end of the map + */ + long locateFree(long size, long alignOffset, long align, @Nonnull PositionAlgorithm alg) { + Preconditions.checkArgument(size > 0, "size <= 0"); + + FileUseMapEntry minimumSizedEntry = FileUseMapEntry.makeFree(0, size); + SortedSet> matches; + + switch (alg) { + case BEST_FIT: + matches = free.tailSet(minimumSizedEntry); + break; + case FIRST_FIT: + matches = map; + break; + default: + throw new AssertionError(); + } + + FileUseMapEntry best = null; + long bestExtraSize = 0; + for (FileUseMapEntry curr : matches) { + /* + * We don't care about blocks that aren't free. + */ + if (!curr.isFree()) { + continue; + } + + /* + * Compute any extra size we need in this block to make sure we verify the alignment. + * There must be a better to do this... + */ + long extraSize; + if (align == 0) { + extraSize = 0; + } else { + extraSize = (align - ((curr.getStart() + alignOffset) % align)) % align; + } + + /* + * We can't leave than mMinFreeSize before. So if the extraSize is less than + * mMinFreeSize, we have to increase it by 'align' as many times as needed. For + * example, if mMinFreeSize is 20, align 4 and extraSize is 5. We need to increase it + * to 21 (5 + 4 * 4) + */ + if (extraSize > 0 && extraSize < mMinFreeSize) { + int addAlignBlocks = + Ints.checkedCast((mMinFreeSize - extraSize + align - 1) / align); + extraSize += addAlignBlocks * align; + } + + /* + * We don't care about blocks where we don't fit in. + */ + if (curr.getSize() < (size + extraSize)) { + continue; + } + + /* + * We don't care about blocks that leave less than the minimum size after. There are + * two exceptions: (1) this is the last block and (2) the next block is free in which + * case, after coalescing, the free block with have at least the minimum size. + */ + long emptySpaceLeft = curr.getSize() - (size + extraSize); + if (emptySpaceLeft > 0 && emptySpaceLeft < mMinFreeSize) { + FileUseMapEntry next = map.higher(curr); + if (next != null && !next.isFree()) { + continue; + } + } + + /* + * We don't care about blocks that are bigger than the best so far (otherwise this + * wouldn't be a best-fit algorithm). + */ + if (best != null && best.getSize() < curr.getSize()) { + continue; + } + + best = curr; + bestExtraSize = extraSize; + + /* + * If we're doing first fit, we don't want to search for a better one :) + */ + if (alg == PositionAlgorithm.FIRST_FIT) { + break; + } + } + + /* + * If no entry that could hold size is found, get the first free byte. + */ + long firstFree = this.size; + if (best == null && !map.isEmpty()) { + FileUseMapEntry last = map.last(); + if (last.isFree()) { + firstFree = last.getStart(); + } + } + + /* + * We're done: either we found something or we didn't, in which the new entry needs to + * be added to the end of the map. + */ + if (best == null) { + long extra = (align - ((firstFree + alignOffset) % align)) % align; + + /* + * If adding this entry at the end would create a space smaller than the minimum, + * push it for 'align' bytes forward. + */ + if (extra > 0) { + if (extra < mMinFreeSize) { + extra += align * (((mMinFreeSize - extra) + (align - 1)) / align); + } + } + + return firstFree + extra; + } else { + return best.getStart() + bestExtraSize; + } + } + + /** + * Obtains all free areas of the map, excluding any trailing free area. + * + * @return all free areas, an empty set if there are no free areas; the areas are returned + * in file order, that is, if area {@code x} starts before area {@code y}, then area {@code x} + * will be stored before area {@code y} in the list + */ + @Nonnull + List> getFreeAreas() { + List> freeAreas = Lists.newArrayList(); + + for (FileUseMapEntry area : map) { + if (area.isFree() && area.getEnd() != size) { + freeAreas.add(area); + } + } + + return freeAreas; + } + + /** + * Obtains the entry that is located before the one provided. + * + * @param entry the map entry to get the previous one for; must belong to the map + * @return the entry before the provided one, {@code null} if {@code entry} is the first entry + * in the map + */ + @Nullable + FileUseMapEntry before(@Nonnull FileUseMapEntry entry) { + Preconditions.checkNotNull(entry, "entry == null"); + + return map.lower(entry); + } + + /** + * Obtains the entry that is located after the one provided. + * + * @param entry the map entry to get the next one for; must belong to the map + * @return the entry after the provided one, {@code null} if {@code entry} is the last entry in + * the map + */ + @Nullable + FileUseMapEntry after(@Nonnull FileUseMapEntry entry) { + Preconditions.checkNotNull(entry, "entry == null"); + + return map.higher(entry); + } + + /** + * Obtains the entry at the given offset. + * + * @param offset the offset to look for + * @return the entry found or {@code null} if there is no entry (not even a free one) at the + * given offset + */ + @Nullable + FileUseMapEntry at(long offset) { + Preconditions.checkArgument(offset >= 0, "offset < 0"); + Preconditions.checkArgument(offset < size, "offset >= size"); + + FileUseMapEntry entry = map.floor(FileUseMapEntry.makeFree(offset, offset + 1)); + if (entry == null) { + return null; + } + + Verify.verify(entry.getStart() <= offset); + Verify.verify(entry.getEnd() > offset); + + return entry; + } + + @Override + public String toString() { + StringJoiner j = new StringJoiner(", "); + map.stream() + .map(e -> e.getStart() + " - " + e.getEnd() + ": " + e.getStore()) + .forEach(j::add); + return "FileUseMap[" + j.toString() + "]"; + } + + /** + * Algorithms used to position entries in blocks. + */ + public enum PositionAlgorithm { + /** + * Best fit: finds the smallest free block that can receive the entry. + */ + BEST_FIT, + + /** + * First fit: finds the first free block that can receive the entry. + */ + FIRST_FIT + } +} diff --git a/src/main/java/com/android/tools/build/apkzlib/zip/FileUseMapEntry.java b/src/main/java/com/android/tools/build/apkzlib/zip/FileUseMapEntry.java new file mode 100644 index 0000000..01cbbfc --- /dev/null +++ b/src/main/java/com/android/tools/build/apkzlib/zip/FileUseMapEntry.java @@ -0,0 +1,162 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * 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.android.tools.build.apkzlib.zip; + +import com.google.common.base.MoreObjects; +import com.google.common.base.Preconditions; +import com.google.common.primitives.Ints; +import java.util.Comparator; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * Represents an entry in the {@link FileUseMap}. Each entry contains an interval of bytes. The + * end of the interval is exclusive. + *

+ * Entries can either be free or used. Used entries must store an object. Free entries + * do not store anything. + *

+ * File map entries are used to keep track of which parts of a file map are used and not. + * @param the type of data stored + */ +class FileUseMapEntry { + + /** + * Comparator that compares entries by their start date. + */ + public static final Comparator> COMPARE_BY_START = + (o1, o2) -> Ints.saturatedCast(o1.getStart() - o2.getStart()); + + /** + * Comparator that compares entries by their size. + */ + public static final Comparator> COMPARE_BY_SIZE = + (o1, o2) -> Ints.saturatedCast(o1.getSize() - o2.getSize()); + + /** + * The first byte in the entry. + */ + private final long start; + + /** + * The first byte no longer in the entry. + */ + private final long end; + + /** + * The stored data. If {@code null} then this entry represents a free entry. + */ + @Nullable + private final T store; + + /** + * Creates a new map entry. + * + * @param start the start of the entry + * @param end the end of the entry (first byte no longer in the entry) + * @param store the data to store in the entry or {@code null} if this is a free entry + */ + private FileUseMapEntry(long start, long end, @Nullable T store) { + Preconditions.checkArgument(start >= 0, "start < 0"); + Preconditions.checkArgument(end > start, "end <= start"); + + this.start = start; + this.end = end; + this.store = store; + } + + /** + * Creates a new free entry. + * + * @param start the start of the entry + * @param end the end of the entry (first byte no longer in the entry) + * @return the entry + */ + public static FileUseMapEntry makeFree(long start, long end) { + return new FileUseMapEntry<>(start, end, null); + } + + /** + * Creates a new used entry. + * + * @param start the start of the entry + * @param end the end of the entry (first byte no longer in the entry) + * @param store the data to store in the entry + * @param the type of data to store in the entry + * @return the entry + */ + public static FileUseMapEntry makeUsed(long start, long end, @Nonnull T store) { + Preconditions.checkNotNull(store, "store == null"); + return new FileUseMapEntry<>(start, end, store); + } + + /** + * Obtains the first byte in the entry. + * + * @return the first byte in the entry (if the same value as {@link #getEnd()} then the entry + * is empty and contains no data) + */ + long getStart() { + return start; + } + + /** + * Obtains the first byte no longer in the entry. + * + * @return the first byte no longer in the entry + */ + long getEnd() { + return end; + } + + /** + * Obtains the size of the entry. + * + * @return the number of bytes contained in the entry + */ + long getSize() { + return end - start; + } + + /** + * Determines if this is a free entry. + * + * @return is this entry free? + */ + boolean isFree() { + return store == null; + } + + /** + * Obtains the data stored in the entry. + * + * @return the data stored or {@code null} if this entry is a free entry + */ + @Nullable + T getStore() { + return store; + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("start", start) + .add("end", end) + .add("store", store) + .toString(); + } +} diff --git a/src/main/java/com/android/tools/build/apkzlib/zip/GPFlags.java b/src/main/java/com/android/tools/build/apkzlib/zip/GPFlags.java new file mode 100644 index 0000000..96062ca --- /dev/null +++ b/src/main/java/com/android/tools/build/apkzlib/zip/GPFlags.java @@ -0,0 +1,179 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * 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.android.tools.build.apkzlib.zip; + +import java.io.IOException; +import javax.annotation.Nonnull; + +/** + * General purpose bit flags. Contains the encoding of the zip's general purpose bits. + * + *

We don't really care about the method bit(s). These are bits 1 and 2. Here are the values: + *

    + *
  • 0 (00): Normal (-en) compression option was used. + *
  • 1 (01): Maximum (-exx/-ex) compression option was used. + *
  • 2 (10): Fast (-ef) compression option was used. + *
  • 3 (11): Super Fast (-es) compression option was used. + *
+ */ +class GPFlags { + + /** + * Is the entry encrypted? + */ + private static final int BIT_ENCRYPTION = 1; + + /** + * Has CRC computation been deferred and, therefore, does a data description block exist? + */ + private static final int BIT_DEFERRED_CRC = (1 << 3); + + /** + * Is enhanced deflating used? + */ + private static final int BIT_ENHANCED_DEFLATING = (1 << 4); + + /** + * Does the entry contain patched data? + */ + private static final int BIT_PATCHED_DATA = (1 << 5); + + /** + * Is strong encryption used? + */ + private static final int BIT_STRONG_ENCRYPTION = (1 << 6) | (1 << 13); + + /** + * If this bit is set the filename and comment fields for this file must be encoded using UTF-8. + */ + private static final int BIT_EFS = (1 << 11); + + /** + * Unused bits. + */ + private static final int BIT_UNUSED = (1 << 7) | (1 << 8) | (1 << 9) | (1 << 10) + | (1 << 14) | (1 << 15); + + /** + * Bit flag value. + */ + private final long value; + + /** + * Has the CRC computation beeen deferred? + */ + private boolean deferredCrc; + + /** + * Is the file name encoded in UTF-8? + */ + private boolean utf8FileName; + + /** + * Creates a new flags object. + * + * @param value the value of the bit mask + */ + private GPFlags(long value) { + this.value = value; + + deferredCrc = ((value & BIT_DEFERRED_CRC) != 0); + utf8FileName = ((value & BIT_EFS) != 0); + } + + /** + * Obtains the flags value. + * + * @return the value of the bit mask + */ + public long getValue() { + return value; + } + + /** + * Is the CRC computation deferred? + * + * @return is the CRC computation deferred? + */ + public boolean isDeferredCrc() { + return deferredCrc; + } + + /** + * Is the file name encoded in UTF-8? + * + * @return is the file name encoded in UTF-8? + */ + public boolean isUtf8FileName() { + return utf8FileName; + } + + /** + * Creates a new bit mask. + * + * @param utf8Encoding should UTF-8 encoding be used? + * @return the new bit mask + */ + @Nonnull + static GPFlags make(boolean utf8Encoding) { + long flags = 0; + + if (utf8Encoding) { + flags |= BIT_EFS; + } + + return new GPFlags(flags); + } + + /** + * Creates the flag information from a byte. This method will also validate that only + * supported options are defined in the flag. + * + * @param bits the bit mask + * @return the created flag information + * @throws IOException unsupported options are used in the bit mask + */ + @Nonnull + static GPFlags from(long bits) throws IOException { + if ((bits & BIT_ENCRYPTION) != 0) { + throw new IOException("Zip files with encrypted of entries not supported."); + } + + if ((bits & BIT_ENHANCED_DEFLATING) != 0) { + throw new IOException("Enhanced deflating not supported."); + } + + if ((bits & BIT_PATCHED_DATA) != 0) { + throw new IOException("Compressed patched data not supported."); + } + + if ((bits & BIT_STRONG_ENCRYPTION) != 0) { + throw new IOException("Strong encryption not supported."); + } + + if ((bits & BIT_UNUSED) != 0) { + throw new IOException("Unused bits set in directory entry. Weird. I don't know what's " + + "going on."); + } + + if ((bits & 0xffffffff00000000L) != 0) { + throw new IOException("Unsupported bits after 32."); + } + + return new GPFlags(bits); + } +} diff --git a/src/main/java/com/android/tools/build/apkzlib/zip/InflaterByteSource.java b/src/main/java/com/android/tools/build/apkzlib/zip/InflaterByteSource.java new file mode 100644 index 0000000..6efe2c7 --- /dev/null +++ b/src/main/java/com/android/tools/build/apkzlib/zip/InflaterByteSource.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * 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.android.tools.build.apkzlib.zip; + +import com.android.tools.build.apkzlib.zip.utils.CloseableByteSource; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.SequenceInputStream; +import java.util.zip.Inflater; +import java.util.zip.InflaterInputStream; +import javax.annotation.Nonnull; + +/** + * Byte source that inflates another byte source. It assumed the inner byte source has deflated + * data. + */ +public class InflaterByteSource extends CloseableByteSource { + + /** + * The stream factory for the deflated data. + */ + @Nonnull + private final CloseableByteSource deflatedSource; + + /** + * Creates a new source. + * @param byteSource the factory for deflated data + */ + public InflaterByteSource(@Nonnull CloseableByteSource byteSource) { + deflatedSource = byteSource; + } + + @Override + public InputStream openStream() throws IOException { + /* + * The extra byte is a dummy byte required by the inflater. Weirdo. + * (see the java.util.Inflater documentation). Looks like a hack... + * "Oh, I need an extra dummy byte to allow for some... err... optimizations..." + */ + ByteArrayInputStream hackByte = new ByteArrayInputStream(new byte[] { 0 }); + return new InflaterInputStream(new SequenceInputStream(deflatedSource.openStream(), + hackByte), new Inflater(true)); + } + + @Override + public void innerClose() throws IOException { + deflatedSource.close(); + } +} diff --git a/src/main/java/com/android/tools/build/apkzlib/zip/LazyDelegateByteSource.java b/src/main/java/com/android/tools/build/apkzlib/zip/LazyDelegateByteSource.java new file mode 100644 index 0000000..b755bae --- /dev/null +++ b/src/main/java/com/android/tools/build/apkzlib/zip/LazyDelegateByteSource.java @@ -0,0 +1,156 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * 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.android.tools.build.apkzlib.zip; + +import com.android.tools.build.apkzlib.zip.utils.CloseableByteSource; +import com.google.common.hash.HashCode; +import com.google.common.hash.HashFunction; +import com.google.common.io.ByteProcessor; +import com.google.common.io.ByteSink; +import com.google.common.io.ByteSource; +import com.google.common.io.CharSource; +import com.google.common.util.concurrent.ListenableFuture; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.Charset; +import java.util.concurrent.ExecutionException; +import javax.annotation.Nonnull; + +/** + * {@code ByteSource} that delegates all operations to another {@code ByteSource}. The other + * byte source, the delegate, may be computed lazily. + */ +public class LazyDelegateByteSource extends CloseableByteSource { + + /** + * Byte source where we delegate operations to. + */ + @Nonnull + private final ListenableFuture delegate; + + /** + * Creates a new byte source that delegates operations to the provided source. + * @param delegate the source that will receive all operations + */ + public LazyDelegateByteSource(@Nonnull ListenableFuture delegate) { + this.delegate = delegate; + } + + /** + * Obtains the delegate future. + * @return the delegate future, that may be computed or not + */ + @Nonnull + public ListenableFuture getDelegate() { + return delegate; + } + + /** + * Obtains the byte source, waiting for the future to be computed. + * @return the byte source + * @throws IOException failed to compute the future :) + */ + @Nonnull + private CloseableByteSource get() throws IOException { + try { + CloseableByteSource r = delegate.get(); + if (r == null) { + throw new IOException("Delegate byte source computation resulted in null."); + } + + return r; + } catch (InterruptedException e) { + throw new IOException("Interrupted while waiting for byte source computation.", e); + } catch (ExecutionException e) { + throw new IOException("Failed to compute byte source.", e); + } + } + + @Override + public CharSource asCharSource(Charset charset) { + try { + return get().asCharSource(charset); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Override + public InputStream openBufferedStream() throws IOException { + return get().openBufferedStream(); + } + + @Override + public ByteSource slice(long offset, long length) { + try { + return get().slice(offset, length); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Override + public boolean isEmpty() throws IOException { + return get().isEmpty(); + } + + @Override + public long size() throws IOException { + return get().size(); + } + + @Override + public long copyTo(@Nonnull OutputStream output) throws IOException { + return get().copyTo(output); + } + + @Override + public long copyTo(@Nonnull ByteSink sink) throws IOException { + return get().copyTo(sink); + } + + @Override + public byte[] read() throws IOException { + return get().read(); + } + + @Override + public T read(@Nonnull ByteProcessor processor) throws IOException { + return get().read(processor); + } + + @Override + public HashCode hash(HashFunction hashFunction) throws IOException { + return get().hash(hashFunction); + } + + @Override + public boolean contentEquals(@Nonnull ByteSource other) throws IOException { + return get().contentEquals(other); + } + + @Override + public InputStream openStream() throws IOException { + return get().openStream(); + } + + @Override + public void innerClose() throws IOException { + get().close(); + } +} diff --git a/src/main/java/com/android/tools/build/apkzlib/zip/ProcessedAndRawByteSources.java b/src/main/java/com/android/tools/build/apkzlib/zip/ProcessedAndRawByteSources.java new file mode 100644 index 0000000..8308406 --- /dev/null +++ b/src/main/java/com/android/tools/build/apkzlib/zip/ProcessedAndRawByteSources.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * 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.android.tools.build.apkzlib.zip; + +import com.android.tools.build.apkzlib.zip.utils.CloseableByteSource; +import com.google.common.io.Closer; +import java.io.Closeable; +import java.io.IOException; +import javax.annotation.Nonnull; + +/** + * Container that has two bytes sources: one representing raw data and another processed data. + * In case of compression, the raw data is the compressed data and the processed data is the + * uncompressed data. It is valid for a RaP ("Raw-and-Processed") to contain the same byte sources + * for both processed and raw data. + */ +public class ProcessedAndRawByteSources implements Closeable { + + /** + * The processed byte source. + */ + @Nonnull + private final CloseableByteSource processedSource; + + /** + * The processed raw source. + */ + @Nonnull + private final CloseableByteSource rawSource; + + /** + * Creates a new container. + * + * @param processedSource the processed source + * @param rawSource the raw source + */ + public ProcessedAndRawByteSources(@Nonnull CloseableByteSource processedSource, + @Nonnull CloseableByteSource rawSource) { + this.processedSource = processedSource; + this.rawSource = rawSource; + } + + /** + * Obtains a byte source that read the processed contents of the entry. + * + * @return a byte source + */ + @Nonnull + public CloseableByteSource getProcessedByteSource() { + return processedSource; + } + + /** + * Obtains a byte source that reads the raw contents of an entry. This is the data that is + * ultimately stored in the file and, in the case of compressed files, is the same data in the + * source returned by {@link #getProcessedByteSource()}. + * + * @return a byte source + */ + @Nonnull + public CloseableByteSource getRawByteSource() { + return rawSource; + } + + @Override + public void close() throws IOException { + Closer closer = Closer.create(); + closer.register(processedSource); + closer.register(rawSource); + closer.close(); + } +} diff --git a/src/main/java/com/android/tools/build/apkzlib/zip/StoredEntry.java b/src/main/java/com/android/tools/build/apkzlib/zip/StoredEntry.java new file mode 100644 index 0000000..16f505e --- /dev/null +++ b/src/main/java/com/android/tools/build/apkzlib/zip/StoredEntry.java @@ -0,0 +1,818 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * 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.android.tools.build.apkzlib.zip; + +import com.android.tools.build.apkzlib.zip.utils.CloseableByteSource; +import com.android.tools.build.apkzlib.zip.utils.CloseableDelegateByteSource; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; +import com.google.common.base.Verify; +import com.google.common.io.ByteSource; +import com.google.common.io.ByteStreams; +import com.google.common.primitives.Ints; +import java.io.BufferedInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.util.Comparator; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * A stored entry represents a file in the zip. The entry may or may not be written to the zip + * file. + * + *

Stored entries provide the operations that are related to the files themselves, not to the + * zip. It is through the {@code StoredEntry} class that entries can be deleted ({@link #delete()}, + * open ({@link #open()}) or realigned ({@link #realign()}). + * + *

Entries are not created directly. They are created using + * {@link ZFile#add(String, InputStream, boolean)} and obtained from the zip file + * using {@link ZFile#get(String)} or {@link ZFile#entries()}. + * + *

Most of the data in the an entry is in the Central Directory Header. This includes the name, + * compression method, file compressed and uncompressed sizes, CRC32 checksum, etc. The CDH can + * be obtained using the {@link #getCentralDirectoryHeader()} method. + */ +public class StoredEntry { + + /** + * Comparator that compares instances of {@link StoredEntry} by their names. + */ + static final Comparator COMPARE_BY_NAME = + (o1, o2) -> { + if (o1 == null && o2 == null) { + return 0; + } + + if (o1 == null) { + return -1; + } + + if (o2 == null) { + return 1; + } + + String name1 = o1.getCentralDirectoryHeader().getName(); + String name2 = o2.getCentralDirectoryHeader().getName(); + return name1.compareTo(name2); + }; + + /** + * Signature of the data descriptor. + */ + private static final int DATA_DESC_SIGNATURE = 0x08074b50; + + /** + * Local header field: signature. + */ + private static final ZipField.F4 F_LOCAL_SIGNATURE = new ZipField.F4(0, 0x04034b50, + "Signature"); + + /** + * Local header field: version to extract, should match the CDH's. + */ + @VisibleForTesting + static final ZipField.F2 F_VERSION_EXTRACT = new ZipField.F2( + F_LOCAL_SIGNATURE.endOffset(), "Version to extract", + new ZipFieldInvariantNonNegative()); + + /** + * Local header field: GP bit flag, should match the CDH's. + */ + private static final ZipField.F2 F_GP_BIT = new ZipField.F2(F_VERSION_EXTRACT.endOffset(), + "GP bit flag"); + + /** + * Local header field: compression method, should match the CDH's. + */ + private static final ZipField.F2 F_METHOD = new ZipField.F2(F_GP_BIT.endOffset(), + "Compression method", new ZipFieldInvariantNonNegative()); + + /** + * Local header field: last modification time, should match the CDH's. + */ + private static final ZipField.F2 F_LAST_MOD_TIME = new ZipField.F2(F_METHOD.endOffset(), + "Last modification time"); + + /** + * Local header field: last modification time, should match the CDH's. + */ + private static final ZipField.F2 F_LAST_MOD_DATE = new ZipField.F2(F_LAST_MOD_TIME.endOffset(), + "Last modification date"); + + /** + * Local header field: CRC32 checksum, should match the CDH's. 0 if there is no data. + */ + private static final ZipField.F4 F_CRC32 = new ZipField.F4(F_LAST_MOD_DATE.endOffset(), + "CRC32"); + + /** + * Local header field: compressed size, size the data takes in the zip file. + */ + private static final ZipField.F4 F_COMPRESSED_SIZE = new ZipField.F4(F_CRC32.endOffset(), + "Compressed size", new ZipFieldInvariantNonNegative()); + + /** + * Local header field: uncompressed size, size the data takes after extraction. + */ + private static final ZipField.F4 F_UNCOMPRESSED_SIZE = new ZipField.F4( + F_COMPRESSED_SIZE.endOffset(), "Uncompressed size", new ZipFieldInvariantNonNegative()); + + /** + * Local header field: length of the file name. + */ + private static final ZipField.F2 F_FILE_NAME_LENGTH = new ZipField.F2( + F_UNCOMPRESSED_SIZE.endOffset(), "@File name length", + new ZipFieldInvariantNonNegative()); + + /** + * Local header filed: length of the extra field. + */ + private static final ZipField.F2 F_EXTRA_LENGTH = new ZipField.F2( + F_FILE_NAME_LENGTH.endOffset(), "Extra length", new ZipFieldInvariantNonNegative()); + + /** + * Local header size (fixed part, not counting file name or extra field). + */ + static final int FIXED_LOCAL_FILE_HEADER_SIZE = F_EXTRA_LENGTH.endOffset(); + + /** + * Type of entry. + */ + @Nonnull + private StoredEntryType type; + + /** + * The central directory header with information about the file. + */ + @Nonnull + private CentralDirectoryHeader cdh; + + /** + * The file this entry is associated with + */ + @Nonnull + private ZFile file; + + /** + * Has this entry been deleted? + */ + private boolean deleted; + + /** + * Extra field specified in the local directory. + */ + @Nonnull + private ExtraField localExtra; + + /** + * Type of data descriptor associated with the entry. + */ + @Nonnull + private DataDescriptorType dataDescriptorType; + + /** + * Source for this entry's data. If this entry is a directory, this source has to have zero + * size. + */ + @Nonnull + private ProcessedAndRawByteSources source; + + /** + * Verify log for the entry. + */ + @Nonnull + private final VerifyLog verifyLog; + + /** + * Creates a new stored entry. + * + * @param header the header with the entry information; if the header does not contain an + * offset it means that this entry is not yet written in the zip file + * @param file the zip file containing the entry + * @param source the entry's data source; it can be {@code null} only if the source can be + * read from the zip file, that is, if {@code header.getOffset()} is non-negative + * @throws IOException failed to create the entry + */ + StoredEntry( + @Nonnull CentralDirectoryHeader header, + @Nonnull ZFile file, + @Nullable ProcessedAndRawByteSources source) + throws IOException { + cdh = header; + this.file = file; + deleted = false; + verifyLog = file.makeVerifyLog(); + + if (header.getOffset() >= 0) { + /* + * This will be overwritten during readLocalHeader. However, IJ complains if we don't + * assign a value to localExtra because of the @Nonnull annotation. + */ + localExtra = new ExtraField(); + + readLocalHeader(); + + Preconditions.checkArgument( + source == null, + "Source was defined but contents already exist on file."); + + /* + * Since the file is already in the zip, dynamically create a source that will read + * the file from the zip when needed. The assignment is not really needed, but we + * would get a warning because of the @NotNull otherwise. + */ + this.source = createSourceFromZip(cdh.getOffset()); + } else { + /* + * There is no local extra data for new files. + */ + localExtra = new ExtraField(); + + Preconditions.checkNotNull( + source, + "Source was not defined, but contents are not on file."); + this.source = source; + } + + /* + * It seems that zip utilities store directories as names ending with "/". + * This seems to be respected by all zip utilities although I could not find there anywhere + * in the specification. + */ + if (cdh.getName().endsWith(Character.toString(ZFile.SEPARATOR))) { + type = StoredEntryType.DIRECTORY; + verifyLog.verify( + this.source.getProcessedByteSource().isEmpty(), + "Directory source is not empty."); + verifyLog.verify(cdh.getCrc32() == 0, "Directory has CRC32 = %s.", cdh.getCrc32()); + verifyLog.verify( + cdh.getUncompressedSize() == 0, + "Directory has uncompressed size = %s.", + cdh.getUncompressedSize()); + + /* + * Some clever (OMG!) tools, like jar will actually try to compress the directory + * contents and generate a 2 byte compressed data. Of course, the uncompressed size is + * zero and we're just wasting space. + */ + long compressedSize = cdh.getCompressionInfoWithWait().getCompressedSize(); + verifyLog.verify( + compressedSize == 0 || compressedSize == 2, + "Directory has compressed size = %s.", compressedSize); + } else { + type = StoredEntryType.FILE; + } + + /* + * By default we assume there is no data descriptor unless the CRC is marked as deferred + * in the header's GP Bit. + */ + dataDescriptorType = DataDescriptorType.NO_DATA_DESCRIPTOR; + if (header.getGpBit().isDeferredCrc()) { + /* + * If the deferred CRC bit exists, then we have an extra descriptor field. This extra + * field may have a signature. + */ + Verify.verify(header.getOffset() >= 0, "Files that are not on disk cannot have the " + + "deferred CRC bit set."); + + try { + readDataDescriptorRecord(); + } catch (IOException e) { + throw new IOException("Failed to read data descriptor record.", e); + } + } + } + + /** + * Obtains the size of the local header of this entry. + * + * @return the local header size in bytes + */ + public int getLocalHeaderSize() { + Preconditions.checkState(!deleted, "deleted"); + return FIXED_LOCAL_FILE_HEADER_SIZE + cdh.getEncodedFileName().length + localExtra.size(); + } + + /** + * Obtains the size of the whole entry on disk, including local header and data descriptor. + * This method will wait until compression information is complete, if needed. + * + * @return the number of bytes + * @throws IOException failed to get compression information + */ + long getInFileSize() throws IOException { + Preconditions.checkState(!deleted, "deleted"); + return cdh.getCompressionInfoWithWait().getCompressedSize() + getLocalHeaderSize() + + dataDescriptorType.size; + } + + /** + * Obtains a stream that allows reading from the entry. + * + * @return a stream that will return as many bytes as the uncompressed entry size + * @throws IOException failed to open the stream + */ + @Nonnull + public InputStream open() throws IOException { + return source.getProcessedByteSource().openStream(); + } + + /** + * Obtains the contents of the file. + * + * @return a byte array with the contents of the file (uncompressed if the file was compressed) + * @throws IOException failed to read the file + */ + @Nonnull + public byte[] read() throws IOException { + try (InputStream is = open()) { + return ByteStreams.toByteArray(is); + } + } + + /** + * Obtains the contents of the file in an existing buffer. + * + * @param bytes buffer to read the file contents in. + * @return the number of bytes read + * @throws IOException failed to read the file. + */ + public int read(byte[] bytes) throws IOException { + if (bytes.length < getCentralDirectoryHeader().getUncompressedSize()) { + throw new RuntimeException( + "Buffer to small while reading {}" + getCentralDirectoryHeader().getName()); + } + try (InputStream is = new BufferedInputStream(open())) { + return ByteStreams.read(is, bytes, 0, bytes.length); + } + } + + /** + * Obtains the type of entry. + * + * @return the type of entry + */ + @Nonnull + public StoredEntryType getType() { + Preconditions.checkState(!deleted, "deleted"); + return type; + } + + /** + * Deletes this entry from the zip file. Invoking this method doesn't update the zip itself. + * To eventually write updates to disk, {@link ZFile#update()} must be called. + * + * @throws IOException failed to delete the entry + * @throws IllegalStateException if the zip file was open in read-only mode + */ + public void delete() throws IOException { + delete(true); + } + + /** + * Deletes this entry from the zip file. Invoking this method doesn't update the zip itself. + * To eventually write updates to disk, {@link ZFile#update()} must be called. + * + * @param notify should listeners be notified of the deletion? This will only be + * {@code false} if the entry is being removed as part of a replacement + * @throws IOException failed to delete the entry + * @throws IllegalStateException if the zip file was open in read-only mode + */ + void delete(boolean notify) throws IOException { + Preconditions.checkState(!deleted, "deleted"); + file.delete(this, notify); + deleted = true; + source.close(); + } + + /** + * Returns {@code true} if this entry has been deleted/replaced. + */ + public boolean isDeleted() { + return deleted; + } + + /** + * Obtains the CDH associated with this entry. + * + * @return the CDH + */ + @Nonnull + public CentralDirectoryHeader getCentralDirectoryHeader() { + return cdh; + } + + /** + * Reads the file's local header and verifies that it matches the Central Directory + * Header provided in the constructor. This method should only be called if the entry already + * exists on disk; new entries do not have local headers. + *

+ * This method will define the {@link #localExtra} field that is only defined in the + * local descriptor. + * + * @throws IOException failed to read the local header + */ + private void readLocalHeader() throws IOException { + byte[] localHeader = new byte[FIXED_LOCAL_FILE_HEADER_SIZE]; + file.directFullyRead(cdh.getOffset(), localHeader); + + CentralDirectoryHeaderCompressInfo compressInfo = cdh.getCompressionInfoWithWait(); + + ByteBuffer bytes = ByteBuffer.wrap(localHeader); + F_LOCAL_SIGNATURE.verify(bytes); + F_VERSION_EXTRACT.verify(bytes, compressInfo.getVersionExtract(), verifyLog); + F_GP_BIT.verify(bytes, cdh.getGpBit().getValue(), verifyLog); + F_METHOD.verify(bytes, compressInfo.getMethod().methodCode, verifyLog); + + if (file.areTimestampsIgnored()) { + F_LAST_MOD_TIME.skip(bytes); + F_LAST_MOD_DATE.skip(bytes); + } else { + F_LAST_MOD_TIME.verify(bytes, cdh.getLastModTime(), verifyLog); + F_LAST_MOD_DATE.verify(bytes, cdh.getLastModDate(), verifyLog); + } + + /* + * If CRC-32, compressed size and uncompressed size are deferred, their values in Local + * File Header must be ignored and their actual values must be read from the Data + * Descriptor following the contents of this entry. See readDataDescriptorRecord(). + */ + if (cdh.getGpBit().isDeferredCrc()) { + F_CRC32.skip(bytes); + F_COMPRESSED_SIZE.skip(bytes); + F_UNCOMPRESSED_SIZE.skip(bytes); + } else { + F_CRC32.verify(bytes, cdh.getCrc32(), verifyLog); + F_COMPRESSED_SIZE.verify(bytes, compressInfo.getCompressedSize(), verifyLog); + F_UNCOMPRESSED_SIZE.verify(bytes, cdh.getUncompressedSize(), verifyLog); + } + + F_FILE_NAME_LENGTH.verify(bytes, cdh.getEncodedFileName().length); + long extraLength = F_EXTRA_LENGTH.read(bytes); + long fileNameStart = cdh.getOffset() + F_EXTRA_LENGTH.endOffset(); + byte[] fileNameData = new byte[cdh.getEncodedFileName().length]; + file.directFullyRead(fileNameStart, fileNameData); + + String fileName = EncodeUtils.decode(fileNameData, cdh.getGpBit()); + if (!fileName.equals(cdh.getName())) { + verifyLog.log( + String.format( + "Central directory reports file as being named '%s' but local header" + + "reports file being named '%s'.", + cdh.getName(), + fileName)); + } + + long localExtraStart = fileNameStart + cdh.getEncodedFileName().length; + byte[] localExtraRaw = new byte[Ints.checkedCast(extraLength)]; + file.directFullyRead(localExtraStart, localExtraRaw); + localExtra = new ExtraField(localExtraRaw); + } + + /** + * Reads the data descriptor record. This method can only be invoked once it is established + * that a data descriptor does exist. It will read the data descriptor and check that the data + * described there matches the data provided in the Central Directory. + *

+ * This method will set the {@link #dataDescriptorType} field to the appropriate type of + * data descriptor record. + * + * @throws IOException failed to read the data descriptor record + */ + private void readDataDescriptorRecord() throws IOException { + CentralDirectoryHeaderCompressInfo compressInfo = cdh.getCompressionInfoWithWait(); + + long ddStart = cdh.getOffset() + FIXED_LOCAL_FILE_HEADER_SIZE + + cdh.getName().length() + localExtra.size() + compressInfo.getCompressedSize(); + byte[] ddData = new byte[DataDescriptorType.DATA_DESCRIPTOR_WITH_SIGNATURE.size]; + file.directFullyRead(ddStart, ddData); + + ByteBuffer ddBytes = ByteBuffer.wrap(ddData); + + ZipField.F4 signatureField = new ZipField.F4(0, "Data descriptor signature"); + int cpos = ddBytes.position(); + long sig = signatureField.read(ddBytes); + if (sig == DATA_DESC_SIGNATURE) { + dataDescriptorType = DataDescriptorType.DATA_DESCRIPTOR_WITH_SIGNATURE; + } else { + dataDescriptorType = DataDescriptorType.DATA_DESCRIPTOR_WITHOUT_SIGNATURE; + ddBytes.position(cpos); + } + + ZipField.F4 crc32Field = new ZipField.F4(0, "CRC32"); + ZipField.F4 compressedField = new ZipField.F4(crc32Field.endOffset(), "Compressed size"); + ZipField.F4 uncompressedField = new ZipField.F4(compressedField.endOffset(), + "Uncompressed size"); + + crc32Field.verify(ddBytes, cdh.getCrc32(), verifyLog); + compressedField.verify(ddBytes, compressInfo.getCompressedSize(), verifyLog); + uncompressedField.verify(ddBytes, cdh.getUncompressedSize(), verifyLog); + } + + /** + * Creates a new source that reads data from the zip. + * + * @param zipOffset the offset into the zip file where the data is, must be non-negative + * @throws IOException failed to close the old source + * @return the created source + */ + @Nonnull + private ProcessedAndRawByteSources createSourceFromZip(final long zipOffset) + throws IOException { + Preconditions.checkArgument(zipOffset >= 0, "zipOffset < 0"); + + final CentralDirectoryHeaderCompressInfo compressInfo; + try { + compressInfo = cdh.getCompressionInfoWithWait(); + } catch (IOException e) { + throw new RuntimeException("IOException should never occur here because compression " + + "information should be immediately available if reading from zip.", e); + } + + /* + * Create a source that will return whatever is on the zip file. + */ + CloseableByteSource rawContents = new CloseableByteSource() { + @Override + public long size() throws IOException { + return compressInfo.getCompressedSize(); + } + + @Nonnull + @Override + public InputStream openStream() throws IOException { + Preconditions.checkState(!deleted, "deleted"); + + long dataStart = zipOffset + getLocalHeaderSize(); + long dataEnd = dataStart + compressInfo.getCompressedSize(); + + file.openReadOnly(); + return file.directOpen(dataStart, dataEnd); + } + + @Override + protected void innerClose() throws IOException { + /* + * Nothing to do here. + */ + } + }; + + return createSourcesFromRawContents(rawContents); + } + + /** + * Creates a {@link ProcessedAndRawByteSources} from the raw data source . The processed source + * will either inflate or do nothing depending on the compression information that, at this + * point, should already be available + * + * @param rawContents the raw data to create the source from + * @return the sources for this entry + */ + @Nonnull + private ProcessedAndRawByteSources createSourcesFromRawContents( + @Nonnull CloseableByteSource rawContents) { + CentralDirectoryHeaderCompressInfo compressInfo; + try { + compressInfo = cdh.getCompressionInfoWithWait(); + } catch (IOException e) { + throw new RuntimeException("IOException should never occur here because compression " + + "information should be immediately available if creating from raw " + + "contents.", e); + } + + CloseableByteSource contents; + + /* + * If the contents are deflated, wrap that source in an inflater source so we get the + * uncompressed data. + */ + if (compressInfo.getMethod() == CompressionMethod.DEFLATE) { + contents = new InflaterByteSource(rawContents); + } else { + contents = rawContents; + } + + return new ProcessedAndRawByteSources(contents, rawContents); + } + + /** + * Replaces {@link #source} with one that reads file data from the zip file. + * + * @param zipFileOffset the offset in the zip file where data is written; must be non-negative + * @throws IOException failed to replace the source + */ + void replaceSourceFromZip(long zipFileOffset) throws IOException { + Preconditions.checkArgument(zipFileOffset >= 0, "zipFileOffset < 0"); + + ProcessedAndRawByteSources oldSource = source; + source = createSourceFromZip(zipFileOffset); + cdh.setOffset(zipFileOffset); + oldSource.close(); + } + + /** + * Loads all data in memory and replaces {@link #source} with one that contains all the data + * in memory. + * + *

If the entry's contents are already in memory, this call does nothing. + * + * @throws IOException failed to replace the source + */ + void loadSourceIntoMemory() throws IOException { + if (cdh.getOffset() == -1) { + /* + * No offset in the CDR means data has not been written to disk which, in turn, + * means data is already loaded into memory. + */ + return; + } + + ProcessedAndRawByteSources oldSource = source; + byte[] rawContents = oldSource.getRawByteSource().read(); + source = createSourcesFromRawContents(new CloseableDelegateByteSource( + ByteSource.wrap(rawContents), rawContents.length)); + cdh.setOffset(-1); + oldSource.close(); + } + + /** + * Obtains the source data for this entry. This method can only be called for files, it + * cannot be called for directories. + * + * @return the entry source + */ + @Nonnull + ProcessedAndRawByteSources getSource() { + return source; + } + + /** + * Obtains the type of data descriptor used in the entry. + * + * @return the type of data descriptor + */ + @Nonnull + public DataDescriptorType getDataDescriptorType() { + return dataDescriptorType; + } + + /** + * Removes the data descriptor, if it has one and resets the data descriptor bit in the + * central directory header. + * + * @return was the data descriptor remove? + */ + boolean removeDataDescriptor() { + if (dataDescriptorType == DataDescriptorType.NO_DATA_DESCRIPTOR) { + return false; + } + + dataDescriptorType = DataDescriptorType.NO_DATA_DESCRIPTOR; + cdh.resetDeferredCrc(); + return true; + } + + /** + * Obtains the local header data. + * + * @return the header data + * @throws IOException failed to get header byte data + */ + @Nonnull + byte[] toHeaderData() throws IOException { + + byte[] encodedFileName = cdh.getEncodedFileName(); + + ByteBuffer out = + ByteBuffer.allocate( + F_EXTRA_LENGTH.endOffset() + encodedFileName.length + localExtra.size()); + + CentralDirectoryHeaderCompressInfo compressInfo = cdh.getCompressionInfoWithWait(); + + F_LOCAL_SIGNATURE.write(out); + F_VERSION_EXTRACT.write(out, compressInfo.getVersionExtract()); + F_GP_BIT.write(out, cdh.getGpBit().getValue()); + F_METHOD.write(out, compressInfo.getMethod().methodCode); + + if (file.areTimestampsIgnored()) { + F_LAST_MOD_TIME.write(out, 0); + F_LAST_MOD_DATE.write(out, 0); + } else { + F_LAST_MOD_TIME.write(out, cdh.getLastModTime()); + F_LAST_MOD_DATE.write(out, cdh.getLastModDate()); + } + + F_CRC32.write(out, cdh.getCrc32()); + F_COMPRESSED_SIZE.write(out, compressInfo.getCompressedSize()); + F_UNCOMPRESSED_SIZE.write(out, cdh.getUncompressedSize()); + F_FILE_NAME_LENGTH.write(out, cdh.getEncodedFileName().length); + F_EXTRA_LENGTH.write(out, localExtra.size()); + + out.put(cdh.getEncodedFileName()); + localExtra.write(out); + + return out.array(); + } + + /** + * Requests that this entry be realigned. If this entry is already aligned according to the + * rules in {@link ZFile} then this method does nothing. Otherwise it will move the file's data + * into memory and place it in a different area of the zip. + * + * @return has this file been changed? Note that if the entry has not yet been written on the + * file, realignment does not count as a change as nothing needs to be updated in the file; + * also, if the entry has been changed, this object may have been marked as deleted and a new + * stored entry may need to be fetched from the file + * @throws IOException failed to realign the entry; the entry may no longer exist in the zip + * file + */ + public boolean realign() throws IOException { + Preconditions.checkState(!deleted, "Entry has been deleted."); + + return file.realign(this); + } + + /** + * Obtains the contents of the local extra field. + * + * @return the contents of the local extra field + */ + @Nonnull + public ExtraField getLocalExtra() { + return localExtra; + } + + /** + * Sets the contents of the local extra field. + * + * @param localExtra the contents of the local extra field + * @throws IOException failed to update the zip file + */ + public void setLocalExtra(@Nonnull ExtraField localExtra) throws IOException { + boolean resized = setLocalExtraNoNotify(localExtra); + file.localHeaderChanged(this, resized); + } + + /** + * Sets the contents of the local extra field, does not notify the {@link ZFile} of the change. + * This is used internally when the {@link ZFile} itself wants to change the local extra and + * doesn't need the callback. + * + * @param localExtra the contents of the local extra field + * @return has the local header size changed? + * @throws IOException failed to load the file + */ + boolean setLocalExtraNoNotify(@Nonnull ExtraField localExtra) throws IOException { + boolean sizeChanged; + + /* + * Make sure we load into memory. + * + * If we change the size of the local header, the actual start of the file changes + * according to our in-memory structures so, if we don't read the file now, we won't be + * able to load it later :) + * + * But, even if the size doesn't change, we need to read it force the entry to be + * rewritten otherwise the changes in the local header aren't written. Of course this case + * may be optimized with some extra complexity added :) + */ + loadSourceIntoMemory(); + + if (this.localExtra.size() != localExtra.size()) { + sizeChanged = true; + } else { + sizeChanged = false; + } + + this.localExtra = localExtra; + return sizeChanged; + } + + /** + * Obtains the verify log for the entry. + * + * @return the verify log + */ + @Nonnull + public VerifyLog getVerifyLog() { + return verifyLog; + } +} diff --git a/src/main/java/com/android/tools/build/apkzlib/zip/StoredEntryType.java b/src/main/java/com/android/tools/build/apkzlib/zip/StoredEntryType.java new file mode 100644 index 0000000..736a813 --- /dev/null +++ b/src/main/java/com/android/tools/build/apkzlib/zip/StoredEntryType.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * 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.android.tools.build.apkzlib.zip; + +/** + * Type of stored entry. + */ +public enum StoredEntryType { + /** + * Entry is a file. + */ + FILE, + + /** + * Entry is a directory. + */ + DIRECTORY +} diff --git a/src/main/java/com/android/tools/build/apkzlib/zip/VerifyLog.java b/src/main/java/com/android/tools/build/apkzlib/zip/VerifyLog.java new file mode 100644 index 0000000..4d8ebf8 --- /dev/null +++ b/src/main/java/com/android/tools/build/apkzlib/zip/VerifyLog.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * 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.android.tools.build.apkzlib.zip; + +import com.google.common.collect.ImmutableList; +import javax.annotation.Nonnull; + +/** + * The verify log contains verification messages. It is used to capture validation issues with a + * zip file or with parts of a zip file. + */ +public interface VerifyLog { + + /** + * Logs a message. + * + * @param message the message to verify + */ + void log(@Nonnull String message); + + /** + * Obtains all save logged messages. + * + * @return the logged messages + */ + @Nonnull + ImmutableList getLogs(); + + /** + * Performs verification of a non-critical condition, logging a message if the condition is + * not verified. + * + * @param condition the condition + * @param message the message to write if {@code condition} is {@code false}. + * @param args arguments for formatting {@code message} using {@code String.format} + */ + default void verify(boolean condition, @Nonnull String message, @Nonnull Object... args) { + if (!condition) { + log(String.format(message, args)); + } + } +} diff --git a/src/main/java/com/android/tools/build/apkzlib/zip/VerifyLogs.java b/src/main/java/com/android/tools/build/apkzlib/zip/VerifyLogs.java new file mode 100644 index 0000000..d1bea7a --- /dev/null +++ b/src/main/java/com/android/tools/build/apkzlib/zip/VerifyLogs.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * 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.android.tools.build.apkzlib.zip; + +import com.google.common.collect.ImmutableList; +import java.util.ArrayList; +import java.util.List; +import javax.annotation.Nonnull; + +/** + * Factory for verification logs. + */ +final class VerifyLogs { + + private VerifyLogs() {} + + /** + * Creates a {@link VerifyLog} that ignores all messages logged. + * + * @return the log + */ + @Nonnull + static VerifyLog devNull() { + return new VerifyLog() { + @Override + public void log(@Nonnull String message) {} + + @Nonnull + @Override + public ImmutableList getLogs() { + return ImmutableList.of(); + } + }; + } + + /** + * Creates a {@link VerifyLog} that stores all log messages. + * + * @return the log + */ + @Nonnull + static VerifyLog unlimited() { + return new VerifyLog() { + + /** + * All saved messages. + */ + @Nonnull + private final List messages = new ArrayList<>(); + + @Override + public void log(@Nonnull String message) { + messages.add(message); + } + + @Nonnull + @Override + public ImmutableList getLogs() { + return ImmutableList.copyOf(messages); + } + }; + } +} diff --git a/src/main/java/com/android/tools/build/apkzlib/zip/ZFile.java b/src/main/java/com/android/tools/build/apkzlib/zip/ZFile.java new file mode 100644 index 0000000..b7949b5 --- /dev/null +++ b/src/main/java/com/android/tools/build/apkzlib/zip/ZFile.java @@ -0,0 +1,2764 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * 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.android.tools.build.apkzlib.zip; + +import com.android.tools.build.apkzlib.utils.CachedFileContents; +import com.android.tools.build.apkzlib.utils.IOExceptionFunction; +import com.android.tools.build.apkzlib.utils.IOExceptionRunnable; +import com.android.tools.build.apkzlib.zip.compress.Zip64NotSupportedException; +import com.android.tools.build.apkzlib.zip.utils.ByteTracker; +import com.android.tools.build.apkzlib.zip.utils.CloseableByteSource; +import com.android.tools.build.apkzlib.zip.utils.LittleEndianUtils; +import com.google.common.base.Preconditions; +import com.google.common.base.Verify; +import com.google.common.base.VerifyException; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.google.common.collect.Sets; +import com.google.common.hash.Hashing; +import com.google.common.io.ByteSource; +import com.google.common.io.Closer; +import com.google.common.io.Files; +import com.google.common.primitives.Ints; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.MoreExecutors; +import com.google.common.util.concurrent.SettableFuture; +import java.io.ByteArrayInputStream; +import java.io.Closeable; +import java.io.EOFException; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.RandomAccessFile; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.SortedSet; +import java.util.TreeMap; +import java.util.TreeSet; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.Supplier; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * The {@code ZFile} provides the main interface for interacting with zip files. A {@code ZFile} + * can be created on a new file or in an existing file. Once created, files can be added or removed + * from the zip file. + * + *

Changes in the zip file are always deferred. Any change requested is made in memory and + * written to disk only when {@link #update()} or {@link #close()} is invoked. + * + *

Zip files are open initially in read-only mode and will switch to read-write when needed. This + * is done automatically. Because modifications to the file are done in-memory, the zip file can + * be manipulated when closed. When invoking {@link #update()} or {@link #close()} the zip file + * will be reopen and changes will be written. However, the zip file cannot be modified outside + * the control of {@code ZFile}. So, if a {@code ZFile} is closed, modified outside and then a file + * is added or removed from the zip file, when reopening the zip file, {@link ZFile} will detect + * the outside modification and will fail. + * + *

In memory manipulation means that files added to the zip file are kept in memory until written + * to disk. This provides much faster operation and allows better zip file allocation (see below). + * It may, however, increase the memory footprint of the application. When adding large files, if + * memory consumption is a concern, a call to {@link #update()} will actually write the file to + * disk and discard the memory buffer. Information about allocation can be obtained from a + * {@link ByteTracker} that can be given to the file on creation. + * + *

{@code ZFile} keeps track of allocation inside of the zip file. If a file is deleted, its + * space is marked as freed and will be reused for an added file if it fits in the space. + * Allocation of files to empty areas is done using a best fit algorithm. When adding a + * file, if it doesn't fit in any free area, the zip file will be extended. + * + *

{@code ZFile} provides a fast way to merge data from another zip file + * (see {@link #mergeFrom(ZFile, Predicate)}) avoiding recompression and copying of equal files. + * When merging, patterns of files may be provided that are ignored. This allows handling special + * files in the merging process, such as files in {@code META-INF}. + * + *

When adding files to the zip file, unless files are explicitly required to be stored, files + * will be deflated. However, deflating will not occur if the deflated file is larger then the + * stored file, e.g. if compression would yield a bigger file. See {@link Compressor} for + * details on how compression works. + * + *

Because {@code ZFile} was designed to be used in a build system and not as general-purpose + * zip utility, it is very strict (and unforgiving) about the zip format and unsupported features. + * + *

{@code ZFile} supports alignment. Alignment means that file data (not entries -- the + * local header must be discounted) must start at offsets that are multiple of a number -- the + * alignment. Alignment is defined by an alignment rules ({@link AlignmentRule} in the + * {@link ZFileOptions} object used to create the {@link ZFile}. + * + *

When a file is added to the zip, the alignment rules will be checked and alignment will be + * honored when positioning the file in the zip. This means that unused spaces in the zip may + * be generated as a result. However, alignment of existing entries will not be changed. + * + *

Entries can be realigned individually (see {@link StoredEntry#realign()} or the full zip file + * may be realigned (see {@link #realign()}). When realigning the full zip entries that are already + * aligned will not be affected. + * + *

Because realignment may cause files to move in the zip, realignment is done in-memory meaning + * that files that need to change location will moved to memory and will only be flushed when + * either {@link #update()} or {@link #close()} are called. + * + *

Alignment only applies to filed that are forced to be uncompressed. This is because alignment + * is used to allow mapping files in the archive directly into memory and compressing defeats the + * purpose of alignment. + * + *

Manipulating zip files with {@link ZFile} may yield zip files with empty spaces between files. + * This happens in two situations: (1) if alignment is required, files may be shifted to conform to + * the request alignment leaving an empty space before the previous file, and (2) if a file is + * removed or replaced with a file that does not fit the space it was in. By default, {@link ZFile} + * does not do any special processing in these situations. Files are indexed by their offsets from + * the central directory and empty spaces can exist in the zip file. + * + *

However, it is possible to tell {@link ZFile} to use the extra field in the local header + * to do cover the empty spaces. This is done by setting + * {@link ZFileOptions#setCoverEmptySpaceUsingExtraField(boolean)} to {@code true}. This has the + * advantage of leaving no gaps between entries in the zip, as required by some tools like Oracle's + * {code jar} tool. However, setting this option will destroy the contents of the file's extra + * field. + * + *

Activating {@link ZFileOptions#setCoverEmptySpaceUsingExtraField(boolean)} may lead to + * virtual files being added to the zip file. Since extra field is limited to 64k, it is not + * possible to cover any space bigger than that using the extra field. In those cases, virtual + * files are added to the file. A virtual file is a file that exists in the actual zip data, + * but is not referenced from the central directory. A zip-compliant utility should ignore these + * files. However, zip utilities that expect the zip to be a stream, such as Oracle's jar, will + * find these files instead of considering the zip to be corrupt. + * + *

{@code ZFile} support sorting zip files. Sorting (done through the {@link #sortZipContents()} + * method) is a process by which all files are re-read into memory, if not already in memory, + * removed from the zip and re-added in alphabetical order, respecting alignment rules. So, in + * general, file {@code b} will come after file {@code a} unless file {@code a} is subject to + * alignment that forces an empty space before that can be occupied by {@code b}. Sorting can be + * used to minimize the changes between two zips. + * + *

Sorting in {@code ZFile} can be done manually or automatically. Manual sorting is done by + * invoking {@link #sortZipContents()}. Automatic sorting is done by setting the + * {@link ZFileOptions#getAutoSortFiles()} option when creating the {@code ZFile}. Automatic + * sorting invokes {@link #sortZipContents()} immediately when doing an {@link #update()} after + * all extensions have processed the {@link ZFileExtension#beforeUpdate()}. This has the guarantee + * that files added by extensions will be sorted, something that does not happen if the invocation + * is sequential, i.e., {@link #sortZipContents()} called before {@link #update()}. The + * drawback of automatic sorting is that sorting will happen every time {@link #update()} is + * called and the file is dirty having a possible penalty in performance. + * + *

To allow whole-apk signing, the {@code ZFile} allows the central directory location to be + * offset by a fixed amount. This amount can be set using the {@link #setExtraDirectoryOffset(long)} + * method. Setting a non-zero value will add extra (unused) space in the zip file before the + * central directory. This value can be changed at any time and it will force the central directory + * rewritten when the file is updated or closed. + * + *

{@code ZFile} provides an extension mechanism to allow objects to register with the file + * and be notified when changes to the file happen. This should be used + * to add extra features to the zip file while providing strong decoupling. See + * {@link ZFileExtension}, {@link ZFile#addZFileExtension(ZFileExtension)} and + * {@link ZFile#removeZFileExtension(ZFileExtension)}. + * + *

This class is not thread-safe. Neither are any of the classes associated with + * it in this package, except when otherwise noticed. + */ +public class ZFile implements Closeable { + + /** + * The file separator in paths in the zip file. This is fixed by the zip specification + * (section 4.4.17). + */ + public static final char SEPARATOR = '/'; + + /** + * Minimum size the EOCD can have. + */ + private static final int MIN_EOCD_SIZE = 22; + + /** + * Number of bytes of the Zip64 EOCD locator record. + */ + private static final int ZIP64_EOCD_LOCATOR_SIZE = 20; + + /** + * Maximum size for the EOCD. + */ + private static final int MAX_EOCD_COMMENT_SIZE = 65535; + + /** + * How many bytes to look back from the end of the file to look for the EOCD signature. + */ + private static final int LAST_BYTES_TO_READ = MIN_EOCD_SIZE + MAX_EOCD_COMMENT_SIZE; + + /** + * Signature of the Zip64 EOCD locator record. + */ + private static final int ZIP64_EOCD_LOCATOR_SIGNATURE = 0x07064b50; + + /** + * Signature of the EOCD record. + */ + private static final byte[] EOCD_SIGNATURE = new byte[] { 0x06, 0x05, 0x4b, 0x50 }; + + /** + * Size of buffer for I/O operations. + */ + private static final int IO_BUFFER_SIZE = 1024 * 1024; + + /** + * When extensions request re-runs, we do maximum number of cycles until we decide to stop and + * flag a infinite recursion problem. + */ + private static final int MAXIMUM_EXTENSION_CYCLE_COUNT = 10; + + /** + * Minimum size for the extra field when we have to add one. We rely on the alignment segment + * to do that so the minimum size for the extra field is the minimum size of an alignment + * segment. + */ + private static final int MINIMUM_EXTRA_FIELD_SIZE = ExtraField.AlignmentSegment.MINIMUM_SIZE; + + /** + * Maximum size of the extra field. + * + *

Theoretically, this is (1 << 16) - 1 = 65535 and not (1 < 15) -1 = 32767. However, due to + * http://b.android.com/221703, we need to keep this limited. + */ + private static final int MAX_LOCAL_EXTRA_FIELD_CONTENTS_SIZE = (1 << 15) - 1; + + /** + * File zip file. + */ + @Nonnull + private final File file; + + /** + * The random access file used to access the zip file. This will be {@code null} if and only + * if {@link #state} is {@link ZipFileState#CLOSED}. + */ + @Nullable + private RandomAccessFile raf; + + /** + * The map containing the in-memory contents of the zip file. It keeps track of which parts of + * the zip file are used and which are not. + */ + @Nonnull + private final FileUseMap map; + + /** + * The EOCD entry. Will be {@code null} if there is no EOCD (because the zip is new) or the + * one that exists on disk is no longer valid (because the zip has been changed). + * + *

If the EOCD is deleted because the zip has been changed and the old EOCD was no longer + * valid, then {@link #eocdComment} will contain the comment saved from the EOCD. + */ + @Nullable + private FileUseMapEntry eocdEntry; + + /** + * The Central Directory entry. Will be {@code null} if there is no Central Directory (because + * the zip is new) or because the one that exists on disk is no longer valid (because the zip + * has been changed). + */ + @Nullable + private FileUseMapEntry directoryEntry; + + /** + * All entries in the zip file. It includes in-memory changes and may not reflect what is + * written on disk. Only entries that have been compressed are in this list. + */ + @Nonnull + private final Map> entries; + + /** + * Entries added to the zip file, but that are not yet compressed. When compression is done, + * these entries are eventually moved to {@link #entries}. uncompressedEntries is a list + * because entries need to be kept in the order by which they were added. It allows adding + * multiple files with the same name and getting the right notifications on which files replaced + * which. + * + *

Files are placed in this list in {@link #add(StoredEntry)} method. This method will + * keep files here temporarily and move then to {@link #entries} when the data is + * available. + * + *

Moving files out of this list to {@link #entries} is done by + * {@link #processAllReadyEntries()}. + */ + @Nonnull + private final List uncompressedEntries; + + /** + * Current state of the zip file. + */ + @Nonnull + private ZipFileState state; + + /** + * Are the in-memory changes that have not been written to the zip file? + * + *

This might be false, but will become true after {@link #processAllReadyEntriesWithWait()} + * is called if there are {@link #uncompressedEntries} compressing in the background. + */ + private boolean dirty; + + /** + * Non-{@code null} only if the file is currently closed. Used to detect if the zip is + * modified outside this object's control. If the file has never been written, this will + * be {@code null} even if it is closed. + */ + @Nullable + private CachedFileContents closedControl; + + /** + * The alignment rule. + */ + @Nonnull + private final AlignmentRule alignmentRule; + + /** + * Extensions registered with the file. + */ + @Nonnull + private final List extensions; + + /** + * When notifying extensions, extensions may request that some runnables are executed. This + * list collects all runnables by the order they were requested. Together with + * {@link #isNotifying}, it is used to avoid reordering notifications. + */ + @Nonnull + private final List toRun; + + /** + * {@code true} when {@link #notify(com.android.tools.build.apkzlib.utils.IOExceptionFunction)} + * is notifying extensions. Used to avoid reordering notifications. + */ + private boolean isNotifying; + + /** + * An extra offset for the central directory location. {@code 0} if the central directory + * should be written in its standard location. + */ + private long extraDirectoryOffset; + + /** + * Should all timestamps be zeroed when reading / writing the zip? + */ + private boolean noTimestamps; + + /** + * Compressor to use. + */ + @Nonnull + private Compressor compressor; + + /** + * Byte tracker to use. + */ + @Nonnull + private final ByteTracker tracker; + + /** + * Use the zip entry's "extra field" field to cover empty space in the zip file? + */ + private boolean coverEmptySpaceUsingExtraField; + + /** + * Should files be automatically sorted when updating? + */ + private boolean autoSortFiles; + + /** + * Verify log factory to use. + */ + @Nonnull + private final Supplier verifyLogFactory; + + /** + * Verify log to use. + */ + @Nonnull + private final VerifyLog verifyLog; + + /** + * This field contains the comment in the zip's EOCD if there is no in-memory EOCD structure. + * This may happen, for example, if the zip has been changed and the Central Directory and + * EOCD have been deleted (in-memory). In that case, this field will save the comment to place + * on the EOCD once it is created. + * + *

This field will only be non-{@code null} if there is no in-memory EOCD structure + * (i.e., {@link #eocdEntry} is {@code null}). If there is an {@link #eocdEntry}, then + * the comment will be there instead of being in this field. + */ + @Nullable + private byte[] eocdComment; + + /** + * Is the file in read-only mode? In read-only mode no changes are allowed. + */ + private boolean readOnly; + + + /** + * Creates a new zip file. If the zip file does not exist, then no file is created at this + * point and {@code ZFile} will contain an empty structure. However, an (empty) zip file will + * be created if either {@link #update()} or {@link #close()} are used. If a zip file exists, + * it will be parsed and read. + * + * @param file the zip file + * @throws IOException some file exists but could not be read + */ + public ZFile(@Nonnull File file) throws IOException { + this(file, new ZFileOptions()); + } + + /** + * Creates a new zip file. If the zip file does not exist, then no file is created at this + * point and {@code ZFile} will contain an empty structure. However, an (empty) zip file will + * be created if either {@link #update()} or {@link #close()} are used. If a zip file exists, + * it will be parsed and read. + * + * @param file the zip file + * @param options configuration options + * @throws IOException some file exists but could not be read + */ + public ZFile(@Nonnull File file, @Nonnull ZFileOptions options) throws IOException { + this(file, options, false); + } + + /** + * Creates a new zip file. If the zip file does not exist, then no file is created at this + * point and {@code ZFile} will contain an empty structure. However, an (empty) zip file will + * be created if either {@link #update()} or {@link #close()} are used. If a zip file exists, + * it will be parsed and read. + * + * @param file the zip file + * @param options configuration options + * @param readOnly should the file be open in read-only mode? If {@code true} then the file must + * exist and no methods can be invoked that could potentially change the file + * @throws IOException some file exists but could not be read + */ + public ZFile(@Nonnull File file, @Nonnull ZFileOptions options, boolean readOnly) + throws IOException { + this.file = file; + map = new FileUseMap( + 0, + options.getCoverEmptySpaceUsingExtraField() + ? MINIMUM_EXTRA_FIELD_SIZE + : 0); + this.readOnly = readOnly; + dirty = false; + closedControl = null; + alignmentRule = options.getAlignmentRule(); + extensions = Lists.newArrayList(); + toRun = Lists.newArrayList(); + noTimestamps = options.getNoTimestamps(); + tracker = options.getTracker(); + compressor = options.getCompressor(); + coverEmptySpaceUsingExtraField = options.getCoverEmptySpaceUsingExtraField(); + autoSortFiles = options.getAutoSortFiles(); + verifyLogFactory = options.getVerifyLogFactory(); + verifyLog = verifyLogFactory.get(); + + /* + * These two values will be overwritten by openReadOnly() below if the file exists. + */ + state = ZipFileState.CLOSED; + raf = null; + + if (file.exists()) { + openReadOnly(); + } else if (readOnly) { + throw new IOException("File does not exist but read-only mode requested"); + } else { + dirty = true; + } + + entries = Maps.newHashMap(); + uncompressedEntries = Lists.newArrayList(); + extraDirectoryOffset = 0; + + try { + if (state != ZipFileState.CLOSED) { + long rafSize = raf.length(); + if (rafSize > Integer.MAX_VALUE) { + throw new IOException("File exceeds size limit of " + Integer.MAX_VALUE + "."); + } + + map.extend(Ints.checkedCast(rafSize)); + readData(); + } + + // If we don't have an EOCD entry, set the comment to empty. + if (eocdEntry == null) { + eocdComment = new byte[0]; + } + + // Notify the extensions if the zip file has been open. + if (state != ZipFileState.CLOSED) { + notify(ZFileExtension::open); + } + } catch (Zip64NotSupportedException e) { + throw e; + } catch (IOException e) { + throw new IOException("Failed to read zip file '" + file.getAbsolutePath() + "'.", e); + } catch (IllegalStateException | IllegalArgumentException | VerifyException e) { + throw new RuntimeException( + "Internal error when trying to read zip file '" + file.getAbsolutePath() + "'.", + e); + } + } + + /** + * Obtains all entries in the file. Entries themselves may be or not written in disk. However, + * all of them can be open for reading. + * + * @return all entries in the zip + */ + @Nonnull + public Set entries() { + Map entries = Maps.newHashMap(); + + for (FileUseMapEntry mapEntry : this.entries.values()) { + StoredEntry entry = mapEntry.getStore(); + assert entry != null; + entries.put(entry.getCentralDirectoryHeader().getName(), entry); + } + + /* + * mUncompressed may override mEntriesReady as we may not have yet processed all + * entries. + */ + for (StoredEntry uncompressed : uncompressedEntries) { + entries.put(uncompressed.getCentralDirectoryHeader().getName(), uncompressed); + } + + return Sets.newHashSet(entries.values()); + } + + /** + * Obtains an entry at a given path in the zip. + * + * @param path the path + * @return the entry at the path or {@code null} if none exists + */ + @Nullable + public StoredEntry get(@Nonnull String path) { + /* + * The latest entries are the last ones in uncompressed and they may eventually override + * files in entries. + */ + for (StoredEntry stillUncompressed : Lists.reverse(uncompressedEntries)) { + if (stillUncompressed.getCentralDirectoryHeader().getName().equals(path)) { + return stillUncompressed; + } + } + + FileUseMapEntry found = entries.get(path); + if (found == null) { + return null; + } + + return found.getStore(); + } + + /** + * Reads all the data in the zip file, except the contents of the entries themselves. This + * method will populate the directory and maps in the instance variables. + * + * @throws IOException failed to read the zip file + */ + private void readData() throws IOException { + Preconditions.checkState(state != ZipFileState.CLOSED, "state == ZipFileState.CLOSED"); + Preconditions.checkState(raf != null, "raf == null"); + + readEocd(); + readCentralDirectory(); + + /* + * Go over all files and create the usage map, verifying there is no overlap in the files. + */ + long entryEndOffset; + long directoryStartOffset; + + if (directoryEntry != null) { + CentralDirectory directory = directoryEntry.getStore(); + assert directory != null; + + entryEndOffset = 0; + + for (StoredEntry entry : directory.getEntries().values()) { + long start = entry.getCentralDirectoryHeader().getOffset(); + long end = start + entry.getInFileSize(); + + /* + * If isExtraAlignmentBlock(entry.getLocalExtra()) is true, we know the entry + * has an extra field that is solely used for alignment. This means the + * actual entry could start at start + extra.length and leave space before. + * + * But, if we did this here, we would be modifying the zip file and that is + * weird because we're just opening it for reading. + * + * The downside is that we will never reuse that space. Maybe one day ZFile + * can be clever enough to remove the local extra when we start modifying the zip + * file. + */ + + Verify.verify(start >= 0, "start < 0"); + Verify.verify(end < map.size(), "end >= map.size()"); + + FileUseMapEntry found = map.at(start); + Verify.verifyNotNull(found); + + // We've got a problem if the found entry is not free or is a free entry but + // doesn't cover the whole file. + if (!found.isFree() || found.getEnd() < end) { + if (found.isFree()) { + found = map.after(found); + Verify.verify(found != null && !found.isFree()); + } + + Object foundEntry = found.getStore(); + Verify.verify(foundEntry != null); + + // Obtains a custom description of an entry. + IOExceptionFunction describe = + e -> + String.format( + "'%s' (offset: %d, size: %d)", + e.getCentralDirectoryHeader().getName(), + e.getCentralDirectoryHeader().getOffset(), + e.getInFileSize()); + + String overlappingEntryDescription; + if (foundEntry instanceof StoredEntry) { + StoredEntry foundStored = (StoredEntry) foundEntry; + overlappingEntryDescription = describe.apply((StoredEntry) foundEntry); + } else { + overlappingEntryDescription = + "Central Directory / EOCD: " + + found.getStart() + + " - " + + found.getEnd(); + } + + throw new IOException( + "Cannot read entry " + + describe.apply(entry) + + " because it overlaps with " + + overlappingEntryDescription); + } + + FileUseMapEntry mapEntry = map.add(start, end, entry); + entries.put(entry.getCentralDirectoryHeader().getName(), mapEntry); + + if (end > entryEndOffset) { + entryEndOffset = end; + } + } + + directoryStartOffset = directoryEntry.getStart(); + } else { + /* + * No directory means an empty zip file. Use the start of the EOCD to compute + * an existing offset. + */ + Verify.verifyNotNull(eocdEntry); + assert eocdEntry != null; + directoryStartOffset = eocdEntry.getStart(); + entryEndOffset = 0; + } + + /* + * Check if there is an extra central directory offset. If there is, save it. Note that + * we can't call extraDirectoryOffset() because that would mark the file as dirty. + */ + long extraOffset = directoryStartOffset - entryEndOffset; + Verify.verify(extraOffset >= 0, "extraOffset (%s) < 0", extraOffset); + extraDirectoryOffset = extraOffset; + } + + /** + * Finds the EOCD marker and reads it. It will populate the {@link #eocdEntry} variable. + * + * @throws IOException failed to read the EOCD + */ + private void readEocd() throws IOException { + Preconditions.checkState(state != ZipFileState.CLOSED, "state == ZipFileState.CLOSED"); + Preconditions.checkState(raf != null, "raf == null"); + + /* + * Read the last part of the zip into memory. If we don't find the EOCD signature by then, + * the file is corrupt. + */ + int lastToRead = LAST_BYTES_TO_READ; + if (lastToRead > raf.length()) { + lastToRead = Ints.checkedCast(raf.length()); + } + + byte[] last = new byte[lastToRead]; + directFullyRead(raf.length() - lastToRead, last); + + + /* + * Start endIdx at the first possible location where the signature can be located and then + * move backwards. Because the EOCD must have at least MIN_EOCD size, the first byte of the + * signature (and first byte of the EOCD) must be located at last.length - MIN_EOCD_SIZE. + * + * Because the EOCD signature may exist in the file comment, when we find a signature we + * will try to read the Eocd. If we fail, we continue searching for the signature. However, + * we will keep the last exception in case we don't find any signature. + */ + Eocd eocd = null; + int foundEocdSignature = -1; + IOException errorFindingSignature = null; + int eocdStart = -1; + + for (int endIdx = last.length - MIN_EOCD_SIZE; endIdx >= 0 && foundEocdSignature == -1; + endIdx--) { + /* + * Remember: little endian... + */ + if (last[endIdx] == EOCD_SIGNATURE[3] + && last[endIdx + 1] == EOCD_SIGNATURE[2] + && last[endIdx + 2] == EOCD_SIGNATURE[1] + && last[endIdx + 3] == EOCD_SIGNATURE[0]) { + + /* + * We found a signature. Try to read the EOCD record. + */ + + foundEocdSignature = endIdx; + ByteBuffer eocdBytes = + ByteBuffer.wrap(last, foundEocdSignature, last.length - foundEocdSignature); + + try { + eocd = new Eocd(eocdBytes); + eocdStart = Ints.checkedCast(raf.length() - lastToRead + foundEocdSignature); + + /* + * Make sure the EOCD takes the whole file up to the end. Log an error if it + * doesn't. + */ + if (eocdStart + eocd.getEocdSize() != raf.length()) { + verifyLog.log("EOCD starts at " + + eocdStart + + " and has " + + eocd.getEocdSize() + + " bytes, but file ends at " + + raf.length() + + "."); + } + } catch (IOException e) { + if (errorFindingSignature != null) { + e.addSuppressed(errorFindingSignature); + } + + errorFindingSignature = e; + foundEocdSignature = -1; + eocd = null; + } + } + } + + if (foundEocdSignature == -1) { + throw new IOException("EOCD signature not found in the last " + + lastToRead + " bytes of the file.", errorFindingSignature); + } + + Verify.verify(eocdStart >= 0); + + /* + * Look for the Zip64 central directory locator. If we find it, then this file is a Zip64 + * file and we do not support it. + */ + int zip64LocatorStart = eocdStart - ZIP64_EOCD_LOCATOR_SIZE; + if (zip64LocatorStart >= 0) { + byte[] possibleZip64Locator = new byte[4]; + directFullyRead(zip64LocatorStart, possibleZip64Locator); + if (LittleEndianUtils.readUnsigned4Le(ByteBuffer.wrap(possibleZip64Locator)) == + ZIP64_EOCD_LOCATOR_SIGNATURE) { + throw new Zip64NotSupportedException( + "Zip64 EOCD locator found but Zip64 format is not supported."); + } + } + + eocdEntry = map.add(eocdStart, eocdStart + eocd.getEocdSize(), eocd); + } + + /** + * Reads the zip's central directory and populates the {@link #directoryEntry} variable. This + * method can only be called after the EOCD has been read. If the central directory is empty + * (if there are no files on the zip archive), then {@link #directoryEntry} will be set to + * {@code null}. + * + * @throws IOException failed to read the central directory + */ + private void readCentralDirectory() throws IOException { + Preconditions.checkNotNull(eocdEntry, "eocdEntry == null"); + Preconditions.checkNotNull(eocdEntry.getStore(), "eocdEntry.getStore() == null"); + Preconditions.checkState(state != ZipFileState.CLOSED, "state == ZipFileState.CLOSED"); + Preconditions.checkState(raf != null, "raf == null"); + Preconditions.checkState(directoryEntry == null, "directoryEntry != null"); + + Eocd eocd = eocdEntry.getStore(); + + long dirSize = eocd.getDirectorySize(); + if (dirSize > Integer.MAX_VALUE) { + throw new IOException("Cannot read central directory with size " + dirSize + "."); + } + + long centralDirectoryEnd = eocd.getDirectoryOffset() + dirSize; + if (centralDirectoryEnd != eocdEntry.getStart()) { + String msg = "Central directory is stored in [" + + eocd.getDirectoryOffset() + + " - " + + (centralDirectoryEnd - 1) + + "] and EOCD starts at " + + eocdEntry.getStart() + + "."; + + /* + * If there is an empty space between the central directory and the EOCD, we proceed + * logging an error. If the central directory ends after the start of the EOCD (and + * therefore, they overlap), throw an exception. + */ + if (centralDirectoryEnd > eocdEntry.getSize()) { + throw new IOException(msg); + } else { + verifyLog.log(msg); + } + } + + byte[] directoryData = new byte[Ints.checkedCast(dirSize)]; + directFullyRead(eocd.getDirectoryOffset(), directoryData); + + CentralDirectory directory = + CentralDirectory.makeFromData( + ByteBuffer.wrap(directoryData), + eocd.getTotalRecords(), + this); + if (eocd.getDirectorySize() > 0) { + directoryEntry = map.add( + eocd.getDirectoryOffset(), + eocd.getDirectoryOffset() + eocd.getDirectorySize(), + directory); + } + } + + /** + * Opens a portion of the zip for reading. The zip must be open for this method to be invoked. + * Note that if the zip has not been updated, the individual zip entries may not have been + * written yet. + * + * @param start the index within the zip file to start reading + * @param end the index within the zip file to end reading (the actual byte pointed by + * end will not be read) + * @return a stream that will read the portion of the file; no decompression is done, data is + * returned as is + * @throws IOException failed to open the zip file + */ + @Nonnull + public InputStream directOpen(final long start, final long end) throws IOException { + Preconditions.checkState(state != ZipFileState.CLOSED, "state == ZipFileState.CLOSED"); + Preconditions.checkState(raf != null, "raf == null"); + Preconditions.checkArgument(start >= 0, "start < 0"); + Preconditions.checkArgument(end >= start, "end < start"); + Preconditions.checkArgument(end <= raf.length(), "end > raf.length()"); + + return new InputStream() { + private long mCurr = start; + + @Override + public int read() throws IOException { + if (mCurr == end) { + return -1; + } + + byte[] b = new byte[1]; + int r = directRead(mCurr, b); + if (r > 0) { + mCurr++; + return b[0]; + } else { + return -1; + } + } + + @Override + public int read(@Nonnull byte[] b, int off, int len) throws IOException { + Preconditions.checkNotNull(b, "b == null"); + Preconditions.checkArgument(off >= 0, "off < 0"); + Preconditions.checkArgument(off <= b.length, "off > b.length"); + Preconditions.checkArgument(len >= 0, "len < 0"); + Preconditions.checkArgument(off + len <= b.length, "off + len > b.length"); + + long availableToRead = end - mCurr; + long toRead = Math.min(len, availableToRead); + + if (toRead == 0) { + return -1; + } + + if (toRead > Integer.MAX_VALUE) { + throw new IOException("Cannot read " + toRead + " bytes."); + } + + int r = directRead(mCurr, b, off, Ints.checkedCast(toRead)); + if (r > 0) { + mCurr += r; + } + + return r; + } + }; + } + + /** + * Deletes an entry from the zip. This method does not actually delete anything on disk. It + * just changes in-memory structures. Use {@link #update()} to update the contents on disk. + * + * @param entry the entry to delete + * @param notify should listeners be notified of the deletion? This will only be + * {@code false} if the entry is being removed as part of a replacement + * @throws IOException failed to delete the entry + * @throws IllegalStateException if open in read-only mode + */ + void delete(@Nonnull final StoredEntry entry, boolean notify) throws IOException { + checkNotInReadOnlyMode(); + + String path = entry.getCentralDirectoryHeader().getName(); + FileUseMapEntry mapEntry = entries.get(path); + Preconditions.checkNotNull(mapEntry, "mapEntry == null"); + Preconditions.checkArgument(entry == mapEntry.getStore(), "entry != mapEntry.getStore()"); + + dirty = true; + + map.remove(mapEntry); + entries.remove(path); + + if (notify) { + notify(ext -> ext.removed(entry)); + } + } + + /** + * Checks that the file is not in read-only mode. + * + * @throws IllegalStateException if the file is in read-only mode + */ + private void checkNotInReadOnlyMode() { + if (readOnly) { + throw new IllegalStateException("Illegal operation in read only model"); + } + } + + /** + * Updates the file writing new entries and removing deleted entries. This will force + * reopening the file as read/write if the file wasn't open in read/write mode. + * + * @throws IOException failed to update the file; this exception may have been thrown by + * the compressor but only reported here + */ + public void update() throws IOException { + checkNotInReadOnlyMode(); + + /* + * Process all background stuff before calling in the extensions. + */ + processAllReadyEntriesWithWait(); + + notify(ZFileExtension::beforeUpdate); + + /* + * Process all background stuff that may be leftover by the extensions. + */ + processAllReadyEntriesWithWait(); + + + if (!dirty) { + return; + } + + reopenRw(); + + /* + * At this point, no more files can be added. We may need to repack to remove extra + * empty spaces or sort. If we sort, we don't need to repack as sorting forces the + * zip file to be as compact as possible. + */ + if (autoSortFiles) { + sortZipContents(); + } else { + packIfNecessary(); + } + + /* + * We're going to change the file so delete the central directory and the EOCD as they + * will have to be rewritten. + */ + deleteDirectoryAndEocd(); + map.truncate(); + + /* + * If we need to use the extra field to cover empty spaces, we do the processing here. + */ + if (coverEmptySpaceUsingExtraField) { + + /* We will go over all files in the zip and check whether there is empty space before + * them. If there is, then we will move the entry to the beginning of the empty space + * (covering it) and extend the extra field with the size of the empty space. + */ + for (FileUseMapEntry entry : new HashSet<>(entries.values())) { + StoredEntry storedEntry = entry.getStore(); + assert storedEntry != null; + + FileUseMapEntry before = map.before(entry); + if (before == null || !before.isFree()) { + continue; + } + + /* + * We have free space before the current entry. However, we do know that it can + * be covered by the extra field, because both sortZipContents() and + * packIfNecessary() guarantee it. + */ + int localExtraSize = + storedEntry.getLocalExtra().size() + Ints.checkedCast(before.getSize()); + Verify.verify(localExtraSize <= MAX_LOCAL_EXTRA_FIELD_CONTENTS_SIZE); + + /* + * Move file back in the zip. + */ + storedEntry.loadSourceIntoMemory(); + + long newStart = before.getStart(); + long newSize = entry.getSize() + before.getSize(); + + /* + * Remove the entry. + */ + String name = storedEntry.getCentralDirectoryHeader().getName(); + map.remove(entry); + Verify.verify(entry == entries.remove(name)); + + /* + * Make a list will all existing segments in the entry's extra field, but remove + * the alignment field, if it exists. Also, sum the size of all kept extra field + * segments. + */ + ImmutableList currentSegments; + try { + currentSegments = storedEntry.getLocalExtra().getSegments(); + } catch (IOException e) { + /* + * Parsing current segments has failed. This means the contents of the extra + * field are not valid. We'll continue discarding the existing segments. + */ + currentSegments = ImmutableList.of(); + } + + List extraFieldSegments = new ArrayList<>(); + int newExtraFieldSize = currentSegments.stream() + .filter(s -> s.getHeaderId() + != ExtraField.ALIGNMENT_ZIP_EXTRA_DATA_FIELD_HEADER_ID) + .peek(extraFieldSegments::add) + .map(ExtraField.Segment::size) + .reduce(0, Integer::sum); + + int spaceToFill = + Ints.checkedCast( + before.getSize() + + storedEntry.getLocalExtra().size() + - newExtraFieldSize); + + extraFieldSegments.add( + new ExtraField.AlignmentSegment(chooseAlignment(storedEntry),spaceToFill)); + + storedEntry.setLocalExtraNoNotify( + new ExtraField(ImmutableList.copyOf(extraFieldSegments))); + entries.put(name, map.add(newStart, newStart + newSize, storedEntry)); + + /* + * Reset the offset to force the file to be rewritten. + */ + storedEntry.getCentralDirectoryHeader().setOffset(-1); + } + } + + /* + * Write new files in the zip. We identify new files because they don't have an offset + * in the zip where they are written although we already know, by their location in the + * file map, where they will be written to. + * + * Before writing the files, we sort them in the order they are written in the file so that + * writes are made in order on disk. + * This is, however, unlikely to optimize anything relevant given the way the Operating + * System does caching, but it certainly won't hurt :) + */ + TreeMap, StoredEntry> toWriteToStore = + new TreeMap<>(FileUseMapEntry.COMPARE_BY_START); + + for (FileUseMapEntry entry : entries.values()) { + StoredEntry entryStore = entry.getStore(); + assert entryStore != null; + if (entryStore.getCentralDirectoryHeader().getOffset() == -1) { + toWriteToStore.put(entry, entryStore); + } + } + + /* + * Add all free entries to the set. + */ + for(FileUseMapEntry freeArea : map.getFreeAreas()) { + toWriteToStore.put(freeArea, null); + } + + /* + * Write everything to file. + */ + for (FileUseMapEntry fileUseMapEntry : toWriteToStore.keySet()) { + StoredEntry entry = toWriteToStore.get(fileUseMapEntry); + if (entry == null) { + int size = Ints.checkedCast(fileUseMapEntry.getSize()); + directWrite(fileUseMapEntry.getStart(), new byte[size]); + } else { + writeEntry(entry, fileUseMapEntry.getStart()); + } + } + + boolean hasCentralDirectory; + int extensionBugDetector = MAXIMUM_EXTENSION_CYCLE_COUNT; + do { + computeCentralDirectory(); + computeEocd(); + + hasCentralDirectory = (directoryEntry != null); + + notify(ext -> { + ext.entriesWritten(); + return null; + }); + + if ((--extensionBugDetector) == 0) { + throw new IOException("Extensions keep resetting the central directory. This is " + + "probably a bug."); + } + } while (hasCentralDirectory && directoryEntry == null); + + appendCentralDirectory(); + appendEocd(); + + Verify.verifyNotNull(raf); + raf.setLength(map.size()); + + dirty = false; + + notify(ext -> { + ext.updated(); + return null; + }); + } + + /** + * Reorganizes the zip so that there are no gaps between files bigger than + * {@link #MAX_LOCAL_EXTRA_FIELD_CONTENTS_SIZE} if {@link #coverEmptySpaceUsingExtraField} + * is set to {@code true}. + * + *

Essentially, this makes sure we can cover any empty space with the extra field, given + * that the local extra field is limited to {@link #MAX_LOCAL_EXTRA_FIELD_CONTENTS_SIZE}. If + * an entry is too far from the previous one, it is removed and re-added. + * + * @throws IOException failed to repack + */ + private void packIfNecessary() throws IOException { + if (!coverEmptySpaceUsingExtraField) { + return; + } + + SortedSet> entriesByLocation = + new TreeSet<>(FileUseMapEntry.COMPARE_BY_START); + entriesByLocation.addAll(entries.values()); + + for (FileUseMapEntry entry : entriesByLocation) { + StoredEntry storedEntry = entry.getStore(); + assert storedEntry != null; + + FileUseMapEntry before = map.before(entry); + if (before == null || !before.isFree()) { + continue; + } + + int localExtraSize = + storedEntry.getLocalExtra().size() + Ints.checkedCast(before.getSize()); + if (localExtraSize > MAX_LOCAL_EXTRA_FIELD_CONTENTS_SIZE) { + /* + * This entry is too far from the previous one. Remove it and re-add it to the + * zip file. + */ + reAdd(storedEntry, PositionHint.LOWEST_OFFSET); + } + } + } + + /** + * Removes a stored entry from the zip and adds it back again. This will force the entry to be + * loaded into memory and repositioned in the zip file. It will also mark the archive as + * being dirty. + * + * @param entry the entry + * @param positionHint hint to where the file should be positioned when re-adding + * @throws IOException failed to load the entry into memory + */ + private void reAdd(@Nonnull StoredEntry entry, @Nonnull PositionHint positionHint) + throws IOException { + String name = entry.getCentralDirectoryHeader().getName(); + FileUseMapEntry mapEntry = entries.get(name); + Preconditions.checkNotNull(mapEntry); + Preconditions.checkState(mapEntry.getStore() == entry); + + entry.loadSourceIntoMemory(); + + map.remove(mapEntry); + entries.remove(name); + FileUseMapEntry positioned = positionInFile(entry, positionHint); + entries.put(name, positioned); + dirty = true; + } + + /** + * Invoked from {@link StoredEntry} when entry has changed in a way that forces the local + * header to be rewritten + * + * @param entry the entry that changed + * @param resized was the local header resized? + * @throws IOException failed to load the entry into memory + */ + void localHeaderChanged(@Nonnull StoredEntry entry, boolean resized) throws IOException { + dirty = true; + + if (resized) { + reAdd(entry, PositionHint.ANYWHERE); + } + } + + /** + * Invoked when the central directory has changed and needs to be rewritten. + */ + void centralDirectoryChanged() { + dirty = true; + deleteDirectoryAndEocd(); + } + + /** + * Updates the file and closes it. + */ + @Override + public void close() throws IOException { + // We need to make sure to release raf, otherwise we end up locking the file on + // Windows. Use try-with-resources to handle exception suppressing. + try (Closeable ignored = this::innerClose) { + if (!readOnly) { + update(); + } + } + + notify(ext -> { + ext.closed(); + return null; + }); + } + + /** + * Removes the Central Directory and EOCD from the file. This will free space for new entries + * as well as allowing the zip file to be truncated if files have been removed. + * + *

This method does not mark the zip as dirty. + */ + private void deleteDirectoryAndEocd() { + if (directoryEntry != null) { + map.remove(directoryEntry); + directoryEntry = null; + } + + if (eocdEntry != null) { + map.remove(eocdEntry); + + Eocd eocd = eocdEntry.getStore(); + Verify.verify(eocd != null); + eocdComment = eocd.getComment(); + eocdEntry = null; + } + } + + /** + * Writes an entry's data in the zip file. This includes everything: the local header and + * the data itself. After writing, the entry is updated with the offset and its source replaced + * with a source that reads from the zip file. + * + * @param entry the entry to write + * @param offset the offset at which the entry should be written + * @throws IOException failed to write the entry + */ + private void writeEntry(@Nonnull StoredEntry entry, long offset) throws IOException { + Preconditions.checkArgument(entry.getDataDescriptorType() + == DataDescriptorType. NO_DATA_DESCRIPTOR, "Cannot write entries with a data " + + "descriptor."); + Preconditions.checkNotNull(raf, "raf == null"); + Preconditions.checkState(state == ZipFileState.OPEN_RW, "state != ZipFileState.OPEN_RW"); + + /* + * Place the cursor and write the local header. + */ + byte[] headerData = entry.toHeaderData(); + directWrite(offset, headerData); + + /* + * Get the raw source data to write. + */ + ProcessedAndRawByteSources source = entry.getSource(); + ByteSource rawContents = source.getRawByteSource(); + + /* + * Write the source data. + */ + byte[] chunk = new byte[IO_BUFFER_SIZE]; + int r; + long writeOffset = offset + headerData.length; + InputStream is = rawContents.openStream(); + while ((r = is.read(chunk)) >= 0) { + directWrite(writeOffset, chunk, 0, r); + writeOffset += r; + } + + is.close(); + + /* + * Set the entry's offset and create the entry source. + */ + entry.replaceSourceFromZip(offset); + } + + /** + * Computes the central directory. The central directory must not have been computed yet. When + * this method finishes, the central directory has been computed {@link #directoryEntry}, + * unless the directory is empty in which case {@link #directoryEntry} + * is left as {@code null}. Nothing is written to disk as a result of this method's invocation. + * + * @throws IOException failed to append the central directory + */ + private void computeCentralDirectory() throws IOException { + Preconditions.checkState(state == ZipFileState.OPEN_RW, "state != ZipFileState.OPEN_RW"); + Preconditions.checkNotNull(raf, "raf == null"); + Preconditions.checkState(directoryEntry == null, "directoryEntry == null"); + + Set newStored = Sets.newHashSet(); + for (FileUseMapEntry mapEntry : entries.values()) { + newStored.add(mapEntry.getStore()); + } + + /* + * Make sure we truncate the map before computing the central directory's location since + * the central directory is the last part of the file. + */ + map.truncate(); + + CentralDirectory newDirectory = CentralDirectory.makeFromEntries(newStored, this); + byte[] newDirectoryBytes = newDirectory.toBytes(); + long directoryOffset = map.size() + extraDirectoryOffset; + + map.extend(directoryOffset + newDirectoryBytes.length); + + if (newDirectoryBytes.length > 0) { + directoryEntry = map.add(directoryOffset, directoryOffset + newDirectoryBytes.length, + newDirectory); + } + } + + /** + * Writes the central directory to the end of the zip file. {@link #directoryEntry} may be + * {@code null} only if there are no files in the archive. + * + * @throws IOException failed to append the central directory + */ + private void appendCentralDirectory() throws IOException { + Preconditions.checkState(state == ZipFileState.OPEN_RW, "state != ZipFileState.OPEN_RW"); + Preconditions.checkNotNull(raf, "raf == null"); + + if (entries.isEmpty()) { + Preconditions.checkState(directoryEntry == null, "directoryEntry != null"); + return; + } + + Preconditions.checkNotNull(directoryEntry, "directoryEntry != null"); + + CentralDirectory newDirectory = directoryEntry.getStore(); + Preconditions.checkNotNull(newDirectory, "newDirectory != null"); + + byte[] newDirectoryBytes = newDirectory.toBytes(); + long directoryOffset = directoryEntry.getStart(); + + /* + * It is fine to seek beyond the end of file. Seeking beyond the end of file will not extend + * the file. Even if we do not have any directory data to write, the extend() call below + * will force the file to be extended leaving exactly extraDirectoryOffset bytes empty at + * the beginning. + */ + directWrite(directoryOffset, newDirectoryBytes); + } + + /** + * Obtains the byte array representation of the central directory. The central directory must + * have been already computed. If there are no entries in the zip, the central directory will be + * empty. + * + * @return the byte representation, or an empty array if there are no entries in the zip + * @throws IOException failed to compute the central directory byte representation + */ + @Nonnull + public byte[] getCentralDirectoryBytes() throws IOException { + if (entries.isEmpty()) { + Preconditions.checkState(directoryEntry == null, "directoryEntry != null"); + return new byte[0]; + } + + Preconditions.checkNotNull(directoryEntry, "directoryEntry == null"); + + CentralDirectory cd = directoryEntry.getStore(); + Preconditions.checkNotNull(cd, "cd == null"); + return cd.toBytes(); + } + + /** + * Computes the EOCD. This creates a new {@link #eocdEntry}. The + * central directory must already be written. If {@link #directoryEntry} is {@code null}, then + * the zip file must not have any entries. + * + * @throws IOException failed to write the EOCD + */ + private void computeEocd() throws IOException { + Preconditions.checkState(state == ZipFileState.OPEN_RW, "state != ZipFileState.OPEN_RW"); + Preconditions.checkNotNull(raf, "raf == null"); + if (directoryEntry == null) { + Preconditions.checkState(entries.isEmpty(), + "directoryEntry == null && !entries.isEmpty()"); + } + + long dirStart; + long dirSize = 0; + + if (directoryEntry != null) { + CentralDirectory directory = directoryEntry.getStore(); + assert directory != null; + + dirStart = directoryEntry.getStart(); + dirSize = directoryEntry.getSize(); + Verify.verify(directory.getEntries().size() == entries.size()); + } else { + /* + * If we do not have a directory, then we must leave any requested offset empty. + */ + dirStart = extraDirectoryOffset; + } + + Verify.verify(eocdComment != null); + Eocd eocd = new Eocd(entries.size(), dirStart, dirSize, eocdComment); + eocdComment = null; + + byte[] eocdBytes = eocd.toBytes(); + long eocdOffset = map.size(); + + map.extend(eocdOffset + eocdBytes.length); + + eocdEntry = map.add(eocdOffset, eocdOffset + eocdBytes.length, eocd); + } + + /** + * Writes the EOCD to the end of the zip file. This creates a new {@link #eocdEntry}. The + * central directory must already be written. If {@link #directoryEntry} is {@code null}, then + * the zip file must not have any entries. + * + * @throws IOException failed to write the EOCD + */ + private void appendEocd() throws IOException { + Preconditions.checkState(state == ZipFileState.OPEN_RW, "state != ZipFileState.OPEN_RW"); + Preconditions.checkNotNull(raf, "raf == null"); + Preconditions.checkNotNull(eocdEntry, "eocdEntry == null"); + + Eocd eocd = eocdEntry.getStore(); + Preconditions.checkNotNull(eocd, "eocd == null"); + + byte[] eocdBytes = eocd.toBytes(); + long eocdOffset = eocdEntry.getStart(); + + directWrite(eocdOffset, eocdBytes); + } + + /** + * Obtains the byte array representation of the EOCD. The EOCD must have already been computed + * for this method to be invoked. + * + * @return the byte representation of the EOCD + * @throws IOException failed to obtain the byte representation of the EOCD + */ + @Nonnull + public byte[] getEocdBytes() throws IOException { + Preconditions.checkNotNull(eocdEntry, "eocdEntry == null"); + + Eocd eocd = eocdEntry.getStore(); + Preconditions.checkNotNull(eocd, "eocd == null"); + return eocd.toBytes(); + } + + /** + * Closes the file, if it is open. + * + * @throws IOException failed to close the file + */ + private void innerClose() throws IOException { + if (state == ZipFileState.CLOSED) { + return; + } + + Verify.verifyNotNull(raf, "raf == null"); + + raf.close(); + raf = null; + state = ZipFileState.CLOSED; + if (closedControl == null) { + closedControl = new CachedFileContents<>(file); + } + + closedControl.closed(null); + } + + /** + * If the zip file is closed, opens it in read-only mode. If it is already open, does nothing. + * In general, it is not necessary to directly invoke this method. However, if directly + * reading the zip file using, for example {@link #directRead(long, byte[])}, then this + * method needs to be called. + * @throws IOException failed to open the file + */ + public void openReadOnly() throws IOException { + if (state != ZipFileState.CLOSED) { + return; + } + + state = ZipFileState.OPEN_RO; + raf = new RandomAccessFile(file, "r"); + } + + /** + * Opens (or reopens) the zip file as read-write. This method will ensure that + * {@link #raf} is not null and open for writing. + * + * @throws IOException failed to open the file, failed to close it or the file was closed and + * has been modified outside the control of this object + */ + private void reopenRw() throws IOException { + // We an never open a file RW in read-only mode. We should never get this far, though. + Verify.verify(!readOnly); + + if (state == ZipFileState.OPEN_RW) { + return; + } + + boolean wasClosed; + if (state == ZipFileState.OPEN_RO) { + /* + * ReadAccessFile does not have a way to reopen as RW so we have to close it and + * open it again. + */ + innerClose(); + wasClosed = false; + } else { + wasClosed = true; + } + + Verify.verify(state == ZipFileState.CLOSED, "state != ZpiFileState.CLOSED"); + Verify.verify(raf == null, "raf != null"); + + if (closedControl != null && !closedControl.isValid()) { + throw new IOException("File '" + file.getAbsolutePath() + "' has been modified " + + "by an external application."); + } + + raf = new RandomAccessFile(file, "rw"); + state = ZipFileState.OPEN_RW; + + /* + * Now that we've open the zip and are ready to write, clear out any data descriptors + * in the zip since we don't need them and they take space in the archive. + */ + for (StoredEntry entry : entries()) { + dirty |= entry.removeDataDescriptor(); + } + + if (wasClosed) { + notify(ZFileExtension::open); + } + } + + /** + * Equivalent to call {@link #add(String, InputStream, boolean)} using + * {@code true} as {@code mayCompress}. + * + * @param name the file name (i.e., path); paths should be defined using slashes + * and the name should not end in slash + * @param stream the source for the file's data + * @throws IOException failed to read the source data + * @throws IllegalStateException if the file is in read-only mode + */ + public void add(@Nonnull String name, @Nonnull InputStream stream) throws IOException { + checkNotInReadOnlyMode(); + add(name, stream, true); + } + + /** + * Creates a stored entry. This does not add the entry to the zip file, it just creates the + * {@link StoredEntry} object. + * + * @param name the name of the entry + * @param stream the input stream with the entry's data + * @param mayCompress can the entry be compressed? + * @return the created entry + * @throws IOException failed to create the entry + */ + @Nonnull + private StoredEntry makeStoredEntry( + @Nonnull String name, + @Nonnull InputStream stream, + boolean mayCompress) + throws IOException { + CloseableByteSource source = tracker.fromStream(stream); + long crc32 = source.hash(Hashing.crc32()).padToLong(); + + boolean encodeWithUtf8 = !EncodeUtils.canAsciiEncode(name); + + SettableFuture compressInfo = + SettableFuture.create(); + GPFlags flags = GPFlags.make(encodeWithUtf8); + CentralDirectoryHeader newFileData = + new CentralDirectoryHeader( + name, + EncodeUtils.encode(name, flags), + source.size(), + compressInfo, + flags, + this); + newFileData.setCrc32(crc32); + + /* + * Create the new entry and sets its data source. Offset should be set to -1 automatically + * because this is a new file. With offset set to -1, StoredEntry does not try to verify the + * local header. Since this is a new file, there is no local header and not checking it is + * what we want to happen. + */ + Verify.verify(newFileData.getOffset() == -1); + return new StoredEntry( + newFileData, + this, + createSources(mayCompress, source, compressInfo, newFileData)); + } + + /** + * Creates the processed and raw sources for an entry. + * + * @param mayCompress can the entry be compressed? + * @param source the entry's data (uncompressed) + * @param compressInfo the compression info future that will be set when the raw entry is + * created and the {@link CentralDirectoryHeaderCompressInfo} object can be created + * @param newFileData the central directory header for the new file + * @return the sources whose data may or may not be already defined + * @throws IOException failed to create the raw sources + */ + @Nonnull + private ProcessedAndRawByteSources createSources( + boolean mayCompress, + @Nonnull CloseableByteSource source, + @Nonnull SettableFuture compressInfo, + @Nonnull CentralDirectoryHeader newFileData) + throws IOException { + if (mayCompress) { + ListenableFuture result = compressor.compress(source); + Futures.addCallback( + result, + new FutureCallback() { + @Override + public void onSuccess(CompressionResult result) { + compressInfo.set( + new CentralDirectoryHeaderCompressInfo( + newFileData, + result.getCompressionMethod(), + result.getSize())); + } + + @Override + public void onFailure(@Nonnull Throwable t) { + compressInfo.setException(t); + } + }, + MoreExecutors.directExecutor()); + + ListenableFuture compressedByteSourceFuture = + Futures.transform( + result, CompressionResult::getSource, MoreExecutors.directExecutor()); + LazyDelegateByteSource compressedByteSource = new LazyDelegateByteSource( + compressedByteSourceFuture); + return new ProcessedAndRawByteSources(source, compressedByteSource); + } else { + compressInfo.set(new CentralDirectoryHeaderCompressInfo(newFileData, + CompressionMethod.STORE, source.size())); + return new ProcessedAndRawByteSources(source, source); + } + } + + /** + * Adds a file to the archive. + * + *

Adding the file will not update the archive immediately. Updating will only happen + * when the {@link #update()} method is invoked. + * + *

Adding a file with the same name as an existing file will replace that file in the + * archive. + * + * @param name the file name (i.e., path); paths should be defined using slashes + * and the name should not end in slash + * @param stream the source for the file's data + * @param mayCompress can the file be compressed? This flag will be ignored if the alignment + * rules force the file to be aligned, in which case the file will not be compressed. + * @throws IOException failed to read the source data + * @throws IllegalStateException if the file is in read-only mode + */ + public void add(@Nonnull String name, @Nonnull InputStream stream, boolean mayCompress) + throws IOException { + checkNotInReadOnlyMode(); + + /* + * Clean pending background work, if needed. + */ + processAllReadyEntries(); + + add(makeStoredEntry(name, stream, mayCompress)); + } + + /** + * Adds a {@link StoredEntry} to the zip. The entry is not immediately added to + * {@link #entries} because data may not yet be available. Instead, it is placed under + * {@link #uncompressedEntries} and later moved to {@link #processAllReadyEntries()} when + * done. + * + *

This method invokes {@link #processAllReadyEntries()} to move the entry if it has already + * been computed so, if there is no delay in compression, and no more files are in waiting + * queue, then the entry is added to {@link #entries} immediately. + * + * @param newEntry the entry to add + * @throws IOException failed to process this entry (or a previous one whose future only + * completed now) + */ + private void add(@Nonnull final StoredEntry newEntry) throws IOException { + uncompressedEntries.add(newEntry); + processAllReadyEntries(); + } + + /** + * Moves all ready entries from {@link #uncompressedEntries} to {@link #entries}. It will + * stop as soon as entry whose future has not been completed is found. + * + * @throws IOException the exception reported in the future computation, if any, or failed + * to add a file to the archive + */ + private void processAllReadyEntries() throws IOException { + /* + * Many things can happen during addToEntries(). Because addToEntries() fires + * notifications to extensions, other files can be added, removed, etc. Ee are *not* + * guaranteed that new stuff does not get into uncompressedEntries: add() will still work + * and will add new entries in there. + * + * However -- important -- processReadyEntries() may be invoked during addToEntries() + * because of the extension mechanism. This means that stuff *can* be removed from + * uncompressedEntries and moved to entries during addToEntries(). + */ + while (!uncompressedEntries.isEmpty()) { + StoredEntry next = uncompressedEntries.get(0); + CentralDirectoryHeader cdh = next.getCentralDirectoryHeader(); + Future compressionInfo = cdh.getCompressionInfo(); + if (!compressionInfo.isDone()) { + /* + * First entry in queue is not yet complete. We can't do anything else. + */ + return; + } + + uncompressedEntries.remove(0); + + try { + compressionInfo.get(); + } catch (InterruptedException e) { + throw new IOException("Impossible I/O exception: get for already computed " + + "future throws InterruptedException", e); + } catch (ExecutionException e) { + throw new IOException("Failed to obtain compression information for entry", e); + } + + addToEntries(next); + } + } + + /** + * Waits until {@link #uncompressedEntries} is empty. + * + * @throws IOException the exception reported in the future computation, if any, or failed + * to add a file to the archive + */ + private void processAllReadyEntriesWithWait() throws IOException { + processAllReadyEntries(); + while (!uncompressedEntries.isEmpty()) { + /* + * Wait for the first future to complete and then try again. Keep looping until we're + * done. + */ + StoredEntry first = uncompressedEntries.get(0); + CentralDirectoryHeader cdh = first.getCentralDirectoryHeader(); + cdh.getCompressionInfoWithWait(); + + processAllReadyEntries(); + } + } + + /** + * Adds a new file to {@link #entries}. This is actually added to the zip and its space + * allocated in the {@link #map}. + * + * @param newEntry the new entry to add + * @throws IOException failed to add the file + */ + private void addToEntries(@Nonnull final StoredEntry newEntry) throws IOException { + Preconditions.checkArgument(newEntry.getDataDescriptorType() == + DataDescriptorType.NO_DATA_DESCRIPTOR, "newEntry has data descriptor"); + + /* + * If there is a file with the same name in the archive, remove it. We remove it by + * calling delete() on the entry (this is the public API to remove a file from the archive). + * StoredEntry.delete() will call {@link ZFile#delete(StoredEntry, boolean)} to perform + * data structure cleanup. + */ + FileUseMapEntry toReplace = entries.get( + newEntry.getCentralDirectoryHeader().getName()); + final StoredEntry replaceStore; + if (toReplace != null) { + replaceStore = toReplace.getStore(); + assert replaceStore != null; + replaceStore.delete(false); + } else { + replaceStore = null; + } + + FileUseMapEntry fileUseMapEntry = + positionInFile(newEntry, PositionHint.ANYWHERE); + entries.put(newEntry.getCentralDirectoryHeader().getName(), fileUseMapEntry); + + dirty = true; + + notify(ext -> ext.added(newEntry, replaceStore)); + } + + /** + * Finds a location in the zip where this entry will be added to and create the map entry. + * This method cannot be called if there is already a map entry for the given entry (if you + * do that, then you're doing something wrong somewhere). + * + *

This may delete the central directory and EOCD (if it deletes one, it deletes the other) + * if there is no space before the central directory. Otherwise, the file would be added + * after the central directory. This would force a new central directory to be written + * when updating the file and would create a hole in the zip. Me no like holes. Holes are evil. + * + * @param entry the entry to place in the zip + * @param positionHint hint to where the file should be positioned + * @return the position in the file where the entry should be placed + */ + @Nonnull + private FileUseMapEntry positionInFile( + @Nonnull StoredEntry entry, + @Nonnull PositionHint positionHint) + throws IOException { + deleteDirectoryAndEocd(); + long size = entry.getInFileSize(); + int localHeaderSize = entry.getLocalHeaderSize(); + int alignment = chooseAlignment(entry); + + FileUseMap.PositionAlgorithm algorithm; + + switch (positionHint) { + case LOWEST_OFFSET: + algorithm = FileUseMap.PositionAlgorithm.FIRST_FIT; + break; + case ANYWHERE: + algorithm = FileUseMap.PositionAlgorithm.BEST_FIT; + break; + default: + throw new AssertionError(); + } + + long newOffset = map.locateFree(size, localHeaderSize, alignment, algorithm); + long newEnd = newOffset + entry.getInFileSize(); + if (newEnd > map.size()) { + map.extend(newEnd); + } + + return map.add(newOffset, newEnd, entry); + } + + /** + * Determines what is the alignment value of an entry. + * + * @param entry the entry + * @return the alignment value, {@link AlignmentRule#NO_ALIGNMENT} if there is no alignment + * required for the entry + * @throws IOException failed to determine the alignment + */ + private int chooseAlignment(@Nonnull StoredEntry entry) throws IOException { + CentralDirectoryHeader cdh = entry.getCentralDirectoryHeader(); + CentralDirectoryHeaderCompressInfo compressionInfo = cdh.getCompressionInfoWithWait(); + + boolean isCompressed = compressionInfo.getMethod() != CompressionMethod.STORE; + if (isCompressed) { + return AlignmentRule.NO_ALIGNMENT; + } else { + return alignmentRule.alignment(cdh.getName()); + } + } + + /** + * Adds all files from another zip file, maintaining their compression. Files specified in + * src that are already on this file will replace the ones in this file. However, if + * their sizes and checksums are equal, they will be ignored. + * + *

This method will not perform any changes in itself, it will only update in-memory data + * structures. To actually write the zip file, invoke either {@link #update()} or + * {@link #close()}. + * + * @param src the source archive + * @param ignoreFilter predicate that, if {@code true}, identifies files in src that + * should be ignored by merging; merging will behave as if these files were not there + * @throws IOException failed to read from src or write on the output + * @throws IllegalStateException if the file is in read-only mode + */ + public void mergeFrom(@Nonnull ZFile src, @Nonnull Predicate ignoreFilter) + throws IOException { + checkNotInReadOnlyMode(); + + for (StoredEntry fromEntry : src.entries()) { + if (ignoreFilter.test(fromEntry.getCentralDirectoryHeader().getName())) { + continue; + } + + boolean replaceCurrent = true; + String path = fromEntry.getCentralDirectoryHeader().getName(); + FileUseMapEntry currentEntry = entries.get(path); + + if (currentEntry != null) { + long fromSize = fromEntry.getCentralDirectoryHeader().getUncompressedSize(); + long fromCrc = fromEntry.getCentralDirectoryHeader().getCrc32(); + + StoredEntry currentStore = currentEntry.getStore(); + assert currentStore != null; + + long currentSize = currentStore.getCentralDirectoryHeader().getUncompressedSize(); + long currentCrc = currentStore.getCentralDirectoryHeader().getCrc32(); + + if (fromSize == currentSize && fromCrc == currentCrc) { + replaceCurrent = false; + } + } + + if (replaceCurrent) { + CentralDirectoryHeader fromCdr = fromEntry.getCentralDirectoryHeader(); + CentralDirectoryHeaderCompressInfo fromCompressInfo = + fromCdr.getCompressionInfoWithWait(); + CentralDirectoryHeader newFileData; + try { + /* + * We make two changes in the central directory from the file to merge: + * we reset the offset to force the entry to be written and we reset the + * deferred CRC bit as we don't need the extra stuff after the file. It takes + * space and is totally useless. + */ + newFileData = fromCdr.clone(); + newFileData.setOffset(-1); + newFileData.resetDeferredCrc(); + } catch (CloneNotSupportedException e) { + throw new IOException("Failed to clone CDR.", e); + } + + /* + * Read the data (read directly the compressed source if there is one). + */ + ProcessedAndRawByteSources fromSource = fromEntry.getSource(); + InputStream fromInput = fromSource.getRawByteSource().openStream(); + long sourceSize = fromSource.getRawByteSource().size(); + if (sourceSize > Integer.MAX_VALUE) { + throw new IOException("Cannot read source with " + sourceSize + " bytes."); + } + + byte[] data = new byte[Ints.checkedCast(sourceSize)]; + int read = 0; + while (read < data.length) { + int r = fromInput.read(data, read, data.length - read); + Verify.verify(r >= 0, "There should be at least 'size' bytes in the stream."); + read += r; + } + + /* + * Build the new source and wrap it around an inflater source if data came from + * a compressed source. + */ + CloseableByteSource rawContents = tracker.fromSource(fromSource.getRawByteSource()); + CloseableByteSource processedContents; + if (fromCompressInfo.getMethod() == CompressionMethod.DEFLATE) { + //noinspection IOResourceOpenedButNotSafelyClosed + processedContents = new InflaterByteSource(rawContents); + } else { + processedContents = rawContents; + } + + ProcessedAndRawByteSources newSource = new ProcessedAndRawByteSources( + processedContents, rawContents); + + /* + * Add will replace any current entry with the same name. + */ + StoredEntry newEntry = new StoredEntry(newFileData, this, newSource); + add(newEntry); + } + } + } + + /** + * Forcibly marks this zip file as touched, forcing it to be updated when {@link #update()} + * or {@link #close()} are invoked. + * + * @throws IllegalStateException if the file is in read-only mode + */ + public void touch() { + checkNotInReadOnlyMode(); + dirty = true; + } + + /** + * Wait for any background tasks to finish and report any errors. In general this method does + * not need to be invoked directly as errors from background tasks are reported during + * {@link #add(String, InputStream, boolean)}, {@link #update()} and {@link #close()}. + * However, if required for some purposes, e.g., ensuring all notifications have been + * done to extensions, then this method may be called. It will wait for all background tasks + * to complete. + * @throws IOException some background work failed + */ + public void finishAllBackgroundTasks() throws IOException { + processAllReadyEntriesWithWait(); + } + + /** + * Realigns all entries in the zip. This is equivalent to call {@link StoredEntry#realign()} + * for all entries in the zip file. + * + * @return has any entry been changed? Note that for entries that have not yet been written on + * the file, realignment does not count as a change as nothing needs to be updated in the file; + * entries that have been updated may have been recreated and the existing references outside + * of {@code ZFile} may refer to {@link StoredEntry}s that are no longer valid + * @throws IOException failed to realign the zip; some entries in the zip may have been lost + * due to the I/O error + * @throws IllegalStateException if the file is in read-only mode + */ + public boolean realign() throws IOException { + checkNotInReadOnlyMode(); + + boolean anyChanges = false; + for (StoredEntry entry : entries()) { + anyChanges |= entry.realign(); + } + + if (anyChanges) { + dirty = true; + } + + return anyChanges; + } + + /** + * Realigns a stored entry, if necessary. Realignment is done by removing and re-adding the file + * if it was not aligned. + * + * @param entry the entry to realign + * @return has the entry been changed? Note that if the entry has not yet been written on the + * file, realignment does not count as a change as nothing needs to be updated in the file + * @throws IOException failed to read/write an entry; the entry may no longer exist in the + * file + */ + boolean realign(@Nonnull StoredEntry entry) throws IOException { + FileUseMapEntry mapEntry = + entries.get(entry.getCentralDirectoryHeader().getName()); + Verify.verify(entry == mapEntry.getStore()); + long currentDataOffset = mapEntry.getStart() + entry.getLocalHeaderSize(); + + int expectedAlignment = chooseAlignment(entry); + long misalignment = currentDataOffset % expectedAlignment; + if (misalignment == 0) { + /* + * Good. File is aligned properly. + */ + return false; + } + + if (entry.getCentralDirectoryHeader().getOffset() == -1) { + /* + * File is not aligned but it is not written. We do not really need to do much other + * than find another place in the map. + */ + map.remove(mapEntry); + long newStart = + map.locateFree( + mapEntry.getSize(), + entry.getLocalHeaderSize(), + expectedAlignment, + FileUseMap.PositionAlgorithm.BEST_FIT); + mapEntry = map.add(newStart, newStart + entry.getInFileSize(), entry); + entries.put(entry.getCentralDirectoryHeader().getName(), mapEntry); + + /* + * Just for safety. We're modifying the in-memory structures but the file should + * already be marked as dirty. + */ + Verify.verify(dirty); + + return false; + + } + + /* + * Get the entry data source, but check if we have a compressed one (we don't want to + * inflate and deflate). + */ + CentralDirectoryHeaderCompressInfo compressInfo = + entry.getCentralDirectoryHeader().getCompressionInfoWithWait(); + + ProcessedAndRawByteSources source = entry.getSource(); + + CentralDirectoryHeader clonedCdh; + try { + clonedCdh = entry.getCentralDirectoryHeader().clone(); + } catch (CloneNotSupportedException e) { + Verify.verify(false); + return false; + } + + /* + * We make two changes in the central directory when realigning: + * we reset the offset to force the entry to be written and we reset the + * deferred CRC bit as we don't need the extra stuff after the file. It takes + * space and is totally useless and we may need the extra space to realign the entry... + */ + clonedCdh.setOffset(-1); + clonedCdh.resetDeferredCrc(); + + CloseableByteSource rawContents = tracker.fromSource(source.getRawByteSource()); + CloseableByteSource processedContents; + + if (compressInfo.getMethod() == CompressionMethod.DEFLATE) { + //noinspection IOResourceOpenedButNotSafelyClosed + processedContents = new InflaterByteSource(rawContents); + } else { + processedContents = rawContents; + } + + ProcessedAndRawByteSources newSource = new ProcessedAndRawByteSources(processedContents, + rawContents); + + /* + * Add the new file. This will replace the existing one. + */ + StoredEntry newEntry = new StoredEntry(clonedCdh, this, newSource); + add(newEntry); + return true; + } + + /** + * Adds an extension to this zip file. + * + * @param extension the listener to add + * @throws IllegalStateException if the file is in read-only mode + */ + public void addZFileExtension(@Nonnull ZFileExtension extension) { + checkNotInReadOnlyMode(); + extensions.add(extension); + } + + /** + * Removes an extension from this zip file. + * + * @param extension the listener to remove + * @throws IllegalStateException if the file is in read-only mode + */ + public void removeZFileExtension(@Nonnull ZFileExtension extension) { + checkNotInReadOnlyMode(); + extensions.remove(extension); + } + + /** + * Notifies all extensions, collecting their execution requests and running them. + * + * @param function the function to apply to all listeners, it will generally invoke the + * notification method on the listener and return the result of that invocation + * @throws IOException failed to process some extensions + */ + private void notify(@Nonnull IOExceptionFunction function) + throws IOException { + for (ZFileExtension fl : Lists.newArrayList(extensions)) { + IOExceptionRunnable r = function.apply(fl); + if (r != null) { + toRun.add(r); + } + } + + if (!isNotifying) { + isNotifying = true; + + try { + while (!toRun.isEmpty()) { + IOExceptionRunnable r = toRun.remove(0); + r.run(); + } + } finally { + isNotifying = false; + } + } + } + + /** + * Directly writes data in the zip file. Incorrect use of this method may corrupt the + * zip file. Invoking this method may force the zip to be reopened in read/write + * mode. + * + * @param offset the offset at which data should be written + * @param data the data to write, may be an empty array + * @param start start offset in {@code data} where data to write is located + * @param count number of bytes of data to write + * @throws IOException failed to write the data + * @throws IllegalStateException if the file is in read-only mode + */ + public void directWrite(long offset, @Nonnull byte[] data, int start, int count) + throws IOException { + checkNotInReadOnlyMode(); + + Preconditions.checkArgument(offset >= 0, "offset < 0"); + Preconditions.checkArgument(start >= 0, "start >= 0"); + Preconditions.checkArgument(count >= 0, "count >= 0"); + + if (data.length == 0) { + return; + } + + Preconditions.checkArgument(start <= data.length, "start > data.length"); + Preconditions.checkArgument(start + count <= data.length, "start + count > data.length"); + + reopenRw(); + assert raf != null; + + raf.seek(offset); + raf.write(data, start, count); + } + + /** + * Same as {@code directWrite(offset, data, 0, data.length)}. + * + * @param offset the offset at which data should be written + * @param data the data to write, may be an empty array + * @throws IOException failed to write the data + * @throws IllegalStateException if the file is in read-only mode + */ + public void directWrite(long offset, @Nonnull byte[] data) throws IOException { + checkNotInReadOnlyMode(); + directWrite(offset, data, 0, data.length); + } + + /** + * Returns the current size (in bytes) of the underlying file. + * + * @throws IOException if an I/O error occurs + */ + public long directSize() throws IOException { + /* + * Only force a reopen if the file is closed. + */ + if (raf == null) { + reopenRw(); + assert raf != null; + } + return raf.length(); + } + + /** + * Directly reads data from the zip file. Invoking this method may force the zip to be reopened + * in read/write mode. + * + * @param offset the offset at which data should be written + * @param data the array where read data should be stored + * @param start start position in the array where to write data to + * @param count how many bytes of data can be written + * @return how many bytes of data have been written or {@code -1} if there are no more bytes + * to be read + * @throws IOException failed to write the data + */ + public int directRead(long offset, @Nonnull byte[] data, int start, int count) + throws IOException { + Preconditions.checkArgument(start >= 0, "start >= 0"); + Preconditions.checkArgument(count >= 0, "count >= 0"); + Preconditions.checkArgument(start <= data.length, "start > data.length"); + Preconditions.checkArgument(start + count <= data.length, "start + count > data.length"); + return directRead(offset, ByteBuffer.wrap(data, start, count)); + } + + /** + * Directly reads data from the zip file. Invoking this method may force the zip to be reopened + * in read/write mode. + * + * @param offset the offset from which data should be read + * @param dest the output buffer to fill with data from the {@code offset}. + * @return how many bytes of data have been written or {@code -1} if there are no more bytes + * to be read + * @throws IOException failed to write the data + */ + public int directRead(long offset, @Nonnull ByteBuffer dest) throws IOException { + Preconditions.checkArgument(offset >= 0, "offset < 0"); + + if (!dest.hasRemaining()) { + return 0; + } + + /* + * Only force a reopen if the file is closed. + */ + if (raf == null) { + reopenRw(); + assert raf != null; + } + + raf.seek(offset); + return raf.getChannel().read(dest); + } + + /** + * Same as {@code directRead(offset, data, 0, data.length)}. + * + * @param offset the offset at which data should be read + * @param data receives the read data, may be an empty array + * @throws IOException failed to read the data + */ + public int directRead(long offset, @Nonnull byte[] data) throws IOException { + return directRead(offset, data, 0, data.length); + } + + /** + * Reads exactly {@code data.length} bytes of data, failing if it was not possible to read all + * the requested data. + * + * @param offset the offset at which to start reading + * @param data the array that receives the data read + * @throws IOException failed to read some data or there is not enough data to read + */ + public void directFullyRead(long offset, @Nonnull byte[] data) throws IOException { + directFullyRead(offset, ByteBuffer.wrap(data)); + } + + /** + * Reads exactly {@code dest.remaining()} bytes of data, failing if it was not possible to read + * all the requested data. + * + * @param offset the offset at which to start reading + * @param dest the output buffer to fill with data + * @throws IOException failed to read some data or there is not enough data to read + */ + public void directFullyRead(long offset, @Nonnull ByteBuffer dest) throws IOException { + Preconditions.checkArgument(offset >= 0, "offset < 0"); + + if (!dest.hasRemaining()) { + return; + } + + /* + * Only force a reopen if the file is closed. + */ + if (raf == null) { + reopenRw(); + assert raf != null; + } + + FileChannel fileChannel = raf.getChannel(); + while (dest.hasRemaining()) { + fileChannel.position(offset); + int chunkSize = fileChannel.read(dest); + if (chunkSize == -1) { + throw new EOFException( + "Failed to read " + dest.remaining() + " more bytes: premature EOF"); + } + offset += chunkSize; + } + } + + /** + * Adds all files and directories recursively. + *

+ * Equivalent to calling {@link #addAllRecursively(File, Function)} using a function that + * always returns {@code true} + * + * @param file a file or directory; if it is a directory, all files and directories will be + * added recursively + * @throws IOException failed to some (or all ) of the files + * @throws IllegalStateException if the file is in read-only mode + */ + public void addAllRecursively(@Nonnull File file) throws IOException { + checkNotInReadOnlyMode(); + addAllRecursively(file, f -> true); + } + + /** + * Adds all files and directories recursively. + * + * @param file a file or directory; if it is a directory, all files and directories will be + * added recursively + * @param mayCompress a function that decides whether files may be compressed + * @throws IOException failed to some (or all ) of the files + * @throws IllegalStateException if the file is in read-only mode + */ + public void addAllRecursively( + @Nonnull File file, + @Nonnull Function mayCompress) throws IOException { + checkNotInReadOnlyMode(); + + /* + * The case of file.isFile() is different because if file.isFile() we will add it to the + * zip in the root. However, if file.isDirectory() we won't add it and add its children. + */ + if (file.isFile()) { + boolean mayCompressFile = Verify.verifyNotNull(mayCompress.apply(file), + "mayCompress.apply() returned null"); + + try (Closer closer = Closer.create()) { + FileInputStream fileInput = closer.register(new FileInputStream(file)); + add(file.getName(), fileInput, mayCompressFile); + } + + return; + } + + for (File f : Files.fileTreeTraverser().preOrderTraversal(file).skip(1)) { + String path = file.toURI().relativize(f.toURI()).getPath(); + + InputStream stream; + try (Closer closer = Closer.create()) { + boolean mayCompressFile; + if (f.isDirectory()) { + stream = closer.register(new ByteArrayInputStream(new byte[0])); + mayCompressFile = false; + } else { + stream = closer.register(new FileInputStream(f)); + mayCompressFile = Verify.verifyNotNull(mayCompress.apply(f), + "mayCompress.apply() returned null"); + } + + add(path, stream, mayCompressFile); + } + } + } + + /** + * Obtains the offset at which the central directory exists, or at which it will be written + * if the zip file were to be flushed immediately. + * + * @return the offset, in bytes, where the central directory is or will be written; this value + * includes any extra offset for the central directory + */ + public long getCentralDirectoryOffset() { + if (directoryEntry != null) { + return directoryEntry.getStart(); + } + + /* + * If there are no entries, the central directory is written at the start of the file. + */ + if (entries.isEmpty()) { + return extraDirectoryOffset; + } + + /* + * The Central Directory is written after all entries. This will be at the end of the file + * if the + */ + return map.usedSize() + extraDirectoryOffset; + } + + /** + * Obtains the size of the central directory, if the central directory is written in the zip + * file. + * + * @return the size of the central directory or {@code -1} if the central directory has not + * been computed + */ + public long getCentralDirectorySize() { + if (directoryEntry != null) { + return directoryEntry.getSize(); + } + + if (entries.isEmpty()) { + return 0; + } + + return 1; + } + + /** + * Obtains the offset of the EOCD record, if the EOCD has been written to the file. + * + * @return the offset of the EOCD or {@code -1} if none exists yet + */ + public long getEocdOffset() { + if (eocdEntry == null) { + return -1; + } + + return eocdEntry.getStart(); + } + + /** + * Obtains the size of the EOCD record, if the EOCD has been written to the file. + * + * @return the size of the EOCD of {@code -1} it none exists yet + */ + public long getEocdSize() { + if (eocdEntry == null) { + return -1; + } + + return eocdEntry.getSize(); + } + + /** + * Obtains the comment in the EOCD. + * + * @return the comment exactly as it was encoded in the EOCD, no encoding conversion is done + */ + @Nonnull + public byte[] getEocdComment() { + if (eocdEntry == null) { + Verify.verify(eocdComment != null); + byte[] eocdCommentCopy = new byte[eocdComment.length]; + System.arraycopy(eocdComment, 0, eocdCommentCopy, 0, eocdComment.length); + return eocdCommentCopy; + } + + Eocd eocd = eocdEntry.getStore(); + Verify.verify(eocd != null); + return eocd.getComment(); + } + + /** + * Sets the comment in the EOCD. + * + * @param comment the new comment; no conversion is done, these exact bytes will be placed in + * the EOCD comment + * @throws IllegalStateException if file is in read-only mode + */ + public void setEocdComment(@Nonnull byte[] comment) { + checkNotInReadOnlyMode(); + + if (comment.length > MAX_EOCD_COMMENT_SIZE) { + throw new IllegalArgumentException( + "EOCD comment size (" + + comment.length + + ") is larger than the maximum allowed (" + + MAX_EOCD_COMMENT_SIZE + + ")"); + } + + // Check if the EOCD signature appears anywhere in the comment we need to check if it + // is valid. + for (int i = 0; i < comment.length - MIN_EOCD_SIZE; i++) { + // Remember: little endian... + if (comment[i] == EOCD_SIGNATURE[3] + && comment[i + 1] == EOCD_SIGNATURE[2] + && comment[i + 2] == EOCD_SIGNATURE[1] + && comment[i + 3] == EOCD_SIGNATURE[0]) { + // We found a possible EOCD signature at position i. Try to read it. + ByteBuffer bytes = ByteBuffer.wrap(comment, i, comment.length - i); + try { + new Eocd(bytes); + throw new IllegalArgumentException( + "Position " + + i + + " of the comment contains a valid EOCD record."); + } catch (IOException e) { + // Fine, this is an invalid record. Move along... + } + } + } + + deleteDirectoryAndEocd(); + eocdComment = new byte[comment.length]; + System.arraycopy(comment, 0, eocdComment, 0, comment.length); + dirty = true; + } + + /** + * Sets an extra offset for the central directory. See class description for details. Changing + * this value will mark the file as dirty and force a rewrite of the central directory when + * updated. + * + * @param offset the offset or {@code 0} to write the central directory at its current location + * @throws IllegalStateException if file is in read-only mode + */ + public void setExtraDirectoryOffset(long offset) { + checkNotInReadOnlyMode(); + Preconditions.checkArgument(offset >= 0, "offset < 0"); + + if (extraDirectoryOffset != offset) { + extraDirectoryOffset = offset; + deleteDirectoryAndEocd(); + dirty = true; + } + } + + /** + * Obtains the extra offset for the central directory. See class description for details. + * + * @return the offset or {@code 0} if no offset is set + */ + public long getExtraDirectoryOffset() { + return extraDirectoryOffset; + } + + /** + * Obtains whether this {@code ZFile} is ignoring timestamps. + * + * @return are the timestamps being ignored? + */ + public boolean areTimestampsIgnored() { + return noTimestamps; + } + + /** + * Sorts all files in the zip. This will force all files to be loaded and will wait for all + * background tasks to complete. Sorting files is never done implicitly and will operate in + * memory only (maybe reading files from the zip disk into memory, if needed). It will leave + * the zip in dirty state, requiring a call to {@link #update()} to force the entries to be + * written to disk. + * + * @throws IOException failed to load or move a file in the zip + * @throws IllegalStateException if file is in read-only mode + */ + public void sortZipContents() throws IOException { + checkNotInReadOnlyMode(); + reopenRw(); + + processAllReadyEntriesWithWait(); + + Verify.verify(uncompressedEntries.isEmpty()); + + SortedSet sortedEntries = Sets.newTreeSet(StoredEntry.COMPARE_BY_NAME); + for (FileUseMapEntry fmEntry : entries.values()) { + StoredEntry entry = fmEntry.getStore(); + Preconditions.checkNotNull(entry); + sortedEntries.add(entry); + entry.loadSourceIntoMemory(); + + map.remove(fmEntry); + } + + entries.clear(); + for (StoredEntry entry : sortedEntries) { + String name = entry.getCentralDirectoryHeader().getName(); + FileUseMapEntry positioned = + positionInFile(entry, PositionHint.LOWEST_OFFSET); + + entries.put(name, positioned); + } + + dirty = true; + } + + /** + * Obtains the filesystem path to the zip file. + * + * @return the file that may or may not exist (depending on whether something existed there + * before the zip was created and on whether the zip has been updated or not) + */ + @Nonnull + public File getFile() { + return file; + } + + /** + * Creates a new verify log. + * + * @return the new verify log + */ + @Nonnull + VerifyLog makeVerifyLog() { + VerifyLog log = verifyLogFactory.get(); + assert log != null; + return log; + } + + /** + * Obtains the zip file's verify log. + * + * @return the verify log + */ + @Nonnull + VerifyLog getVerifyLog() { + return verifyLog; + } + + /** + * Are there in-memory changes that have not been written to the zip file? + * + *

Waits for all pending processing which may make changes. + */ + public boolean hasPendingChangesWithWait() throws IOException { + processAllReadyEntriesWithWait(); + return dirty; + } + + /** Hint to where files should be positioned. */ + enum PositionHint { + /** + * File may be positioned anywhere, caller doesn't care. + */ + ANYWHERE, + + /** + * File should be positioned at the lowest offset possible. + */ + LOWEST_OFFSET + } +} diff --git a/src/main/java/com/android/tools/build/apkzlib/zip/ZFileExtension.java b/src/main/java/com/android/tools/build/apkzlib/zip/ZFileExtension.java new file mode 100644 index 0000000..2723a61 --- /dev/null +++ b/src/main/java/com/android/tools/build/apkzlib/zip/ZFileExtension.java @@ -0,0 +1,146 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * 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.android.tools.build.apkzlib.zip; + +import com.android.tools.build.apkzlib.utils.IOExceptionRunnable; +import java.io.IOException; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * An extension of a {@link ZFile}. Extensions are notified when files are open, updated, closed and + * when files are added or removed from the zip. These notifications are received after the zip + * has been updated in memory for open, when files are added or removed and when the zip has been + * updated on disk or closed. + *

+ * An extension is also notified before the file is updated, allowing it to modify the file before + * the update happens. If it does, then all extensions are notified of the changes on the zip file. + * Because the order of the notifications is preserved, all extensions are notified in the same + * order. For example, if two extensions E1 and E2 are registered and they both add a file at + * update time, this would be the flow: + *

    + *
  • E1 receives {@code beforeUpdate} notification.
  • + *
  • E1 adds file F1 to the zip (notifying the addition is suspended because another + * notification is in progress).
  • + *
  • E2 receives {@code beforeUpdate} notification.
  • + *
  • E2 adds file F2 to the zip (notifying the addition is suspended because another + * notification is in progress).
  • + *
  • E1 is notified F1 was added.
  • + *
  • E2 is notified F1 was added.
  • + *
  • E1 is notified F2 was added.
  • + *
  • E2 is notified F2 was added.
  • + *
  • (zip file is updated on disk)
  • + *
  • E1 is notified the zip was updated.
  • + *
  • E2 is notified the zip was updated.
  • + *
+ *

+ * An extension should not modify the zip file when notified of changes. If allowed, this would + * break event notification order in case multiple extensions are registered with the zip file. + * To allow performing changes to the zip file, all notification method return a + * {@code IOExceptionRunnable} that is invoked when {@link ZFile} has finished notifying all + * extensions. + */ +public abstract class ZFileExtension { + + /** + * The zip file has been open and the zip's contents have been read. The default implementation + * does nothing and returns {@code null}. + * + * @return an optional runnable to run when notification of all listeners has ended + * @throws IOException failed to process the event + */ + @Nullable + public IOExceptionRunnable open() throws IOException { + return null; + } + + /** + * The zip will be updated. This method allows the extension to register changes to the zip + * file before the file is written. The default implementation does nothing and returns + * {@code null}. + *

+ * After this notification is received, the extension will receive further + * {@link #added(StoredEntry, StoredEntry)} and {@link #removed(StoredEntry)} notifications if + * it or other extensions add or remove files before update. + *

+ * When no more files are updated, the {@link #entriesWritten()} notification is sent. + * + * @return an optional runnable to run when notification of all listeners has ended + * @throws IOException failed to process the event + */ + @Nullable + public IOExceptionRunnable beforeUpdate() throws IOException { + return null; + } + + /** + * This notification is sent when all entries have been written in the file but the central + * directory and the EOCD have not yet been written. No entries should be added, removed or + * updated during this notification. If this method forces an update of either the central + * directory or EOCD, then this method will be invoked again for all extensions with the new + * central directory and EOCD. + *

+ * After this notification, {@link #updated()} is sent. + * + * @throws IOException failed to process the event + */ + public void entriesWritten() throws IOException { + } + + /** + * The zip file has been updated on disk. The default implementation does nothing. + * + * @throws IOException failed to perform update tasks + */ + public void updated() throws IOException { + } + + /** + * The zip file has been closed. Note that if {@link ZFile#close()} requires that the zip file + * be updated (because it had in-memory changes), {@link #updated()} will be called before + * this method. The default implementation does nothing. + */ + public void closed() { + } + + /** + * A new entry has been added to the zip, possibly replacing an entry in there. The + * default implementation does nothing and returns {@code null}. + * + * @param entry the entry that was added + * @param replaced the entry that was replaced, if any + * @return an optional runnable to run when notification of all listeners has ended + */ + @Nullable + public IOExceptionRunnable added(@Nonnull StoredEntry entry, @Nullable StoredEntry replaced) { + return null; + } + + /** + * An entry has been removed from the zip. This method is not invoked for entries that have + * been replaced. Those entries are notified using replaced in + * {@link #added(StoredEntry, StoredEntry)}. The default implementation does nothing and + * returns {@code null}. + * + * @param entry the entry that was deleted + * @return an optional runnable to run when notification of all listeners has ended + */ + @Nullable + public IOExceptionRunnable removed(@Nonnull StoredEntry entry) { + return null; + } +} diff --git a/src/main/java/com/android/tools/build/apkzlib/zip/ZFileOptions.java b/src/main/java/com/android/tools/build/apkzlib/zip/ZFileOptions.java new file mode 100644 index 0000000..e260455 --- /dev/null +++ b/src/main/java/com/android/tools/build/apkzlib/zip/ZFileOptions.java @@ -0,0 +1,214 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * 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.android.tools.build.apkzlib.zip; + +import com.android.tools.build.apkzlib.zip.compress.DeflateExecutionCompressor; +import com.android.tools.build.apkzlib.zip.utils.ByteTracker; +import java.util.function.Supplier; +import java.util.zip.Deflater; +import javax.annotation.Nonnull; + +/** + * Options to create a {@link ZFile}. + */ +public class ZFileOptions { + + /** + * The byte tracker. + */ + @Nonnull + private ByteTracker tracker; + + /** + * The compressor to use. + */ + @Nonnull + private Compressor compressor; + + /** + * Should timestamps be zeroed? + */ + private boolean noTimestamps; + + /** + * The alignment rule to use. + */ + @Nonnull + private AlignmentRule alignmentRule; + + /** + * Should the extra field be used to cover empty space? + */ + private boolean coverEmptySpaceUsingExtraField; + + /** + * Should files be automatically sorted before update? + */ + private boolean autoSortFiles; + + /** + * Factory creating verification logs to use. + */ + @Nonnull + private Supplier verifyLogFactory; + + /** + * Creates a new options object. All options are set to their defaults. + */ + public ZFileOptions() { + tracker = new ByteTracker(); + compressor = + new DeflateExecutionCompressor( + Runnable::run, + tracker, + Deflater.DEFAULT_COMPRESSION); + alignmentRule = AlignmentRules.compose(); + verifyLogFactory = VerifyLogs::devNull; + } + + /** + * Obtains the ZFile's byte tracker. + * + * @return the byte tracker + */ + @Nonnull + public ByteTracker getTracker() { + return tracker; + } + + /** + * Obtains the compressor to use. + * + * @return the compressor + */ + @Nonnull + public Compressor getCompressor() { + return compressor; + } + + /** + * Sets the compressor to use. + * + * @param compressor the compressor + */ + public ZFileOptions setCompressor(@Nonnull Compressor compressor) { + this.compressor = compressor; + return this; + } + + /** + * Obtains whether timestamps should be zeroed. + * + * @return should timestamps be zeroed? + */ + public boolean getNoTimestamps() { + return noTimestamps; + } + + /** + * Sets whether timestamps should be zeroed. + * + * @param noTimestamps should timestamps be zeroed? + */ + public ZFileOptions setNoTimestamps(boolean noTimestamps) { + this.noTimestamps = noTimestamps; + return this; + } + + /** + * Obtains the alignment rule. + * + * @return the alignment rule + */ + @Nonnull + public AlignmentRule getAlignmentRule() { + return alignmentRule; + } + + /** + * Sets the alignment rule. + * + * @param alignmentRule the alignment rule + */ + public ZFileOptions setAlignmentRule(@Nonnull AlignmentRule alignmentRule) { + this.alignmentRule = alignmentRule; + return this; + } + + /** + * Obtains whether the extra field should be used to cover empty spaces. See {@link ZFile} for + * an explanation on using the extra field for covering empty spaces. + * + * @return should the extra field be used to cover empty spaces? + */ + public boolean getCoverEmptySpaceUsingExtraField() { + return coverEmptySpaceUsingExtraField; + } + + /** + * Sets whether the extra field should be used to cover empty spaces. See {@link ZFile} for an + * explanation on using the extra field for covering empty spaces. + * + * @param coverEmptySpaceUsingExtraField should the extra field be used to cover empty spaces? + */ + public ZFileOptions setCoverEmptySpaceUsingExtraField(boolean coverEmptySpaceUsingExtraField) { + this.coverEmptySpaceUsingExtraField = coverEmptySpaceUsingExtraField; + return this; + } + + /** + * Obtains whether files should be automatically sorted before updating the zip file. See + * {@link ZFile} for an explanation on automatic sorting. + * + * @return should the file be automatically sorted? + */ + public boolean getAutoSortFiles() { + return autoSortFiles; + } + + /** + * Sets whether files should be automatically sorted before updating the zip file. See {@link + * ZFile} for an explanation on automatic sorting. + * + * @param autoSortFiles should the file be automatically sorted? + */ + public ZFileOptions setAutoSortFiles(boolean autoSortFiles) { + this.autoSortFiles = autoSortFiles; + return this; + } + + /** + * Sets the verification log factory. + * + * @param verifyLogFactory verification log factory + */ + public ZFileOptions setVerifyLogFactory(@Nonnull Supplier verifyLogFactory) { + this.verifyLogFactory = verifyLogFactory; + return this; + } + + /** + * Obtains the verification log factory. By default, the verification log doesn't store + * anything and will always return an empty log. + * + * @return the verification log factory + */ + @Nonnull + public Supplier getVerifyLogFactory() { + return verifyLogFactory; + } +} diff --git a/src/main/java/com/android/tools/build/apkzlib/zip/ZipField.java b/src/main/java/com/android/tools/build/apkzlib/zip/ZipField.java new file mode 100644 index 0000000..8e8d27d --- /dev/null +++ b/src/main/java/com/android/tools/build/apkzlib/zip/ZipField.java @@ -0,0 +1,364 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * 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.android.tools.build.apkzlib.zip; + +import com.android.tools.build.apkzlib.zip.utils.LittleEndianUtils; +import com.google.common.base.Preconditions; +import com.google.common.base.Verify; +import com.google.common.collect.Sets; +import com.google.common.primitives.Ints; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.Set; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * The ZipField class represents a field in a record in a zip file. Zip files are made with records + * that have fields. This class makes it easy to read, write and verify field values. + *

+ * There are two main types of fields: 2-byte fields and 4-byte fields. We represent each one as + * a subclass of {@code ZipField}, {@code F2} for the 2-byte field and {@code F4} for the 4-byte + * field. Because Java's {@code int} data type is guaranteed to be 4-byte, all methods use Java's + * native {@link int} as data type. + *

+ * For each field we can either read, write or verify. Verification is used for fields whose value + * we know. Some fields, e.g. signature fields, have fixed value. Other fields have + * variable values, but in some situations we know which value they have. For example, the last + * modification time of a file's local header will have to match the value of the file's + * modification time as stored in the central directory. + *

+ * Because records are compact, i.e. fields are stored sequentially with no empty spaces, + * fields are generally created in the sequence they exist and the end offset of a field is used + * as the offset of the next one. The end of a field can be obtained by invoking + * {@link #endOffset()}. This allows creating fields in sequence without doing offset computation: + *

+ * ZipField.F2 firstField = new ZipField.F2(0, "First field");
+ * ZipField.F4 secondField = new ZipField(firstField.endOffset(), "Second field");
+ * 
+ */ +abstract class ZipField { + + /** + * Field name. Used for providing (more) useful error messages. + */ + @Nonnull + private final String name; + + /** + * Offset of the file in the record. + */ + protected final int offset; + + /** + * Size of the field. Only 2 or 4 allowed. + */ + private final int size; + + /** + * If a fixed value exists for the field, then this attribute will contain that value. + */ + @Nullable + private final Long expected; + + /** + * All invariants that this field must verify. + */ + @Nonnull + private Set invariants; + + /** + * Creates a new field that does not contain a fixed value. + * + * @param offset the field's offset in the record + * @param size the field size + * @param name the field's name + * @param invariants the invariants that must be verified by the field + */ + ZipField(int offset, int size, @Nonnull String name, ZipFieldInvariant... invariants) { + Preconditions.checkArgument(offset >= 0, "offset >= 0"); + Preconditions.checkArgument(size == 2 || size == 4, "size != 2 && size != 4"); + + this.name = name; + this.offset = offset; + this.size = size; + expected = null; + this.invariants = Sets.newHashSet(invariants); + } + + /** + * Creates a new field that contains a fixed value. + * + * @param offset the field's offset in the record + * @param size the field size + * @param expected the expected field value + * @param name the field's name + */ + ZipField(int offset, int size, long expected, @Nonnull String name) { + Preconditions.checkArgument(offset >= 0, "offset >= 0"); + Preconditions.checkArgument(size == 2 || size == 4, "size != 2 && size != 4"); + + this.name = name; + this.offset = offset; + this.size = size; + this.expected = expected; + invariants = Sets.newHashSet(); + } + + /** + * Checks whether a value verifies the field's invariants. Nothing happens if the value verifies + * the invariants. + * + * @param value the value + * @throws IOException the invariants are not verified + */ + private void checkVerifiesInvariants(long value) throws IOException { + for (ZipFieldInvariant invariant : invariants) { + if (!invariant.isValid(value)) { + throw new IOException("Value " + value + " of field " + name + " is invalid " + + "(fails '" + invariant.getName() + "')."); + } + } + } + + /** + * Advances the position in the provided byte buffer by the size of this field. + * + * @param bytes the byte buffer; at the end of the method its position will be greater by + * the size of this field + * @throws IOException failed to advance the buffer + */ + void skip(@Nonnull ByteBuffer bytes) throws IOException { + if (bytes.remaining() < size) { + throw new IOException("Cannot skip field " + name + " because only " + + bytes.remaining() + " remain in the buffer."); + } + + bytes.position(bytes.position() + size); + } + + /** + * Reads a field value. + * + * @param bytes the byte buffer with the record data; after this method finishes, the buffer + * will be positioned at the first byte after the field + * @return the value of the field + * @throws IOException failed to read the field + */ + long read(@Nonnull ByteBuffer bytes) throws IOException { + if (bytes.remaining() < size) { + throw new IOException("Cannot skip field " + name + " because only " + + bytes.remaining() + " remain in the buffer."); + } + + bytes.order(ByteOrder.LITTLE_ENDIAN); + + long r; + if (size == 2) { + r = LittleEndianUtils.readUnsigned2Le(bytes); + } else { + r = LittleEndianUtils.readUnsigned4Le(bytes); + } + + checkVerifiesInvariants(r); + return r; + } + + /** + * Verifies that the field at the current buffer position has the expected value. The field + * must have been created with the constructor that defines the expected value. + * + * @param bytes the byte buffer with the record data; after this method finishes, the buffer + * will be positioned at the first byte after the field + * @throws IOException failed to read the field or the field does not have the expected value + */ + void verify(@Nonnull ByteBuffer bytes) throws IOException { + verify(bytes, null); + } + + /** + * Verifies that the field at the current buffer position has the expected value. The field + * must have been created with the constructor that defines the expected value. + * + * @param bytes the byte buffer with the record data; after this method finishes, the buffer + * will be positioned at the first byte after the field + * @param verifyLog if non-{@code null}, will log the verification error + * @throws IOException failed to read the data or the field does not have the expected value; + * only thrown if {@code verifyLog} is {@code null} + */ + void verify(@Nonnull ByteBuffer bytes, @Nullable VerifyLog verifyLog) throws IOException { + Preconditions.checkState(expected != null, "expected == null"); + verify(bytes, expected, verifyLog); + } + + /** + * Verifies that the field has an expected value. + * + * @param bytes the byte buffer with the record data; after this method finishes, the buffer + * will be positioned at the first byte after the field + * @param expected the value we expect the field to have; if this field has invariants, the + * value must verify them + * @throws IOException failed to read the data or the field does not have the expected value + */ + void verify(@Nonnull ByteBuffer bytes, long expected) throws IOException { + verify(bytes, expected, null); + } + + /** + * Verifies that the field has an expected value. + * + * @param bytes the byte buffer with the record data; after this method finishes, the buffer + * will be positioned at the first byte after the field + * @param expected the value we expect the field to have; if this field has invariants, the + * value must verify them + * @param verifyLog if non-{@code null}, will log the verification error + * @throws IOException failed to read the data or the field does not have the expected value; + * only thrown if {@code verifyLog} is {@code null} + */ + void verify( + @Nonnull ByteBuffer bytes, + long expected, + @Nullable VerifyLog verifyLog) throws IOException { + checkVerifiesInvariants(expected); + long r = read(bytes); + if (r != expected) { + String error = + String.format( + "Incorrect value for field '%s': value is %s but %s expected.", + name, + r, + expected); + + if (verifyLog == null) { + throw new IOException(error); + } else { + verifyLog.log(error); + } + } + } + + /** + * Writes the value of the field. + * + * @param output where to write the field; the field will be written at the current position + * of the buffer + * @param value the value to write + * @throws IOException failed to write the value in the stream + */ + void write(@Nonnull ByteBuffer output, long value) throws IOException { + checkVerifiesInvariants(value); + + Preconditions.checkArgument(value >= 0, "value (%s) < 0", value); + + if (size == 2) { + Preconditions.checkArgument(value <= 0x0000ffff, "value (%s) > 0x0000ffff", value); + LittleEndianUtils.writeUnsigned2Le(output, Ints.checkedCast(value)); + } else { + Verify.verify(size == 4); + Preconditions.checkArgument(value <= 0x00000000ffffffffL, + "value (%s) > 0x00000000ffffffffL", value); + LittleEndianUtils.writeUnsigned4Le(output, value); + } + } + + /** + * Writes the value of the field. The field must have an expected value set in the constructor. + * + * @param output where to write the field; the field will be written at the current position + * of the buffer + * @throws IOException failed to write the value in the stream + */ + void write(@Nonnull ByteBuffer output) throws IOException { + Preconditions.checkState(expected != null, "expected == null"); + write(output, expected); + } + + /** + * Obtains the offset at which the field starts. + * + * @return the start offset + */ + int offset() { + return offset; + } + + /** + * Obtains the offset at which the field ends. This is the exact offset at which the next + * field starts. + * + * @return the end offset + */ + int endOffset() { + return offset + size; + } + + /** + * Concrete implementation of {@link ZipField} that represents a 2-byte field. + */ + static class F2 extends ZipField { + + /** + * Creates a new field. + * + * @param offset the field's offset in the record + * @param name the field's name + * @param invariants the invariants that must be verified by the field + */ + F2(int offset, @Nonnull String name, ZipFieldInvariant... invariants) { + super(offset, 2, name, invariants); + } + + /** + * Creates a new field that contains a fixed value. + * + * @param offset the field's offset in the record + * @param expected the expected field value + * @param name the field's name + */ + F2(int offset, long expected, @Nonnull String name) { + super(offset, 2, expected, name); + } + } + + /** + * Concrete implementation of {@link ZipField} that represents a 4-byte field. + */ + static class F4 extends ZipField { + /** + * Creates a new field. + * + * @param offset the field's offset in the record + * @param name the field's name + * @param invariants the invariants that must be verified by the field + */ + F4(int offset, @Nonnull String name, ZipFieldInvariant... invariants) { + super(offset, 4, name, invariants); + } + + /** + * Creates a new field that contains a fixed value. + * + * @param offset the field's offset in the record + * @param expected the expected field value + * @param name the field's name + */ + F4(int offset, long expected, @Nonnull String name) { + super(offset, 4, expected, name); + } + } +} diff --git a/src/main/java/com/android/tools/build/apkzlib/zip/ZipFieldInvariant.java b/src/main/java/com/android/tools/build/apkzlib/zip/ZipFieldInvariant.java new file mode 100644 index 0000000..7207080 --- /dev/null +++ b/src/main/java/com/android/tools/build/apkzlib/zip/ZipFieldInvariant.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * 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.android.tools.build.apkzlib.zip; + +/** + * A field rule defines an invariant (i.e., a constraint) that has to be verified by a + * field value. + */ +interface ZipFieldInvariant { + + /** + * Evalutes the invariant against a value. + * + * @param value the value to check the invariant + * @return is the invariant valid? + */ + boolean isValid(long value); + + /** + * Obtains the name of the invariant. Used for information purposes. + * + * @return the name of the invariant + */ + String getName(); +} diff --git a/src/main/java/com/android/tools/build/apkzlib/zip/ZipFieldInvariantMaxValue.java b/src/main/java/com/android/tools/build/apkzlib/zip/ZipFieldInvariantMaxValue.java new file mode 100644 index 0000000..65c4c5b --- /dev/null +++ b/src/main/java/com/android/tools/build/apkzlib/zip/ZipFieldInvariantMaxValue.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * 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.android.tools.build.apkzlib.zip; + +/** + * Invariant checking a zip field does not exceed a threshold. + */ +class ZipFieldInvariantMaxValue implements ZipFieldInvariant { + + /** + * The maximum value allowed. + */ + private long max; + + /** + * Creates a new invariant. + * + * @param max the maximum value allowed for the field + */ + ZipFieldInvariantMaxValue(int max) { + this.max = max; + } + + @Override + public boolean isValid(long value) { + return value <= max; + } + + @Override + public String getName() { + return "Maximum value " + max; + } +} diff --git a/src/main/java/com/android/tools/build/apkzlib/zip/ZipFieldInvariantNonNegative.java b/src/main/java/com/android/tools/build/apkzlib/zip/ZipFieldInvariantNonNegative.java new file mode 100644 index 0000000..76c2fb2 --- /dev/null +++ b/src/main/java/com/android/tools/build/apkzlib/zip/ZipFieldInvariantNonNegative.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * 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.android.tools.build.apkzlib.zip; + +/** + * Invariant that verifies a field's value is not negative. + */ +class ZipFieldInvariantNonNegative implements ZipFieldInvariant { + + @Override + public boolean isValid(long value) { + return value >= 0; + } + + @Override + public String getName() { + return "Is positive"; + } +} diff --git a/src/main/java/com/android/tools/build/apkzlib/zip/ZipFileState.java b/src/main/java/com/android/tools/build/apkzlib/zip/ZipFileState.java new file mode 100644 index 0000000..6d72816 --- /dev/null +++ b/src/main/java/com/android/tools/build/apkzlib/zip/ZipFileState.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * 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.android.tools.build.apkzlib.zip; + +/** + * The {@code ZipFileState} enumeration holds the state of a {@link ZFile}. + */ +enum ZipFileState { + /** + * Zip file is closed. + */ + CLOSED, + + /** + * File file is open in read-only mode. + */ + OPEN_RO, + + /** + * File file is open in read-write mode. + */ + OPEN_RW +} diff --git a/src/main/java/com/android/tools/build/apkzlib/zip/compress/BestAndDefaultDeflateExecutorCompressor.java b/src/main/java/com/android/tools/build/apkzlib/zip/compress/BestAndDefaultDeflateExecutorCompressor.java new file mode 100644 index 0000000..0e646cf --- /dev/null +++ b/src/main/java/com/android/tools/build/apkzlib/zip/compress/BestAndDefaultDeflateExecutorCompressor.java @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * 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.android.tools.build.apkzlib.zip.compress; + +import com.android.tools.build.apkzlib.zip.CompressionResult; +import com.android.tools.build.apkzlib.zip.utils.ByteTracker; +import com.android.tools.build.apkzlib.zip.utils.CloseableByteSource; +import com.google.common.base.Preconditions; +import java.util.concurrent.Executor; +import java.util.zip.Deflater; +import javax.annotation.Nonnull; + +/** + * Compressor that tries both the best and default compression algorithms and picks the default + * unless the best is at least a given percentage smaller. + */ +public class BestAndDefaultDeflateExecutorCompressor extends ExecutorCompressor { + + /** + * Deflater using the default compression level. + */ + @Nonnull + private final DeflateExecutionCompressor defaultDeflater; + + /** + * Deflater using the best compression level. + */ + @Nonnull + private final DeflateExecutionCompressor bestDeflater; + + /** + * Minimum best compression size / default compression size ratio needed to pick the default + * compression size. + */ + private final double minRatio; + + /** + * Creates a new compressor. + * + * @param executor the executor used to perform compression activities. + * @param tracker the byte tracker to keep track of allocated bytes + * @param minRatio the minimum best compression size / default compression size needed to pick + * the default compression size; if {@code 0.0} then the default compression is always picked, + * if {@code 1.0} then the best compression is always picked unless it produces the exact same + * size as the default compression. + */ + public BestAndDefaultDeflateExecutorCompressor(@Nonnull Executor executor, + @Nonnull ByteTracker tracker, double minRatio) { + super(executor); + + Preconditions.checkArgument(minRatio >= 0.0, "minRatio < 0.0"); + Preconditions.checkArgument(minRatio <= 1.0, "minRatio > 1.0"); + + defaultDeflater = + new DeflateExecutionCompressor(executor, tracker, Deflater.DEFAULT_COMPRESSION); + bestDeflater = + new DeflateExecutionCompressor(executor, tracker, Deflater.BEST_COMPRESSION); + this.minRatio = minRatio; + } + + @Nonnull + @Override + protected CompressionResult immediateCompress(@Nonnull CloseableByteSource source) + throws Exception { + CompressionResult defaultResult = defaultDeflater.immediateCompress(source); + CompressionResult bestResult = bestDeflater.immediateCompress(source); + + double sizeRatio = bestResult.getSize() / (double) defaultResult.getSize(); + if (sizeRatio >= minRatio) { + return defaultResult; + } else { + return bestResult; + } + } +} diff --git a/src/main/java/com/android/tools/build/apkzlib/zip/compress/DeflateExecutionCompressor.java b/src/main/java/com/android/tools/build/apkzlib/zip/compress/DeflateExecutionCompressor.java new file mode 100644 index 0000000..dbaeff0 --- /dev/null +++ b/src/main/java/com/android/tools/build/apkzlib/zip/compress/DeflateExecutionCompressor.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * 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.android.tools.build.apkzlib.zip.compress; + +import com.android.tools.build.apkzlib.zip.CompressionMethod; +import com.android.tools.build.apkzlib.zip.CompressionResult; +import com.android.tools.build.apkzlib.zip.utils.ByteTracker; +import com.android.tools.build.apkzlib.zip.utils.CloseableByteSource; +import java.io.ByteArrayOutputStream; +import java.util.concurrent.Executor; +import java.util.zip.Deflater; +import java.util.zip.DeflaterOutputStream; +import javax.annotation.Nonnull; + +/** + * Compressor that uses deflate with an executor. + */ +public class DeflateExecutionCompressor extends ExecutorCompressor { + + + /** + * Deflate compression level. + */ + private final int level; + + /** + * Byte tracker to use to create byte sources. + */ + @Nonnull + private final ByteTracker tracker; + + /** + * Creates a new compressor. + * + * @param executor the executor to run deflation tasks + * @param tracker the byte tracker to use to keep track of memory usage + * @param level the compression level + */ + public DeflateExecutionCompressor( + @Nonnull Executor executor, + @Nonnull ByteTracker tracker, + int level) { + super(executor); + + this.level = level; + this.tracker = tracker; + } + + @Nonnull + @Override + protected CompressionResult immediateCompress(@Nonnull CloseableByteSource source) + throws Exception { + ByteArrayOutputStream output = new ByteArrayOutputStream(); + Deflater deflater = new Deflater(level, true); + + try (DeflaterOutputStream dos = new DeflaterOutputStream(output, deflater)) { + dos.write(source.read()); + } + + CloseableByteSource result = tracker.fromStream(output); + if (result.size() >= source.size()) { + return new CompressionResult(source, CompressionMethod.STORE, source.size()); + } else { + return new CompressionResult(result, CompressionMethod.DEFLATE, result.size()); + } + } +} diff --git a/src/main/java/com/android/tools/build/apkzlib/zip/compress/ExecutorCompressor.java b/src/main/java/com/android/tools/build/apkzlib/zip/compress/ExecutorCompressor.java new file mode 100644 index 0000000..6a19907 --- /dev/null +++ b/src/main/java/com/android/tools/build/apkzlib/zip/compress/ExecutorCompressor.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * 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.android.tools.build.apkzlib.zip.compress; + +import com.android.tools.build.apkzlib.zip.CompressionResult; +import com.android.tools.build.apkzlib.zip.Compressor; +import com.android.tools.build.apkzlib.zip.utils.CloseableByteSource; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.SettableFuture; +import java.util.concurrent.Executor; +import javax.annotation.Nonnull; + +/** + * A synchronous compressor is a compressor that computes the result of compression immediately + * and never returns an uncomputed future object. + */ +public abstract class ExecutorCompressor implements Compressor { + + /** + * The executor that does the work. + */ + @Nonnull + private final Executor executor; + + /** + * Compressor that delegates execution into the given executor. + * @param executor the executor that will do the compress + */ + public ExecutorCompressor(@Nonnull Executor executor) { + this.executor = executor; + } + + @Nonnull + @Override + public ListenableFuture compress( + @Nonnull final CloseableByteSource source) { + final SettableFuture future = SettableFuture.create(); + executor.execute(() -> { + try { + future.set(immediateCompress(source)); + } catch (Throwable e) { + future.setException(e); + } + }); + + return future; + } + + /** + * Immediately compresses a source. + * @param source the source to compress + * @return the result of compression + * @throws Exception failed to compress + */ + @Nonnull + protected abstract CompressionResult immediateCompress(@Nonnull CloseableByteSource source) + throws Exception; +} diff --git a/src/main/java/com/android/tools/build/apkzlib/zip/compress/Zip64NotSupportedException.java b/src/main/java/com/android/tools/build/apkzlib/zip/compress/Zip64NotSupportedException.java new file mode 100644 index 0000000..bf77e34 --- /dev/null +++ b/src/main/java/com/android/tools/build/apkzlib/zip/compress/Zip64NotSupportedException.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * 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.android.tools.build.apkzlib.zip.compress; + +import java.io.IOException; + +/** Exception raised by ZFile when encountering unsupported Zip64 format jar files. */ +public class Zip64NotSupportedException extends IOException { + + public Zip64NotSupportedException(String message) { + super(message); + } +} diff --git a/src/main/java/com/android/tools/build/apkzlib/zip/compress/package-info.java b/src/main/java/com/android/tools/build/apkzlib/zip/compress/package-info.java new file mode 100644 index 0000000..cdc85f8 --- /dev/null +++ b/src/main/java/com/android/tools/build/apkzlib/zip/compress/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * 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. + */ + +/** + * Compressors to use with the {@code zip} package. + */ +package com.android.tools.build.apkzlib.zip.compress; diff --git a/src/main/java/com/android/tools/build/apkzlib/zip/utils/ByteTracker.java b/src/main/java/com/android/tools/build/apkzlib/zip/utils/ByteTracker.java new file mode 100644 index 0000000..d956dd2 --- /dev/null +++ b/src/main/java/com/android/tools/build/apkzlib/zip/utils/ByteTracker.java @@ -0,0 +1,120 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * 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.android.tools.build.apkzlib.zip.utils; + +import com.google.common.io.ByteSource; +import com.google.common.io.ByteStreams; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import javax.annotation.Nonnull; + +/** + * Keeps track of used bytes allowing gauging memory usage. + */ +public class ByteTracker { + + /** + * Number of bytes currently in use. + */ + private long bytesUsed; + + /** + * Maximum number of bytes used. + */ + private long maxBytesUsed; + + /** + * Creates a new byte source by fully reading an input stream. + * + * @param stream the input stream + * @return a byte source containing the cached data from the given stream + * @throws IOException failed to read the stream + */ + public CloseableDelegateByteSource fromStream(@Nonnull InputStream stream) throws IOException { + byte[] data = ByteStreams.toByteArray(stream); + updateUsage(data.length); + return new CloseableDelegateByteSource(ByteSource.wrap(data), data.length) { + @Override + public synchronized void innerClose() throws IOException { + super.innerClose(); + updateUsage(-sizeNoException()); + } + }; + } + + /** + * Creates a new byte source by snapshotting the provided stream. + * + * @param stream the stream with the data + * @return a byte source containing the cached data from the given stream + * @throws IOException failed to read the stream + */ + public CloseableDelegateByteSource fromStream(@Nonnull ByteArrayOutputStream stream) + throws IOException { + byte[] data = stream.toByteArray(); + updateUsage(data.length); + return new CloseableDelegateByteSource(ByteSource.wrap(data), data.length) { + @Override + public synchronized void innerClose() throws IOException { + super.innerClose(); + updateUsage(-sizeNoException()); + } + }; + } + + /** + * Creates a new byte source from another byte source. + * + * @param source the byte source to copy data from + * @return the tracked byte source + * @throws IOException failed to read data from the byte source + */ + public CloseableDelegateByteSource fromSource(@Nonnull ByteSource source) throws IOException { + return fromStream(source.openStream()); + } + + /** + * Updates the memory used by this tracker. + * + * @param delta the number of bytes to add or remove, if negative + */ + private synchronized void updateUsage(long delta) { + bytesUsed += delta; + if (maxBytesUsed < bytesUsed) { + maxBytesUsed = bytesUsed; + } + } + + /** + * Obtains the number of bytes currently used. + * + * @return the number of bytes + */ + public synchronized long getBytesUsed() { + return bytesUsed; + } + + /** + * Obtains the maximum number of bytes ever used by this tracker. + * + * @return the number of bytes + */ + public synchronized long getMaxBytesUsed() { + return maxBytesUsed; + } +} diff --git a/src/main/java/com/android/tools/build/apkzlib/zip/utils/CloseableByteSource.java b/src/main/java/com/android/tools/build/apkzlib/zip/utils/CloseableByteSource.java new file mode 100644 index 0000000..9af9671 --- /dev/null +++ b/src/main/java/com/android/tools/build/apkzlib/zip/utils/CloseableByteSource.java @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * 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.android.tools.build.apkzlib.zip.utils; + +import com.google.common.io.ByteSource; +import java.io.Closeable; +import java.io.IOException; + +/** + * Byte source that can be closed. Closing a byte source allows releasing any resources associated + * with it. This should not be confused with closing streams. For example, {@link ByteTracker} uses + * {@code CloseableByteSources} to know when the data associated with the byte source can be + * released. + */ +public abstract class CloseableByteSource extends ByteSource implements Closeable { + + /** + * Has the source been closed? + */ + private boolean closed; + + /** + * Creates a new byte source. + */ + public CloseableByteSource() { + closed = false; + } + + @Override + public final synchronized void close() throws IOException { + if (closed) { + return; + } + + try { + innerClose(); + } finally { + closed = true; + } + } + + /** + * Closes the by source. This method is only invoked once, even if {@link #close()} is + * called multiple times. + * + * @throws IOException failed to close + */ + protected abstract void innerClose() throws IOException; +} diff --git a/src/main/java/com/android/tools/build/apkzlib/zip/utils/CloseableDelegateByteSource.java b/src/main/java/com/android/tools/build/apkzlib/zip/utils/CloseableDelegateByteSource.java new file mode 100644 index 0000000..df084d4 --- /dev/null +++ b/src/main/java/com/android/tools/build/apkzlib/zip/utils/CloseableDelegateByteSource.java @@ -0,0 +1,171 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * 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.android.tools.build.apkzlib.zip.utils; + +import com.google.common.hash.HashCode; +import com.google.common.hash.HashFunction; +import com.google.common.io.ByteProcessor; +import com.google.common.io.ByteSink; +import com.google.common.io.ByteSource; +import com.google.common.io.CharSource; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.Charset; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * Closeable byte source that delegates to another byte source. + */ +public class CloseableDelegateByteSource extends CloseableByteSource { + + /** + * The byte source we delegate all operations to. {@code null} if disposed. + */ + @Nullable + private ByteSource inner; + + /** + * Size of the byte source. This is the same as {@code inner.size()} (when {@code inner} + * is not {@code null}), but we keep it separate to avoid calling {@code inner.size()} + * because it might throw {@code IOException}. + */ + private final long mSize; + + /** + * Creates a new byte source. + * + * @param inner the inner byte source + * @param size the size of the source + */ + public CloseableDelegateByteSource(@Nonnull ByteSource inner, long size) { + this.inner = inner; + mSize = size; + } + + /** + * Obtains the inner byte source. Will throw an exception if the inner by byte source has + * been disposed of. + * + * @return the inner byte source + */ + @Nonnull + private synchronized ByteSource get() { + if (inner == null) { + throw new ByteSourceDisposedException(); + } + + return inner; + } + + /** + * Mark the byte source as disposed. + */ + @Override + protected synchronized void innerClose() throws IOException { + if (inner == null) { + return; + } + + inner = null; + } + + /** + * Obtains the size of this byte source. Equivalent to {@link #size()} but not throwing + * {@code IOException}. + * + * @return the size of the byte source + */ + public long sizeNoException() { + return mSize; + } + + @Override + public CharSource asCharSource(Charset charset) { + return get().asCharSource(charset); + } + + @Override + public InputStream openBufferedStream() throws IOException { + return get().openBufferedStream(); + } + + @Override + public ByteSource slice(long offset, long length) { + return get().slice(offset, length); + } + + @Override + public boolean isEmpty() throws IOException { + return get().isEmpty(); + } + + @Override + public long size() throws IOException { + return get().size(); + } + + @Override + public long copyTo(@Nonnull OutputStream output) throws IOException { + return get().copyTo(output); + } + + @Override + public long copyTo(@Nonnull ByteSink sink) throws IOException { + return get().copyTo(sink); + } + + @Override + public byte[] read() throws IOException { + return get().read(); + } + + @Override + public T read(@Nonnull ByteProcessor processor) throws IOException { + return get().read(processor); + } + + @Override + public HashCode hash(HashFunction hashFunction) throws IOException { + return get().hash(hashFunction); + } + + @Override + public boolean contentEquals(@Nonnull ByteSource other) throws IOException { + return get().contentEquals(other); + } + + @Override + public InputStream openStream() throws IOException { + return get().openStream(); + } + + /** + * Exception thrown when trying to use a byte source that has been disposed. + */ + private static class ByteSourceDisposedException extends RuntimeException { + + /** + * Creates a new exception. + */ + private ByteSourceDisposedException() { + super("Byte source was created by a ByteTracker and is now disposed. If you see " + + "this message, then there is a bug."); + } + } +} diff --git a/src/main/java/com/android/tools/build/apkzlib/zip/utils/LittleEndianUtils.java b/src/main/java/com/android/tools/build/apkzlib/zip/utils/LittleEndianUtils.java new file mode 100644 index 0000000..1fd056a --- /dev/null +++ b/src/main/java/com/android/tools/build/apkzlib/zip/utils/LittleEndianUtils.java @@ -0,0 +1,129 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * 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.android.tools.build.apkzlib.zip.utils; + +import com.google.common.base.Preconditions; +import com.google.common.base.Verify; +import java.io.EOFException; +import java.io.IOException; +import java.nio.ByteBuffer; +import javax.annotation.Nonnull; + +/** + * Utilities to read and write 16 and 32 bit integers with support for little-endian + * encoding, as used in zip files. Zip files actually use unsigned data types. We use Java's native + * (signed) data types but will use long (64 bit) to ensure we can fit the whole range. + */ +public class LittleEndianUtils { + /** + * Utility class, no constructor. + */ + private LittleEndianUtils() { + } + + /** + * Reads 4 bytes in little-endian format and converts them into a 32-bit value. + * + * @param bytes from where should the bytes be read; the first 4 bytes of the source will be + * read + * @return the 32-bit value + * @throws IOException failed to read the value + */ + public static long readUnsigned4Le(@Nonnull ByteBuffer bytes) throws IOException { + Preconditions.checkNotNull(bytes, "bytes == null"); + + if (bytes.remaining() < 4) { + throw new EOFException("Not enough data: 4 bytes expected, " + bytes.remaining() + + " available."); + } + + byte b0 = bytes.get(); + byte b1 = bytes.get(); + byte b2 = bytes.get(); + byte b3 = bytes.get(); + long r = (b0 & 0xff) | ((b1 & 0xff) << 8) | ((b2 & 0xff) << 16) | ((b3 & 0xffL) << 24); + Verify.verify(r >= 0); + Verify.verify(r <= 0x00000000ffffffffL); + return r; + } + + /** + * Reads 2 bytes in little-endian format and converts them into a 16-bit value. + * + * @param bytes from where should the bytes be read; the first 2 bytes of the source will be + * read + * @return the 16-bit value + * @throws IOException failed to read the value + */ + public static int readUnsigned2Le(@Nonnull ByteBuffer bytes) throws IOException { + Preconditions.checkNotNull(bytes, "bytes == null"); + + if (bytes.remaining() < 2) { + throw new EOFException( + "Not enough data: 2 bytes expected, " + + bytes.remaining() + + " available."); + } + + byte b0 = bytes.get(); + byte b1 = bytes.get(); + int r = (b0 & 0xff) | ((b1 & 0xff) << 8); + + Verify.verify(r >= 0); + Verify.verify(r <= 0x0000ffff); + return r; + } + + /** + * Writes 4 bytes in little-endian format, converting them from a 32-bit value. + * + * @param output the output stream where the bytes will be written + * @param value the 32-bit value to convert + * @throws IOException failed to write the value data + */ + public static void writeUnsigned4Le(@Nonnull ByteBuffer output, long value) + throws IOException { + Preconditions.checkNotNull(output, "output == null"); + Preconditions.checkArgument(value >= 0, "value (%s) < 0", value); + Preconditions.checkArgument( + value <= 0x00000000ffffffffL, + "value (%s) > 0x00000000ffffffffL", + value); + + output.put((byte) (value & 0xff)); + output.put((byte) ((value >> 8) & 0xff)); + output.put((byte) ((value >> 16) & 0xff)); + output.put((byte) ((value >> 24) & 0xff)); + } + + /** + * Writes 2 bytes in little-endian format, converting them from a 16-bit value. + * + * @param output the output stream where the bytes will be written + * @param value the 16-bit value to convert + * @throws IOException failed to write the value data + */ + public static void writeUnsigned2Le(@Nonnull ByteBuffer output, int value) + throws IOException { + Preconditions.checkNotNull(output, "output == null"); + Preconditions.checkArgument(value >= 0, "value (%s) < 0", value); + Preconditions.checkArgument(value <= 0x0000ffff, "value (%s) > 0x0000ffff", value); + + output.put((byte) (value & 0xff)); + output.put((byte) ((value >> 8) & 0xff)); + } +} diff --git a/src/main/java/com/android/tools/build/apkzlib/zip/utils/MsDosDateTimeUtils.java b/src/main/java/com/android/tools/build/apkzlib/zip/utils/MsDosDateTimeUtils.java new file mode 100644 index 0000000..cc16cb4 --- /dev/null +++ b/src/main/java/com/android/tools/build/apkzlib/zip/utils/MsDosDateTimeUtils.java @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * 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.android.tools.build.apkzlib.zip.utils; + +import com.google.common.base.Verify; +import java.util.Calendar; +import java.util.Date; + +/** + * Yes. This actually refers to MS-DOS in 2015. That's all I have to say about legacy stuff. + */ +public class MsDosDateTimeUtils { + /** + * Utility class: no constructor. + */ + private MsDosDateTimeUtils() { + } + + /** + * Packs java time value into an MS-DOS time value. + * + * @param time the time value + * @return the MS-DOS packed time + */ + public static int packTime(long time) { + Calendar c = Calendar.getInstance(); + c.setTime(new Date(time)); + + int seconds = c.get(Calendar.SECOND); + int minutes = c.get(Calendar.MINUTE); + int hours = c.get(Calendar.HOUR_OF_DAY); + + /* + * Here is how MS-DOS packs a time value: + * 0-4: seconds (divided by 2 because we only have 5 bits = 32 different numbers) + * 5-10: minutes (6 bits = 64 possible values) + * 11-15: hours (5 bits = 32 possible values) + * + * source: https://msdn.microsoft.com/en-us/library/windows/desktop/ms724247(v=vs.85).aspx + */ + return (hours << 11) | (minutes << 5) | (seconds / 2); + } + + /** + * Packs the current time value into an MS-DOS time value. + * + * @return the MS-DOS packed time + */ + public static int packCurrentTime() { + return packTime(new Date().getTime()); + } + + /** + * Packs java time value into an MS-DOS date value. + * + * @param time the time value + * @return the MS-DOS packed date + */ + public static int packDate(long time) { + Calendar c = Calendar.getInstance(); + c.setTime(new Date(time)); + + /* + * Even MS-DOS used 1 for January. Someone wasn't really thinking when they decided on Java + * it would start at 0... + */ + int day = c.get(Calendar.DAY_OF_MONTH); + int month = c.get(Calendar.MONTH) + 1; + + /* + * MS-DOS counts years starting from 1980. Since its launch date was in 81, it was obviously + * not necessary to talk about dates earlier than that. + */ + int year = c.get(Calendar.YEAR) - 1980; + Verify.verify(year >= 0 && year < 128); + + /* + * Here is how MS-DOS packs a date value: + * 0-4: day (5 bits = 32 values) + * 5-8: month (4 bits = 16 values) + * 9-15: year (7 bits = 128 values) + * + * source: https://msdn.microsoft.com/en-us/library/windows/desktop/ms724247(v=vs.85).aspx + */ + return (year << 9) | (month << 5) | day; + } + + /** + * Packs the current time value into an MS-DOS date value. + * + * @return the MS-DOS packed date + */ + public static int packCurrentDate() { + return packDate(new Date().getTime()); + } +} diff --git a/src/main/java/com/android/tools/build/apkzlib/zip/utils/RandomAccessFileUtils.java b/src/main/java/com/android/tools/build/apkzlib/zip/utils/RandomAccessFileUtils.java new file mode 100644 index 0000000..17c2d6c --- /dev/null +++ b/src/main/java/com/android/tools/build/apkzlib/zip/utils/RandomAccessFileUtils.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * 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.android.tools.build.apkzlib.zip.utils; + +import java.io.IOException; +import java.io.RandomAccessFile; +import javax.annotation.Nonnull; + +/** + * Utility class with utility methods for random access files. + */ +public final class RandomAccessFileUtils { + + private RandomAccessFileUtils() {} + + /** + * Reads from an random access file until the provided array is filled. Data is read from the + * current position in the file. + * + * @param raf the file to read data from + * @param data the array that will receive the data + * @throws IOException failed to read the data + */ + public static void fullyRead(@Nonnull RandomAccessFile raf, @Nonnull byte[] data) + throws IOException { + int r; + int p = 0; + + while ((r = raf.read(data, p, data.length - p)) > 0) { + p += r; + if (p == data.length) { + break; + } + } + + if (p < data.length) { + throw new IOException( + "Failed to read " + + data.length + + " bytes from file. Only " + + p + + " bytes could be read."); + } + } +} diff --git a/src/test/java/com/android/apkzlib/sign/FullApkSignTest.java b/src/test/java/com/android/apkzlib/sign/FullApkSignTest.java deleted file mode 100644 index f72f63c..0000000 --- a/src/test/java/com/android/apkzlib/sign/FullApkSignTest.java +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * 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.android.apkzlib.sign; - -import static org.junit.Assert.assertArrayEquals; -import static org.junit.Assert.assertNotNull; - -import com.android.apkzlib.utils.ApkZLibPair; -import com.android.apkzlib.zip.AlignmentRule; -import com.android.apkzlib.zip.AlignmentRules; -import com.android.apkzlib.zip.StoredEntry; -import com.android.apkzlib.zip.ZFile; -import com.android.apkzlib.zip.ZFileOptions; -import com.android.apkzlib.zip.ZFileTestConstants; -import com.android.apkzlib.utils.ApkZFileTestUtils; -import java.io.ByteArrayInputStream; -import java.io.File; -import java.security.PrivateKey; -import java.security.cert.X509Certificate; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.TemporaryFolder; - -/** - * Tests that verify APK Signature Scheme v2 signing using {@link SigningExtension}. - */ -public class FullApkSignTest { - - /** - * Folder used for tests. - */ - @Rule - public TemporaryFolder mTemporaryFolder = new TemporaryFolder(); - - @Test - public void testSignature() throws Exception { - File out = new File(mTemporaryFolder.getRoot(), "apk"); - - ApkZLibPair signData = - SignatureTestUtils.generateSignaturePre18(); - - // The byte arrays below are larger when compressed, so we end up storing them uncompressed, - // which would normally cause them to be 4-aligned. Disable that, to make calculations - // easier. - ZFileOptions options = new ZFileOptions(); - options.setAlignmentRule(AlignmentRules.constant(AlignmentRule.NO_ALIGNMENT)); - - /* - * Generate a signed zip. - */ - ZFile zf = new ZFile(out, options); - new SigningExtension(13, signData.v2, signData.v1, false, true) - .register(zf); - String f1Name = "abc"; - byte[] f1Data = new byte[] { 1, 1, 1, 1 }; - zf.add(f1Name, new ByteArrayInputStream(f1Data)); - String f2Name = "defg"; - byte[] f2Data = new byte[] { 2, 2, 2, 2, 3, 3, 3, 3}; - zf.add(f2Name, new ByteArrayInputStream(f2Data)); - zf.close(); - - /* - * We should see the data in place. - */ - int f1DataStart = ZFileTestConstants.LOCAL_HEADER_SIZE + f1Name.length(); - int f1DataEnd = f1DataStart + f1Data.length; - int f2DataStart = f1DataEnd + ZFileTestConstants.LOCAL_HEADER_SIZE + f2Name.length(); - int f2DataEnd = f2DataStart + f2Data.length; - - byte[] read1 = ApkZFileTestUtils.readSegment(out, f1DataStart, f1Data.length); - assertArrayEquals(f1Data, read1); - byte[] read2 = ApkZFileTestUtils.readSegment(out, f2DataStart, f2Data.length); - assertArrayEquals(f2Data, read2); - - /* - * Read the signed zip. - */ - ZFile zf2 = new ZFile(out); - - StoredEntry se1 = zf2.get(f1Name); - assertNotNull(se1); - assertArrayEquals(f1Data, se1.read()); - - StoredEntry se2 = zf2.get(f2Name); - assertNotNull(se2); - assertArrayEquals(f2Data, se2.read()); - - zf2.close(); - } -} diff --git a/src/test/java/com/android/apkzlib/sign/JarSigningTest.java b/src/test/java/com/android/apkzlib/sign/JarSigningTest.java deleted file mode 100644 index 35aeeaf..0000000 --- a/src/test/java/com/android/apkzlib/sign/JarSigningTest.java +++ /dev/null @@ -1,378 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * 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.android.apkzlib.sign; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotEquals; -import static org.junit.Assert.assertNotNull; - -import com.android.apkzlib.utils.ApkZFileTestUtils; -import com.android.apkzlib.utils.ApkZLibPair; -import com.android.apkzlib.zip.StoredEntry; -import com.android.apkzlib.zip.ZFile; -import com.google.common.base.Charsets; -import com.google.common.hash.Hashing; -import java.io.ByteArrayInputStream; -import java.io.File; -import java.io.InputStream; -import java.security.PrivateKey; -import java.security.cert.X509Certificate; -import java.util.Base64; -import java.util.jar.Attributes; -import java.util.jar.Manifest; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.TemporaryFolder; - -public class JarSigningTest { - - @Rule - public TemporaryFolder mTemporaryFolder = new TemporaryFolder(); - - @Test - public void signEmptyJar() throws Exception { - File zipFile = new File(mTemporaryFolder.getRoot(), "a.zip"); - - try (ZFile zf = new ZFile(zipFile)) { - ApkZFileTestUtils.addAndroidManifest(zf); - ManifestGenerationExtension manifestExtension = - new ManifestGenerationExtension("Me", "Me"); - manifestExtension.register(zf); - - ApkZLibPair p = - SignatureTestUtils.generateSignaturePre18(); - - new SigningExtension(12, p.v2, p.v1, true, false).register(zf); - } - - try (ZFile verifyZFile = new ZFile(zipFile)) { - StoredEntry manifestEntry = verifyZFile.get("META-INF/MANIFEST.MF"); - assertNotNull(manifestEntry); - - Manifest manifest = new Manifest(new ByteArrayInputStream(manifestEntry.read())); - assertEquals(3, manifest.getMainAttributes().size()); - assertEquals("1.0", manifest.getMainAttributes().getValue("Manifest-Version")); - assertEquals("Me", manifest.getMainAttributes().getValue("Created-By")); - assertEquals("Me", manifest.getMainAttributes().getValue("Built-By")); - } - } - - @Test - public void signJarWithPrexistingSimpleTextFilePre18() throws Exception { - File zipFile = new File(mTemporaryFolder.getRoot(), "a.zip"); - ApkZLibPair p = SignatureTestUtils.generateSignaturePre18(); - - try (ZFile zf1 = new ZFile(zipFile)) { - ApkZFileTestUtils.addAndroidManifest(zf1); - zf1.add("directory/file", - new ByteArrayInputStream("useless text".getBytes(Charsets.US_ASCII))); - } - - try (ZFile zf2 = new ZFile(zipFile)) { - ManifestGenerationExtension me = new ManifestGenerationExtension("Merry", "Christmas"); - me.register(zf2); - new SigningExtension(10, p.v2, p.v1, true, false).register(zf2); - } - - try (ZFile zf3 = new ZFile(zipFile)) { - StoredEntry manifestEntry = zf3.get("META-INF/MANIFEST.MF"); - assertNotNull(manifestEntry); - - Manifest manifest = new Manifest(new ByteArrayInputStream(manifestEntry.read())); - assertEquals(3, manifest.getMainAttributes().size()); - assertEquals("1.0", manifest.getMainAttributes().getValue("Manifest-Version")); - assertEquals("Merry", manifest.getMainAttributes().getValue("Built-By")); - assertEquals("Christmas", manifest.getMainAttributes().getValue("Created-By")); - - Attributes attrs = manifest.getAttributes("directory/file"); - assertNotNull(attrs); - assertEquals(1, attrs.size()); - assertEquals("OOQgIEXBissIvva3ydRoaXk29Rk=", attrs.getValue("SHA1-Digest")); - - StoredEntry signatureEntry = zf3.get("META-INF/CERT.SF"); - assertNotNull(signatureEntry); - - Manifest signature = new Manifest(new ByteArrayInputStream(signatureEntry.read())); - assertEquals(3, signature.getMainAttributes().size()); - assertEquals("1.0", signature.getMainAttributes().getValue("Signature-Version")); - assertEquals("1.0 (Android)", signature.getMainAttributes().getValue("Created-By")); - - byte[] manifestTextBytes = manifestEntry.read(); - byte[] manifestSha1Bytes = Hashing.sha1().hashBytes(manifestTextBytes).asBytes(); - String manifestSha1 = Base64.getEncoder().encodeToString(manifestSha1Bytes); - - assertEquals(manifestSha1, - signature.getMainAttributes().getValue("SHA1-Digest-Manifest")); - - Attributes signAttrs = signature.getAttributes("directory/file"); - assertNotNull(signAttrs); - assertEquals(1, signAttrs.size()); - assertEquals("LGSOwy4uGcUWoc+ZhS8ukzmf0fY=", signAttrs.getValue("SHA1-Digest")); - - StoredEntry rsaEntry = zf3.get("META-INF/CERT.RSA"); - assertNotNull(rsaEntry); - } - } - - @Test - public void signJarWithPrexistingSimpleTextFilePos18() throws Exception { - File zipFile = new File(mTemporaryFolder.getRoot(), "a.zip"); - try (ZFile zf1 = new ZFile(zipFile)) { - ApkZFileTestUtils.addAndroidManifest(zf1); - zf1.add("directory/file", new ByteArrayInputStream("useless text".getBytes( - Charsets.US_ASCII))); - } - - ApkZLibPair p = SignatureTestUtils.generateSignaturePos18(); - - try (ZFile zf2 = new ZFile(zipFile)) { - ManifestGenerationExtension me = new ManifestGenerationExtension("Merry", "Christmas"); - me.register(zf2); - new SigningExtension(21, p.v2, p.v1, true, false).register(zf2); - } - - try (ZFile zf3 = new ZFile(zipFile)) { - StoredEntry manifestEntry = zf3.get("META-INF/MANIFEST.MF"); - assertNotNull(manifestEntry); - - Manifest manifest = new Manifest(new ByteArrayInputStream(manifestEntry.read())); - assertEquals(3, manifest.getMainAttributes().size()); - assertEquals("1.0", manifest.getMainAttributes().getValue("Manifest-Version")); - assertEquals("Merry", manifest.getMainAttributes().getValue("Built-By")); - assertEquals("Christmas", manifest.getMainAttributes().getValue("Created-By")); - - Attributes attrs = manifest.getAttributes("directory/file"); - assertNotNull(attrs); - assertEquals(1, attrs.size()); - assertEquals("QjupZsopQM/01O6+sWHqH64ilMmoBEtljg9VEqN6aI4=", - attrs.getValue("SHA-256-Digest")); - - StoredEntry signatureEntry = zf3.get("META-INF/CERT.SF"); - assertNotNull(signatureEntry); - - Manifest signature = new Manifest(new ByteArrayInputStream(signatureEntry.read())); - assertEquals(3, signature.getMainAttributes().size()); - assertEquals("1.0", signature.getMainAttributes().getValue("Signature-Version")); - assertEquals("1.0 (Android)", signature.getMainAttributes().getValue("Created-By")); - - byte[] manifestTextBytes = manifestEntry.read(); - byte[] manifestSha256Bytes = Hashing.sha256().hashBytes(manifestTextBytes).asBytes(); - String manifestSha256 = Base64.getEncoder().encodeToString(manifestSha256Bytes); - - assertEquals(manifestSha256, signature.getMainAttributes().getValue( - "SHA-256-Digest-Manifest")); - - Attributes signAttrs = signature.getAttributes("directory/file"); - assertNotNull(signAttrs); - assertEquals(1, signAttrs.size()); - assertEquals("dBnaLpqNjmUnLlZF4tNqOcDWL8wy8Tsw1ZYFqTZhjIs=", - signAttrs.getValue("SHA-256-Digest")); - - StoredEntry ecdsaEntry = zf3.get("META-INF/CERT.EC"); - assertNotNull(ecdsaEntry); - } - } - - @Test - public void v2SignAddsApkSigningBlock() throws Exception { - File zipFile = new File(mTemporaryFolder.getRoot(), "a.zip"); - try (ZFile zf = new ZFile(zipFile)) { - ApkZFileTestUtils.addAndroidManifest(zf); - ManifestGenerationExtension manifestExtension = - new ManifestGenerationExtension("Me", "Me"); - manifestExtension.register(zf); - - ApkZLibPair p = SignatureTestUtils.generateSignaturePre18(); - - new SigningExtension(12, p.v2, p.v1, false, true).register(zf); - } - - try (ZFile verifyZFile = new ZFile(zipFile)) { - long centralDirOffset = verifyZFile.getCentralDirectoryOffset(); - byte[] apkSigningBlockMagic = new byte[16]; - verifyZFile.directFullyRead( - centralDirOffset - apkSigningBlockMagic.length, apkSigningBlockMagic); - assertEquals("APK Sig Block 42", new String(apkSigningBlockMagic, "US-ASCII")); - } - } - - @Test - public void v1ReSignOnFileChange() throws Exception { - File zipFile = new File(mTemporaryFolder.getRoot(), "a.zip"); - ApkZLibPair p = SignatureTestUtils.generateSignaturePos18(); - - byte[] file1Contents = "I am a test file".getBytes(Charsets.US_ASCII); - String file1Name = "path/to/file1"; - byte[] file1Sha = Hashing.sha256().hashBytes(file1Contents).asBytes(); - String file1ShaTxt = Base64.getEncoder().encodeToString(file1Sha); - - String builtBy = "Santa Claus"; - String createdBy = "Uses Android"; - - try (ZFile zf1 = new ZFile(zipFile)) { - ApkZFileTestUtils.addAndroidManifest(zf1); - zf1.add(file1Name, new ByteArrayInputStream(file1Contents)); - ManifestGenerationExtension me = new ManifestGenerationExtension(builtBy, createdBy); - me.register(zf1); - new SigningExtension(21, p.v2, p.v1, true, false).register(zf1); - - zf1.update(); - - StoredEntry manifestEntry = zf1.get("META-INF/MANIFEST.MF"); - assertNotNull(manifestEntry); - - try (InputStream manifestIs = manifestEntry.open()) { - Manifest manifest = new Manifest(manifestIs); - - assertEquals(2, manifest.getEntries().size()); - - Attributes file1Attrs = manifest.getEntries().get(file1Name); - assertNotNull(file1Attrs); - assertEquals(file1ShaTxt, file1Attrs.getValue("SHA-256-Digest")); - } - - /* - * Change the file without closing the zip. - */ - file1Contents = "I am a modified test file".getBytes(Charsets.US_ASCII); - file1Sha = Hashing.sha256().hashBytes(file1Contents).asBytes(); - file1ShaTxt = Base64.getEncoder().encodeToString(file1Sha); - - zf1.add(file1Name, new ByteArrayInputStream(file1Contents)); - - zf1.update(); - - manifestEntry = zf1.get("META-INF/MANIFEST.MF"); - assertNotNull(manifestEntry); - - try (InputStream manifestIs = manifestEntry.open()) { - Manifest manifest = new Manifest(manifestIs); - - assertEquals(2, manifest.getEntries().size()); - - Attributes file1Attrs = manifest.getEntries().get(file1Name); - assertNotNull(file1Attrs); - assertEquals(file1ShaTxt, file1Attrs.getValue("SHA-256-Digest")); - } - } - - /* - * Change the file closing the zip. - */ - file1Contents = "I have changed again!".getBytes(Charsets.US_ASCII); - file1Sha = Hashing.sha256().hashBytes(file1Contents).asBytes(); - file1ShaTxt = Base64.getEncoder().encodeToString(file1Sha); - - try (ZFile zf2 = new ZFile(zipFile)) { - ApkZFileTestUtils.addAndroidManifest(zf2); - ManifestGenerationExtension me = new ManifestGenerationExtension(builtBy, createdBy); - me.register(zf2); - new SigningExtension(21, p.v2, p.v1, true, false).register(zf2); - - zf2.add(file1Name, new ByteArrayInputStream(file1Contents)); - - zf2.update(); - - StoredEntry manifestEntry = zf2.get("META-INF/MANIFEST.MF"); - assertNotNull(manifestEntry); - - try (InputStream manifestIs = manifestEntry.open()) { - Manifest manifest = new Manifest(manifestIs); - - assertEquals(2, manifest.getEntries().size()); - - Attributes file1Attrs = manifest.getEntries().get(file1Name); - assertNotNull(file1Attrs); - assertEquals(file1ShaTxt, file1Attrs.getValue("SHA-256-Digest")); - } - } - } - - @Test - public void openSignedJarDoesNotForcesWriteIfSignatureIsNotCorrect() throws Exception { - File zipFile = new File(mTemporaryFolder.getRoot(), "a.zip"); - - ApkZLibPair p = SignatureTestUtils.generateSignaturePos18(); - - String fileName = "file"; - byte[] fileContents = "Very interesting contents".getBytes(Charsets.US_ASCII); - - try (ZFile zf = new ZFile(zipFile)) { - ApkZFileTestUtils.addAndroidManifest(zf); - ManifestGenerationExtension me = new ManifestGenerationExtension("I", "Android"); - me.register(zf); - new SigningExtension(21, p.v2, p.v1, true, false).register(zf); - - zf.add(fileName, new ByteArrayInputStream(fileContents)); - } - - long fileTimestamp = zipFile.lastModified(); - - ApkZFileTestUtils.waitForFileSystemTick(fileTimestamp); - - /* - * Open the zip file, but don't touch it. - */ - try (ZFile zf = new ZFile(zipFile)) { - ManifestGenerationExtension me = new ManifestGenerationExtension("I", "Android"); - me.register(zf); - new SigningExtension(21, p.v2, p.v1, true, false).register(zf); - } - - /* - * Check the file wasn't touched. - */ - assertEquals(fileTimestamp, zipFile.lastModified()); - - /* - * Change the file contents ignoring any signing. - */ - fileContents = "Not so interesting contents".getBytes(Charsets.US_ASCII); - try (ZFile zf = new ZFile(zipFile)) { - zf.add(fileName, new ByteArrayInputStream(fileContents)); - } - - fileTimestamp = zipFile.lastModified(); - - /* - * Wait to make sure the timestamp can increase. - */ - while (true) { - File notUsed = mTemporaryFolder.newFile(); - long notTimestamp = notUsed.lastModified(); - notUsed.delete(); - if (notTimestamp > fileTimestamp) { - break; - } - } - - /* - * Open the zip file, but do any changes. The need to updating the signature should force - * a file update. - */ - try (ZFile zf = new ZFile(zipFile)) { - ManifestGenerationExtension me = new ManifestGenerationExtension("I", "Android"); - me.register(zf); - new SigningExtension(21, p.v2, p.v1, true, false).register(zf); - } - - /* - * Check the file was touched. - */ - assertNotEquals(fileTimestamp, zipFile.lastModified()); - } -} diff --git a/src/test/java/com/android/apkzlib/sign/ManifestGenerationTest.java b/src/test/java/com/android/apkzlib/sign/ManifestGenerationTest.java deleted file mode 100644 index f0817d0..0000000 --- a/src/test/java/com/android/apkzlib/sign/ManifestGenerationTest.java +++ /dev/null @@ -1,180 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * 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.android.apkzlib.sign; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; - -import com.android.apkzlib.utils.ApkZFileTestUtils; -import com.android.apkzlib.zip.StoredEntry; -import com.android.apkzlib.zip.ZFile; -import com.google.common.base.Charsets; -import com.google.common.io.Closer; -import java.io.ByteArrayInputStream; -import java.io.File; -import java.io.InputStream; -import java.util.HashSet; -import java.util.Set; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.TemporaryFolder; - -public class ManifestGenerationTest { - - private static final String WIKI_PATH = "/testData/packaging/text-files/wikipedia.html"; - - @Rule - public TemporaryFolder mTemporaryFolder = new TemporaryFolder(); - - @Test - public void elementaryManifestGeneration() throws Exception { - File zip = new File(mTemporaryFolder.getRoot(), "f.zip"); - - try (ZFile zf = new ZFile(zip)) { - zf.add("abc", new ByteArrayInputStream(new byte[]{1})); - zf.add("x/", new ByteArrayInputStream(new byte[0])); - zf.add("x/abc", new ByteArrayInputStream(new byte[]{2})); - - ManifestGenerationExtension extension = - new ManifestGenerationExtension("Me, of course", "Myself"); - extension.register(zf); - - zf.update(); - - StoredEntry se = zf.get("META-INF/MANIFEST.MF"); - assertNotNull(se); - - String text = new String(se.read(), Charsets.US_ASCII); - text = text.trim(); - String lines[] = text.split(System.getProperty("line.separator")); - assertEquals(3, lines.length); - - assertEquals("Manifest-Version: 1.0", lines[0].trim()); - - Set linesSet = new HashSet<>(); - for (String l : lines) { - linesSet.add(l.trim()); - } - - assertTrue(linesSet.contains("Built-By: Me, of course")); - assertTrue(linesSet.contains("Created-By: Myself")); - } - } - - @Test - public void manifestGenerationOnHalfWrittenFile() throws Exception { - File zip = new File(mTemporaryFolder.getRoot(), "f.zip"); - try (Closer closer = Closer.create()) { - ZFile zf = closer.register(new ZFile(zip)); - - try (InputStream wiki = getClass().getResourceAsStream(WIKI_PATH)) { - zf.add("wiki", wiki); - } - - ManifestGenerationExtension extension = - new ManifestGenerationExtension("Me, of course", "Myself"); - extension.register(zf); - - zf.close(); - - StoredEntry se = zf.get("META-INF/MANIFEST.MF"); - assertNotNull(se); - - String text = new String(se.read(), Charsets.US_ASCII); - text = text.trim(); - String lines[] = text.split(System.getProperty("line.separator")); - assertEquals(3, lines.length); - - assertEquals("Manifest-Version: 1.0", lines[0].trim()); - - Set linesSet = new HashSet<>(); - for (String l : lines) { - linesSet.add(l.trim()); - } - - assertTrue(linesSet.contains("Built-By: Me, of course")); - assertTrue(linesSet.contains("Created-By: Myself")); - } - } - - @Test - public void manifestGenerationOnExistingFile() throws Exception { - File zip = new File(mTemporaryFolder.getRoot(), "f.zip"); - try (Closer closer = Closer.create()) { - ZFile zf = closer.register(new ZFile(zip)); - - try (InputStream wiki = getClass().getResourceAsStream(WIKI_PATH)) { - zf.add("wiki", wiki); - } - - zf.close(); - - ManifestGenerationExtension extension = - new ManifestGenerationExtension("Me, of course", "Myself"); - extension.register(zf); - - zf.close(); - - StoredEntry se = zf.get("META-INF/MANIFEST.MF"); - assertNotNull(se); - - String text = new String(se.read(), Charsets.US_ASCII); - text = text.trim(); - String lines[] = text.split(System.getProperty("line.separator")); - assertEquals(3, lines.length); - - assertEquals("Manifest-Version: 1.0", lines[0].trim()); - - Set linesSet = new HashSet<>(); - for (String l : lines) { - linesSet.add(l.trim()); - } - - assertTrue(linesSet.contains("Built-By: Me, of course")); - assertTrue(linesSet.contains("Created-By: Myself")); - } - } - - @Test - public void manifestGenerationOnIncrementalNoChanges() throws Exception { - File zip = new File(mTemporaryFolder.getRoot(), "f.zip"); - try (Closer closer = Closer.create()) { - ZFile zf = closer.register(new ZFile(zip)); - - ManifestGenerationExtension extension = - new ManifestGenerationExtension("Me, of course", "Myself"); - extension.register(zf); - - try (InputStream wiki = getClass().getResourceAsStream(WIKI_PATH)) { - zf.add("wiki", wiki); - } - - zf.close(); - - long timeOfWriting = zip.lastModified(); - - ApkZFileTestUtils.waitForFileSystemTick(timeOfWriting); - - zf = closer.register(new ZFile(zip)); - zf.close(); - - long secondTimeOfWriting = zip.lastModified(); - assertEquals(timeOfWriting, secondTimeOfWriting); - } - } -} diff --git a/src/test/java/com/android/apkzlib/sign/SignatureTestUtils.java b/src/test/java/com/android/apkzlib/sign/SignatureTestUtils.java deleted file mode 100644 index fb1d322..0000000 --- a/src/test/java/com/android/apkzlib/sign/SignatureTestUtils.java +++ /dev/null @@ -1,133 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * 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.android.apkzlib.sign; - -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.fail; - -import com.android.apkzlib.utils.ApkZLibPair; -import java.math.BigInteger; -import java.security.KeyPair; -import java.security.KeyPairGenerator; -import java.security.NoSuchAlgorithmException; -import java.security.PrivateKey; -import java.security.cert.X509Certificate; -import java.security.interfaces.ECPublicKey; -import java.security.interfaces.RSAPublicKey; -import java.util.Date; -import javax.annotation.Nonnull; -import javax.security.auth.x500.X500Principal; -import org.bouncycastle.asn1.x500.X500Name; -import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo; -import org.bouncycastle.cert.X509CertificateHolder; -import org.bouncycastle.cert.X509v1CertificateBuilder; -import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; -import org.bouncycastle.crypto.params.RSAKeyParameters; -import org.bouncycastle.crypto.util.SubjectPublicKeyInfoFactory; -import org.bouncycastle.jce.provider.BouncyCastleProvider; -import org.bouncycastle.operator.ContentSigner; -import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; -import org.junit.Assume; - -/** - * Utilities to use signatures in tests. - */ -public class SignatureTestUtils { - - /** - * Generates a private key / certificate for pre-18 systems. - * - * @return the pair with the private key and certificate - * @throws Exception failed to generate the signature data - */ - @Nonnull - public static ApkZLibPair generateSignaturePre18() - throws Exception { - return generateSignature("RSA", "SHA1withRSA"); - } - - /** - * Generates a private key / certificate for post-18 systems. - * - * @return the pair with the private key and certificate - * @throws Exception failed to generate the signature data - */ - @Nonnull - public static ApkZLibPair generateSignaturePos18() - throws Exception { - return generateSignature("EC", "SHA256withECDSA"); - } - - /** - * Generates a private key / certificate. - * - * @param sign the asymmetric cypher, e.g., {@code RSA} - * @param full the full signature algorithm name, e.g., {@code SHA1withRSA} - * @return the pair with the private key and certificate - * @throws Exception failed to generate the signature data - */ - @Nonnull - public static ApkZLibPair generateSignature( - @Nonnull String sign, - @Nonnull String full) - throws Exception { - // http://stackoverflow.com/questions/28538785/ - // easy-way-to-generate-a-self-signed-certificate-for-java-security-keystore-using - - KeyPairGenerator generator = null; - try { - generator = KeyPairGenerator.getInstance(sign); - } catch (NoSuchAlgorithmException e) { - Assume.assumeNoException("Algorithm " + sign + " not supported.", e); - } - - assertNotNull(generator); - KeyPair keyPair = generator.generateKeyPair(); - - Date notBefore = new Date(System.currentTimeMillis() - 24 * 60 * 60 * 1000); - Date notAfter = new Date(System.currentTimeMillis() + 365L * 24 * 60 * 60 * 1000); - - X500Name issuer = new X500Name(new X500Principal("cn=Myself").getName()); - - SubjectPublicKeyInfo publicKeyInfo; - - if (keyPair.getPublic() instanceof RSAPublicKey) { - RSAPublicKey rsaPublicKey = (RSAPublicKey) keyPair.getPublic(); - publicKeyInfo = SubjectPublicKeyInfoFactory.createSubjectPublicKeyInfo( - new RSAKeyParameters(false, rsaPublicKey.getModulus(), - rsaPublicKey.getPublicExponent())); - } else if (keyPair.getPublic() instanceof ECPublicKey) { - publicKeyInfo = SubjectPublicKeyInfo.getInstance(keyPair.getPublic().getEncoded()); - } else { - fail(); - publicKeyInfo = null; - } - - X509v1CertificateBuilder builder = new X509v1CertificateBuilder(issuer, BigInteger.ONE, - notBefore, notAfter, issuer, publicKeyInfo); - - ContentSigner signer = new JcaContentSignerBuilder(full).setProvider( - new BouncyCastleProvider()).build(keyPair.getPrivate()); - X509CertificateHolder holder = builder.build(signer); - - JcaX509CertificateConverter converter = new JcaX509CertificateConverter() - .setProvider(new BouncyCastleProvider()); - - return new ApkZLibPair(keyPair.getPrivate(), converter.getCertificate(holder)); - } - -} diff --git a/src/test/java/com/android/apkzlib/utils/ApkZFileTestUtils.java b/src/test/java/com/android/apkzlib/utils/ApkZFileTestUtils.java deleted file mode 100644 index 1ef087f..0000000 --- a/src/test/java/com/android/apkzlib/utils/ApkZFileTestUtils.java +++ /dev/null @@ -1,139 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * 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.android.apkzlib.utils; - -import static org.junit.Assert.assertTrue; - -import com.android.apkzlib.zip.ZFile; -import com.android.testutils.TestResources; -import com.google.common.base.Preconditions; -import com.google.common.io.ByteSource; -import com.google.common.io.Resources; -import java.io.ByteArrayInputStream; -import java.io.EOFException; -import java.io.File; -import java.io.IOException; -import java.io.RandomAccessFile; -import javax.annotation.Nonnull; - -/** - * Utility functions for tests. - */ -public final class ApkZFileTestUtils { - - /** - * Reads a portion of a file to memory. - * - * @param file the file to read data from - * @param start the offset in the file to start reading - * @param length the number of bytes to read - * @return the bytes read - * @throws Exception failed to read the file - */ - @Nonnull - public static byte[] readSegment(@Nonnull File file, long start, int length) throws Exception { - Preconditions.checkArgument(start >= 0, "start < 0"); - Preconditions.checkArgument(length >= 0, "length < 0"); - - byte data[]; - try (RandomAccessFile raf = new RandomAccessFile(file, "r")) { - raf.seek(start); - - data = new byte[length]; - int tot = 0; - while (tot < length) { - int r = raf.read(data, tot, length - tot); - if (r < 0) { - throw new EOFException(); - } - - tot += r; - } - } - - return data; - } - - /** - * Obtains the test resource with the given path. - * - * @param path the path - * @return the test resource - */ - @Nonnull - public static File getResource(@Nonnull String path) { - File resource = TestResources.getFile(ApkZFileTestUtils.class, path); - assertTrue(resource.exists()); - return resource; - } - - /** - * Obtains the test resource with the given path. - * - * @param path the path - * @return the test resource - */ - @Nonnull - public static ByteSource getResourceBytes(@Nonnull String path) { - return Resources.asByteSource(Resources.getResource(ApkZFileTestUtils.class, path)); - } - - /** - * Sleeps the current thread for enough time to ensure that the local file system had enough - * time to notice a "tick". This method is usually called in tests when it is necessary to - * ensure filesystem writes are detected through timestamp modification. - * - * @param currentTimestamp last timestamp read from disk - * @throws InterruptedException waiting interrupted - * @throws IOException issues creating a temporary file - */ - public static void waitForFileSystemTick(long currentTimestamp) - throws InterruptedException, IOException { - while (getFreshTimestamp() <= currentTimestamp) { - Thread.sleep(100); - } - } - - /* - * Adds a basic compiled AndroidManifest to the given ZFile containing minSdkVersion equal 15 - * and targetSdkVersion equal 25. - */ - public static void addAndroidManifest(ZFile zf) throws IOException { - zf.add("AndroidManifest.xml", new ByteArrayInputStream(getAndroidManifest())); - } - - /* - * Provides a basic compiled AndroidManifest containing minSdkVersion equal 15 and - * targetSdkVersion equal 25. - */ - public static byte[] getAndroidManifest() throws IOException { - return ApkZFileTestUtils.getResourceBytes("/testData/packaging/AndroidManifest.xml").read(); - } - - /** - * Obtains the timestamp of a newly-created file. - * - * @return the timestamp - * @throws IOException the I/O Exception - */ - private static long getFreshTimestamp() throws IOException { - File notUsed = File.createTempFile(ApkZFileTestUtils.class.getName(), "waitForFSTick"); - long freshTimestamp = notUsed.lastModified(); - assertTrue(notUsed.delete()); - return freshTimestamp; - } -} diff --git a/src/test/java/com/android/apkzlib/utils/CachedFileContentsTest.java b/src/test/java/com/android/apkzlib/utils/CachedFileContentsTest.java deleted file mode 100644 index f9654d7..0000000 --- a/src/test/java/com/android/apkzlib/utils/CachedFileContentsTest.java +++ /dev/null @@ -1,123 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * 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.android.apkzlib.utils; - -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertSame; -import static org.junit.Assert.assertTrue; - -import com.google.common.base.Charsets; -import com.google.common.io.Files; - -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.TemporaryFolder; - -import java.io.File; - -public class CachedFileContentsTest { - @Rule - public TemporaryFolder mTemporaryFolder = new TemporaryFolder(); - - @Test - public void createFileAndCheckWithNoChanges() throws Exception { - File f = mTemporaryFolder.newFile("test"); - Files.write("abc", f, Charsets.US_ASCII); - - Object cache = new Object(); - - CachedFileContents cachedFile = new CachedFileContents<>(f); - cachedFile.closed(cache); - - assertTrue(cachedFile.isValid()); - assertSame(cache, cachedFile.getCache()); - } - - @Test - public void createFileAndCheckChanges() throws Exception { - File f = mTemporaryFolder.newFile("test"); - Files.write("abc", f, Charsets.US_ASCII); - - Object cache = new Object(); - - CachedFileContents cachedFile = new CachedFileContents<>(f); - cachedFile.closed(cache); - - Files.write("def", f, Charsets.US_ASCII); - - assertFalse(cachedFile.isValid()); - assertNull(cachedFile.getCache()); - } - - @Test - public void createFileUpdateAndCheckChanges() throws Exception { - File f = mTemporaryFolder.newFile("test"); - Files.write("abc", f, Charsets.US_ASCII); - - Object cache = new Object(); - - CachedFileContents cachedFile = new CachedFileContents<>(f); - cachedFile.closed(cache); - - Files.write("def", f, Charsets.US_ASCII); - cachedFile.closed(cache); - - assertTrue(cachedFile.isValid()); - assertSame(cache, cachedFile.getCache()); - } - - @Test - public void immediateChangesDetected() throws Exception { - File f = mTemporaryFolder.newFile("foo"); - Files.write("bar", f, Charsets.US_ASCII); - - CachedFileContents cachedFile = new CachedFileContents<>(f); - cachedFile.closed(null); - - Files.write("xpto", f, Charsets.US_ASCII); - assertFalse(cachedFile.isValid()); - } - - @Test - public void immediateChangesDetectedEvenWithHackedTs() throws Exception { - File f = mTemporaryFolder.newFile("foo"); - Files.write("bar", f, Charsets.US_ASCII); - - CachedFileContents cachedFile = new CachedFileContents<>(f); - cachedFile.closed(null); - long lastTs = f.lastModified(); - - Files.write("xpto", f, Charsets.US_ASCII); - f.setLastModified(lastTs); - assertFalse(cachedFile.isValid()); - } - - @Test - public void immediateChangesWithNoContentChangeNotDetected() throws Exception { - File f = mTemporaryFolder.newFile("foo"); - Files.write("bar", f, Charsets.US_ASCII); - - CachedFileContents cachedFile = new CachedFileContents<>(f); - cachedFile.closed(null); - long lastTs = f.lastModified(); - - Files.write("bar", f, Charsets.US_ASCII); - f.setLastModified(lastTs); - assertTrue(cachedFile.isValid()); - } -} diff --git a/src/test/java/com/android/apkzlib/utils/CachedSupplierTest.java b/src/test/java/com/android/apkzlib/utils/CachedSupplierTest.java deleted file mode 100644 index e687bf3..0000000 --- a/src/test/java/com/android/apkzlib/utils/CachedSupplierTest.java +++ /dev/null @@ -1,112 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * 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.android.apkzlib.utils; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; - -import org.junit.Test; - -import java.util.function.Supplier; - -public class CachedSupplierTest { - - @Test - public void testGetsOnlyOnce() { - TestSupplier ts = new TestSupplier(); - CachedSupplier cs = new CachedSupplier<>(ts); - assertFalse(cs.isValid()); - - ts.value = "foo"; - assertEquals(0, ts.invocationCount); - assertEquals("foo", cs.get()); - assertEquals(1, ts.invocationCount); - assertTrue(cs.isValid()); - - ts.value = "bar"; - assertEquals("foo", cs.get()); - assertEquals(1, ts.invocationCount); - assertTrue(cs.isValid()); - } - - @Test - public void cacheCanBePreset() { - TestSupplier ts = new TestSupplier(); - ts.value = "foo"; - CachedSupplier cs = new CachedSupplier<>(ts); - cs.precomputed("bar"); - assertTrue(cs.isValid()); - - assertEquals("bar", cs.get()); - assertEquals(0, ts.invocationCount); - } - - @Test - public void exceptionThrownBySupplier() { - CachedSupplier cs = new CachedSupplier<>(() -> { - throw new RuntimeException("foo"); - }); - assertFalse(cs.isValid()); - - try { - cs.get(); - fail(); - } catch (RuntimeException e) { - assertEquals("foo", e.getMessage()); - } - - assertFalse(cs.isValid()); - - try { - cs.get(); - fail(); - } catch (RuntimeException e) { - assertEquals("foo", e.getMessage()); - } - } - - @Test - public void reset() { - TestSupplier ts = new TestSupplier(); - ts.value = "foo"; - CachedSupplier cs = new CachedSupplier<>(ts); - assertFalse(cs.isValid()); - - assertEquals("foo", cs.get()); - assertEquals(1, ts.invocationCount); - assertTrue(cs.isValid()); - ts.value = "bar"; - - cs.reset(); - assertFalse(cs.isValid()); - assertEquals("bar", cs.get()); - assertEquals(2, ts.invocationCount); - } - - static class TestSupplier implements Supplier { - int invocationCount = 0; - String value; - - @Override - public String get() { - invocationCount++; - return value; - } - } -} diff --git a/src/test/java/com/android/apkzlib/zfile/ApkAlignmentTest.java b/src/test/java/com/android/apkzlib/zfile/ApkAlignmentTest.java deleted file mode 100644 index 1731ba9..0000000 --- a/src/test/java/com/android/apkzlib/zfile/ApkAlignmentTest.java +++ /dev/null @@ -1,231 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * 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.android.apkzlib.zfile; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; - -import com.android.apkzlib.zip.CompressionMethod; -import com.android.apkzlib.zip.StoredEntry; -import com.android.apkzlib.zip.ZFile; -import com.android.apkzlib.zip.ZFileOptions; -import java.io.ByteArrayInputStream; -import java.io.File; -import java.nio.file.Files; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.TemporaryFolder; - -public class ApkAlignmentTest { - @Rule public TemporaryFolder mTemporaryFolder = new TemporaryFolder(); - - @Test - public void soFilesUncompressedAndAligned() throws Exception { - File apk = new File(mTemporaryFolder.getRoot(), "a.apk"); - - File soFile = new File(mTemporaryFolder.getRoot(), "doesnt_work.so"); - Files.write(soFile.toPath(), new byte[500]); - - ApkZFileCreatorFactory cf = new ApkZFileCreatorFactory(new ZFileOptions()); - ApkCreatorFactory.CreationData creationData = - new ApkCreatorFactory.CreationData( - apk, - null, - null, - false, - false, - null, - null, - 20, - NativeLibrariesPackagingMode.UNCOMPRESSED_AND_ALIGNED, - path -> false); - - ApkCreator creator = cf.make(creationData); - - creator.writeFile(soFile, "/doesnt_work.so"); - creator.close(); - - try (ZFile zf = new ZFile(apk)) { - StoredEntry soEntry = zf.get("/doesnt_work.so"); - assertNotNull(soEntry); - assertEquals( - CompressionMethod.STORE, - soEntry.getCentralDirectoryHeader().getCompressionInfoWithWait().getMethod()); - long offset = - soEntry.getCentralDirectoryHeader().getOffset() + soEntry.getLocalHeaderSize(); - assertTrue(offset % 4096 == 0); - } - } - - @Test - public void soFilesMergedFromZipsCanBeUncompressedAndAligned() throws Exception { - - // Create a zip file with a compressed, unaligned so file. - File zipToMerge = new File(mTemporaryFolder.getRoot(), "a.zip"); - try (ZFile zf = new ZFile(zipToMerge)) { - zf.add("/zero.so", new ByteArrayInputStream(new byte[500])); - } - - try (ZFile zf = new ZFile(zipToMerge)) { - StoredEntry zeroSo = zf.get("/zero.so"); - assertNotNull(zeroSo); - assertEquals( - CompressionMethod.DEFLATE, - zeroSo.getCentralDirectoryHeader().getCompressionInfoWithWait().getMethod()); - long offset = - zeroSo.getCentralDirectoryHeader().getOffset() + zeroSo.getLocalHeaderSize(); - assertFalse(offset % 4096 == 0); - } - - // Create an APK and merge the zip file. - File apk = new File(mTemporaryFolder.getRoot(), "b.apk"); - ApkZFileCreatorFactory cf = new ApkZFileCreatorFactory(new ZFileOptions()); - ApkCreatorFactory.CreationData creationData = - new ApkCreatorFactory.CreationData( - apk, - null, - null, - false, - false, - null, - null, - 20, - NativeLibrariesPackagingMode.UNCOMPRESSED_AND_ALIGNED, - path -> false); - - try (ApkCreator creator = cf.make(creationData)) { - creator.writeZip(zipToMerge, null, null); - } - - // Make sure the file is uncompressed and aligned. - try (ZFile zf = new ZFile(apk)) { - StoredEntry soEntry = zf.get("/zero.so"); - assertNotNull(soEntry); - assertEquals( - CompressionMethod.STORE, - soEntry.getCentralDirectoryHeader().getCompressionInfoWithWait().getMethod()); - long offset = - soEntry.getCentralDirectoryHeader().getOffset() + soEntry.getLocalHeaderSize(); - assertTrue(offset % 4096 == 0); - - byte[] data = soEntry.read(); - assertEquals(500, data.length); - for (int i = 0; i < data.length; i++) { - assertEquals(0, data[i]); - } - } - } - - @Test - public void soFilesUncompressedAndNotAligned() throws Exception { - File apk = new File(mTemporaryFolder.getRoot(), "a.apk"); - - File soFile = new File(mTemporaryFolder.getRoot(), "doesnt_work.so"); - Files.write(soFile.toPath(), new byte[500]); - - ApkZFileCreatorFactory cf = new ApkZFileCreatorFactory(new ZFileOptions()); - ApkCreatorFactory.CreationData creationData = - new ApkCreatorFactory.CreationData( - apk, - null, - null, - false, - false, - null, - null, - 20, - NativeLibrariesPackagingMode.COMPRESSED, - path -> false); - - ApkCreator creator = cf.make(creationData); - - creator.writeFile(soFile, "/doesnt_work.so"); - creator.close(); - - try (ZFile zf = new ZFile(apk)) { - StoredEntry soEntry = zf.get("/doesnt_work.so"); - assertNotNull(soEntry); - assertEquals( - CompressionMethod.DEFLATE, - soEntry.getCentralDirectoryHeader().getCompressionInfoWithWait().getMethod()); - long offset = - soEntry.getCentralDirectoryHeader().getOffset() + soEntry.getLocalHeaderSize(); - assertTrue(offset % 4096 != 0); - } - } - - @Test - public void soFilesMergedFromZipsCanBeUncompressedAndNotAligned() throws Exception { - - // Create a zip file with a compressed, unaligned so file. - File zipToMerge = new File(mTemporaryFolder.getRoot(), "a.zip"); - try (ZFile zf = new ZFile(zipToMerge)) { - zf.add("/zero.so", new ByteArrayInputStream(new byte[500])); - } - - try (ZFile zf = new ZFile(zipToMerge)) { - StoredEntry zeroSo = zf.get("/zero.so"); - assertNotNull(zeroSo); - assertEquals( - CompressionMethod.DEFLATE, - zeroSo.getCentralDirectoryHeader().getCompressionInfoWithWait().getMethod()); - long offset = - zeroSo.getCentralDirectoryHeader().getOffset() + zeroSo.getLocalHeaderSize(); - assertFalse(offset % 4096 == 0); - } - - // Create an APK and merge the zip file. - File apk = new File(mTemporaryFolder.getRoot(), "b.apk"); - ApkZFileCreatorFactory cf = new ApkZFileCreatorFactory(new ZFileOptions()); - ApkCreatorFactory.CreationData creationData = - new ApkCreatorFactory.CreationData( - apk, - null, - null, - false, - false, - null, - null, - 20, - NativeLibrariesPackagingMode.COMPRESSED, - path -> false); - - try (ApkCreator creator = cf.make(creationData)) { - creator.writeZip(zipToMerge, null, null); - } - - // Make sure the file is uncompressed and aligned. - try (ZFile zf = new ZFile(apk)) { - StoredEntry soEntry = zf.get("/zero.so"); - assertNotNull(soEntry); - assertEquals( - CompressionMethod.DEFLATE, - soEntry.getCentralDirectoryHeader().getCompressionInfoWithWait().getMethod()); - long offset = - soEntry.getCentralDirectoryHeader().getOffset() + soEntry.getLocalHeaderSize(); - assertTrue(offset % 4096 != 0); - - byte[] data = soEntry.read(); - assertEquals(500, data.length); - for (int i = 0; i < data.length; i++) { - assertEquals(0, data[i]); - } - } - } -} diff --git a/src/test/java/com/android/apkzlib/zip/AlignmentTest.java b/src/test/java/com/android/apkzlib/zip/AlignmentTest.java deleted file mode 100644 index e94a876..0000000 --- a/src/test/java/com/android/apkzlib/zip/AlignmentTest.java +++ /dev/null @@ -1,856 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * 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.android.apkzlib.zip; - -import static com.android.apkzlib.utils.ApkZFileTestUtils.readSegment; -import static junit.framework.TestCase.assertEquals; -import static org.junit.Assert.assertArrayEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; - -import com.google.common.base.Charsets; -import java.io.ByteArrayInputStream; -import java.io.File; -import java.io.FileOutputStream; -import java.util.Random; -import java.util.Set; -import java.util.zip.ZipEntry; -import java.util.zip.ZipOutputStream; -import org.junit.Ignore; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.TemporaryFolder; - -public class AlignmentTest { - - private static final AlignmentRule SUFFIX_ALIGNMENT_RULES = - AlignmentRules.compose( - // Disable 4-aligning of uncompressed *.u files, so we can more easily - // calculate offsets for testing. - AlignmentRules.constantForSuffix(".u", 1), - AlignmentRules.constantForSuffix(".a", 1024)); - @Rule - public TemporaryFolder mTemporaryFolder = new TemporaryFolder(); - - @Test - public void addAlignedFile() throws Exception { - File newZFile = new File(mTemporaryFolder.getRoot(), "test.zip"); - - byte testBytes[] = "This is some text.".getBytes(Charsets.US_ASCII); - - ZFileOptions options = new ZFileOptions(); - options.setAlignmentRule(AlignmentRules.constantForSuffix(".txt", 1024)); - try (ZFile zf = new ZFile(newZFile, options)) { - zf.add("test.txt", new ByteArrayInputStream(testBytes), false); - } - - byte found[] = readSegment(newZFile, 1024, testBytes.length); - assertArrayEquals(testBytes, found); - } - - @Test - public void addNonAlignedFile() throws Exception { - File newZFile = new File(mTemporaryFolder.getRoot(), "test.zip"); - - byte testBytes[] = "This is some text.".getBytes(Charsets.US_ASCII); - - ZFileOptions options = new ZFileOptions(); - options.setAlignmentRule(AlignmentRules.constantForSuffix(".txt", 1024)); - try (ZFile zf = new ZFile(newZFile, options)) { - zf.add("test.txt.foo", new ByteArrayInputStream(testBytes), false); - } - - assertTrue(newZFile.length() < 1024); - } - - @Test - public void realignSingleFile() throws Exception { - File newZFile = new File(mTemporaryFolder.getRoot(), "test.zip"); - - byte testBytes0[] = "Text number 1".getBytes(Charsets.US_ASCII); - byte testBytes1[] = "Text number 2, which is actually 1".getBytes(Charsets.US_ASCII); - - long offset0; - try (ZFile zf = new ZFile(newZFile)) { - zf.add("file1.txt", new ByteArrayInputStream(testBytes1), false); - zf.add("file0.txt", new ByteArrayInputStream(testBytes0), false); - zf.close(); - - StoredEntry se0 = zf.get("file0.txt"); - assertNotNull(se0); - offset0 = se0.getCentralDirectoryHeader().getOffset(); - - StoredEntry se1 = zf.get("file1.txt"); - assertNotNull(se1); - - assertTrue(newZFile.length() < 1024); - } - - ZFileOptions options = new ZFileOptions(); - options.setAlignmentRule(AlignmentRules.constantForSuffix(".txt", 1024)); - try (ZFile zf = new ZFile(newZFile, options)) { - StoredEntry se1 = zf.get("file1.txt"); - assertNotNull(se1); - se1.realign(); - zf.close(); - - StoredEntry se0 = zf.get("file0.txt"); - assertNotNull(se0); - assertEquals(offset0, se0.getCentralDirectoryHeader().getOffset()); - - se1 = zf.get("file1.txt"); - assertNotNull(se1); - assertTrue(se1.getCentralDirectoryHeader().getOffset() > 950); - assertTrue(se1.getCentralDirectoryHeader().getOffset() < 1024); - assertArrayEquals(testBytes1, readSegment(newZFile, 1024, testBytes1.length)); - - assertTrue(newZFile.length() > 1024); - } - } - - @Test - public void realignFile() throws Exception { - File newZFile = new File(mTemporaryFolder.getRoot(), "test.zip"); - - byte testBytes0[] = "Text number 1".getBytes(Charsets.US_ASCII); - byte testBytes1[] = "Text number 2, which is actually 1".getBytes(Charsets.US_ASCII); - - try (ZFile zf = new ZFile(newZFile)) { - zf.add("file0.txt", new ByteArrayInputStream(testBytes0), false); - zf.add("file1.txt", new ByteArrayInputStream(testBytes1), false); - } - - assertTrue(newZFile.length() < 1024); - - ZFileOptions options = new ZFileOptions(); - options.setAlignmentRule(AlignmentRules.constantForSuffix(".txt", 1024)); - try (ZFile zf = new ZFile(newZFile, options)) { - zf.realign(); - zf.update(); - - StoredEntry se0 = zf.get("file0.txt"); - assertNotNull(se0); - long off0 = 1024; - - StoredEntry se1 = zf.get("file1.txt"); - assertNotNull(se1); - long off1 = 2048; - - /* - * ZFile does not guarantee any order. - */ - if (se1.getCentralDirectoryHeader().getOffset() < - se0.getCentralDirectoryHeader().getOffset()) { - off0 = 2048; - off1 = 1024; - } - - assertArrayEquals(testBytes0, readSegment(newZFile, off0, testBytes0.length)); - assertArrayEquals(testBytes1, readSegment(newZFile, off1, testBytes1.length)); - } - } - - @Test - public void realignAlignedEntry() throws Exception { - File newZFile = new File(mTemporaryFolder.getRoot(), "test.zip"); - - byte testBytes[] = "This is some text.".getBytes(Charsets.US_ASCII); - - ZFileOptions options = new ZFileOptions(); - options.setAlignmentRule(AlignmentRules.constantForSuffix(".txt", 1024)); - try (ZFile zf = new ZFile(newZFile, options)) { - zf.add("test.txt", new ByteArrayInputStream(testBytes), false); - } - - assertArrayEquals(testBytes, readSegment(newZFile, 1024, testBytes.length)); - - int flen = (int) newZFile.length(); - - try (ZFile zf = new ZFile(newZFile)) { - StoredEntry entry = zf.get("test.txt"); - assertNotNull(entry); - assertFalse(entry.realign()); - } - - assertEquals(flen, (int) newZFile.length()); - assertArrayEquals(testBytes, readSegment(newZFile, 1024, testBytes.length)); - } - - @Test - public void alignmentRulesDoNotAffectAddedFiles() throws Exception { - File newZFile = new File(mTemporaryFolder.getRoot(), "test.zip"); - - byte testBytes0[] = "Text number 1".getBytes(Charsets.US_ASCII); - byte testBytes1[] = "Text number 2, which is actually 1".getBytes(Charsets.US_ASCII); - - try (ZFile zf = new ZFile(newZFile)) { - zf.add("file0.txt", new ByteArrayInputStream(testBytes0), false); - } - - ZFileOptions options = new ZFileOptions(); - options.setAlignmentRule(AlignmentRules.constantForSuffix(".txt", 1024)); - try (ZFile zf = new ZFile(newZFile, options)) { - zf.add("file1.txt", new ByteArrayInputStream(testBytes1), false); - zf.update(); - - StoredEntry se0 = zf.get("file0.txt"); - assertNotNull(se0); - - StoredEntry se1 = zf.get("file1.txt"); - assertNotNull(se1); - assertArrayEquals(testBytes1, readSegment(newZFile, 1024, testBytes1.length)); - } - } - - @Test - public void realignStreamedZip() throws Exception { - File zipFile = new File(mTemporaryFolder.getRoot(), "test.zip"); - - byte[] pattern = new byte[1024]; - new Random().nextBytes(pattern); - - String name = ""; - try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zipFile))) { - for (int j = 0; j < 10; j++) { - name = name + "a"; - ZipEntry ze = new ZipEntry(name); - zos.putNextEntry(ze); - for (int i = 0; i < 1000; i++) { - zos.write(pattern); - } - } - } - - ZFileOptions options = new ZFileOptions(); - options.setAlignmentRule(AlignmentRules.constant(10)); - try (ZFile zf = new ZFile(zipFile, options)) { - zf.realign(); - } - } - - @Test - public void alignFirstEntryUsingExtraField() throws Exception { - File zipFile = new File(mTemporaryFolder.getRoot(), "test.zip"); - - byte[] recognizable = new byte[] { 1, 2, 3, 4, 4, 3, 2, 1 }; - - ZFileOptions options = new ZFileOptions(); - options.setCoverEmptySpaceUsingExtraField(true); - options.setAlignmentRule(AlignmentRules.constant(1024)); - try (ZFile zf = new ZFile(zipFile, options)) { - zf.add("foo", new ByteArrayInputStream(recognizable), false); - } - - /* - * Contents should be at 1024 bytes. - */ - assertArrayEquals(recognizable, readSegment(zipFile, 1024, recognizable.length)); - - /* - * But local header should be in the beginning. - */ - try (ZFile zf = new ZFile(zipFile)) { - StoredEntry entry = zf.get("foo"); - assertNotNull(entry); - assertEquals(0, entry.getCentralDirectoryHeader().getOffset()); - } - } - - @Test - public void alignFirstEntryUsingOffset() throws Exception { - File zipFile = new File(mTemporaryFolder.getRoot(), "test.zip"); - - byte[] recognizable = new byte[] { 1, 2, 3, 4, 4, 3, 2, 1 }; - - ZFileOptions options = new ZFileOptions(); - options.setCoverEmptySpaceUsingExtraField(false); - options.setAlignmentRule(AlignmentRules.constant(1024)); - try (ZFile zf = new ZFile(zipFile, options)) { - zf.add("foo", new ByteArrayInputStream(recognizable), false); - } - - /* - * Contents should be at 1024 bytes. - */ - assertArrayEquals(recognizable, readSegment(zipFile, 1024, recognizable.length)); - - /* - * Local header should start at 991 (1024 - LOCAL_HEADER_SIZE - 3). - */ - try (ZFile zf = new ZFile(zipFile)) { - StoredEntry entry = zf.get("foo"); - assertNotNull(entry); - assertEquals(991, entry.getCentralDirectoryHeader().getOffset()); - } - } - - @Test - public void alignMiddleEntryUsingExtraField() throws Exception { - File zipFile = new File(mTemporaryFolder.getRoot(), "test.zip"); - - byte[] recognizable = new byte[] { 1, 2, 3, 4, 4, 3, 2, 1 }; - - ZFileOptions options = new ZFileOptions(); - options.setCoverEmptySpaceUsingExtraField(true); - options.setAlignmentRule(SUFFIX_ALIGNMENT_RULES); - try (ZFile zf = new ZFile(zipFile, options)) { - zf.add("first.u", new ByteArrayInputStream(new byte[1024]), false); - zf.add("middle.a", new ByteArrayInputStream(recognizable), false); - zf.add("last.u", new ByteArrayInputStream(new byte[1024]), false); - } - - /* - * Contents should be at 2048 bytes. - */ - assertArrayEquals(recognizable, readSegment(zipFile, 2048, recognizable.length)); - - /* - * But local header should be right after the first entry. - */ - try (ZFile zf = new ZFile(zipFile)) { - StoredEntry middleEntry = zf.get("middle.a"); - assertNotNull(middleEntry); - assertEquals( - ZFileTestConstants.LOCAL_HEADER_SIZE + "first.u".length() + 1024, - middleEntry.getCentralDirectoryHeader().getOffset()); - } - } - - @Test - public void alignMiddleEntryUsingOffset() throws Exception { - File zipFile = new File(mTemporaryFolder.getRoot(), "test.zip"); - - byte[] recognizable = new byte[] { 1, 2, 3, 4, 4, 3, 2, 1 }; - - ZFileOptions options = new ZFileOptions(); - options.setCoverEmptySpaceUsingExtraField(false); - options.setAlignmentRule(AlignmentRules.constantForSuffix(".a", 1024)); - try (ZFile zf = new ZFile(zipFile, options)) { - zf.add("bar1", new ByteArrayInputStream(new byte[1024]), false); - zf.add("foo.a", new ByteArrayInputStream(recognizable), false); - zf.add("bar2", new ByteArrayInputStream(new byte[1024]), false); - } - - /* - * Contents should be at 2048 bytes. - */ - assertArrayEquals(recognizable, readSegment(zipFile, 2048, recognizable.length)); - - /* - * Local header should start at 2015 (2048 - LOCAL_HEADER_SIZE - 5). - */ - try (ZFile zf = new ZFile(zipFile)) { - StoredEntry entry = zf.get("foo.a"); - assertNotNull(entry); - assertEquals(2013, entry.getCentralDirectoryHeader().getOffset()); - } - } - - @Test - public void alignUsingOffsetAllowsSmallSpaces() throws Exception { - File zipFile = new File(mTemporaryFolder.getRoot(), "test.zip"); - - int fixedLh = ZFileTestConstants.LOCAL_HEADER_SIZE + 3; - - byte[] recognizable = new byte[] { 1, 2, 3, 4, 4, 3, 2, 1 }; - - ZFileOptions options = new ZFileOptions(); - options.setCoverEmptySpaceUsingExtraField(false); - options.setAlignmentRule(AlignmentRules.constant(fixedLh)); - try (ZFile zf = new ZFile(zipFile, options)) { - zf.add("f", new ByteArrayInputStream(recognizable), false); - } - - assertArrayEquals(recognizable, readSegment(zipFile, fixedLh, recognizable.length)); - } - - @Test - public void alignUsingExtraFieldDoesNotAllowSmallSpaces() throws Exception { - File zipFile = new File(mTemporaryFolder.getRoot(), "test.zip"); - - int fixedLh = ZFileTestConstants.LOCAL_HEADER_SIZE + 3; - - byte[] recognizable = new byte[] { 1, 2, 3, 4, 4, 3, 2, 1 }; - - ZFileOptions options = new ZFileOptions(); - options.setCoverEmptySpaceUsingExtraField(true); - options.setAlignmentRule(AlignmentRules.constant(fixedLh)); - try (ZFile zf = new ZFile(zipFile, options)) { - zf.add("f", new ByteArrayInputStream(recognizable), false); - } - - assertArrayEquals(recognizable, readSegment(zipFile, fixedLh * 2, recognizable.length)); - } - - @Test - public void extraFieldSpaceUsedForAlignmentCanBeReclaimedBeforeUpdate() throws Exception { - File zipFile = new File(mTemporaryFolder.getRoot(), "test.zip"); - - byte[] recognizable1 = new byte[] { 1, 2, 3, 4, 4, 3, 2, 1 }; - byte[] recognizable2 = new byte[] { 9, 9, 8, 8, 7, 7, 6, 6, 5, 5, 4, 4 }; - - ZFileOptions options = new ZFileOptions(); - options.setCoverEmptySpaceUsingExtraField(true); - options.setAlignmentRule(SUFFIX_ALIGNMENT_RULES); - try (ZFile zf = new ZFile(zipFile, options)) { - zf.add("f.a", new ByteArrayInputStream(recognizable1), false); - zf.add("f.u", new ByteArrayInputStream(recognizable2), false); - } - - assertArrayEquals(recognizable1, readSegment(zipFile, 1024, recognizable1.length)); - assertArrayEquals( - recognizable2, - readSegment( - zipFile, - ZFileTestConstants.LOCAL_HEADER_SIZE + "f.u".length(), - recognizable2.length)); - } - - @Test - @Ignore("See ZFile.readData() contents to understand why this is ignored") - public void extraFieldSpaceUsedForAlignmentCanBeReclaimedAfterUpdate() throws Exception { - File zipFile = new File(mTemporaryFolder.getRoot(), "test.zip"); - - byte[] recognizable1 = new byte[] { 1, 2, 3, 4, 4, 3, 2, 1 }; - byte[] recognizable2 = new byte[] { 9, 9, 8, 8, 7, 7, 6, 6, 5, 5, 4, 4 }; - - ZFileOptions options = new ZFileOptions(); - options.setCoverEmptySpaceUsingExtraField(true); - options.setAlignmentRule(AlignmentRules.constantForSuffix(".a", 1024)); - try (ZFile zf = new ZFile(zipFile, options)) { - zf.add("f.a", new ByteArrayInputStream(recognizable1), false); - } - - try (ZFile zf = new ZFile(zipFile, options)) { - zf.add("f.b", new ByteArrayInputStream(recognizable2), false); - } - - assertArrayEquals(recognizable1, readSegment(zipFile, 1024, recognizable1.length)); - assertArrayEquals( - recognizable2, - readSegment( - zipFile, - ZFileTestConstants.LOCAL_HEADER_SIZE + "f.b".length(), - recognizable2.length)); - } - - @Test - public void fillEmptySpaceWithExtraFieldAfterDelete() throws Exception { - File zipFile = new File(mTemporaryFolder.getRoot(), "large.zip"); - - byte[] recognizable1 = new byte[] { 1, 2, 3, 4, 4, 3, 2, 1 }; - byte[] recognizable2 = new byte[] { 9, 8, 7, 6, 5, 4, 3, 2 }; - - ZFileOptions options = new ZFileOptions(); - options.setCoverEmptySpaceUsingExtraField(true); - options.setAlignmentRule(SUFFIX_ALIGNMENT_RULES); - try (ZFile zf = new ZFile(zipFile, options)) { - zf.add("first.u", new ByteArrayInputStream(recognizable1), false); - zf.add("second.u", new ByteArrayInputStream(recognizable2), false); - - zf.update(); - - StoredEntry firstEntry = zf.get("first.u"); - assertNotNull(firstEntry); - firstEntry.delete(); - } - - try (ZFile zf = new ZFile(zipFile)) { - Set entries = zf.entries(); - assertEquals(1, entries.size()); - - StoredEntry entry = entries.iterator().next(); - assertEquals("second.u", entry.getCentralDirectoryHeader().getName()); - assertEquals(0, entry.getCentralDirectoryHeader().getOffset()); - assertEquals( - ZFileTestConstants.LOCAL_HEADER_SIZE - + "first.u".length() - + recognizable1.length, - entry.getLocalExtra().size()); - } - } - - @Test - public void fillInLargeGapsWithExtraField() throws Exception { - File zipFile = new File(mTemporaryFolder.getRoot(), "large.zip"); - - byte[] recognizable1 = new byte[] { 1, 2, 3, 4, 4, 3, 2, 1 }; - byte[] recognizable2 = new byte[] { 9, 8, 7, 6, 5, 4, 3, 2 }; - byte[] bigEmpty = new byte[10 * 1024]; - - ZFileOptions options = new ZFileOptions(); - options.setCoverEmptySpaceUsingExtraField(true); - options.setAlignmentRule(SUFFIX_ALIGNMENT_RULES); - try (ZFile zf = new ZFile(zipFile, options)) { - zf.add("begin.u", new ByteArrayInputStream(recognizable1), false); - zf.add("middle.u", new ByteArrayInputStream(bigEmpty), false); - zf.add("end.u", new ByteArrayInputStream(recognizable2), false); - - zf.update(); - - StoredEntry middleEntry = zf.get("middle.u"); - assertNotNull(middleEntry); - middleEntry.delete(); - } - - /* - * Find the two recognizable files. - */ - int recognizable1Start = ZFileTestConstants.LOCAL_HEADER_SIZE + "begin.u".length(); - assertArrayEquals( - recognizable1, - readSegment(zipFile, recognizable1Start, recognizable1.length)); - - int recognizable2Start = - 3 * ZFileTestConstants.LOCAL_HEADER_SIZE - + "begin.u".length() - + "middle.u".length() - + "end.u".length() - + recognizable1.length - + bigEmpty.length; - assertArrayEquals( - recognizable2, - readSegment(zipFile, recognizable2Start, recognizable2.length)); - } - - @Test - public void fillHoleWithExactEntry() throws Exception { - File zipFile = new File(mTemporaryFolder.getRoot(), "a.zip"); - - Random random = new Random(); - - byte[] fourtyFour = new byte[44]; - random.nextBytes(fourtyFour); - byte[] recognizable = new byte[] { 1, 5, 5, 1, 5, 1, 1, 5 }; - byte[] twoHundred = new byte[200]; - random.nextBytes(twoHundred); - - /* - * Start | Header End | Name end | Contents End | Name - * 0 | 30 | 59 | 103 | "File taking exactly 103 bytes" - * 103 | 133 | 136 | 144 | "foo" - * 144 | 174 | 196 | 396 | "File taking more space" - */ - try (ZFile zf = new ZFile(zipFile)) { - zf.add("File taking exactly 103 bytes", new ByteArrayInputStream(fourtyFour), false); - zf.add("foo", new ByteArrayInputStream(recognizable), false); - zf.add("File taking more space", new ByteArrayInputStream(twoHundred), false); - } - - assertArrayEquals(fourtyFour, readSegment(zipFile, 59, fourtyFour.length)); - assertArrayEquals(recognizable, readSegment(zipFile, 136, recognizable.length)); - assertArrayEquals(twoHundred, readSegment(zipFile, 196, twoHundred.length)); - - /* - * Remove the middle file. - */ - try (ZFile zf = new ZFile(zipFile)) { - StoredEntry fooEntry = zf.get("foo"); - assertNotNull(fooEntry); - fooEntry.delete(); - } - - /* - * Add the file again with 4-byte alignment. Because the file fits exactly in the hole, it - * is placed there. - */ - byte[] recognizable2 = new byte[] { 2, 6, 6, 2, 6, 2, 2, 6 }; - - ZFileOptions zfo = new ZFileOptions(); - zfo.setCoverEmptySpaceUsingExtraField(true); - zfo.setAlignmentRule(AlignmentRules.constant(4)); - try (ZFile zf = new ZFile(zipFile, zfo)) { - zf.add("bar", new ByteArrayInputStream(recognizable2), false); - } - - assertArrayEquals(fourtyFour, readSegment(zipFile, 59, fourtyFour.length)); - assertArrayEquals(recognizable2, readSegment(zipFile, 136, recognizable2.length)); - assertArrayEquals(twoHundred, readSegment(zipFile, 196, twoHundred.length)); - } - - @Test - public void fillHoleWithSmallEntry() throws Exception { - File zipFile = new File(mTemporaryFolder.getRoot(), "a.zip"); - - Random random = new Random(); - - byte[] fourtyFour = new byte[44]; - random.nextBytes(fourtyFour); - byte[] recognizable = new byte[] { 1, 5, 5, 1, 5, 1, 1, 5, 1, 5, 5, 1, 5, 1, 1, 5 }; - byte[] twoHundred = new byte[200]; - random.nextBytes(twoHundred); - - /* - * Start | Header End | Name end | Contents End | Name - * 0 | 30 | 59 | 103 | "File taking exactly 103 bytes" - * 103 | 133 | 136 | 152 | "foo" - * 152 | 182 | 204 | 404 | "File taking more space" - */ - try (ZFile zf = new ZFile(zipFile)) { - zf.add("File taking exactly 103 bytes", new ByteArrayInputStream(fourtyFour), false); - zf.add("foo", new ByteArrayInputStream(recognizable), false); - zf.add("File taking more space", new ByteArrayInputStream(twoHundred), false); - } - - assertArrayEquals(fourtyFour, readSegment(zipFile, 59, fourtyFour.length)); - assertArrayEquals(recognizable, readSegment(zipFile, 136, recognizable.length)); - assertArrayEquals(twoHundred, readSegment(zipFile, 204, twoHundred.length)); - - /* - * Remove the middle file. - */ - try (ZFile zf = new ZFile(zipFile)) { - StoredEntry fooEntry = zf.get("foo"); - assertNotNull(fooEntry); - fooEntry.delete(); - } - - /* - * Add a smaller file. It should fit nicely as: - * - * Start | Header End | Name end | Contents End | Name - * 0 | 30 | 59 | 103 | "File taking exactly 103 bytes" - * 103 | 133 | 136 | 140 | "bar" - * 140 - 152 (empty) - * 152 | 182 | 204 | 404 | "File taking more space" - */ - byte[] recognizable2 = new byte[] { 7, 7, 7, 7 }; - - ZFileOptions zfo = new ZFileOptions(); - zfo.setCoverEmptySpaceUsingExtraField(true); - zfo.setAlignmentRule(AlignmentRules.constant(4)); - try (ZFile zf = new ZFile(zipFile, zfo)) { - zf.add("bar", new ByteArrayInputStream(recognizable2), false); - } - - assertArrayEquals(fourtyFour, readSegment(zipFile, 59, fourtyFour.length)); - assertArrayEquals(recognizable2, readSegment(zipFile, 136, recognizable2.length)); - assertArrayEquals(twoHundred, readSegment(zipFile, 204, twoHundred.length)); - } - - @Test - public void fillHoleWithSmallerEntryNotEnoughFreeSpace() throws Exception { - File zipFile = new File(mTemporaryFolder.getRoot(), "a.zip"); - - Random random = new Random(); - - byte[] fourtyFour = new byte[44]; - random.nextBytes(fourtyFour); - byte[] recognizable = new byte[] { 1, 5, 5, 1, 5, 1, 1, 5, 1, 5, 5, 1, 5, 1, 1, 5 }; - byte[] twoHundred = new byte[200]; - random.nextBytes(twoHundred); - - /* - * Start | Header End | Name end | Contents End | Name - * 0 | 30 | 59 | 103 | "File taking exactly 103 bytes" - * 103 | 133 | 136 | 152 | "foo" - * 152 | 182 | 204 | 404 | "File taking more space" - */ - try (ZFile zf = new ZFile(zipFile)) { - zf.add("File taking exactly 103 bytes", new ByteArrayInputStream(fourtyFour), false); - zf.add("foo", new ByteArrayInputStream(recognizable), false); - zf.add("File taking more space", new ByteArrayInputStream(twoHundred), false); - } - - assertArrayEquals(fourtyFour, readSegment(zipFile, 59, fourtyFour.length)); - assertArrayEquals(recognizable, readSegment(zipFile, 136, recognizable.length)); - assertArrayEquals(twoHundred, readSegment(zipFile, 204, twoHundred.length)); - - /* - * Remove the middle file. - */ - try (ZFile zf = new ZFile(zipFile)) { - StoredEntry fooEntry = zf.get("foo"); - assertNotNull(fooEntry); - fooEntry.delete(); - } - - /* - * Add a smaller file. But it can't fit because it would leave less than 6 bytes to - * cover in the next file: - * - * Start | Header End | Name end | Contents End | Name - * 0 | 30 | 59 | 103 | "File taking exactly 103 bytes" - * 103 | 133 | 136 | 148 | "foo" - * 148 - 152 (empty) - * 152 | 182 | 204 | 404 | "File taking more space" - * - * So we end up with: - * - * Start | Header End | Name end | Contents End | Name - * 0 | 30 | 59 | 103 | "File taking exactly 103 bytes" - * 152 | 182 | 204 | 404 | "File taking more space" - * 404 | 434 -> 441 | 444 | 456 | "bar" - */ - byte[] recognizable2 = new byte[] { 7, 7, 7, 7, 8, 8, 8, 8, 9, 9, 9, 9 }; - - ZFileOptions zfo = new ZFileOptions(); - zfo.setCoverEmptySpaceUsingExtraField(true); - zfo.setAlignmentRule(AlignmentRules.constant(4)); - try (ZFile zf = new ZFile(zipFile, zfo)) { - zf.add("bar", new ByteArrayInputStream(recognizable2), false); - } - - assertArrayEquals(fourtyFour, readSegment(zipFile, 59, fourtyFour.length)); - assertArrayEquals(recognizable2, readSegment(zipFile, 444, recognizable2.length)); - assertArrayEquals(twoHundred, readSegment(zipFile, 204, twoHundred.length)); - } - - @Test - public void fillHoleWithSmallerEntryEnoughFreeSpaceButRequiresExtraOffset() throws Exception { - File zipFile = new File(mTemporaryFolder.getRoot(), "a.zip"); - - Random random = new Random(); - - byte[] fourtyFour = new byte[44]; - random.nextBytes(fourtyFour); - byte[] recognizable = new byte[] { 1, 5, 5, 1, 5, 1, 1, 5, 1, 5, 5, 1, 5, 1, 1, 5 }; - byte[] twoHundred = new byte[200]; - random.nextBytes(twoHundred); - - /* - * Start | Header End | Name end | Contents End | Name - * 0 | 30 | 59 | 103 | "File taking exactly 103 bytes" - * 103 | 133 | 136 | 152 | "foo" - * 152 | 182 | 204 | 404 | "File taking more space" - */ - try (ZFile zf = new ZFile(zipFile)) { - zf.add("File taking exactly 103 bytes", new ByteArrayInputStream(fourtyFour), false); - zf.add("foo", new ByteArrayInputStream(recognizable), false); - zf.add("File taking more space", new ByteArrayInputStream(twoHundred), false); - } - - assertArrayEquals(fourtyFour, readSegment(zipFile, 59, fourtyFour.length)); - assertArrayEquals(recognizable, readSegment(zipFile, 136, recognizable.length)); - assertArrayEquals(twoHundred, readSegment(zipFile, 204, twoHundred.length)); - - /* - * Remove the middle file. - */ - try (ZFile zf = new ZFile(zipFile)) { - StoredEntry fooEntry = zf.get("foo"); - assertNotNull(fooEntry); - fooEntry.delete(); - } - - /* - * Add a smaller file. It will fit, but not aligned at 140 because that would require - * adding less than 6 bytes in the local header. It has to move to 150. - * - * Start | Header End | Name end | Contents End | Name - * 0 | 30 | 59 | 103 | "File taking exactly 103 bytes" - * 103 | 133 | 150 | 152 | "foo" - * 152 | 182 | 204 | 404 | "File taking more space" - */ - byte[] recognizable2 = new byte[] { 10, 10 }; - - ZFileOptions zfo = new ZFileOptions(); - zfo.setCoverEmptySpaceUsingExtraField(true); - zfo.setAlignmentRule(AlignmentRules.constant(10)); - try (ZFile zf = new ZFile(zipFile, zfo)) { - zf.add("bar", new ByteArrayInputStream(recognizable2), false); - } - - assertArrayEquals(fourtyFour, readSegment(zipFile, 59, fourtyFour.length)); - assertArrayEquals(recognizable2, readSegment(zipFile, 150, recognizable2.length)); - assertArrayEquals(twoHundred, readSegment(zipFile, 204, twoHundred.length)); - } - - @Test - public void alignCoveringEmptySpaceWhenExtraFieldIsInvalid() throws Exception { - File zipFile = new File(mTemporaryFolder.getRoot(), "a.zip"); - ZFileOptions options = new ZFileOptions(); - options.setCoverEmptySpaceUsingExtraField(true); - options.setAlignmentRule(AlignmentRules.constant(100)); - try (ZFile zf = new ZFile(zipFile, options)) { - zf.add("foo", new ByteArrayInputStream(new byte[] { 1, 2, 3, 4 })); - StoredEntry foo = zf.get("foo"); - assertNotNull(foo); - foo.setLocalExtra(new ExtraField(new byte[] { 0, 0 })); - zf.add("bar", new ByteArrayInputStream(new byte[] { 5, 6, 7, 8 })); - } - } - - @Test - public void fourByteAlignment() throws Exception { - // When aligning with 4 bytes, there are are only 3 possible cases: - // - We're 2 bytes short and so need to add +6 bytes (6 bytes for header + no zeroes) - // - We're 3 bytes short and so need to add +7 bytes (6 bytes for header + 1 zero) - // - We're 1 byte short and so need to add +9 bytes (6 bytes for header + 3 zeroes) - - File zipFile = new File(mTemporaryFolder.getRoot(), "a.zip"); - ZFileOptions options = new ZFileOptions(); - options.setCoverEmptySpaceUsingExtraField(true); - options.setAlignmentRule(AlignmentRules.constant(4)); - try (ZFile zf = new ZFile(zipFile, options)) { - // File header starts at 0. - // File name starts at 30 (LOCAL_HEADER_SIZE). - // If unaligned we would have data starting at 33, but with aligned we have data - // starting at 40 (36 isn't enough for the extra data header). - String fooName = "foo"; - byte[] fooData = new byte[] { 1, 2, 3, 4, 5 }; - zf.add(fooName, new ByteArrayInputStream(fooData), false); - zf.update(); - StoredEntry foo = zf.get(fooName); - long fooOffset = ZFileTestConstants.LOCAL_HEADER_SIZE + fooName.length() + 7; - assertEquals(fooOffset, foo.getLocalHeaderSize()); - - // Bar header starts at 45 (foo data starts at 40 and is 5 bytes long). - // Bar header ends at 75. - // If unaligned we would have data starting at 78, but with aligned we have data - // starting at 84 (80 isn't enough for the extra header). - String barName = "bar"; - byte[] barData = new byte[] { 6 }; - zf.add(barName, new ByteArrayInputStream(barData), false); - zf.update(); - - StoredEntry bar = zf.get(barName); - long barStart = bar.getCentralDirectoryHeader().getOffset(); - assertEquals(fooOffset + fooData.length, barStart); - - long barStartOffset = ZFileTestConstants.LOCAL_HEADER_SIZE + barName.length() + 6; - assertEquals(barStartOffset, bar.getLocalHeaderSize()); - - // Xpto header starts at 85 (bar data starts at 84 and is 1 byte long). - // Xpto header ends at 115. - // If unaligned we would have data starting at 119, but with aligned we have data - // starting at 128 (120 & 124 are not enough for the extra header). - String xptoName = "xpto"; - byte[] xptoData = new byte[] { 7, 8, 9, 10 }; - zf.add(xptoName, new ByteArrayInputStream(xptoData), false); - zf.update(); - - StoredEntry xpto = zf.get(xptoName); - long xptoStart = xpto.getCentralDirectoryHeader().getOffset(); - assertEquals(barStart + barStartOffset + barData.length, xptoStart); - - long xptoStartOffset = ZFileTestConstants.LOCAL_HEADER_SIZE + xptoName.length() + 9; - assertEquals(xptoStartOffset, xpto.getLocalHeaderSize()); - - // Dummy header starts at 133 (xpto data starts at 128 and is 6 bytes long). - String dummyName = "dummy"; - byte[] dummyData = new byte[] { 11 }; - zf.add(dummyName, new ByteArrayInputStream(dummyData), false); - zf.update(); - - StoredEntry dummy = zf.get(dummyName); - long dummyStart = dummy.getCentralDirectoryHeader().getOffset(); - assertEquals(xptoStart + xptoStartOffset + xptoData.length, dummyStart); - } - } -} diff --git a/src/test/java/com/android/apkzlib/zip/EncodeUtilsTest.java b/src/test/java/com/android/apkzlib/zip/EncodeUtilsTest.java deleted file mode 100644 index 8648aa0..0000000 --- a/src/test/java/com/android/apkzlib/zip/EncodeUtilsTest.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * 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.android.apkzlib.zip; - -import static org.junit.Assert.assertArrayEquals; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; - -import org.junit.Test; - -public class EncodeUtilsTest { - @Test - public void canEncodeAsciiWithAsciiString() { - assertTrue(EncodeUtils.canAsciiEncode("foo")); - } - - @Test - public void cannotEncodeAscuuWithUtf8String() { - String greekInGreek ="\u3b53\ubb3b\ub3b7\u3bd3\ub93b\ua3ac"; - assertFalse(EncodeUtils.canAsciiEncode(greekInGreek)); - } - - @Test - public void asciiEncodeAndDecode() { - String text = "foo"; - GPFlags flags = GPFlags.make(false); - - byte[] encoded = EncodeUtils.encode(text, flags); - assertArrayEquals(new byte[] { 0x66, 0x6f, 0x6f }, encoded); - assertEquals(text, EncodeUtils.decode(encoded, flags)); - } - - @Test - public void utf8EncodeAndDecode() { - String kazakhCapital = "\u0410\u0441\u0442\u0430\u043d\u0430"; - GPFlags flags = GPFlags.make(true); - - byte[] encoded = EncodeUtils.encode(kazakhCapital, flags); - assertArrayEquals(new byte[] { (byte) 0xd0, (byte) 0x90, (byte) 0xd1, (byte) 0x81, - (byte) 0xd1, (byte) 0x82, (byte) 0xd0, (byte) 0xb0, (byte) 0xd0, (byte) 0xbd, - (byte) 0xd0, (byte) 0xb0 }, encoded); - assertEquals(kazakhCapital, EncodeUtils.decode(encoded, flags)); - } - - @Test - public void asciiDecodeAsUtf8() { - byte[] greatWallChinese = - new byte[] { - (byte) 0xe9, (byte) 0x95, (byte) 0xB7, (byte) 0xe5, (byte) 0x9F, (byte) 0x8E - }; - - GPFlags flags = GPFlags.make(false); - - String text = EncodeUtils.decode(greatWallChinese, flags); - assertEquals("\u9577\u57ce", text); - } -} diff --git a/src/test/java/com/android/apkzlib/zip/ExtraFieldTest.java b/src/test/java/com/android/apkzlib/zip/ExtraFieldTest.java deleted file mode 100644 index 2371849..0000000 --- a/src/test/java/com/android/apkzlib/zip/ExtraFieldTest.java +++ /dev/null @@ -1,361 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * 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.android.apkzlib.zip; - -import static org.junit.Assert.assertArrayEquals; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.fail; - -import com.google.common.collect.ImmutableList; - -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.TemporaryFolder; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; - -import java.io.ByteArrayInputStream; -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.nio.ByteBuffer; -import java.util.function.BiConsumer; -import java.util.function.Function; -import java.util.zip.ZipEntry; -import java.util.zip.ZipOutputStream; - -/** - * Test setting, removing and updating the extra field of zip entries. - */ -@RunWith(Parameterized.class) -public class ExtraFieldTest { - - @Rule - public TemporaryFolder mTemporaryFolder = new TemporaryFolder(); - - private File mZipFile; - - @Parameterized.Parameter - public Function mExtraFieldGetter; - - @Parameterized.Parameter(1) - public BiConsumer mExtraFieldSetter; - - @Before - public final void before() throws Exception { - mZipFile = mTemporaryFolder.newFile(); - mZipFile.delete(); - } - - @Parameterized.Parameters - public static ImmutableList getParameters() { - Function localGet = StoredEntry::getLocalExtra; - BiConsumer localSet = (se, ef) -> { - try { - se.setLocalExtra(ef); - } catch (IOException e) { - throw new AssertionError(e); - } - }; - - Function centralGet = - se -> se.getCentralDirectoryHeader().getExtraField(); - BiConsumer centralSet = (se, ef) -> { - try { - se.getCentralDirectoryHeader().setExtraField(ef); - } catch (Exception e) { - throw new AssertionError(e); - } - }; - - return ImmutableList.of( - new Object[]{ localGet, localSet }, - new Object[]{ centralGet, centralSet }); - } - - @Test - public void readEntryWithNoExtraField() throws Exception { - try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(mZipFile))) { - zos.putNextEntry(new ZipEntry("foo")); - zos.write(new byte[] { 1, 2, 3 }); - } - - try (ZFile zf = new ZFile(mZipFile)) { - StoredEntry foo = zf.get("foo"); - assertNotNull(foo); - assertEquals(3, foo.getCentralDirectoryHeader().getUncompressedSize()); - assertEquals(0, mExtraFieldGetter.apply(foo).size()); - } - } - - @Test - public void readSingleExtraField() throws Exception { - /* - * Header ID: 0x0A0B - * Data Size: 0x0004 - * Data: 0x01 0x02 0x03 0x04 - * - * In little endian is: - * - * 0xCDAB040001020304 - */ - byte[] extraField = new byte[] { 0x0B, 0x0A, 0x04, 0x00, 0x01, 0x02, 0x03, 0x04 }; - - try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(mZipFile))) { - ZipEntry ze = new ZipEntry("foo"); - ze.setExtra(extraField); - zos.putNextEntry(ze); - zos.write(new byte[] { 1, 2, 3 }); - } - - try (ZFile zf = new ZFile(mZipFile)) { - StoredEntry foo = zf.get("foo"); - assertNotNull(foo); - assertEquals(3, foo.getCentralDirectoryHeader().getUncompressedSize()); - assertEquals(8, mExtraFieldGetter.apply(foo).size()); - ImmutableList segments = mExtraFieldGetter.apply(foo).getSegments(); - assertEquals(1, segments.size()); - assertEquals(0x0A0B, segments.get(0).getHeaderId()); - byte[] segData = new byte[8]; - segments.get(0).write(ByteBuffer.wrap(segData)); - assertArrayEquals(extraField, segData); - } - } - - @Test - public void readMultipleExtraFields() throws Exception { - /* - * Header ID: 0x0A01 - * Data Size: 0x0002 - * Data: 0x01 0x02 - * - * Header ID: 0x0A02 - * Data Size: 0x0001 - * Data: 0x03 - * - * Header ID: 0x0A02 - * Data Size: 0x0001 - * Dataa: 0x04 - * - * In little endian is: - * - * 0x010A02000102 020A010003 020A010004 - */ - byte[] extraField = - new byte[] { - 0x01, 0x0A, 0x02, 0x00, 0x01, 0x02, - 0x02, 0x0A, 0x01, 0x00, 0x03, - 0x02, 0x0A, 0x01, 0x00, 0x04 }; - - try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(mZipFile))) { - ZipEntry ze = new ZipEntry("foo"); - - ze.setExtra(extraField); - zos.putNextEntry(ze); - zos.write(new byte[] { 1, 2, 3 }); - } - - try (ZFile zf = new ZFile(mZipFile)) { - StoredEntry foo = zf.get("foo"); - assertNotNull(foo); - assertEquals(3, foo.getCentralDirectoryHeader().getUncompressedSize()); - assertEquals(16, mExtraFieldGetter.apply(foo).size()); - ImmutableList segments = mExtraFieldGetter.apply(foo).getSegments(); - assertEquals(3, segments.size()); - - assertEquals(0x0A01, segments.get(0).getHeaderId()); - byte[] segData = new byte[6]; - segments.get(0).write(ByteBuffer.wrap(segData)); - assertArrayEquals(new byte[] { 0x01, 0x0A, 0x02, 0x00, 0x01, 0x02 }, segData); - - assertEquals(0x0A02, segments.get(1).getHeaderId()); - segData = new byte[5]; - segments.get(1).write(ByteBuffer.wrap(segData)); - assertArrayEquals(new byte[] { 0x02, 0x0A, 0x01, 0x00, 0x03 }, segData); - - assertEquals(0x0A02, segments.get(2).getHeaderId()); - segData = new byte[5]; - segments.get(2).write(ByteBuffer.wrap(segData)); - assertArrayEquals(new byte[] { 0x02, 0x0A, 0x01, 0x00, 0x04 }, segData); - } - } - - @Test - public void addExtraFieldToExistingEntry() throws Exception { - try (ZFile zf = new ZFile(mZipFile)) { - zf.add("before", new ByteArrayInputStream(new byte[] { 0, 1, 2 })); - zf.add("extra", new ByteArrayInputStream(new byte[] { 3, 4, 5 })); - zf.add("after", new ByteArrayInputStream(new byte[] { 6, 7, 8 })); - } - - try (ZFile zf = new ZFile(mZipFile)) { - StoredEntry ex = zf.get("extra"); - assertNotNull(ex); - mExtraFieldSetter.accept(ex, - new ExtraField( - ImmutableList.of( - new ExtraField.RawDataSegment( - 0x7654, - new byte[] { 1, 1, 3, 3 })))); - } - - try (ZFile zf = new ZFile(mZipFile)) { - StoredEntry before = zf.get("before"); - assertNotNull(before); - assertArrayEquals(new byte[] { 0, 1, 2 }, before.read()); - - StoredEntry extra = zf.get("extra"); - assertNotNull(extra); - assertArrayEquals(new byte[] { 3, 4, 5 }, extra.read()); - - StoredEntry after = zf.get("after"); - assertNotNull(after); - assertArrayEquals(new byte[] { 6, 7, 8 }, after.read()); - - ExtraField ef = mExtraFieldGetter.apply(extra); - assertEquals(1, ef.getSegments().size()); - ExtraField.Segment s = ef.getSingleSegment(0x7654); - assertNotNull(s); - byte[] sData = new byte[8]; - s.write(ByteBuffer.wrap(sData)); - assertArrayEquals(new byte[] { 0x54, 0x76, 0x04, 0x00, 1, 1, 3, 3 }, sData); - } - } - - @Test - public void removeExtraFieldFromExistingEntry() throws Exception { - try (ZFile zf = new ZFile(mZipFile)) { - zf.add("before", new ByteArrayInputStream(new byte[] { 0, 1, 2 })); - zf.add("extra", new ByteArrayInputStream(new byte[] { 3, 4, 5 })); - zf.add("after", new ByteArrayInputStream(new byte[] { 6, 7, 8 })); - } - - try (ZFile zf = new ZFile(mZipFile)) { - StoredEntry ex = zf.get("extra"); - assertNotNull(ex); - mExtraFieldSetter.accept(ex, - new ExtraField( - ImmutableList.of( - new ExtraField.RawDataSegment( - 0x7654, - new byte[] { 1, 1, 3, 3 })))); - } - - try (ZFile zf = new ZFile(mZipFile)) { - StoredEntry ex = zf.get("extra"); - assertNotNull(ex); - mExtraFieldSetter.accept(ex, new ExtraField()); - } - - try (ZFile zf = new ZFile(mZipFile)) { - StoredEntry before = zf.get("before"); - assertNotNull(before); - assertArrayEquals(new byte[] { 0, 1, 2 }, before.read()); - - StoredEntry extra = zf.get("extra"); - assertNotNull(extra); - assertArrayEquals(new byte[] { 3, 4, 5 }, extra.read()); - - StoredEntry after = zf.get("after"); - assertNotNull(after); - assertArrayEquals(new byte[] { 6, 7, 8 }, after.read()); - - ExtraField ef = mExtraFieldGetter.apply(extra); - assertEquals(0, ef.getSegments().size()); - } - } - - @Test - public void updateExtraFieldOfExistingEntry() throws Exception { - try (ZFile zf = new ZFile(mZipFile)) { - zf.add("before", new ByteArrayInputStream(new byte[] { 0, 1, 2 })); - zf.add("extra", new ByteArrayInputStream(new byte[] { 3, 4, 5 })); - zf.add("after", new ByteArrayInputStream(new byte[] { 6, 7, 8 })); - } - - try (ZFile zf = new ZFile(mZipFile)) { - StoredEntry ex = zf.get("extra"); - assertNotNull(ex); - mExtraFieldSetter.accept(ex, - new ExtraField( - ImmutableList.of( - new ExtraField.RawDataSegment( - 0x7654, - new byte[] { 1, 1, 3, 3 })))); - } - - try (ZFile zf = new ZFile(mZipFile)) { - StoredEntry ex = zf.get("extra"); - assertNotNull(ex); - mExtraFieldSetter.accept(ex, - new ExtraField( - ImmutableList.of( - new ExtraField.RawDataSegment( - 0x7654, - new byte[] { 2, 4, 2, 4 })))); - } - - try (ZFile zf = new ZFile(mZipFile)) { - StoredEntry before = zf.get("before"); - assertNotNull(before); - assertArrayEquals(new byte[] { 0, 1, 2 }, before.read()); - - StoredEntry extra = zf.get("extra"); - assertNotNull(extra); - assertArrayEquals(new byte[] { 3, 4, 5 }, extra.read()); - - StoredEntry after = zf.get("after"); - assertNotNull(after); - assertArrayEquals(new byte[] { 6, 7, 8 }, after.read()); - - ExtraField ef = mExtraFieldGetter.apply(extra); - assertEquals(1, ef.getSegments().size()); - ExtraField.Segment s = ef.getSingleSegment(0x7654); - assertNotNull(s); - byte[] sData = new byte[8]; - s.write(ByteBuffer.wrap(sData)); - assertArrayEquals(new byte[] { 0x54, 0x76, 0x04, 0x00, 2, 4, 2, 4 }, sData); - } - } - - @Test - public void parseInvalidExtraFieldWithInvalidHeader() throws Exception { - byte[] raw = new byte[1]; - ExtraField ef = new ExtraField(raw); - try { - ef.getSegments(); - fail(); - } catch (IOException e) { - // Expected. - } - } - - @Test - public void parseInvalidExtraFieldWithInsufficientData() throws Exception { - // Remember: 0x05, 0x00 = 5 in little endian! - byte[] raw = new byte[] { /* Header */ 0x01, 0x02, /* Size */ 0x05, 0x00, /* Data */ 0x01 }; - ExtraField ef = new ExtraField(raw); - try { - ef.getSegments(); - fail(); - } catch (IOException e) { - // Expected. - } - } -} diff --git a/src/test/java/com/android/apkzlib/zip/FileUseMapTest.java b/src/test/java/com/android/apkzlib/zip/FileUseMapTest.java deleted file mode 100644 index 0ee0129..0000000 --- a/src/test/java/com/android/apkzlib/zip/FileUseMapTest.java +++ /dev/null @@ -1,134 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * 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.android.apkzlib.zip; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.fail; - -import com.google.common.base.Stopwatch; - -import org.junit.Ignore; -import org.junit.Test; - -import java.text.DecimalFormat; -import java.util.Random; -import java.util.concurrent.TimeUnit; - -/** - * Tests for {@link FileUseMap}. - */ -public class FileUseMapTest { - - /** - * Verifies that as elements are added to the map, the performance of adding new elements - * is not significantly downgraded. This test creates a map and does several runs until - * a maximum is reached or a time limit is reached. - * - *

In each run, a random block is requested from the map with a random alignment and offset. - * The time for each run is saved. - * - *

After all runs are completed, the average time of the first runs (the head time) and - * the average time of the last runs (the tail time) is computed, as well as the average - * time. - * - *

The test passes if the average tail set time is (1) at most twice as long as the average - * and (2) is at most three times as long as the head set. This ensures that performance can - * degrade somewhat as the file map size increases, but not too much. - */ - @Test - @Ignore("This test relies on magic ratios to detect when performance is bad.") - public void addPerformanceTest() { - final long MAP_SIZE = 10000000; - final int MAX_RUNS = 10000; - final long MAX_TEST_DURATION_MS = 1000; - final int MAX_RANDOM_BLOCK_SIZE = 1000; - final int MAX_RANDOM_ALIGNMENT = 10; - final int HEAD_SET_SIZE = 1000; - final int TAIL_SET_SIZE = 1000; - final double MAX_TAIL_HEAD_RATIO = 3.0; - final double MAX_TAIL_TOTAL_RATIO = 2.0; - - long mapSize = MAP_SIZE; - FileUseMap map = new FileUseMap(mapSize, 0); - Random rand = new Random(0); - - long[] runs = new long[MAX_RUNS]; - int currentRun = 0; - - Stopwatch testStopwatch = Stopwatch.createStarted(); - while (testStopwatch.elapsed(TimeUnit.MILLISECONDS) < MAX_TEST_DURATION_MS - && currentRun < runs.length) { - Stopwatch runStopwatch = Stopwatch.createStarted(); - - long blockSize = 1 + rand.nextInt(MAX_RANDOM_BLOCK_SIZE); - long start = map.locateFree(blockSize, rand.nextInt(MAX_RANDOM_ALIGNMENT), - rand.nextInt(MAX_RANDOM_ALIGNMENT), FileUseMap.PositionAlgorithm.BEST_FIT); - long end = start + blockSize; - if (end >= mapSize) { - mapSize *= 2; - map.extend(mapSize); - } - - map.add(start, end, new Object()); - - runs[currentRun] = runStopwatch.elapsed(TimeUnit.NANOSECONDS); - currentRun++; - } - - double initialAvg = 0; - for (int i = 0; i < HEAD_SET_SIZE; i++) { - initialAvg += runs[i]; - } - - initialAvg /= HEAD_SET_SIZE; - - double endAvg = 0; - for (int i = currentRun - TAIL_SET_SIZE; i < currentRun; i++) { - endAvg += runs[i]; - } - - endAvg /= TAIL_SET_SIZE; - - double totalAvg = 0; - for (int i = 0; i < runs.length; i++) { - totalAvg += runs[i]; - } - - totalAvg /= currentRun; - - if (endAvg > totalAvg * MAX_TAIL_TOTAL_RATIO || endAvg > initialAvg * MAX_TAIL_HEAD_RATIO) { - DecimalFormat df = new DecimalFormat("#,###"); - - fail("Add performance at end is too bad. Performance in the beginning is " - + df.format(initialAvg) + "ns per insertion and at the end is " - + df.format(endAvg) + "ns. Average over the total of " + currentRun + " runs " - + "is " + df.format(totalAvg) + "ns."); - } - } - - @Test - public void testSizeComputation() { - FileUseMap m = new FileUseMap(200, 0); - - assertEquals(200, m.size()); - assertEquals(0, m.usedSize()); - - m.add(10, 20, new Object()); - assertEquals(200, m.size()); - assertEquals(20, m.usedSize()); - } -} diff --git a/src/test/java/com/android/apkzlib/zip/OldApkReadTest.java b/src/test/java/com/android/apkzlib/zip/OldApkReadTest.java deleted file mode 100644 index 61a08d7..0000000 --- a/src/test/java/com/android/apkzlib/zip/OldApkReadTest.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * 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.android.apkzlib.zip; - -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; - -import com.android.apkzlib.utils.ApkZFileTestUtils; -import java.io.File; -import org.junit.Test; - -public class OldApkReadTest { - - @Test - public void testReadOldApk() throws Exception { - File apkFile = ApkZFileTestUtils.getResource("/testData/packaging/test.apk"); - assertTrue(apkFile.exists()); - - try (ZFile zf = new ZFile(apkFile, new ZFileOptions())) { - StoredEntry classesDex = zf.get("classes.dex"); - assertNotNull(classesDex); - } - } -} diff --git a/src/test/java/com/android/apkzlib/zip/ReadWithDifferentCompressionLevelsTest.java b/src/test/java/com/android/apkzlib/zip/ReadWithDifferentCompressionLevelsTest.java deleted file mode 100644 index 4301710..0000000 --- a/src/test/java/com/android/apkzlib/zip/ReadWithDifferentCompressionLevelsTest.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * 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.android.apkzlib.zip; - -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; - -import com.android.apkzlib.utils.ApkZFileTestUtils; -import java.io.File; -import org.junit.Test; - -public class ReadWithDifferentCompressionLevelsTest { - - @Test - public void readL9() throws Exception { - File l9File = ApkZFileTestUtils.getResource("/testData/packaging/l9.zip"); - assertTrue(l9File.isFile()); - - try (ZFile read = new ZFile(l9File, new ZFileOptions())) { - assertNotNull(read.get("text-files/rfc2460.txt")); - } - } - - @Test - public void readL1() throws Exception { - File l1File = ApkZFileTestUtils.getResource("/testData/packaging/l1.zip"); - assertTrue(l1File.isFile()); - - try (ZFile read = new ZFile(l1File, new ZFileOptions())) { - assertNotNull(read.get("text-files/rfc2460.txt")); - } - } -} diff --git a/src/test/java/com/android/apkzlib/zip/ZFileNotificationTest.java b/src/test/java/com/android/apkzlib/zip/ZFileNotificationTest.java deleted file mode 100644 index 84be460..0000000 --- a/src/test/java/com/android/apkzlib/zip/ZFileNotificationTest.java +++ /dev/null @@ -1,420 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * 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.android.apkzlib.zip; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertSame; - -import com.android.apkzlib.utils.ApkZLibPair; -import com.android.apkzlib.utils.IOExceptionRunnable; -import com.google.common.collect.Lists; -import java.io.ByteArrayInputStream; -import java.io.File; -import java.io.IOException; -import java.util.List; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.TemporaryFolder; -import org.mockito.Mockito; - -public class ZFileNotificationTest { - private static class KeepListener extends ZFileExtension { - public int open; - public int beforeUpdated; - public int updated; - public int closed; - public List> added; - public List removed; - public IOExceptionRunnable returnRunnable; - - KeepListener() { - reset(); - } - - @Nullable - @Override - public IOExceptionRunnable open() { - open++; - return returnRunnable; - } - - @Nullable - @Override - public IOExceptionRunnable beforeUpdate() { - beforeUpdated++; - return returnRunnable; - } - - @Override - public void updated() { - updated++; - } - - @Override - public void closed() { - closed++; - } - - @Nullable - @Override - public IOExceptionRunnable added(@Nonnull StoredEntry entry, - @Nullable StoredEntry replaced) { - added.add(new ApkZLibPair<>(entry, replaced)); - return returnRunnable; - } - - @Nullable - @Override - public IOExceptionRunnable removed(@Nonnull StoredEntry entry) { - removed.add(entry); - return returnRunnable; - } - - void reset() { - open = 0; - beforeUpdated = 0; - updated = 0; - closed = 0; - added = Lists.newArrayList(); - removed = Lists.newArrayList(); - } - - void assertClear() { - assertEquals(0, open); - assertEquals(0, beforeUpdated); - assertEquals(0, updated); - assertEquals(0, closed); - assertEquals(0, added.size()); - assertEquals(0, removed.size()); - } - } - - @Rule - public TemporaryFolder mTemporaryFolder = new TemporaryFolder(); - - @Test - public void notifyAddFile() throws Exception { - try (ZFile zf = new ZFile(new File(mTemporaryFolder.getRoot(), "a.zip"))) { - KeepListener kl = new KeepListener(); - zf.addZFileExtension(kl); - - kl.assertClear(); - - zf.add("foo", new ByteArrayInputStream(new byte[] { 1, 2 })); - zf.finishAllBackgroundTasks(); - assertEquals(1, kl.added.size()); - StoredEntry addedSe = kl.added.get(0).v1; - assertNull(kl.added.get(0).v2); - kl.added.clear(); - kl.assertClear(); - - StoredEntry foo = zf.get("foo"); - assertNotNull(foo); - - assertSame(foo, addedSe); - } - } - - @Test - public void notifyRemoveFile() throws Exception { - try (ZFile zf = new ZFile(new File(mTemporaryFolder.getRoot(), "a.zip"))) { - KeepListener kl = new KeepListener(); - zf.addZFileExtension(kl); - - kl.assertClear(); - - zf.add("foo", new ByteArrayInputStream(new byte[] { 1, 2 })); - zf.finishAllBackgroundTasks(); - kl.reset(); - - StoredEntry foo = zf.get("foo"); - assertNotNull(foo); - - foo.delete(); - assertEquals(1, kl.removed.size()); - assertSame(foo, kl.removed.get(0)); - kl.removed.clear(); - kl.assertClear(); - } - } - - @Test - public void notifyUpdateFile() throws Exception { - try (ZFile zf = new ZFile(new File(mTemporaryFolder.getRoot(), "a.zip"))) { - KeepListener kl = new KeepListener(); - zf.addZFileExtension(kl); - - kl.assertClear(); - - zf.add("foo", new ByteArrayInputStream(new byte[] { 1, 2 })); - zf.finishAllBackgroundTasks(); - StoredEntry foo1 = zf.get("foo"); - kl.reset(); - - zf.add("foo", new ByteArrayInputStream(new byte[] { 2, 3 })); - zf.finishAllBackgroundTasks(); - StoredEntry foo2 = zf.get("foo"); - - assertEquals(1, kl.added.size()); - assertSame(foo2, kl.added.get(0).v1); - assertSame(foo1, kl.added.get(0).v2); - - kl.added.clear(); - kl.assertClear(); - } - } - - @Test - public void notifyOpenUpdateClose() throws Exception { - KeepListener kl = new KeepListener(); - try (ZFile zf = new ZFile(new File(mTemporaryFolder.getRoot(), "a.zip"))) { - zf.addZFileExtension(kl); - - kl.assertClear(); - - zf.add("foo", new ByteArrayInputStream(new byte[] { 1, 2 })); - zf.finishAllBackgroundTasks(); - kl.reset(); - } - - assertEquals(1, kl.open); - kl.open = 0; - assertEquals(1, kl.beforeUpdated); - assertEquals(1, kl.updated); - kl.beforeUpdated = 0; - kl.updated = 0; - assertEquals(1, kl.closed); - kl.closed = 0; - kl.assertClear(); - } - - @Test - public void notifyOpenUpdate() throws Exception { - KeepListener kl = new KeepListener(); - try (ZFile zf = new ZFile(new File(mTemporaryFolder.getRoot(), "a.zip"))) { - zf.addZFileExtension(kl); - - kl.assertClear(); - - zf.add("foo", new ByteArrayInputStream(new byte[] { 1, 2 })); - zf.finishAllBackgroundTasks(); - kl.reset(); - zf.update(); - - assertEquals(1, kl.open); - kl.open = 0; - assertEquals(1, kl.beforeUpdated); - assertEquals(1, kl.updated); - kl.beforeUpdated = 0; - kl.updated = 0; - kl.assertClear(); - } - } - - @Test - public void notifyUpdate() throws Exception { - try (ZFile zf = new ZFile(new File(mTemporaryFolder.getRoot(), "a.zip"))) { - KeepListener kl = new KeepListener(); - zf.addZFileExtension(kl); - - kl.assertClear(); - - zf.add("foo", new ByteArrayInputStream(new byte[] { 1, 2 })); - zf.update(); - kl.reset(); - - zf.add("bar", new ByteArrayInputStream(new byte[] { 2, 3 })); - zf.finishAllBackgroundTasks(); - kl.reset(); - - zf.update(); - assertEquals(1, kl.beforeUpdated); - assertEquals(1, kl.updated); - kl.beforeUpdated = 0; - kl.updated = 0; - kl.assertClear(); - } - } - - @Test - public void removedListenersAreNotNotified() throws Exception { - try (ZFile zf = new ZFile(new File(mTemporaryFolder.getRoot(), "a.zip"))) { - KeepListener kl = new KeepListener(); - zf.addZFileExtension(kl); - - kl.assertClear(); - - zf.add("foo", new ByteArrayInputStream(new byte[] { 1, 2 })); - zf.finishAllBackgroundTasks(); - assertEquals(1, kl.added.size()); - kl.added.clear(); - kl.assertClear(); - - zf.removeZFileExtension(kl); - - zf.add("foo", new ByteArrayInputStream(new byte[] { 2, 3 })); - zf.finishAllBackgroundTasks(); - kl.assertClear(); - } - } - - @Test - public void actionsExecutedAtEndOfNotification() throws Exception { - try (ZFile zf = new ZFile(new File(mTemporaryFolder.getRoot(), "a.zip"))) { - - IOException death[] = new IOException[1]; - - KeepListener kl1 = new KeepListener(); - zf.addZFileExtension(kl1); - kl1.returnRunnable = new IOExceptionRunnable() { - private boolean once = false; - - @Override - public void run() { - if (once) { - return; - } - - once = true; - - try { - zf.add("foo", new ByteArrayInputStream(new byte[] { 1, 2 })); - } catch (IOException e) { - death[0] = e; - } - } - }; - - KeepListener kl2 = new KeepListener(); - zf.addZFileExtension(kl2); - kl2.returnRunnable = new IOExceptionRunnable() { - private boolean once = false; - - @Override - public void run() { - if (once) { - return; - } - - once = true; - try { - zf.add("bar", new ByteArrayInputStream(new byte[] { 1, 2 })); - } catch (IOException e) { - death[0] = e; - } - } - }; - - kl1.assertClear(); - kl2.assertClear(); - - zf.add("xpto", new ByteArrayInputStream(new byte[] { 1, 2 })); - zf.finishAllBackgroundTasks(); - - assertEquals(3, kl1.added.size()); - kl1.added.clear(); - kl1.assertClear(); - assertEquals(3, kl2.added.size()); - kl2.added.clear(); - kl2.assertClear(); - - assertNull(death[0]); - } - } - - @Test - public void canAddFilesDuringUpdateNotification() throws Exception { - File zipFile = new File(mTemporaryFolder.getRoot(), "a.zip"); - try (ZFile zf = new ZFile(zipFile)) { - IOException death[] = new IOException[1]; - - KeepListener kl1 = new KeepListener(); - zf.addZFileExtension(kl1); - - zf.add("foo", new ByteArrayInputStream(new byte[] { 1, 2 })); - zf.finishAllBackgroundTasks(); - - kl1.returnRunnable = new IOExceptionRunnable() { - private boolean once = false; - - @Override - public void run() { - if (once) { - return; - } - - once = true; - - try { - zf.add("bar", new ByteArrayInputStream(new byte[] { 1, 2 })); - } catch (IOException e) { - death[0] = e; - } - } - }; - } - - try (ZFile zf2 = new ZFile(zipFile)) { - StoredEntry fooFile = zf2.get("foo"); - assertNotNull(fooFile); - StoredEntry barFile = zf2.get("bar"); - assertNotNull(barFile); - } - } - - @Test - public void notifyOnceEntriesWritten() throws Exception { - File zipFile = new File(mTemporaryFolder.getRoot(), "a.zip"); - ZFileExtension ext = Mockito.mock(ZFileExtension.class); - try (ZFile zf = new ZFile(zipFile)) { - zf.addZFileExtension(ext); - - zf.add("foo", new ByteArrayInputStream(new byte[] { 1, 2 })); - zf.finishAllBackgroundTasks(); - - Mockito.verify(ext, Mockito.times(0)).entriesWritten(); - } - - Mockito.verify(ext, Mockito.times(1)).entriesWritten(); - } - - @Test - public void notifyTwiceEntriesWrittenIfCdChanged() throws Exception { - File zipFile = new File(mTemporaryFolder.getRoot(), "a.zip"); - ZFileExtension ext = Mockito.mock(ZFileExtension.class); - try (ZFile zf = new ZFile(zipFile)) { - Mockito.doAnswer((invocation) -> { - zf.setExtraDirectoryOffset(10); - Mockito.doNothing().when(ext).entriesWritten(); - return null; - }).when(ext).entriesWritten(); - - zf.addZFileExtension(ext); - - zf.add("foo", new ByteArrayInputStream(new byte[] { 1, 2 })); - zf.finishAllBackgroundTasks(); - - Mockito.verify(ext, Mockito.times(0)).entriesWritten(); - } - - Mockito.verify(ext, Mockito.times(2)).entriesWritten(); - } -} diff --git a/src/test/java/com/android/apkzlib/zip/ZFileReadOnlyTest.java b/src/test/java/com/android/apkzlib/zip/ZFileReadOnlyTest.java deleted file mode 100644 index a030a83..0000000 --- a/src/test/java/com/android/apkzlib/zip/ZFileReadOnlyTest.java +++ /dev/null @@ -1,240 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * 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.android.apkzlib.zip; - -import static org.junit.Assert.assertArrayEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.fail; - -import java.io.ByteArrayInputStream; -import java.io.File; -import java.io.IOException; -import javax.annotation.Nonnull; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.TemporaryFolder; - -public class ZFileReadOnlyTest { - @Rule - public TemporaryFolder temporaryFolder = new TemporaryFolder(); - - @Test - public void cannotCreateRoFileOnNonExistingFile() throws Exception { - try { - new ZFile(new File(temporaryFolder.getRoot(), "foo.zip"), new ZFileOptions(), true); - fail(); - } catch (IOException e) { - // Expected. - } - } - - @Nonnull - private File makeTestZip() throws IOException { - File zip = new File(temporaryFolder.getRoot(), "foo.zip"); - try (ZFile zf = new ZFile(zip)) { - zf.add("bar", new ByteArrayInputStream(new byte[] { 0, 1, 2, 3, 4, 5 })); - } - - return zip; - } - - @Test - public void cannotUpdateInRoMode() throws Exception { - try (ZFile zf = new ZFile(makeTestZip(), new ZFileOptions(), true)) { - try { - zf.update(); - fail(); - } catch (IllegalStateException e) { - // Expeted. - } - } - } - - @Test - public void cannotAddFilesInRoMode() throws Exception { - try (ZFile zf = new ZFile(makeTestZip(), new ZFileOptions(), true)) { - try { - zf.add("bar2", new ByteArrayInputStream(new byte[] { 6, 7, })); - fail(); - } catch (IllegalStateException e) { - // Expeted. - } - } - } - - @Test - public void cannotAddRecursivelyInRoMode() throws Exception { - File folder = temporaryFolder.newFolder(); - try (ZFile zf = new ZFile(makeTestZip(), new ZFileOptions(), true)) { - try { - zf.addAllRecursively(folder); - fail(); - } catch (IllegalStateException e) { - // Expeted. - } - } - } - - @Test - public void cannotReplaceFilesInRoMode() throws Exception { - try (ZFile zf = new ZFile(makeTestZip(), new ZFileOptions(), true)) { - try { - zf.add("bar", new ByteArrayInputStream(new byte[] { 6, 7 })); - fail(); - } catch (IllegalStateException e) { - // Expeted. - } - } - } - - @Test - public void cannotDeleteFilesInRoMode() throws Exception { - try (ZFile zf = new ZFile(makeTestZip(), new ZFileOptions(), true)) { - StoredEntry bar = zf.get("bar"); - assertNotNull(bar); - try { - bar.delete(); - fail(); - } catch (IllegalStateException e) { - // Expeted. - } - } - } - - @Test - public void cannotMergeInRoMode() throws Exception { - try (ZFile toMerge = new ZFile(new File(temporaryFolder.getRoot(), "a.zip"))) { - try (ZFile zf = new ZFile(makeTestZip(), new ZFileOptions(), true)) { - try { - zf.mergeFrom(toMerge, s -> false); - fail(); - } catch (IllegalStateException e) { - // Expeted. - } - } - } - } - - @Test - public void cannotTouchInRoMode() throws Exception { - try (ZFile zf = new ZFile(makeTestZip(), new ZFileOptions(), true)) { - try { - zf.touch(); - fail(); - } catch (IllegalStateException e) { - // Expeted. - } - } - } - - @Test - public void cannotRealignInRoMode() throws Exception { - try (ZFile zf = new ZFile(makeTestZip(), new ZFileOptions(), true)) { - try { - zf.realign(); - fail(); - } catch (IllegalStateException e) { - // Expeted. - } - } - } - - @Test - public void cannotAddExtensionInRoMode() throws Exception { - try (ZFile zf = new ZFile(makeTestZip(), new ZFileOptions(), true)) { - try { - zf.addZFileExtension(new ZFileExtension() {}); - fail(); - } catch (IllegalStateException e) { - // Expeted. - } - } - } - - @Test - public void cannotDirectWriteInRoMode() throws Exception { - try (ZFile zf = new ZFile(makeTestZip(), new ZFileOptions(), true)) { - try { - zf.directWrite(0, new byte[1]); - fail(); - } catch (IllegalStateException e) { - // Expeted. - } - } - } - - @Test - public void cannotSetEocdCommentInRoMode() throws Exception { - try (ZFile zf = new ZFile(makeTestZip(), new ZFileOptions(), true)) { - try { - zf.setEocdComment(new byte[2]); - fail(); - } catch (IllegalStateException e) { - // Expeted. - } - } - } - - @Test - public void cannotSetCentralDirectoryOffsetInRoMode() throws Exception { - try (ZFile zf = new ZFile(makeTestZip(), new ZFileOptions(), true)) { - try { - zf.setExtraDirectoryOffset(4); - fail(); - } catch (IllegalStateException e) { - // Expeted. - } - } - } - - @Test - public void cannotSortZipContentsInRoMode() throws Exception { - try (ZFile zf = new ZFile(makeTestZip(), new ZFileOptions(), true)) { - try { - zf.sortZipContents(); - fail(); - } catch (IllegalStateException e) { - // Expeted. - } - } - } - - @Test - public void canOpenAndReadFilesInRoMode() throws Exception { - try (ZFile zf = new ZFile(makeTestZip(), new ZFileOptions(), true)) { - StoredEntry bar = zf.get("bar"); - assertNotNull(bar); - assertArrayEquals(new byte[] { 0, 1, 2, 3, 4, 5 }, bar.read()); - } - } - - @Test - public void canGetDirectoryAndEocdBytesInRoMode() throws Exception { - try (ZFile zf = new ZFile(makeTestZip(), new ZFileOptions(), true)) { - zf.getCentralDirectoryBytes(); - zf.getEocdBytes(); - zf.getEocdComment(); - } - } - - @Test - public void canDirectReadInRoMode() throws Exception { - try (ZFile zf = new ZFile(makeTestZip(), new ZFileOptions(), true)) { - zf.directRead(0, new byte[2]); - } - } -} diff --git a/src/test/java/com/android/apkzlib/zip/ZFileSortTest.java b/src/test/java/com/android/apkzlib/zip/ZFileSortTest.java deleted file mode 100644 index 869f73a..0000000 --- a/src/test/java/com/android/apkzlib/zip/ZFileSortTest.java +++ /dev/null @@ -1,218 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * 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.android.apkzlib.zip; - -import static org.junit.Assert.assertArrayEquals; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; - -import java.io.ByteArrayInputStream; -import java.io.File; -import javax.annotation.Nullable; -import org.junit.After; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.TemporaryFolder; - -public class ZFileSortTest { - @Rule - public TemporaryFolder mTemporaryFolder = new TemporaryFolder(); - private File mFile; - private ZFile mZFile; - private StoredEntry mMaryEntry; - private long mMaryOffset; - private StoredEntry mAndrewEntry; - private long mAndrewOffset; - private StoredEntry mBethEntry; - private long mBethOffset; - private StoredEntry mPeterEntry; - private long mPeterOffset; - - @Before - public final void before() throws Exception { - mFile = new File(mTemporaryFolder.getRoot(), "a.zip"); - setupZFile(null); - } - - @After - public final void after() throws Exception { - mZFile.close(); - } - - /** - * Recreates the zip file, if one already exist. - * - * @param options the options for the file, may be {@code null} in which case the default - * options will be used - * @throws Exception failed to re-create the file - */ - private void setupZFile(@Nullable ZFileOptions options) throws Exception { - if (mZFile != null) { - mZFile.close(); - } - - if (mFile.exists()) { - assertTrue(mFile.delete()); - } - - if (options == null) { - options = new ZFileOptions(); - } - - mZFile = new ZFile(mFile, options); - - mZFile.add("Mary.xml", new ByteArrayInputStream(new byte[] { 1, 2, 3 })); - mZFile.add("Andrew.txt", new ByteArrayInputStream(new byte[] { 4, 5 })); - mZFile.add("Beth.png", new ByteArrayInputStream(new byte[] { 6, 7, 8, 9 })); - mZFile.add("Peter.html", new ByteArrayInputStream(new byte[] { 10 })); - mZFile.finishAllBackgroundTasks(); - } - - private void readEntries() throws Exception { - mMaryEntry = mZFile.get("Mary.xml"); - assertNotNull(mMaryEntry); - mMaryOffset = mMaryEntry.getCentralDirectoryHeader().getOffset(); - assertArrayEquals(new byte[] { 1, 2, 3 }, mMaryEntry.read()); - - mAndrewEntry = mZFile.get("Andrew.txt"); - assertNotNull(mAndrewEntry); - mAndrewOffset = mAndrewEntry.getCentralDirectoryHeader().getOffset(); - assertArrayEquals(new byte[] { 4, 5 }, mAndrewEntry.read()); - - mBethEntry = mZFile.get("Beth.png"); - assertNotNull(mBethEntry); - mBethOffset = mBethEntry.getCentralDirectoryHeader().getOffset(); - assertArrayEquals(new byte[] { 6, 7, 8, 9 }, mBethEntry.read()); - - mPeterEntry = mZFile.get("Peter.html"); - assertNotNull(mPeterEntry); - mPeterOffset = mPeterEntry.getCentralDirectoryHeader().getOffset(); - assertArrayEquals(new byte[] { 10 }, mPeterEntry.read()); - } - - @Test - public void noSort() throws Exception { - readEntries(); - - assertEquals(-1, mMaryOffset); - assertEquals(-1, mAndrewOffset); - assertEquals(-1, mBethOffset); - assertEquals(-1, mPeterOffset); - - mZFile.update(); - - readEntries(); - - assertTrue(mMaryOffset >= 0); - assertTrue(mMaryOffset < mAndrewOffset); - assertTrue(mAndrewOffset < mBethOffset); - assertTrue(mBethOffset < mPeterOffset); - } - - @Test - public void sortFilesBeforeUpdate() throws Exception { - readEntries(); - mZFile.sortZipContents(); - - mZFile.update(); - - readEntries(); - - assertTrue(mAndrewOffset >= 0); - assertTrue(mBethOffset > mAndrewOffset); - assertTrue(mMaryOffset > mBethOffset); - assertTrue(mPeterOffset > mMaryOffset); - } - - @Test - public void autoSort() throws Exception { - ZFileOptions options = new ZFileOptions(); - options.setAutoSortFiles(true); - setupZFile(options); - - readEntries(); - - mZFile.update(); - - readEntries(); - - assertTrue(mAndrewOffset >= 0); - assertTrue(mBethOffset > mAndrewOffset); - assertTrue(mMaryOffset > mBethOffset); - assertTrue(mPeterOffset > mMaryOffset); - } - - @Test - public void sortFilesAfterUpdate() throws Exception { - readEntries(); - - mZFile.update(); - - mZFile.sortZipContents(); - - readEntries(); - - assertEquals(-1, mMaryOffset); - assertEquals(-1, mAndrewOffset); - assertEquals(-1, mBethOffset); - assertEquals(-1, mPeterOffset); - - mZFile.update(); - - readEntries(); - - assertTrue(mAndrewOffset >= 0); - assertTrue(mBethOffset > mAndrewOffset); - assertTrue(mMaryOffset > mBethOffset); - assertTrue(mPeterOffset > mMaryOffset); - } - - @Test - public void sortFilesWithAlignment() throws Exception { - mZFile.close(); - - ZFileOptions options = new ZFileOptions(); - options.setAlignmentRule(AlignmentRules.constantForSuffix(".xml", 1024)); - mZFile = new ZFile(mFile, options); - - mZFile.sortZipContents(); - mZFile.update(); - - readEntries(); - assertTrue(mAndrewOffset >= 0); - assertTrue(mBethOffset > mAndrewOffset); - assertTrue(mPeterOffset > mBethOffset); - assertTrue(mMaryOffset > mPeterOffset); - } - - @Test - public void sortFilesOnClosedFile() throws Exception { - mZFile.close(); - mZFile = new ZFile(mFile); - mZFile.sortZipContents(); - mZFile.update(); - - readEntries(); - - assertTrue(mAndrewOffset >= 0); - assertTrue(mBethOffset > mAndrewOffset); - assertTrue(mMaryOffset > mBethOffset); - assertTrue(mPeterOffset > mMaryOffset); - } -} diff --git a/src/test/java/com/android/apkzlib/zip/ZFileTest.java b/src/test/java/com/android/apkzlib/zip/ZFileTest.java deleted file mode 100644 index b7f2979..0000000 --- a/src/test/java/com/android/apkzlib/zip/ZFileTest.java +++ /dev/null @@ -1,1821 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * 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.android.apkzlib.zip; - -import static com.android.apkzlib.utils.ApkZFileTestUtils.readSegment; -import static org.junit.Assert.assertArrayEquals; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNotSame; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertSame; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; - -import com.android.apkzlib.zip.compress.DeflateExecutionCompressor; -import com.android.apkzlib.zip.utils.CloseableByteSource; -import com.android.apkzlib.zip.utils.RandomAccessFileUtils; -import com.google.common.base.Charsets; -import com.google.common.base.Strings; -import com.google.common.base.Throwables; -import com.google.common.hash.Hashing; -import com.google.common.io.ByteStreams; -import com.google.common.io.Closer; -import com.google.common.io.Files; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.RandomAccessFile; -import java.util.Locale; -import java.util.Random; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.zip.Deflater; -import java.util.zip.ZipEntry; -import java.util.zip.ZipFile; -import java.util.zip.ZipInputStream; -import java.util.zip.ZipOutputStream; -import javax.annotation.Nonnull; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.TemporaryFolder; - -public class ZFileTest { - @Rule - public TemporaryFolder mTemporaryFolder = new TemporaryFolder(); - - @Test - public void getZipPath() throws Exception { - File temporaryDir = mTemporaryFolder.getRoot(); - File zpath = new File(temporaryDir, "a"); - try (ZFile zf = new ZFile(zpath)) { - assertEquals(zpath, zf.getFile()); - } - } - - @Test - public void readNonExistingFile() throws Exception { - File temporaryDir = mTemporaryFolder.getRoot(); - File zf = new File(temporaryDir, "a"); - try (ZFile azf = new ZFile(zf)) { - azf.touch(); - } - - assertTrue(zf.exists()); - } - - @Test(expected = IOException.class) - public void readExistingEmptyFile() throws Exception { - File temporaryDir = mTemporaryFolder.getRoot(); - File zf = new File(temporaryDir, "a"); - Files.write(new byte[0], zf); - try (ZFile azf = new ZFile(zf)) { - /* - * Just open and close. - */ - } - } - - @Test - public void readAlmostEmptyZip() throws Exception { - File zf = ZipTestUtils.cloneRsrc("empty-zip.zip", mTemporaryFolder); - - try (ZFile azf = new ZFile(zf)) { - assertEquals(1, azf.entries().size()); - - StoredEntry z = azf.get("z/"); - assertNotNull(z); - assertSame(StoredEntryType.DIRECTORY, z.getType()); - } - } - - @Test - public void readZipWithTwoFilesOneDirectory() throws Exception { - File zf = ZipTestUtils.cloneRsrc("simple-zip.zip", mTemporaryFolder); - - try (ZFile azf = new ZFile(zf)) { - assertEquals(3, azf.entries().size()); - - StoredEntry e0 = azf.get("dir/"); - assertNotNull(e0); - assertSame(StoredEntryType.DIRECTORY, e0.getType()); - - StoredEntry e1 = azf.get("dir/inside"); - assertNotNull(e1); - assertSame(StoredEntryType.FILE, e1.getType()); - ByteArrayOutputStream e1BytesOut = new ByteArrayOutputStream(); - ByteStreams.copy(e1.open(), e1BytesOut); - byte e1Bytes[] = e1BytesOut.toByteArray(); - String e1Txt = new String(e1Bytes, Charsets.US_ASCII); - assertEquals("inside", e1Txt); - - StoredEntry e2 = azf.get("file.txt"); - assertNotNull(e2); - assertSame(StoredEntryType.FILE, e2.getType()); - ByteArrayOutputStream e2BytesOut = new ByteArrayOutputStream(); - ByteStreams.copy(e2.open(), e2BytesOut); - byte e2Bytes[] = e2BytesOut.toByteArray(); - String e2Txt = new String(e2Bytes, Charsets.US_ASCII); - assertEquals("file with more text to allow deflating to be useful", e2Txt); - } - } - - @Test - public void readOnlyZipSupport() throws Exception { - File testZip = ZipTestUtils.cloneRsrc("empty-zip.zip", mTemporaryFolder); - - assertTrue(testZip.setWritable(false)); - - try (ZFile zf = new ZFile(testZip)) { - assertEquals(1, zf.entries().size()); - assertTrue(zf.getCentralDirectoryOffset() > 0); - assertTrue(zf.getEocdOffset() > 0); - } - } - - @Test - public void readOnlyV2SignedApkSupport() throws Exception { - File testZip = ZipTestUtils.cloneRsrc("v2-signed.apk", mTemporaryFolder); - - assertTrue(testZip.setWritable(false)); - - try (ZFile zf = new ZFile(testZip)) { - assertEquals(416, zf.entries().size()); - assertTrue(zf.getCentralDirectoryOffset() > 0); - assertTrue(zf.getEocdOffset() > 0); - } - } - - @Test - public void compressedFilesReadableByJavaZip() throws Exception { - File testZip = new File(mTemporaryFolder.getRoot(), "t.zip"); - - File wiki = ZipTestUtils - .cloneRsrc("text-files/wikipedia.html", mTemporaryFolder, "wiki"); - File rfc = ZipTestUtils.cloneRsrc("text-files/rfc2460.txt", mTemporaryFolder, "rfc"); - File lena = ZipTestUtils.cloneRsrc("images/lena.png", mTemporaryFolder, "lena"); - byte[] wikiData = Files.toByteArray(wiki); - byte[] rfcData = Files.toByteArray(rfc); - byte[] lenaData = Files.toByteArray(lena); - - try (ZFile zf = new ZFile(testZip)) { - zf.add("wiki", new ByteArrayInputStream(wikiData)); - zf.add("rfc", new ByteArrayInputStream(rfcData)); - zf.add("lena", new ByteArrayInputStream(lenaData)); - } - - try(ZipFile jz = new ZipFile(testZip)) { - ZipEntry ze = jz.getEntry("wiki"); - assertNotNull(ze); - assertEquals(ZipEntry.DEFLATED, ze.getMethod()); - assertTrue(ze.getCompressedSize() < wikiData.length); - InputStream zeis = jz.getInputStream(ze); - assertArrayEquals(wikiData, ByteStreams.toByteArray(zeis)); - zeis.close(); - - ze = jz.getEntry("rfc"); - assertNotNull(ze); - assertEquals(ZipEntry.DEFLATED, ze.getMethod()); - assertTrue(ze.getCompressedSize() < rfcData.length); - zeis = jz.getInputStream(ze); - assertArrayEquals(rfcData, ByteStreams.toByteArray(zeis)); - zeis.close(); - - ze = jz.getEntry("lena"); - assertNotNull(ze); - assertEquals(ZipEntry.STORED, ze.getMethod()); - assertTrue(ze.getCompressedSize() == lenaData.length); - zeis = jz.getInputStream(ze); - assertArrayEquals(lenaData, ByteStreams.toByteArray(zeis)); - zeis.close(); - } - } - - @Test - public void removeFileFromZip() throws Exception { - File zipFile = mTemporaryFolder.newFile("test.zip"); - - try(ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zipFile))) { - ZipEntry entry = new ZipEntry("foo/"); - entry.setMethod(ZipEntry.STORED); - entry.setSize(0); - entry.setCompressedSize(0); - entry.setCrc(0); - zos.putNextEntry(entry); - zos.putNextEntry(new ZipEntry("foo/bar")); - zos.write(new byte[] { 1, 2, 3, 4 }); - zos.closeEntry(); - } - - try (ZFile zf = new ZFile(zipFile)) { - assertEquals(2, zf.entries().size()); - for (StoredEntry e : zf.entries()) { - if (e.getType() == StoredEntryType.FILE) { - e.delete(); - } - } - - zf.update(); - - try (ZipInputStream zis = new ZipInputStream(new FileInputStream(zipFile))) { - ZipEntry e1 = zis.getNextEntry(); - assertNotNull(e1); - - assertEquals("foo/", e1.getName()); - - ZipEntry e2 = zis.getNextEntry(); - assertNull(e2); - } - } - } - - @Test - public void addFileToZip() throws Exception { - File zipFile = mTemporaryFolder.newFile("test.zip"); - - try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zipFile))) { - ZipEntry fooDir = new ZipEntry("foo/"); - fooDir.setCrc(0); - fooDir.setCompressedSize(0); - fooDir.setSize(0); - fooDir.setMethod(ZipEntry.STORED); - zos.putNextEntry(fooDir); - zos.closeEntry(); - } - - ZFile zf = new ZFile(zipFile); - assertEquals(1, zf.entries().size()); - - zf.update(); - - try (ZipInputStream zis = new ZipInputStream(new FileInputStream(zipFile))) { - ZipEntry e1 = zis.getNextEntry(); - assertNotNull(e1); - - assertEquals("foo/", e1.getName()); - - ZipEntry e2 = zis.getNextEntry(); - assertNull(e2); - } - } - - @Test - public void createNewZip() throws Exception { - File zipFile = new File(mTemporaryFolder.getRoot(), "test.zip"); - - ZFile zf = new ZFile(zipFile); - zf.add("foo", new ByteArrayInputStream(new byte[] { 0, 1 })); - zf.close(); - - try (ZipFile jzf = new ZipFile(zipFile)) { - assertEquals(1, jzf.size()); - - ZipEntry fooEntry = jzf.getEntry("foo"); - assertNotNull(fooEntry); - assertEquals("foo", fooEntry.getName()); - assertEquals(2, fooEntry.getSize()); - - InputStream is = jzf.getInputStream(fooEntry); - assertEquals(0, is.read()); - assertEquals(1, is.read()); - assertEquals(-1, is.read()); - - is.close(); - } - } - - @Test - public void replaceFileWithSmallerInMiddle() throws Exception { - File zipFile = new File(mTemporaryFolder.getRoot(), "test.zip"); - - try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zipFile))) { - zos.putNextEntry(new ZipEntry("file1")); - zos.write(new byte[] { 1, 2, 3, 4, 5 }); - zos.putNextEntry(new ZipEntry("file2")); - zos.write(new byte[] { 6, 7, 8 }); - zos.putNextEntry(new ZipEntry("file3")); - zos.write(new byte[] { 9, 0, 1, 2, 3, 4 }); - } - - int totalSize = (int) zipFile.length(); - - try (ZFile zf = new ZFile(zipFile)) { - assertEquals(3, zf.entries().size()); - - StoredEntry file2 = zf.get("file2"); - assertNotNull(file2); - assertEquals(3, file2.getCentralDirectoryHeader().getUncompressedSize()); - - assertArrayEquals(new byte[] { 6, 7, 8 }, file2.read()); - - zf.add("file2", new ByteArrayInputStream(new byte[] { 11, 12 })); - - int newTotalSize = (int) zipFile.length(); - assertTrue(newTotalSize + " == " + totalSize, newTotalSize == totalSize); - - file2 = zf.get("file2"); - assertNotNull(file2); - assertArrayEquals(new byte[] { 11, 12 }, file2.read()); - } - - try (ZFile zf2 = new ZFile(zipFile)) { - StoredEntry file2 = zf2.get("file2"); - assertNotNull(file2); - assertArrayEquals(new byte[] { 11, 12 }, file2.read()); - } - } - - @Test - public void replaceFileWithSmallerAtEnd() throws Exception { - File zipFile = new File(mTemporaryFolder.getRoot(), "test.zip"); - try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zipFile))) { - zos.putNextEntry(new ZipEntry("file1")); - zos.write(new byte[]{1, 2, 3, 4, 5}); - zos.putNextEntry(new ZipEntry("file2")); - zos.write(new byte[]{6, 7, 8}); - zos.putNextEntry(new ZipEntry("file3")); - zos.write(new byte[]{9, 0, 1, 2, 3, 4}); - } - - int totalSize = (int) zipFile.length(); - - try (ZFile zf = new ZFile(zipFile)) { - assertEquals(3, zf.entries().size()); - - StoredEntry file3 = zf.get("file3"); - assertNotNull(file3); - assertEquals(6, file3.getCentralDirectoryHeader().getUncompressedSize()); - - assertArrayEquals(new byte[]{9, 0, 1, 2, 3, 4}, file3.read()); - - zf.add("file3", new ByteArrayInputStream(new byte[]{11, 12})); - zf.close(); - - int newTotalSize = (int) zipFile.length(); - assertTrue(newTotalSize + " < " + totalSize, newTotalSize < totalSize); - - file3 = zf.get("file3"); - assertNotNull(file3); - assertArrayEquals(new byte[]{11, 12,}, file3.read()); - } - - try (ZFile zf2 = new ZFile(zipFile)) { - StoredEntry file3 = zf2.get("file3"); - assertNotNull(file3); - assertArrayEquals(new byte[]{11, 12,}, file3.read()); - } - } - - @Test - public void replaceFileWithBiggerAtBegin() throws Exception { - File zipFile = new File(mTemporaryFolder.getRoot(), "test.zip"); - - try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zipFile))) { - zos.putNextEntry(new ZipEntry("file1")); - zos.write(new byte[]{1, 2, 3, 4, 5}); - zos.putNextEntry(new ZipEntry("file2")); - zos.write(new byte[]{6, 7, 8}); - zos.putNextEntry(new ZipEntry("file3")); - zos.write(new byte[]{9, 0, 1, 2, 3, 4}); - } - - int totalSize = (int) zipFile.length(); - byte[] newData = new byte[100]; - - try (ZFile zf = new ZFile(zipFile)) { - assertEquals(3, zf.entries().size()); - - StoredEntry file1 = zf.get("file1"); - assertNotNull(file1); - assertEquals(5, file1.getCentralDirectoryHeader().getUncompressedSize()); - - assertArrayEquals(new byte[]{1, 2, 3, 4, 5}, file1.read()); - - /* - * Need some data because java zip API uses data descriptors which we don't and makes - * the entries bigger (meaning just adding a couple of bytes would still fit in the - * same place). - */ - Random r = new Random(); - r.nextBytes(newData); - - zf.add("file1", new ByteArrayInputStream(newData)); - zf.close(); - - int newTotalSize = (int) zipFile.length(); - assertTrue(newTotalSize + " > " + totalSize, newTotalSize > totalSize); - - file1 = zf.get("file1"); - assertNotNull(file1); - assertArrayEquals(newData, file1.read()); - } - - try (ZFile zf2 = new ZFile(zipFile)) { - StoredEntry file1 = zf2.get("file1"); - assertNotNull(file1); - assertArrayEquals(newData, file1.read()); - } - } - - @Test - public void replaceFileWithBiggerAtEnd() throws Exception { - File zipFile = new File(mTemporaryFolder.getRoot(), "test.zip"); - - try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zipFile))) { - zos.putNextEntry(new ZipEntry("file1")); - zos.write(new byte[]{1, 2, 3, 4, 5}); - zos.putNextEntry(new ZipEntry("file2")); - zos.write(new byte[]{6, 7, 8}); - zos.putNextEntry(new ZipEntry("file3")); - zos.write(new byte[]{9, 0, 1, 2, 3, 4}); - } - - int totalSize = (int) zipFile.length(); - byte[] newData = new byte[100]; - - try (ZFile zf = new ZFile(zipFile)) { - assertEquals(3, zf.entries().size()); - - StoredEntry file3 = zf.get("file3"); - assertNotNull(file3); - assertEquals(6, file3.getCentralDirectoryHeader().getUncompressedSize()); - - assertArrayEquals(new byte[]{9, 0, 1, 2, 3, 4}, file3.read()); - - /* - * Need some data because java zip API uses data descriptors which we don't and makes - * the entries bigger (meaning just adding a couple of bytes would still fit in the - * same place). - */ - Random r = new Random(); - r.nextBytes(newData); - - zf.add("file3", new ByteArrayInputStream(newData)); - zf.close(); - - int newTotalSize = (int) zipFile.length(); - assertTrue(newTotalSize + " > " + totalSize, newTotalSize > totalSize); - - file3 = zf.get("file3"); - assertNotNull(file3); - assertArrayEquals(newData, file3.read()); - } - - try (ZFile zf2 = new ZFile(zipFile)) { - StoredEntry file3 = zf2.get("file3"); - assertNotNull(file3); - assertArrayEquals(newData, file3.read()); - } - } - - @Test - public void ignoredFilesDuringMerge() throws Exception { - File zip1 = mTemporaryFolder.newFile("t1.zip"); - - try (ZipOutputStream zos1 = new ZipOutputStream(new FileOutputStream(zip1))) { - zos1.putNextEntry(new ZipEntry("only_in_1")); - zos1.write(new byte[] { 1, 2 }); - zos1.putNextEntry(new ZipEntry("overridden_by_2")); - zos1.write(new byte[] { 2, 3 }); - zos1.putNextEntry(new ZipEntry("not_overridden_by_2")); - zos1.write(new byte[] { 3, 4 }); - } - - File zip2 = mTemporaryFolder.newFile("t2.zip"); - try (ZipOutputStream zos2 = new ZipOutputStream(new FileOutputStream(zip2))) { - zos2.putNextEntry(new ZipEntry("only_in_2")); - zos2.write(new byte[] { 4, 5 }); - zos2.putNextEntry(new ZipEntry("overridden_by_2")); - zos2.write(new byte[] { 5, 6 }); - zos2.putNextEntry(new ZipEntry("not_overridden_by_2")); - zos2.write(new byte[] { 6, 7 }); - zos2.putNextEntry(new ZipEntry("ignored_in_2")); - zos2.write(new byte[] { 7, 8 }); - } - - try ( - ZFile zf1 = new ZFile(zip1); - ZFile zf2 = new ZFile(zip2)) { - zf1.mergeFrom(zf2, (input) -> input.matches("not.*") || input.matches(".*gnored.*")); - - StoredEntry only_in_1 = zf1.get("only_in_1"); - assertNotNull(only_in_1); - assertArrayEquals(new byte[]{1, 2}, only_in_1.read()); - - StoredEntry overridden_by_2 = zf1.get("overridden_by_2"); - assertNotNull(overridden_by_2); - assertArrayEquals(new byte[]{5, 6}, overridden_by_2.read()); - - StoredEntry not_overridden_by_2 = zf1.get("not_overridden_by_2"); - assertNotNull(not_overridden_by_2); - assertArrayEquals(new byte[]{3, 4}, not_overridden_by_2.read()); - - StoredEntry only_in_2 = zf1.get("only_in_2"); - assertNotNull(only_in_2); - assertArrayEquals(new byte[]{4, 5}, only_in_2.read()); - - StoredEntry ignored_in_2 = zf1.get("ignored_in_2"); - assertNull(ignored_in_2); - } - } - - @Test - public void addingFileDoesNotAddDirectoriesAutomatically() throws Exception { - File zip = new File(mTemporaryFolder.getRoot(), "z.zip"); - try (ZFile zf = new ZFile(zip)) { - zf.add("a/b/c", new ByteArrayInputStream(new byte[]{1, 2, 3})); - zf.update(); - assertEquals(1, zf.entries().size()); - - StoredEntry c = zf.get("a/b/c"); - assertNotNull(c); - assertEquals(3, c.read().length); - } - } - - @Test - public void zipFileWithEocdSignatureInComment() throws Exception { - File zip = mTemporaryFolder.newFile("f.zip"); - try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zip))) { - zos.putNextEntry(new ZipEntry("a")); - zos.write(new byte[] { 1, 2, 3 }); - zos.setComment("Random comment with XXXX weird characters. There must be enough " - + "characters to survive skipping back the EOCD size."); - } - - byte zipBytes[] = Files.toByteArray(zip); - boolean didX4 = false; - for (int i = 0; i < zipBytes.length - 3; i++) { - boolean x4 = true; - for (int j = 0; j < 4; j++) { - if (zipBytes[i + j] != 'X') { - x4 = false; - break; - } - } - - if (x4) { - zipBytes[i] = (byte) 0x50; - zipBytes[i + 1] = (byte) 0x4b; - zipBytes[i + 2] = (byte) 0x05; - zipBytes[i + 3] = (byte) 0x06; - didX4 = true; - break; - } - } - - assertTrue(didX4); - - Files.write(zipBytes, zip); - - try (ZFile zf = new ZFile(zip)) { - assertEquals(1, zf.entries().size()); - StoredEntry a = zf.get("a"); - assertNotNull(a); - assertArrayEquals(new byte[]{1, 2, 3}, a.read()); - } - } - - @Test - public void addFileRecursively() throws Exception { - File tdir = mTemporaryFolder.newFolder(); - File tfile = new File(tdir, "blah-blah"); - Files.write("blah", tfile, Charsets.US_ASCII); - - File zip = new File(tdir, "f.zip"); - try (ZFile zf = new ZFile(zip)) { - zf.addAllRecursively(tfile); - - StoredEntry blahEntry = zf.get("blah-blah"); - assertNotNull(blahEntry); - String contents = new String(blahEntry.read(), Charsets.US_ASCII); - assertEquals("blah", contents); - } - } - - @Test - public void addDirectoryRecursively() throws Exception { - File tdir = mTemporaryFolder.newFolder(); - - String boom = Strings.repeat("BOOM!", 100); - String kaboom = Strings.repeat("KABOOM!", 100); - - Files.write(boom, new File(tdir, "danger"), Charsets.US_ASCII); - Files.write(kaboom, new File(tdir, "do not touch"), Charsets.US_ASCII); - File safeDir = new File(tdir, "safe"); - assertTrue(safeDir.mkdir()); - - String iLoveChocolate = Strings.repeat("I love chocolate! ", 200); - String iLoveOrange = Strings.repeat("I love orange! ", 50); - String loremIpsum = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean vitae " - + "turpis quis justo scelerisque vulputate in et magna. Suspendisse eleifend " - + "ultricies nisi, placerat consequat risus accumsan et. Pellentesque habitant " - + "morbi tristique senectus et netus et malesuada fames ac turpis egestas. " - + "Integer vitae leo purus. Nulla facilisi. Duis ligula libero, lacinia a " - + "malesuada a, viverra tempor sapien. Donec eget consequat sapien, ultrices" - + "interdum diam. Maecenas ipsum erat, suscipit at iaculis a, mollis nec risus. " - + "Quisque tristique ac velit sed auctor. Nulla lacus diam, tristique id sem non, " - + "pellentesque commodo mauris."; - - Files.write(iLoveChocolate, new File(safeDir, "eat.sweet"), Charsets.US_ASCII); - Files.write(iLoveOrange, new File(safeDir, "eat.fruit"), Charsets.US_ASCII); - Files.write(loremIpsum, new File(safeDir, "bedtime.reading.txt"), Charsets.US_ASCII); - - File zip = new File(tdir, "f.zip"); - try (ZFile zf = new ZFile(zip)) { - zf.addAllRecursively(tdir, (f) -> !f.getName().startsWith("eat.")); - - assertEquals(6, zf.entries().size()); - - StoredEntry boomEntry = zf.get("danger"); - assertNotNull(boomEntry); - assertEquals(CompressionMethod.DEFLATE, - boomEntry.getCentralDirectoryHeader().getCompressionInfoWithWait().getMethod()); - assertEquals(boom, new String(boomEntry.read(), Charsets.US_ASCII)); - - StoredEntry kaboomEntry = zf.get("do not touch"); - assertNotNull(kaboomEntry); - assertEquals(CompressionMethod.DEFLATE, - kaboomEntry - .getCentralDirectoryHeader() - .getCompressionInfoWithWait() - .getMethod()); - assertEquals(kaboom, new String(kaboomEntry.read(), Charsets.US_ASCII)); - - StoredEntry safeEntry = zf.get("safe/"); - assertNotNull(safeEntry); - assertEquals(0, safeEntry.read().length); - - StoredEntry choc = zf.get("safe/eat.sweet"); - assertNotNull(choc); - assertEquals(CompressionMethod.STORE, - choc.getCentralDirectoryHeader().getCompressionInfoWithWait().getMethod()); - assertEquals(iLoveChocolate, new String(choc.read(), Charsets.US_ASCII)); - - StoredEntry orangeEntry = zf.get("safe/eat.fruit"); - assertNotNull(orangeEntry); - assertEquals(CompressionMethod.STORE, - orangeEntry - .getCentralDirectoryHeader() - .getCompressionInfoWithWait() - .getMethod()); - assertEquals(iLoveOrange, new String(orangeEntry.read(), Charsets.US_ASCII)); - - StoredEntry loremEntry = zf.get("safe/bedtime.reading.txt"); - assertNotNull(loremEntry); - assertEquals(CompressionMethod.DEFLATE, - loremEntry - .getCentralDirectoryHeader() - .getCompressionInfoWithWait() - .getMethod()); - assertEquals(loremIpsum, new String(loremEntry.read(), Charsets.US_ASCII)); - } - } - - @Test - public void extraDirectoryOffsetEmptyFile() throws Exception { - File zipNoOffsetFile = new File(mTemporaryFolder.getRoot(), "a.zip"); - File zipWithOffsetFile = new File(mTemporaryFolder.getRoot(), "b.zip"); - - int offset = 31; - - long zipNoOffsetSize; - try ( - ZFile zipNoOffset = new ZFile(zipNoOffsetFile); - ZFile zipWithOffset = new ZFile(zipWithOffsetFile)) { - zipWithOffset.setExtraDirectoryOffset(offset); - - zipNoOffset.close(); - zipWithOffset.close(); - - zipNoOffsetSize = zipNoOffsetFile.length(); - long zipWithOffsetSize = zipWithOffsetFile.length(); - - assertEquals(zipNoOffsetSize + offset, zipWithOffsetSize); - - /* - * EOCD with no comment has 22 bytes. - */ - assertEquals(0, zipNoOffset.getCentralDirectoryOffset()); - assertEquals(0, zipNoOffset.getCentralDirectorySize()); - assertEquals(0, zipNoOffset.getEocdOffset()); - assertEquals(ZFileTestConstants.EOCD_SIZE, zipNoOffset.getEocdSize()); - assertEquals(offset, zipWithOffset.getCentralDirectoryOffset()); - assertEquals(0, zipWithOffset.getCentralDirectorySize()); - assertEquals(offset, zipWithOffset.getEocdOffset()); - assertEquals(ZFileTestConstants.EOCD_SIZE, zipWithOffset.getEocdSize()); - } - - /* - * The EOCDs should not differ up until the end of the Central Directory size and should - * not differ after the offset - */ - int p1Start = 0; - int p1Size = Eocd.F_CD_SIZE.endOffset(); - int p2Start = Eocd.F_CD_OFFSET.endOffset(); - int p2Size = (int) zipNoOffsetSize - p2Start; - - byte[] noOffsetData1 = readSegment(zipNoOffsetFile, p1Start, p1Size); - byte[] noOffsetData2 = readSegment(zipNoOffsetFile, p2Start, p2Size); - byte[] withOffsetData1 = readSegment(zipWithOffsetFile, offset, p1Size); - byte[] withOffsetData2 = readSegment(zipWithOffsetFile, offset + p2Start, p2Size); - - assertArrayEquals(noOffsetData1, withOffsetData1); - assertArrayEquals(noOffsetData2, withOffsetData2); - - try (ZFile readWithOffset = new ZFile(zipWithOffsetFile)) { - assertEquals(0, readWithOffset.entries().size()); - } - } - - @Test - public void extraDirectoryOffsetNonEmptyFile() throws Exception { - File zipNoOffsetFile = new File(mTemporaryFolder.getRoot(), "a.zip"); - File zipWithOffsetFile = new File(mTemporaryFolder.getRoot(), "b.zip"); - - int cdSize; - - // The byte arrays below are larger when compressed, so we end up storing them uncompressed, - // which would normally cause them to be 4-aligned. Disable that, to make calculations - // easier. - ZFileOptions options = new ZFileOptions(); - options.setAlignmentRule(AlignmentRules.constant(AlignmentRule.NO_ALIGNMENT)); - - try (ZFile zipNoOffset = new ZFile(zipNoOffsetFile, options); - ZFile zipWithOffset = new ZFile(zipWithOffsetFile, options)) { - zipWithOffset.setExtraDirectoryOffset(37); - - zipNoOffset.add("x", new ByteArrayInputStream(new byte[]{1, 2})); - zipWithOffset.add("x", new ByteArrayInputStream(new byte[]{1, 2})); - - zipNoOffset.close(); - zipWithOffset.close(); - - long zipNoOffsetSize = zipNoOffsetFile.length(); - long zipWithOffsetSize = zipWithOffsetFile.length(); - - assertEquals(zipNoOffsetSize + 37, zipWithOffsetSize); - - /* - * Local file header has 30 bytes + name. - * Central directory entry has 46 bytes + name - * EOCD with no comment has 22 bytes. - */ - assertEquals(ZFileTestConstants.LOCAL_HEADER_SIZE + 1 + 2, - zipNoOffset.getCentralDirectoryOffset()); - cdSize = (int) zipNoOffset.getCentralDirectorySize(); - assertEquals(ZFileTestConstants.CENTRAL_DIRECTORY_ENTRY_SIZE + 1, cdSize); - assertEquals(ZFileTestConstants.LOCAL_HEADER_SIZE + 1 + 2 + cdSize, - zipNoOffset.getEocdOffset()); - assertEquals(ZFileTestConstants.EOCD_SIZE, zipNoOffset.getEocdSize()); - assertEquals(ZFileTestConstants.LOCAL_HEADER_SIZE + 1 + 2 + 37, - zipWithOffset.getCentralDirectoryOffset()); - assertEquals(cdSize, zipWithOffset.getCentralDirectorySize()); - assertEquals(ZFileTestConstants.LOCAL_HEADER_SIZE + 1 + 2 + 37 + cdSize, - zipWithOffset.getEocdOffset()); - assertEquals(ZFileTestConstants.EOCD_SIZE, zipWithOffset.getEocdSize()); - } - - /* - * The files should be equal: until the end of the first entry, from the beginning of the - * central directory until the offset field in the EOCD and after the offset field. - */ - int p1Start = 0; - int p1Size = ZFileTestConstants.LOCAL_HEADER_SIZE + 1 + 2; - int p2Start = ZFileTestConstants.LOCAL_HEADER_SIZE + 1 + 2; - int p2Size = cdSize + Eocd.F_CD_SIZE.endOffset(); - int p3Start = p2Start + cdSize + Eocd.F_CD_OFFSET.endOffset(); - int p3Size = ZFileTestConstants.EOCD_SIZE - Eocd.F_CD_OFFSET.endOffset(); - - byte[] noOffsetData1 = readSegment(zipNoOffsetFile, p1Start, p1Size); - byte[] noOffsetData2 = readSegment(zipNoOffsetFile, p2Start, p2Size); - byte[] noOffsetData3 = readSegment(zipNoOffsetFile, p3Start, p3Size); - byte[] withOffsetData1 = readSegment(zipWithOffsetFile, p1Start, p1Size); - byte[] withOffsetData2 = readSegment(zipWithOffsetFile, 37 + p2Start, p2Size); - byte[] withOffsetData3 = readSegment(zipWithOffsetFile, 37 + p3Start, p3Size); - - assertArrayEquals(noOffsetData1, withOffsetData1); - assertArrayEquals(noOffsetData2, withOffsetData2); - assertArrayEquals(noOffsetData3, withOffsetData3); - - try (ZFile readWithOffset = new ZFile(zipWithOffsetFile)) { - assertEquals(1, readWithOffset.entries().size()); - } - } - - @Test - public void changeExtraDirectoryOffset() throws Exception { - File zipFile = new File(mTemporaryFolder.getRoot(), "a.zip"); - - try (ZFile zip = new ZFile(zipFile)) { - zip.add("x", new ByteArrayInputStream(new byte[]{1, 2})); - zip.close(); - - long noOffsetSize = zipFile.length(); - - zip.setExtraDirectoryOffset(177); - zip.close(); - - long withOffsetSize = zipFile.length(); - - assertEquals(noOffsetSize + 177, withOffsetSize); - } - } - - @Test - public void computeOffsetWhenReadingEmptyFile() throws Exception { - File zipFile = new File(mTemporaryFolder.getRoot(), "a.zip"); - - try (ZFile zip = new ZFile(zipFile)) { - zip.setExtraDirectoryOffset(18); - } - - try (ZFile zip = new ZFile(zipFile)) { - assertEquals(18, zip.getExtraDirectoryOffset()); - } - } - - @Test - public void computeOffsetWhenReadingNonEmptyFile() throws Exception { - File zipFile = new File(mTemporaryFolder.getRoot(), "a.zip"); - - try (ZFile zip = new ZFile(zipFile)) { - zip.setExtraDirectoryOffset(287); - zip.add("x", new ByteArrayInputStream(new byte[]{1, 2})); - } - - try (ZFile zip = new ZFile(zipFile)) { - assertEquals(287, zip.getExtraDirectoryOffset()); - } - } - - @Test - public void obtainingCDAndEocdWhenEntriesWrittenOnEmptyZip() throws Exception { - File zipFile = new File(mTemporaryFolder.getRoot(), "a.zip"); - - byte[][] cd = new byte[1][]; - byte[][] eocd = new byte[1][]; - - try (ZFile zip = new ZFile(zipFile)) { - zip.addZFileExtension(new ZFileExtension() { - @Override - public void entriesWritten() throws IOException { - cd[0] = zip.getCentralDirectoryBytes(); - eocd[0] = zip.getEocdBytes(); - } - }); - } - - assertNotNull(cd[0]); - assertEquals(0, cd[0].length); - assertNotNull(eocd[0]); - assertEquals(22, eocd[0].length); - } - - @Test - public void obtainingCDAndEocdWhenEntriesWrittenOnNonEmptyZip() throws Exception { - File zipFile = new File(mTemporaryFolder.getRoot(), "a.zip"); - - byte[][] cd = new byte[1][]; - byte[][] eocd = new byte[1][]; - - try (ZFile zip = new ZFile(zipFile)) { - zip.add("foo", new ByteArrayInputStream(new byte[0])); - zip.addZFileExtension(new ZFileExtension() { - @Override - public void entriesWritten() throws IOException { - cd[0] = zip.getCentralDirectoryBytes(); - eocd[0] = zip.getEocdBytes(); - } - }); - } - - /* - * Central directory entry has 46 bytes + name - * EOCD with no comment has 22 bytes. - */ - assertNotNull(cd[0]); - assertEquals(46 + 3, cd[0].length); - assertNotNull(eocd[0]); - assertEquals(22, eocd[0].length); - } - - @Test - public void java7JarSupported() throws Exception { - File jar = ZipTestUtils.cloneRsrc("j7.jar", mTemporaryFolder); - - try (ZFile j = new ZFile(jar)) { - assertEquals(8, j.entries().size()); - } - } - - @Test - public void java8JarSupported() throws Exception { - File jar = ZipTestUtils.cloneRsrc("j8.jar", mTemporaryFolder); - - try (ZFile j = new ZFile(jar)) { - assertEquals(8, j.entries().size()); - } - } - - @Test - public void utf8NamesSupportedOnReading() throws Exception { - File zip = ZipTestUtils.cloneRsrc("zip-with-utf8-filename.zip", mTemporaryFolder); - - try (ZFile f = new ZFile(zip)) { - assertEquals(1, f.entries().size()); - - StoredEntry entry = f.entries().iterator().next(); - String filetMignonKorean = "\uc548\uc2eC \uc694\ub9ac"; - String isGoodJapanese = "\u3068\u3066\u3082\u826f\u3044"; - - assertEquals( - filetMignonKorean + " " + isGoodJapanese, - entry.getCentralDirectoryHeader().getName()); - assertArrayEquals( - "Stuff about food is good.\n".getBytes(Charsets.US_ASCII), entry.read()); - } - } - - @Test - public void utf8NamesSupportedOnReadingWithoutUtf8Flag() throws Exception { - File zip = ZipTestUtils.cloneRsrc("zip-with-utf8-filename.zip", mTemporaryFolder); - - // Reset bytes 7 and 122 that have the flag in the local header and central directory. - byte[] data = Files.toByteArray(zip); - data[7] = 0; - data[122] = 0; - Files.write(data, zip); - - try (ZFile f = new ZFile(zip)) { - assertEquals(1, f.entries().size()); - - StoredEntry entry = f.entries().iterator().next(); - String filetMignonKorean = "\uc548\uc2eC \uc694\ub9ac"; - String isGoodJapanese = "\u3068\u3066\u3082\u826f\u3044"; - - assertEquals( - filetMignonKorean + " " + isGoodJapanese, - entry.getCentralDirectoryHeader().getName()); - assertArrayEquals( - "Stuff about food is good.\n".getBytes(Charsets.US_ASCII), - entry.read()); - } - } - - @Test - public void utf8NamesSupportedOnWriting() throws Exception { - File zipFile = new File(mTemporaryFolder.getRoot(), "a.zip"); - String lettuceIsHealthyArmenian = "\u0533\u0561\u0566\u0561\u0580\u0020\u0561\u057C" - + "\u0578\u0572\u057B"; - - try (ZFile zip = new ZFile(zipFile)) { - zip.add(lettuceIsHealthyArmenian, new ByteArrayInputStream(new byte[]{0})); - } - - try (ZFile zip2 = new ZFile(zipFile)) { - assertEquals(1, zip2.entries().size()); - StoredEntry entry = zip2.entries().iterator().next(); - assertEquals(lettuceIsHealthyArmenian, entry.getCentralDirectoryHeader().getName()); - } - } - - @Test - public void zipMemoryUsageIsZeroAfterClose() throws Exception { - File zipFile = new File(mTemporaryFolder.getRoot(), "a.zip"); - - ZFileOptions options = new ZFileOptions(); - long used; - try (ZFile zip = new ZFile(zipFile, options)) { - - assertEquals(0, options.getTracker().getBytesUsed()); - assertEquals(0, options.getTracker().getMaxBytesUsed()); - - zip.add("Blah", new ByteArrayInputStream(new byte[500])); - used = options.getTracker().getBytesUsed(); - assertTrue(used > 500); - assertEquals(used, options.getTracker().getMaxBytesUsed()); - } - - assertEquals(0, options.getTracker().getBytesUsed()); - assertEquals(used, options.getTracker().getMaxBytesUsed()); - } - - @Test - public void unusedZipAreasAreClearedOnWrite() throws Exception { - File zipFile = new File(mTemporaryFolder.getRoot(), "a.zip"); - ZFileOptions options = new ZFileOptions(); - options.setAlignmentRule(AlignmentRules.constantForSuffix(".txt", 1000)); - try (ZFile zf = new ZFile(zipFile, options)) { - zf.add("test1.txt", new ByteArrayInputStream(new byte[]{1}), false); - } - - /* - * Write dummy data in some unused portion of the file. - */ - try (RandomAccessFile raf = new RandomAccessFile(zipFile, "rw")) { - - raf.seek(500); - byte[] dummyData = "Dummy".getBytes(Charsets.US_ASCII); - raf.write(dummyData); - } - - try (ZFile zf = new ZFile(zipFile)) { - zf.touch(); - } - - try (RandomAccessFile raf = new RandomAccessFile(zipFile, "r")) { - - /* - * test1.txt won't take more than 200 bytes. Additionally, the header for - */ - byte[] data = new byte[900]; - RandomAccessFileUtils.fullyRead(raf, data); - - byte[] zeroData = new byte[data.length]; - assertArrayEquals(zeroData, data); - } - } - - @Test - public void deferredCompression() throws Exception { - File zipFile = new File(mTemporaryFolder.getRoot(), "a.zip"); - - ExecutorService executor = Executors.newSingleThreadExecutor(); - - ZFileOptions options = new ZFileOptions(); - boolean[] done = new boolean[1]; - options.setCompressor(new DeflateExecutionCompressor(executor, options.getTracker(), - Deflater.BEST_COMPRESSION) { - @Nonnull - @Override - protected CompressionResult immediateCompress(@Nonnull CloseableByteSource source) - throws Exception { - Thread.sleep(500); - CompressionResult cr = super.immediateCompress(source); - done[0] = true; - return cr; - } - }); - - try (ZFile zip = new ZFile(zipFile, options)) { - byte sequences = 100; - int seqCount = 1000; - byte[] compressableData = new byte[sequences * seqCount]; - for (byte i = 0; i < sequences; i++) { - for (int j = 0; j < seqCount; j++) { - compressableData[i * seqCount + j] = i; - } - } - - zip.add("compressedFile", new ByteArrayInputStream(compressableData)); - assertFalse(done[0]); - - /* - * Even before closing, eventually all the stream will be read. - */ - long tooLong = System.currentTimeMillis() + 10000; - while (!done[0] && System.currentTimeMillis() < tooLong) { - Thread.sleep(10); - } - - assertTrue(done[0]); - } - - executor.shutdownNow(); - } - - @Test - public void zipFileWithEocdMarkerInComment() throws Exception { - File zipFile = new File(mTemporaryFolder.getRoot(), "x"); - - try (Closer closer = Closer.create()) { - ZipOutputStream zos = closer.register( - new ZipOutputStream(new FileOutputStream(zipFile))); - zos.setComment("\u0065\u4b50"); - zos.putNextEntry(new ZipEntry("foo")); - zos.write(new byte[] { 1, 2, 3, 4 }); - zos.close(); - - ZFile zf = closer.register(new ZFile(zipFile)); - StoredEntry entry = zf.get("foo"); - assertNotNull(entry); - assertEquals(4, entry.getCentralDirectoryHeader().getUncompressedSize()); - } - } - - @Test - public void zipFileWithEocdMarkerInFileName() throws Exception { - File zipFile = new File(mTemporaryFolder.getRoot(), "x"); - - String fname = "tricky-\u0050\u004b\u0005\u0006"; - byte[] bytes = new byte[] { 1, 2, 3, 4 }; - - try (Closer closer = Closer.create()) { - ZipOutputStream zos = closer.register( - new ZipOutputStream(new FileOutputStream(zipFile))); - zos.putNextEntry(new ZipEntry(fname)); - zos.write(bytes); - zos.close(); - - ZFile zf = closer.register(new ZFile(zipFile)); - StoredEntry entry = zf.get(fname); - assertNotNull(entry); - assertEquals(4, entry.getCentralDirectoryHeader().getUncompressedSize()); - } - } - - @Test - public void zipFileWithEocdMarkerInFileContents() throws Exception { - File zipFile = new File(mTemporaryFolder.getRoot(), "x"); - - byte[] bytes = new byte[] { 0x50, 0x4b, 0x05, 0x06 }; - - try (Closer closer = Closer.create()) { - ZipOutputStream zos = closer.register( - new ZipOutputStream(new FileOutputStream(zipFile))); - ZipEntry zipEntry = new ZipEntry("file"); - zipEntry.setMethod(ZipEntry.STORED); - zipEntry.setCompressedSize(4); - zipEntry.setSize(4); - zipEntry.setCrc(Hashing.crc32().hashBytes(bytes).padToLong()); - zos.putNextEntry(zipEntry); - zos.write(bytes); - zos.close(); - - ZFile zf = closer.register(new ZFile(zipFile)); - StoredEntry entry = zf.get("file"); - assertNotNull(entry); - assertEquals(4, entry.getCentralDirectoryHeader().getUncompressedSize()); - } - } - - @Test - public void replaceVeryLargeFileWithBiggerInMiddleOfZip() throws Exception { - File zipFile = new File(mTemporaryFolder.getRoot(), "x"); - - long small1Offset; - long small2Offset; - ZFileOptions coverOptions = new ZFileOptions(); - coverOptions.setCoverEmptySpaceUsingExtraField(true); - try (ZFile zf = new ZFile(zipFile, coverOptions)) { - zf.add("small1", new ByteArrayInputStream(new byte[] { 0, 1 })); - } - - try (ZFile zf = new ZFile(zipFile, coverOptions)) { - zf.add("verybig", new ByteArrayInputStream(new byte[100_000]), false); - } - - try (ZFile zf = new ZFile(zipFile, coverOptions)) { - zf.add("small2", new ByteArrayInputStream(new byte[] { 0, 1 })); - } - - try (ZFile zf = new ZFile(zipFile, coverOptions)) { - StoredEntry se = zf.get("small1"); - assertNotNull(se); - small1Offset = se.getCentralDirectoryHeader().getOffset(); - - se = zf.get("small2"); - assertNotNull(se); - small2Offset = se.getCentralDirectoryHeader().getOffset(); - - se = zf.get("verybig"); - assertNotNull(se); - se.delete(); - - zf.add("evenbigger", new ByteArrayInputStream(new byte[110_000]), false); - } - - try (ZFile zf = new ZFile(zipFile, coverOptions)) { - StoredEntry se = zf.get("small1"); - assertNotNull(se); - assertEquals(se.getCentralDirectoryHeader().getOffset(), small1Offset); - - se = zf.get("small2"); - assertNotNull(se); - assertNotEquals(se.getCentralDirectoryHeader().getOffset(), small2Offset); - } - } - - @Test - public void regressionRepackingDoesNotFail() throws Exception { - File zipFile = new File(mTemporaryFolder.getRoot(), "x"); - - ZFileOptions coverOptions = new ZFileOptions(); - coverOptions.setCoverEmptySpaceUsingExtraField(true); - try (ZFile zf = new ZFile(zipFile, coverOptions)) { - zf.add("small_1", new ByteArrayInputStream(new byte[] { 0, 1 })); - zf.add("very_big", new ByteArrayInputStream(new byte[100_000]), false); - zf.add("small_2", new ByteArrayInputStream(new byte[] { 0, 1 })); - zf.add("big", new ByteArrayInputStream(new byte[10_000]), false); - zf.add("small_3", new ByteArrayInputStream(new byte[] { 0, 1 })); - } - - /* - * Regression we're covering is that small_2 cannot be extended to cover up for the space - * taken by very_big and needs to be repositioned. However, the algorithm to reposition - * will put it in the best-fitting block, which is the one in "big", failing to actually - * move it backwards in the file. - */ - try (ZFile zf = new ZFile(zipFile, coverOptions)) { - StoredEntry se = zf.get("big"); - assertNotNull(se); - se.delete(); - - se = zf.get("very_big"); - assertNotNull(se); - se.delete(); - } - } - - @Test - public void cannotAddMoreThan0x7fffExtraField() throws Exception { - File zipFile = new File(mTemporaryFolder.getRoot(), "a.zip"); - - ZFileOptions zfo = new ZFileOptions(); - zfo.setCoverEmptySpaceUsingExtraField(true); - - /* - * Create a zip file with: - * - * [small file][large file with exactly 0x8000 bytes][small file 2] - */ - long smallFile1Offset; - long smallFile2Offset; - long largeFileOffset; - String largeFileName = "Large file"; - try (ZFile zf = new ZFile(zipFile, zfo)) { - zf.add("Small file", new ByteArrayInputStream(new byte[] { 0, 1 })); - - int largeFileTotalSize = 0x8000; - int largeFileContentsSize = - largeFileTotalSize - - ZFileTestConstants.LOCAL_HEADER_SIZE - - largeFileName.length(); - - zf.add(largeFileName, new ByteArrayInputStream(new byte[largeFileContentsSize]), false); - zf.add("Small file 2", new ByteArrayInputStream(new byte[] { 0, 1 })); - - zf.update(); - - StoredEntry sfEntry = zf.get("Small file"); - assertNotNull(sfEntry); - smallFile1Offset = sfEntry.getCentralDirectoryHeader().getOffset(); - assertEquals(0, smallFile1Offset); - - StoredEntry lfEntry = zf.get(largeFileName); - assertNotNull(lfEntry); - largeFileOffset = lfEntry.getCentralDirectoryHeader().getOffset(); - - StoredEntry sf2Entry = zf.get("Small file 2"); - assertNotNull(sf2Entry); - smallFile2Offset = sf2Entry.getCentralDirectoryHeader().getOffset(); - - assertEquals(largeFileTotalSize, smallFile2Offset - largeFileOffset); - } - - /* - * Remove the large file from the zip file and check that small file 2 has been moved, but - * no extra field has been added. - */ - try (ZFile zf = new ZFile(zipFile, zfo)) { - StoredEntry lfEntry = zf.get(largeFileName); - assertNotNull(lfEntry); - lfEntry.delete(); - - zf.update(); - - StoredEntry sfEntry = zf.get("Small file"); - assertNotNull(sfEntry); - smallFile1Offset = sfEntry.getCentralDirectoryHeader().getOffset(); - assertEquals(0, smallFile1Offset); - - StoredEntry sf2Entry = zf.get("Small file 2"); - assertNotNull(sf2Entry); - long newSmallFile2Offset = sf2Entry.getCentralDirectoryHeader().getOffset(); - assertEquals(largeFileOffset, newSmallFile2Offset); - - assertEquals(0, sf2Entry.getLocalExtra().size()); - } - } - - @Test - public void canAddMoreThan0x7fffExtraField() throws Exception { - File zipFile = new File(mTemporaryFolder.getRoot(), "a.zip"); - - ZFileOptions zfo = new ZFileOptions(); - zfo.setCoverEmptySpaceUsingExtraField(true); - - /* - * Create a zip file with: - * - * [small file][large file with exactly 0x7fff bytes][small file 2] - */ - long smallFile1Offset; - long smallFile2Offset; - long largeFileOffset; - String largeFileName = "Large file"; - int largeFileTotalSize = 0x7fff; - try (ZFile zf = new ZFile(zipFile, zfo)) { - zf.add("Small file", new ByteArrayInputStream(new byte[] { 0, 1 })); - - int largeFileContentsSize = - largeFileTotalSize - - ZFileTestConstants.LOCAL_HEADER_SIZE - - largeFileName.length(); - - zf.add(largeFileName, new ByteArrayInputStream(new byte[largeFileContentsSize]), false); - zf.add("Small file 2", new ByteArrayInputStream(new byte[] { 0, 1 })); - - zf.update(); - - StoredEntry sfEntry = zf.get("Small file"); - assertNotNull(sfEntry); - smallFile1Offset = sfEntry.getCentralDirectoryHeader().getOffset(); - assertEquals(0, smallFile1Offset); - - StoredEntry lfEntry = zf.get(largeFileName); - assertNotNull(lfEntry); - largeFileOffset = lfEntry.getCentralDirectoryHeader().getOffset(); - - StoredEntry sf2Entry = zf.get("Small file 2"); - assertNotNull(sf2Entry); - smallFile2Offset = sf2Entry.getCentralDirectoryHeader().getOffset(); - - assertEquals(largeFileTotalSize, smallFile2Offset - largeFileOffset); - } - - /* - * Remove the large file from the zip file and check that small file 2 has been moved back - * but with 0x7fff extra space added. - */ - try (ZFile zf = new ZFile(zipFile, zfo)) { - StoredEntry lfEntry = zf.get(largeFileName); - assertNotNull(lfEntry); - lfEntry.delete(); - - zf.update(); - - StoredEntry sfEntry = zf.get("Small file"); - assertNotNull(sfEntry); - smallFile1Offset = sfEntry.getCentralDirectoryHeader().getOffset(); - assertEquals(0, smallFile1Offset); - - StoredEntry sf2Entry = zf.get("Small file 2"); - assertNotNull(sf2Entry); - long newSmallFile2Offset = sf2Entry.getCentralDirectoryHeader().getOffset(); - - assertEquals(largeFileOffset, newSmallFile2Offset); - assertEquals(largeFileTotalSize, sf2Entry.getLocalExtra().size()); - } - } - - @Test - public void detectIncorrectCRC32InLocalHeader() throws Exception { - File zipFile = new File(mTemporaryFolder.getRoot(), "a.zip"); - - /* - * Zip files created by ZFile never have data descriptors so we need to create one using - * java's zip. - */ - try ( - FileOutputStream fos = new FileOutputStream(zipFile); - ZipOutputStream zos = new ZipOutputStream(fos)) { - ZipEntry ze = new ZipEntry("foo"); - zos.putNextEntry(ze); - byte[] randomBytes = new byte[512]; - new Random().nextBytes(randomBytes); - zos.write(randomBytes); - } - - /* - * Open the zip file and compute where the local header CRC32 is. - */ - long crcOffset; - try (ZFile zf = new ZFile(zipFile)) { - StoredEntry se = zf.get("foo"); - assertNotNull(se); - long cdOffset = zf.getCentralDirectoryOffset(); - - /* - * Twelve bytes from the CD offset, we have the start of the CRC32 of the zip entry. - */ - crcOffset = cdOffset - 12; - } - - /* - * Corrupt the CRC32. - */ - byte[] crc = readSegment(zipFile, crcOffset, 4); - crc[0]++; - try (RandomAccessFile raf = new RandomAccessFile(zipFile, "rw")) { - raf.seek(crcOffset); - raf.write(crc); - } - - /* - * Now open the zip file and it should write a message in the log. - */ - ZFileOptions options = new ZFileOptions(); - options.setVerifyLogFactory(VerifyLogs::unlimited); - try (ZFile zf = new ZFile(zipFile, options)) { - VerifyLog vl = zf.getVerifyLog(); - assertTrue(vl.getLogs().isEmpty()); - StoredEntry fooEntry = zf.get("foo"); - vl = fooEntry.getVerifyLog(); - assertEquals(1, vl.getLogs().size()); - assertTrue(vl.getLogs().get(0).contains("CRC32")); - } - } - - @Test - public void detectIncorrectVersionToExtractInCentralDirectory() throws Exception { - File zipFile = new File(mTemporaryFolder.getRoot(), "a.zip"); - - /* - * Create a valid zip file. - */ - try (ZFile zf = new ZFile(zipFile)) { - zf.add("foo", new ByteArrayInputStream(new byte[0])); - } - - /* - * Change the "version to extract" in the central directory to 0x7777. - */ - int versionToExtractOffset = - ZFileTestConstants.LOCAL_HEADER_SIZE - + 3 - + CentralDirectory.F_VERSION_EXTRACT.offset(); - byte[] allZipBytes = Files.toByteArray(zipFile); - allZipBytes[versionToExtractOffset] = 0x77; - allZipBytes[versionToExtractOffset + 1] = 0x77; - Files.write(allZipBytes, zipFile); - - /* - * Opening the file and it should write a message in the log. The entry has the right - * version to extract (20), but it issues a warning because it is not equal to the one - * in the central directory. - */ - ZFileOptions options = new ZFileOptions(); - options.setVerifyLogFactory(VerifyLogs::unlimited); - try (ZFile zf = new ZFile(zipFile, options)) { - VerifyLog vl = zf.getVerifyLog(); - assertEquals(1, vl.getLogs().size()); - assertTrue(vl.getLogs().get(0).toLowerCase(Locale.US).contains("version")); - assertTrue(vl.getLogs().get(0).toLowerCase(Locale.US).contains("extract")); - StoredEntry fooEntry = zf.get("foo"); - vl = fooEntry.getVerifyLog(); - assertEquals(1, vl.getLogs().size()); - assertTrue(vl.getLogs().get(0).toLowerCase(Locale.US).contains("version")); - assertTrue(vl.getLogs().get(0).toLowerCase(Locale.US).contains("extract")); - } - } - - @Test - public void detectIncorrectVersionToExtractInLocalHeader() throws Exception { - File zipFile = new File(mTemporaryFolder.getRoot(), "a.zip"); - - /* - * Create a valid zip file. - */ - try (ZFile zf = new ZFile(zipFile)) { - zf.add("foo", new ByteArrayInputStream(new byte[0])); - } - - /* - * Change the "version to extract" in the local header to 0x7777. - */ - int versionToExtractOffset = StoredEntry.F_VERSION_EXTRACT.offset(); - byte[] allZipBytes = Files.toByteArray(zipFile); - allZipBytes[versionToExtractOffset] = 0x77; - allZipBytes[versionToExtractOffset + 1] = 0x77; - Files.write(allZipBytes, zipFile); - - /* - * Opening the file should log an error message. - */ - ZFileOptions options = new ZFileOptions(); - options.setVerifyLogFactory(VerifyLogs::unlimited); - try (ZFile zf = new ZFile(zipFile, options)) { - VerifyLog vl = zf.getVerifyLog(); - assertTrue(vl.getLogs().isEmpty()); - StoredEntry fooEntry = zf.get("foo"); - vl = fooEntry.getVerifyLog(); - assertEquals(1, vl.getLogs().size()); - assertTrue(vl.getLogs().get(0).toLowerCase(Locale.US).contains("version")); - assertTrue(vl.getLogs().get(0).toLowerCase(Locale.US).contains("extract")); - } - } - - @Test - public void sortZipContentsWithDeferredCrc32() throws Exception { - /* - * Create a zip file with deferred CRC32 and files in non-alphabetical order. - * ZipOutputStream always creates deferred CRC32 entries. - */ - File zipFile = new File(mTemporaryFolder.getRoot(), "a.zip"); - try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zipFile))) { - zos.putNextEntry(new ZipEntry("b")); - zos.write(new byte[1000]); - zos.putNextEntry(new ZipEntry("a")); - zos.write(new byte[1000]); - } - - /* - * Now open the zip using a ZFile and sort the contents and check that the deferred CRC32 - * bits were reset. - */ - try (ZFile zf = new ZFile(zipFile)) { - StoredEntry a = zf.get("a"); - assertNotNull(a); - assertNotSame(DataDescriptorType.NO_DATA_DESCRIPTOR, a.getDataDescriptorType()); - StoredEntry b = zf.get("b"); - assertNotNull(b); - assertNotSame(DataDescriptorType.NO_DATA_DESCRIPTOR, b.getDataDescriptorType()); - assertTrue( - a.getCentralDirectoryHeader().getOffset() - > b.getCentralDirectoryHeader().getOffset()); - - zf.sortZipContents(); - zf.update(); - - a = zf.get("a"); - assertNotNull(a); - assertSame(DataDescriptorType.NO_DATA_DESCRIPTOR, a.getDataDescriptorType()); - b = zf.get("b"); - assertNotNull(b); - assertSame(DataDescriptorType.NO_DATA_DESCRIPTOR, b.getDataDescriptorType()); - - assertTrue( - a.getCentralDirectoryHeader().getOffset() - < b.getCentralDirectoryHeader().getOffset()); - } - - /* - * Open the file again and check there are no warnings. - */ - try (ZFile zf = new ZFile(zipFile)) { - VerifyLog vl = zf.getVerifyLog(); - assertEquals(0, vl.getLogs().size()); - - StoredEntry a = zf.get("a"); - assertNotNull(a); - vl = a.getVerifyLog(); - assertEquals(0, vl.getLogs().size()); - - StoredEntry b = zf.get("b"); - assertNotNull(b); - vl = b.getVerifyLog(); - assertEquals(0, vl.getLogs().size()); - } - } - - @Test - public void alignZipContentsWithDeferredCrc32() throws Exception { - /* - * Create an unaligned zip file with deferred CRC32 and files in non-alphabetical order. - * We need an uncompressed file to make realigning have any effect. - */ - File zipFile = new File(mTemporaryFolder.getRoot(), "a.zip"); - try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zipFile))) { - zos.putNextEntry(new ZipEntry("x")); - zos.write(new byte[1000]); - zos.putNextEntry(new ZipEntry("y")); - zos.write(new byte[1000]); - ZipEntry zEntry = new ZipEntry("z"); - zEntry.setSize(1000); - zEntry.setMethod(ZipEntry.STORED); - zEntry.setCrc(Hashing.crc32().hashBytes(new byte[1000]).asInt()); - zos.putNextEntry(zEntry); - zos.write(new byte[1000]); - } - - /* - * Now open the zip using a ZFile and realign the contents and check that the deferred CRC32 - * bits were reset. - */ - ZFileOptions options = new ZFileOptions(); - options.setAlignmentRule(AlignmentRules.constant(2000)); - try (ZFile zf = new ZFile(zipFile, options)) { - StoredEntry x = zf.get("x"); - assertNotNull(x); - assertNotSame(DataDescriptorType.NO_DATA_DESCRIPTOR, x.getDataDescriptorType()); - StoredEntry y = zf.get("y"); - assertNotNull(y); - assertNotSame(DataDescriptorType.NO_DATA_DESCRIPTOR, y.getDataDescriptorType()); - StoredEntry z = zf.get("z"); - assertNotNull(z); - assertSame(DataDescriptorType.NO_DATA_DESCRIPTOR, z.getDataDescriptorType()); - - zf.realign(); - zf.update(); - - x = zf.get("x"); - assertNotNull(x); - assertSame(DataDescriptorType.NO_DATA_DESCRIPTOR, x.getDataDescriptorType()); - y = zf.get("y"); - assertNotNull(y); - assertSame(DataDescriptorType.NO_DATA_DESCRIPTOR, y.getDataDescriptorType()); - z = zf.get("z"); - assertNotNull(z); - assertSame(DataDescriptorType.NO_DATA_DESCRIPTOR, z.getDataDescriptorType()); - } - } - - @Test - public void openingZFileDoesNotRemoveDataDescriptors() throws Exception { - /* - * Create a zip file with deferred CRC32. - */ - File zipFile = new File(mTemporaryFolder.getRoot(), "a.zip"); - try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zipFile))) { - zos.putNextEntry(new ZipEntry("a")); - zos.write(new byte[1000]); - } - - /* - * Open using ZFile and check that the deferred CRC32 is there. - */ - try (ZFile zf = new ZFile(zipFile)) { - StoredEntry se = zf.get("a"); - assertNotNull(se); - assertNotEquals(DataDescriptorType.NO_DATA_DESCRIPTOR, se.getDataDescriptorType()); - } - - /* - * Open using ZFile (again) and check that the deferred CRC32 is there. - */ - try (ZFile zf = new ZFile(zipFile)) { - StoredEntry se = zf.get("a"); - assertNotNull(se); - assertNotEquals(DataDescriptorType.NO_DATA_DESCRIPTOR, se.getDataDescriptorType()); - } - } - - @Test - public void zipCommentsAreSaved() throws Exception { - File zipFileWithComments = new File(mTemporaryFolder.getRoot(), "a.zip"); - try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zipFileWithComments))) { - zos.setComment("foo"); - } - - /* - * Open the zip file and check the comment is there. - */ - try (ZFile zf = new ZFile(zipFileWithComments)) { - byte[] comment = zf.getEocdComment(); - assertArrayEquals(new byte[] { 'f', 'o', 'o' }, comment); - - /* - * Modify the comment and write the file. - */ - zf.setEocdComment(new byte[] { 'b', 'a', 'r', 'r' }); - } - - /* - * Open the file and see that the comment is there (both with java and zfile). - */ - try (ZipFile zf2 = new ZipFile(zipFileWithComments)) { - assertEquals("barr", zf2.getComment()); - } - - try (ZFile zf3 = new ZFile(zipFileWithComments)) { - assertArrayEquals(new byte[] { 'b', 'a', 'r', 'r' }, zf3.getEocdComment()); - } - } - - @Test - public void eocdCommentsWithMoreThan64kNotAllowed() throws Exception { - File zipFileWithComments = new File(mTemporaryFolder.getRoot(), "a.zip"); - try (ZFile zf = new ZFile(zipFileWithComments)) { - try { - zf.setEocdComment(new byte[65536]); - fail(); - } catch (IllegalArgumentException e) { - // Expected. - } - - zf.setEocdComment(new byte[65535]); - } - } - - @Test - public void eocdCommentsWithTheEocdMarkerAreAllowed() throws Exception { - File zipFileWithComments = new File(mTemporaryFolder.getRoot(), "a.zip"); - byte[] data = new byte[100]; - data[50] = 0x50; // Signature - data[51] = 0x4b; - data[52] = 0x05; - data[53] = 0x06; - data[54] = 0x00; // Number of disk - data[55] = 0x00; - data[56] = 0x00; // Disk CD start - data[57] = 0x00; - data[54] = 0x01; // Total records 1 - data[55] = 0x00; - data[56] = 0x02; // Total records 2, must be = to total records 1 - data[57] = 0x00; - - try (ZFile zf = new ZFile(zipFileWithComments)) { - zf.setEocdComment(data); - } - - try (ZFile zf = new ZFile(zipFileWithComments)) { - assertArrayEquals(data, zf.getEocdComment()); - } - } - - @Test - public void eocdCommentsWithTheEocdMarkerThatAreInvalidAreNotAllowed() throws Exception { - File zipFileWithComments = new File(mTemporaryFolder.getRoot(), "a.zip"); - byte[] data = new byte[100]; - data[50] = 0x50; - data[51] = 0x4b; - data[52] = 0x05; - data[53] = 0x06; - data[67] = 0x00; - - try (ZFile zf = new ZFile(zipFileWithComments)) { - try { - zf.setEocdComment(data); - fail(); - } catch (IllegalArgumentException e) { - // Expected. - } - } - } - - @Test - public void zipCommentsArePreservedWithFileChanges() throws Exception { - File zipFileWithComments = new File(mTemporaryFolder.getRoot(), "a.zip"); - byte[] comment = new byte[] { 1, 3, 4 }; - try (ZFile zf = new ZFile(zipFileWithComments)) { - zf.add("foo", new ByteArrayInputStream(new byte[50])); - zf.setEocdComment(comment); - } - - try (ZFile zf = new ZFile(zipFileWithComments)) { - assertArrayEquals(comment, zf.getEocdComment()); - zf.add("bar", new ByteArrayInputStream(new byte[100])); - } - - try (ZFile zf = new ZFile(zipFileWithComments)) { - assertArrayEquals(comment, zf.getEocdComment()); - } - } - - @Test - public void overlappingZipEntries() throws Exception { - File myZip = ZipTestUtils.cloneRsrc("overlapping.zip", mTemporaryFolder); - try (ZFile zf = new ZFile(myZip)) { - fail(); - } catch (IOException e) { - assertTrue(Throwables.getStackTraceAsString(e).contains("overlapping/bbb")); - assertTrue(Throwables.getStackTraceAsString(e).contains("overlapping/ddd")); - assertFalse(Throwables.getStackTraceAsString(e).contains("Central Directory")); - } - } - - @Test - public void overlappingZipEntryWithCentralDirectory() throws Exception { - File myZip = ZipTestUtils.cloneRsrc("overlapping2.zip", mTemporaryFolder); - try (ZFile zf = new ZFile(myZip)) { - fail(); - } catch (IOException e) { - assertFalse(Throwables.getStackTraceAsString(e).contains("overlapping/bbb")); - assertTrue(Throwables.getStackTraceAsString(e).contains("overlapping/ddd")); - assertTrue(Throwables.getStackTraceAsString(e).contains("Central Directory")); - } - } - - @Test - public void readFileWithOffsetBeyondFileEnd() throws Exception { - File myZip = ZipTestUtils.cloneRsrc("entry-outside-file.zip", mTemporaryFolder); - try (ZFile zf = new ZFile(myZip)) { - fail(); - } catch (IOException e) { - assertTrue(Throwables.getStackTraceAsString(e).contains("entry-outside-file/foo")); - assertTrue(Throwables.getStackTraceAsString(e).contains("EOF")); - } - } -} diff --git a/src/test/java/com/android/apkzlib/zip/ZFileTestConstants.java b/src/test/java/com/android/apkzlib/zip/ZFileTestConstants.java deleted file mode 100644 index fbf5739..0000000 --- a/src/test/java/com/android/apkzlib/zip/ZFileTestConstants.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * 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.android.apkzlib.zip; - -/** - * Constants used in tests. - */ -public interface ZFileTestConstants { - - /** - * Number of bytes in a zip entry local header, not considering name and comment. - */ - int LOCAL_HEADER_SIZE = 30; - - /** - * Number of bytes in a zip central directory entry, not considering name and comment. - */ - int CENTRAL_DIRECTORY_ENTRY_SIZE = 46; - - /** - * Number of bytes in an EOCD without comment. - */ - int EOCD_SIZE = 22; -} diff --git a/src/test/java/com/android/apkzlib/zip/ZipMergeTest.java b/src/test/java/com/android/apkzlib/zip/ZipMergeTest.java deleted file mode 100644 index 0090bc7..0000000 --- a/src/test/java/com/android/apkzlib/zip/ZipMergeTest.java +++ /dev/null @@ -1,205 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * 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.android.apkzlib.zip; - -import static org.junit.Assert.assertArrayEquals; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertSame; -import static org.junit.Assert.assertTrue; - -import com.android.apkzlib.utils.CachedFileContents; -import com.google.common.base.Charsets; -import com.google.common.hash.Hashing; -import com.google.common.io.ByteStreams; -import com.google.common.io.Closer; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.FileOutputStream; -import java.util.zip.ZipEntry; -import java.util.zip.ZipOutputStream; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.TemporaryFolder; - -public class ZipMergeTest { - @Rule - public TemporaryFolder mTemporaryFolder = new TemporaryFolder(); - - @Test - public void mergeZip() throws Exception { - File aZip = ZipTestUtils.cloneRsrc("simple-zip.zip", mTemporaryFolder, "a.zip"); - - CachedFileContents changeDetector; - File merged = new File(mTemporaryFolder.getRoot(), "r.zip"); - try (ZFile mergedZf = new ZFile(merged)) { - mergedZf.mergeFrom(new ZFile(aZip), f -> false); - mergedZf.close(); - - assertEquals(3, mergedZf.entries().size()); - - StoredEntry e0 = mergedZf.get("dir/"); - assertNotNull(e0); - assertSame(StoredEntryType.DIRECTORY, e0.getType()); - - StoredEntry e1 = mergedZf.get("dir/inside"); - assertNotNull(e1); - assertSame(StoredEntryType.FILE, e1.getType()); - ByteArrayOutputStream e1BytesOut = new ByteArrayOutputStream(); - ByteStreams.copy(e1.open(), e1BytesOut); - byte e1Bytes[] = e1BytesOut.toByteArray(); - String e1Txt = new String(e1Bytes, Charsets.US_ASCII); - assertEquals("inside", e1Txt); - - StoredEntry e2 = mergedZf.get("file.txt"); - assertNotNull(e2); - assertSame(StoredEntryType.FILE, e2.getType()); - ByteArrayOutputStream e2BytesOut = new ByteArrayOutputStream(); - ByteStreams.copy(e2.open(), e2BytesOut); - byte e2Bytes[] = e2BytesOut.toByteArray(); - String e2Txt = new String(e2Bytes, Charsets.US_ASCII); - assertEquals("file with more text to allow deflating to be useful", e2Txt); - - changeDetector = new CachedFileContents<>(merged); - changeDetector.closed(null); - - /* - * Clone aZip into bZip and merge. Should have no effect on the final zip file. - */ - File bZip = ZipTestUtils.cloneRsrc("simple-zip.zip", mTemporaryFolder, "b.zip"); - - mergedZf.mergeFrom(new ZFile(bZip), f -> false); - } - - assertTrue(changeDetector.isValid()); - } - - @Test - public void mergeZipWithDeferredCrc() throws Exception { - File foo = mTemporaryFolder.newFile("foo"); - - byte[] wBytes = ZipTestUtils.rsrcBytes("text-files/wikipedia.html"); - - try (ZipOutputStream fooOut = new ZipOutputStream(new FileOutputStream(foo))) { - fooOut.putNextEntry(new ZipEntry("w")); - fooOut.write(wBytes); - } - - try (Closer closer = Closer.create()) { - ZFile fooZf = closer.register(new ZFile(foo)); - StoredEntry wStored = fooZf.get("w"); - assertNotNull(wStored); - assertTrue(wStored.getCentralDirectoryHeader().getGpBit().isDeferredCrc()); - assertEquals(CompressionMethod.DEFLATE, - wStored.getCentralDirectoryHeader().getCompressionInfoWithWait().getMethod()); - - ZFile merged = closer.register(new ZFile(new File(mTemporaryFolder.getRoot(), "bar"))); - merged.mergeFrom(fooZf, f -> false); - merged.update(); - - StoredEntry wmStored = merged.get("w"); - assertNotNull(wmStored); - assertFalse(wmStored.getCentralDirectoryHeader().getGpBit().isDeferredCrc()); - assertEquals(CompressionMethod.DEFLATE, - wmStored.getCentralDirectoryHeader().getCompressionInfoWithWait().getMethod()); - } - } - - @Test - public void mergeZipKeepsDeflatedAndStored() throws Exception { - File foo = mTemporaryFolder.newFile("foo"); - - byte[] wBytes = ZipTestUtils.rsrcBytes("text-files/wikipedia.html"); - byte[] lBytes = ZipTestUtils.rsrcBytes("images/lena.png"); - - try (ZipOutputStream fooOut = new ZipOutputStream(new FileOutputStream(foo))) { - fooOut.putNextEntry(new ZipEntry("w")); - fooOut.write(wBytes); - ZipEntry le = new ZipEntry("l"); - le.setMethod(ZipEntry.STORED); - le.setSize(lBytes.length); - le.setCrc(Hashing.crc32().hashBytes(lBytes).padToLong()); - fooOut.putNextEntry(le); - fooOut.write(lBytes); - } - - try (Closer closer = Closer.create()) { - ZFile fooZf = closer.register(new ZFile(foo)); - StoredEntry wStored = fooZf.get("w"); - assertNotNull(wStored); - assertEquals(CompressionMethod.DEFLATE, - wStored.getCentralDirectoryHeader().getCompressionInfoWithWait().getMethod()); - StoredEntry lStored = fooZf.get("l"); - assertNotNull(lStored); - assertEquals(CompressionMethod.STORE, - lStored.getCentralDirectoryHeader().getCompressionInfoWithWait().getMethod()); - - ZFile merged = closer.register(new ZFile(new File(mTemporaryFolder.getRoot(), "bar"))); - merged.mergeFrom(fooZf, f -> false); - merged.update(); - - StoredEntry wmStored = merged.get("w"); - assertNotNull(wmStored); - assertFalse(wmStored.getCentralDirectoryHeader().getGpBit().isDeferredCrc()); - assertEquals(CompressionMethod.DEFLATE, - wmStored.getCentralDirectoryHeader().getCompressionInfoWithWait().getMethod()); - assertArrayEquals(wBytes, wmStored.read()); - - StoredEntry lmStored = merged.get("l"); - assertNotNull(lmStored); - assertEquals(CompressionMethod.STORE, - lmStored.getCentralDirectoryHeader().getCompressionInfoWithWait().getMethod()); - assertArrayEquals(lBytes, lmStored.read()); - } - } - - @Test - public void mergeZipWithSorting() throws Exception { - File foo = mTemporaryFolder.newFile("foo"); - - byte[] wBytes = ZipTestUtils.rsrcBytes("text-files/wikipedia.html"); - byte[] lBytes = ZipTestUtils.rsrcBytes("images/lena.png"); - - try (ZipOutputStream fooOut = new ZipOutputStream(new FileOutputStream(foo))) { - fooOut.putNextEntry(new ZipEntry("w")); - fooOut.write(wBytes); - ZipEntry le = new ZipEntry("l"); - le.setMethod(ZipEntry.STORED); - le.setSize(lBytes.length); - le.setCrc(Hashing.crc32().hashBytes(lBytes).padToLong()); - fooOut.putNextEntry(le); - fooOut.write(lBytes); - } - - try ( - ZFile fooZf = new ZFile(foo); - ZFile merged = new ZFile(new File(mTemporaryFolder.getRoot(), "bar"))) { - merged.mergeFrom(fooZf, f -> false); - merged.sortZipContents(); - merged.update(); - - StoredEntry wmStored = merged.get("w"); - assertNotNull(wmStored); - assertArrayEquals(wBytes, wmStored.read()); - - StoredEntry lmStored = merged.get("l"); - assertNotNull(lmStored); - assertArrayEquals(lBytes, lmStored.read()); - } - } -} diff --git a/src/test/java/com/android/apkzlib/zip/ZipTestUtils.java b/src/test/java/com/android/apkzlib/zip/ZipTestUtils.java deleted file mode 100644 index aaff18b..0000000 --- a/src/test/java/com/android/apkzlib/zip/ZipTestUtils.java +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * 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.android.apkzlib.zip; - -import static org.junit.Assert.assertFalse; - -import com.android.apkzlib.utils.ApkZFileTestUtils; -import com.google.common.io.Files; -import java.io.File; -import java.io.IOException; -import javax.annotation.Nonnull; -import org.junit.rules.TemporaryFolder; - -/** - * Utility method for zip tests. - */ -class ZipTestUtils { - - /** - * Obtains the data of a resource with the given name. - * - * @param rsrcName the resource name inside packaging resource folder - * @return the resource data - * @throws IOException I/O failed - */ - @Nonnull - static byte[] rsrcBytes(@Nonnull String rsrcName) throws IOException { - return ApkZFileTestUtils.getResourceBytes("/testData/packaging/" + rsrcName).read(); - } - - /** - * Clones a resource to a temporary folder. Generally, resources do not need to be cloned to - * be used. However, in code where there is danger of changing resource files and corrupting - * the source directory, cloning should be done before accessing the resources. - * - * @param rsrcName the resource name - * @param folder the temporary folder - * @return the file that was created with the resource - * @throws IOException failed to clone the resource - */ - static File cloneRsrc(@Nonnull String rsrcName, @Nonnull TemporaryFolder folder) - throws IOException { - String cloneName; - if (rsrcName.contains("/")) { - cloneName = rsrcName.substring(rsrcName.lastIndexOf('/') + 1); - } else { - cloneName = rsrcName; - } - - return cloneRsrc(rsrcName, folder, cloneName); - } - - /** - * Clones a resource to a temporary folder. Generally, resources do not need to be cloned to - * be used. However, in code where there is danger of changing resource files and corrupting - * the source directory, cloning should be done before accessing the resources. - * - * @param rsrcName the resource name - * @param folder the temporary folder - * @param cloneName the name of the cloned resource that will be created inside the temporary - * folder - * @return the file that was created with the resource - * @throws IOException failed to clone the resource - */ - static File cloneRsrc( - @Nonnull String rsrcName, - @Nonnull TemporaryFolder folder, - @Nonnull String cloneName) - throws IOException { - File result = new File(folder.getRoot(), cloneName); - assertFalse(result.exists()); - - Files.write(rsrcBytes(rsrcName), result); - return result; - } -} diff --git a/src/test/java/com/android/apkzlib/zip/ZipToolsTest.java b/src/test/java/com/android/apkzlib/zip/ZipToolsTest.java deleted file mode 100644 index e45f117..0000000 --- a/src/test/java/com/android/apkzlib/zip/ZipToolsTest.java +++ /dev/null @@ -1,223 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * 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.android.apkzlib.zip; - -import static org.junit.Assert.assertArrayEquals; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; - -import com.google.common.base.Charsets; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.Lists; -import com.google.common.collect.Maps; -import com.google.common.io.ByteStreams; -import com.google.common.io.Files; -import java.io.ByteArrayInputStream; -import java.io.File; -import java.io.InputStream; -import java.util.Arrays; -import java.util.List; -import java.util.Map; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import org.junit.Assume; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.TemporaryFolder; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; - -@RunWith(Parameterized.class) -public class ZipToolsTest { - - @Parameterized.Parameter(0) - @Nullable - public String mZipFile; - - @Parameterized.Parameter(1) - @Nullable - public List mUnzipCommand; - - @Parameterized.Parameter(2) - @Nullable - public String mUnzipLineRegex; - - @Parameterized.Parameter(3) - public boolean mToolStoresDirectories; - - @Parameterized.Parameter(4) - public String mName; - - @Rule - @Nonnull - public TemporaryFolder mTemporaryFolder = new TemporaryFolder(); - - @Parameterized.Parameters(name = "{4} {index}") - public static Iterable getConfigurations() { - return Arrays.asList(new Object[][] { - { - "linux-zip.zip", - ImmutableList.of("/usr/bin/unzip", "-v"), - "^\\s*(?\\d+)\\s+(?:Stored|Defl:N).*\\s(?\\S+)\\S*$", - true, - "Linux Zip" - }, - { - "windows-7zip.zip", - ImmutableList.of("c:\\Program Files\\7-Zip\\7z.exe", "l"), - "^(?:\\S+\\s+){3}(?\\d+)\\s+\\d+\\s+(?\\S+)\\s*$", - true, - "Windows 7-Zip" - }, - { - "windows-cf.zip", - ImmutableList.of( - "Cannot use compressed folders from cmd line to list zip contents"), - "", - false, - "Windows Compressed Folders" - } - }); - } - - private File cloneZipFile() throws Exception { - File zfile = mTemporaryFolder.newFile("file.zip"); - Files.write(ZipTestUtils.rsrcBytes(mZipFile), zfile); - return zfile; - } - - private static void assertFileInZip(@Nonnull ZFile zfile, @Nonnull String name) throws Exception { - StoredEntry root = zfile.get(name); - assertNotNull(root); - - InputStream is = root.open(); - byte[] inZipData = ByteStreams.toByteArray(is); - is.close(); - - byte[] inFileData = ZipTestUtils.rsrcBytes(name); - assertArrayEquals(inFileData, inZipData); - } - - @Test - public void zfileReadsZipFile() throws Exception { - try (ZFile zf = new ZFile(cloneZipFile())) { - if (mToolStoresDirectories) { - assertEquals(6, zf.entries().size()); - } else { - assertEquals(4, zf.entries().size()); - } - - assertFileInZip(zf, "root"); - assertFileInZip(zf, "images/lena.png"); - assertFileInZip(zf, "text-files/rfc2460.txt"); - assertFileInZip(zf, "text-files/wikipedia.html"); - } - } - - @Test - public void toolReadsZfFile() throws Exception { - testReadZFile(false); - } - - @Test - public void toolReadsAlignedZfFile() throws Exception { - testReadZFile(true); - } - - private void testReadZFile(boolean align) throws Exception { - String unzipcmd = mUnzipCommand.get(0); - Assume.assumeTrue(new File(unzipcmd).canExecute()); - - ZFileOptions options = new ZFileOptions(); - if (align) { - options.setAlignmentRule(AlignmentRules.constant(500)); - } - - File zfile = new File (mTemporaryFolder.getRoot(), "zfile.zip"); - try (ZFile zf = new ZFile(zfile, options)) { - zf.add("root", new ByteArrayInputStream(ZipTestUtils.rsrcBytes("root"))); - zf.add("images/", new ByteArrayInputStream(new byte[0])); - zf.add( - "images/lena.png", - new ByteArrayInputStream(ZipTestUtils.rsrcBytes("images/lena.png"))); - zf.add("text-files/", new ByteArrayInputStream(new byte[0])); - zf.add( - "text-files/rfc2460.txt", - new ByteArrayInputStream(ZipTestUtils.rsrcBytes("text-files/rfc2460.txt"))); - zf.add( - "text-files/wikipedia.html", - new ByteArrayInputStream(ZipTestUtils.rsrcBytes("text-files/wikipedia.html"))); - } - - List command = Lists.newArrayList(mUnzipCommand); - command.add(zfile.getAbsolutePath()); - ProcessBuilder pb = new ProcessBuilder(command); - Process proc = pb.start(); - InputStream is = proc.getInputStream(); - byte output[] = ByteStreams.toByteArray(is); - String text = new String(output, Charsets.US_ASCII); - String lines[] = text.split("\n"); - Map sizes = Maps.newHashMap(); - for (String l : lines) { - Matcher m = Pattern.compile(mUnzipLineRegex).matcher(l); - if (m.matches()) { - String sizeTxt = m.group("size"); - int size = Integer.parseInt(sizeTxt); - String name = m.group("name"); - sizes.put(name, size); - } - } - - assertEquals(6, sizes.size()); - - /* - * The "images" directory may show up as "images" or "images/". - */ - String imagesKey = "images/"; - if (!sizes.containsKey(imagesKey)) { - imagesKey = "images"; - } - - assertTrue(sizes.containsKey(imagesKey)); - assertEquals(0, sizes.get(imagesKey).intValue()); - - assertSize(new String[] { "images/", "images" }, 0, sizes); - assertSize(new String[] { "text-files/", "text-files"}, 0, sizes); - assertSize(new String[] { "root" }, ZipTestUtils.rsrcBytes("root").length, sizes); - assertSize(new String[] { "images/lena.png", "images\\lena.png" }, - ZipTestUtils.rsrcBytes("images/lena.png").length, sizes); - assertSize(new String[] { "text-files/rfc2460.txt", "text-files\\rfc2460.txt" }, - ZipTestUtils.rsrcBytes("text-files/rfc2460.txt").length, sizes); - assertSize(new String[] { "text-files/wikipedia.html", "text-files\\wikipedia.html" }, - ZipTestUtils.rsrcBytes("text-files/wikipedia.html").length, sizes); - } - - private static void assertSize(String[] names, long size, Map sizes) { - for (String n : names) { - if (sizes.containsKey(n)) { - assertEquals((long) sizes.get(n), size); - return; - } - } - - fail(); - } -} diff --git a/src/test/java/com/android/apkzlib/zip/compress/MultiCompressorTest.java b/src/test/java/com/android/apkzlib/zip/compress/MultiCompressorTest.java deleted file mode 100644 index 4f2eaf0..0000000 --- a/src/test/java/com/android/apkzlib/zip/compress/MultiCompressorTest.java +++ /dev/null @@ -1,155 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * 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.android.apkzlib.zip.compress; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; - -import com.android.apkzlib.utils.ApkZFileTestUtils; -import com.android.apkzlib.zip.CentralDirectoryHeaderCompressInfo; -import com.android.apkzlib.zip.CompressionMethod; -import com.android.apkzlib.zip.StoredEntry; -import com.android.apkzlib.zip.ZFile; -import com.android.apkzlib.zip.ZFileOptions; -import com.google.common.util.concurrent.MoreExecutors; -import java.io.ByteArrayInputStream; -import java.io.File; -import java.util.zip.Deflater; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.TemporaryFolder; - -public class MultiCompressorTest { - @Rule - public TemporaryFolder mTemporaryFolder = new TemporaryFolder(); - - private static byte[] getCompressibleData() throws Exception { - return ApkZFileTestUtils - .getResourceBytes("/testData/packaging/text-files/wikipedia.html") - .read(); - } - - private static byte[] compress(byte[] data, int level) throws Exception { - Deflater deflater = new Deflater(level); - deflater.setInput(data); - deflater.finish(); - - byte[] resultAll = new byte[data.length * 2]; - int resultAllCount = deflater.deflate(resultAll); - - byte[] result = new byte[resultAllCount]; - System.arraycopy(resultAll, 0, result, 0, resultAllCount); - return result; - } - - @Test - public void storeIsBest() throws Exception { - File zip = new File(mTemporaryFolder.getRoot(), "test.zip"); - - try (ZFile zf = new ZFile(zip)) { - zf.add("file", new ByteArrayInputStream(new byte[0])); - StoredEntry entry = zf.get("file"); - assertNotNull(entry); - - CentralDirectoryHeaderCompressInfo ci = - entry.getCentralDirectoryHeader().getCompressionInfoWithWait(); - - assertEquals(0, ci.getCompressedSize()); - assertEquals(CompressionMethod.STORE, ci.getMethod()); - } - } - - @Test - public void sameCompressionResultButBetterThanStore() throws Exception { - File zip = new File(mTemporaryFolder.getRoot(), "test.zip"); - - byte[] data = new byte[] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; - - try (ZFile zf = new ZFile(zip)) { - zf.add("file", new ByteArrayInputStream(data)); - StoredEntry entry = zf.get("file"); - assertNotNull(entry); - - CentralDirectoryHeaderCompressInfo ci = - entry.getCentralDirectoryHeader().getCompressionInfoWithWait(); - - assertEquals(CompressionMethod.DEFLATE, ci.getMethod()); - assertTrue(ci.getCompressedSize() < data.length); - } - } - - @Test - public void bestBetterThanDefault() throws Exception { - byte[] data = getCompressibleData(); - int bestSize = compress(data, Deflater.BEST_COMPRESSION).length; - int defaultSize = compress(data, Deflater.DEFAULT_COMPRESSION).length; - - double ratio = bestSize / (double) defaultSize; - assertTrue(ratio < 1.0); - - File defaultFile = new File(mTemporaryFolder.getRoot(), "default.zip"); - File resultFile = new File(mTemporaryFolder.getRoot(), "result.zip"); - - ZFileOptions resultOptions = new ZFileOptions(); - resultOptions.setCompressor( - new BestAndDefaultDeflateExecutorCompressor( - MoreExecutors.directExecutor(), resultOptions.getTracker(), ratio + 0.001)); - - try ( - ZFile defaultZFile = new ZFile(defaultFile); - ZFile resultZFile = new ZFile(resultFile, resultOptions)) { - defaultZFile.add("wikipedia.html", new ByteArrayInputStream(data)); - resultZFile.add("wikipedia.html", new ByteArrayInputStream(data)); - } - - long defaultFileSize = defaultFile.length(); - long resultFileSize = resultFile.length(); - - assertTrue(resultFileSize < defaultFileSize); - } - - @Test - public void bestBetterThanDefaultButNotEnough() throws Exception { - byte[] data = getCompressibleData(); - int bestSize = compress(data, Deflater.BEST_COMPRESSION).length; - int defaultSize = compress(data, Deflater.DEFAULT_COMPRESSION).length; - - double ratio = bestSize / (double) defaultSize; - assertTrue(ratio < 1.0); - - File defaultFile = new File(mTemporaryFolder.getRoot(), "default.zip"); - File resultFile = new File(mTemporaryFolder.getRoot(), "result.zip"); - - ZFileOptions resultOptions = new ZFileOptions(); - resultOptions.setCompressor( - new BestAndDefaultDeflateExecutorCompressor( - MoreExecutors.directExecutor(), resultOptions.getTracker(), ratio - 0.001)); - - try ( - ZFile defaultZFile = new ZFile(defaultFile); - ZFile resultZFile = new ZFile(resultFile, resultOptions)) { - defaultZFile.add("wikipedia.html", new ByteArrayInputStream(data)); - resultZFile.add("wikipedia.html", new ByteArrayInputStream(data)); - } - - long defaultFileSize = defaultFile.length(); - long resultFileSize = resultFile.length(); - - assertTrue(resultFileSize == defaultFileSize); - } -} diff --git a/src/test/java/com/android/apkzlib/zip/utils/LittleEndianUtilsTest.java b/src/test/java/com/android/apkzlib/zip/utils/LittleEndianUtilsTest.java deleted file mode 100644 index 3264290..0000000 --- a/src/test/java/com/android/apkzlib/zip/utils/LittleEndianUtilsTest.java +++ /dev/null @@ -1,112 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * 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.android.apkzlib.zip.utils; - -import static junit.framework.TestCase.assertEquals; -import static org.junit.Assert.assertArrayEquals; - -import org.junit.Test; - -import java.nio.ByteBuffer; -import java.util.Random; - -public class LittleEndianUtilsTest { - @Test - public void read2Le() throws Exception { - assertEquals(0x0102, LittleEndianUtils.readUnsigned2Le(ByteBuffer.wrap( - new byte[] { 2, 1 }))); - assertEquals(0xfedc, LittleEndianUtils.readUnsigned2Le(ByteBuffer.wrap( - new byte[] { (byte) 0xdc, (byte) 0xfe }))); - } - - @Test - public void write2Le() throws Exception { - ByteBuffer out = ByteBuffer.allocate(2); - LittleEndianUtils.writeUnsigned2Le(out, 0x0102); - assertArrayEquals(new byte[] { 2, 1 }, out.array()); - - out = ByteBuffer.allocate(2); - LittleEndianUtils.writeUnsigned2Le(out, 0xfedc); - assertArrayEquals(new byte[] { (byte) 0xdc, (byte) 0xfe }, out.array()); - } - - @Test - public void readWrite2Le() throws Exception { - Random r = new Random(); - - int range = 0x0000ffff; - - final int COUNT = 1000; - int[] data = new int[COUNT]; - for (int i = 0; i < data.length; i++) { - data[i] = r.nextInt(range); - } - - ByteBuffer out = ByteBuffer.allocate(COUNT * 2); - for (int d : data) { - LittleEndianUtils.writeUnsigned2Le(out, d); - } - - ByteBuffer in = ByteBuffer.wrap(out.array()); - for (int i = 0; i < data.length; i++) { - assertEquals(data[i], LittleEndianUtils.readUnsigned2Le(in)); - } - } - - @Test - public void read4Le() throws Exception { - assertEquals(0x01020304, LittleEndianUtils.readUnsigned4Le(ByteBuffer.wrap( - new byte[] { 4, 3, 2, 1 }))); - assertEquals(0xfedcba98L, LittleEndianUtils.readUnsigned4Le(ByteBuffer.wrap( - new byte[] { (byte) 0x98, (byte) 0xba, (byte) 0xdc, (byte) 0xfe }))); - } - - @Test - public void write4Le() throws Exception { - ByteBuffer out = ByteBuffer.allocate(4); - LittleEndianUtils.writeUnsigned4Le(out, 0x01020304); - assertArrayEquals(new byte[] { 4, 3, 2, 1 }, out.array()); - - out = ByteBuffer.allocate(4); - LittleEndianUtils.writeUnsigned4Le(out, 0xfedcba98L); - assertArrayEquals(new byte[] { (byte) 0x98, (byte) 0xba, (byte) 0xdc, (byte) 0xfe }, - out.array()); - } - - @Test - public void readWrite4Le() throws Exception { - Random r = new Random(); - - final int COUNT = 1000; - long[] data = new long[COUNT]; - for (int i = 0; i < data.length; i++) { - do { - data[i] = r.nextInt() - (long) Integer.MIN_VALUE; - } while (data[i] < 0); - } - - ByteBuffer out = ByteBuffer.allocate(COUNT * 4); - for (long d : data) { - LittleEndianUtils.writeUnsigned4Le(out, d); - } - - ByteBuffer in = ByteBuffer.wrap(out.array()); - for (int i = 0; i < data.length; i++) { - assertEquals(data[i], LittleEndianUtils.readUnsigned4Le(in)); - } - } -} diff --git a/src/test/java/com/android/apkzlib/zip/utils/MsDosDateTimeUtilsTest.java b/src/test/java/com/android/apkzlib/zip/utils/MsDosDateTimeUtilsTest.java deleted file mode 100644 index 012d587..0000000 --- a/src/test/java/com/android/apkzlib/zip/utils/MsDosDateTimeUtilsTest.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * 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.android.apkzlib.zip.utils; - -import static org.junit.Assert.assertEquals; - -import org.junit.Test; - -import java.util.Calendar; - -public class MsDosDateTimeUtilsTest { - @Test - public void packDate() throws Exception { - Calendar c = Calendar.getInstance(); - - c.set(Calendar.YEAR, 2016); - c.set(Calendar.MONTH, 0); - c.set(Calendar.DAY_OF_MONTH, 5); - - long time = c.getTime().getTime(); - - int packed = MsDosDateTimeUtils.packDate(time); - - // Year = 2016 - 1980 = 36 (0000 0000 0[010 0100]) - // Month = 1 (0000 0000 0000 [0001]) - // Day = 5 (0000 0000 000[0 0101]) - // Packs as 010 0100 | 0001 | 00101 - // Or 0100 1000 0010 0101 - // In hex 4 8 2 5 - - int expectedDateBits = 0x4825; - assertEquals(expectedDateBits, packed); - } - - @Test - public void packTime() throws Exception { - Calendar c = Calendar.getInstance(); - - c.set(Calendar.HOUR_OF_DAY, 8); - c.set(Calendar.MINUTE, 45); - c.set(Calendar.SECOND, 20); - - long time = c.getTime().getTime(); - - int packed = MsDosDateTimeUtils.packTime(time); - - // Hour = 8 (0000 0000 000[0 1000]) - // Minute = 45 (0000 0000 00[10 1101]) - // Second = 20 / 2 = 10 (0000 0000 000[0 1010]) - // Pack as 0 1000 | 10 1101 | 0 1010 - // Or 0100 0101 1010 1010 - // In hex 4 5 A A - - int expectedTimeBits = 0x45AA; - assertEquals(expectedTimeBits, packed); - } -} diff --git a/src/test/java/com/android/tools/build/apkzlib/sign/FullApkSignTest.java b/src/test/java/com/android/tools/build/apkzlib/sign/FullApkSignTest.java new file mode 100644 index 0000000..7514bb7 --- /dev/null +++ b/src/test/java/com/android/tools/build/apkzlib/sign/FullApkSignTest.java @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * 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.android.tools.build.apkzlib.sign; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertNotNull; + +import com.android.tools.build.apkzlib.utils.ApkZFileTestUtils; +import com.android.tools.build.apkzlib.utils.ApkZLibPair; +import com.android.tools.build.apkzlib.zip.AlignmentRule; +import com.android.tools.build.apkzlib.zip.AlignmentRules; +import com.android.tools.build.apkzlib.zip.StoredEntry; +import com.android.tools.build.apkzlib.zip.ZFile; +import com.android.tools.build.apkzlib.zip.ZFileOptions; +import com.android.tools.build.apkzlib.zip.ZFileTestConstants; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.security.PrivateKey; +import java.security.cert.X509Certificate; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +/** + * Tests that verify APK Signature Scheme v2 signing using {@link SigningExtension}. + */ +public class FullApkSignTest { + + /** + * Folder used for tests. + */ + @Rule + public TemporaryFolder mTemporaryFolder = new TemporaryFolder(); + + @Test + public void testSignature() throws Exception { + File out = new File(mTemporaryFolder.getRoot(), "apk"); + + ApkZLibPair signData = + SignatureTestUtils.generateSignaturePre18(); + + // The byte arrays below are larger when compressed, so we end up storing them uncompressed, + // which would normally cause them to be 4-aligned. Disable that, to make calculations + // easier. + ZFileOptions options = new ZFileOptions(); + options.setAlignmentRule(AlignmentRules.constant(AlignmentRule.NO_ALIGNMENT)); + + /* + * Generate a signed zip. + */ + ZFile zf = new ZFile(out, options); + new SigningExtension(13, signData.v2, signData.v1, false, true) + .register(zf); + String f1Name = "abc"; + byte[] f1Data = new byte[] { 1, 1, 1, 1 }; + zf.add(f1Name, new ByteArrayInputStream(f1Data)); + String f2Name = "defg"; + byte[] f2Data = new byte[] { 2, 2, 2, 2, 3, 3, 3, 3}; + zf.add(f2Name, new ByteArrayInputStream(f2Data)); + zf.close(); + + /* + * We should see the data in place. + */ + int f1DataStart = ZFileTestConstants.LOCAL_HEADER_SIZE + f1Name.length(); + int f1DataEnd = f1DataStart + f1Data.length; + int f2DataStart = f1DataEnd + ZFileTestConstants.LOCAL_HEADER_SIZE + f2Name.length(); + int f2DataEnd = f2DataStart + f2Data.length; + + byte[] read1 = ApkZFileTestUtils.readSegment(out, f1DataStart, f1Data.length); + assertArrayEquals(f1Data, read1); + byte[] read2 = ApkZFileTestUtils.readSegment(out, f2DataStart, f2Data.length); + assertArrayEquals(f2Data, read2); + + /* + * Read the signed zip. + */ + ZFile zf2 = new ZFile(out); + + StoredEntry se1 = zf2.get(f1Name); + assertNotNull(se1); + assertArrayEquals(f1Data, se1.read()); + + StoredEntry se2 = zf2.get(f2Name); + assertNotNull(se2); + assertArrayEquals(f2Data, se2.read()); + + zf2.close(); + } +} diff --git a/src/test/java/com/android/tools/build/apkzlib/sign/JarSigningTest.java b/src/test/java/com/android/tools/build/apkzlib/sign/JarSigningTest.java new file mode 100644 index 0000000..359686c --- /dev/null +++ b/src/test/java/com/android/tools/build/apkzlib/sign/JarSigningTest.java @@ -0,0 +1,378 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * 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.android.tools.build.apkzlib.sign; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; + +import com.android.tools.build.apkzlib.utils.ApkZFileTestUtils; +import com.android.tools.build.apkzlib.utils.ApkZLibPair; +import com.android.tools.build.apkzlib.zip.StoredEntry; +import com.android.tools.build.apkzlib.zip.ZFile; +import com.google.common.base.Charsets; +import com.google.common.hash.Hashing; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.InputStream; +import java.security.PrivateKey; +import java.security.cert.X509Certificate; +import java.util.Base64; +import java.util.jar.Attributes; +import java.util.jar.Manifest; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +public class JarSigningTest { + + @Rule + public TemporaryFolder mTemporaryFolder = new TemporaryFolder(); + + @Test + public void signEmptyJar() throws Exception { + File zipFile = new File(mTemporaryFolder.getRoot(), "a.zip"); + + try (ZFile zf = new ZFile(zipFile)) { + ApkZFileTestUtils.addAndroidManifest(zf); + ManifestGenerationExtension manifestExtension = + new ManifestGenerationExtension("Me", "Me"); + manifestExtension.register(zf); + + ApkZLibPair p = + SignatureTestUtils.generateSignaturePre18(); + + new SigningExtension(12, p.v2, p.v1, true, false).register(zf); + } + + try (ZFile verifyZFile = new ZFile(zipFile)) { + StoredEntry manifestEntry = verifyZFile.get("META-INF/MANIFEST.MF"); + assertNotNull(manifestEntry); + + Manifest manifest = new Manifest(new ByteArrayInputStream(manifestEntry.read())); + assertEquals(3, manifest.getMainAttributes().size()); + assertEquals("1.0", manifest.getMainAttributes().getValue("Manifest-Version")); + assertEquals("Me", manifest.getMainAttributes().getValue("Created-By")); + assertEquals("Me", manifest.getMainAttributes().getValue("Built-By")); + } + } + + @Test + public void signJarWithPrexistingSimpleTextFilePre18() throws Exception { + File zipFile = new File(mTemporaryFolder.getRoot(), "a.zip"); + ApkZLibPair p = SignatureTestUtils.generateSignaturePre18(); + + try (ZFile zf1 = new ZFile(zipFile)) { + ApkZFileTestUtils.addAndroidManifest(zf1); + zf1.add("directory/file", + new ByteArrayInputStream("useless text".getBytes(Charsets.US_ASCII))); + } + + try (ZFile zf2 = new ZFile(zipFile)) { + ManifestGenerationExtension me = new ManifestGenerationExtension("Merry", "Christmas"); + me.register(zf2); + new SigningExtension(10, p.v2, p.v1, true, false).register(zf2); + } + + try (ZFile zf3 = new ZFile(zipFile)) { + StoredEntry manifestEntry = zf3.get("META-INF/MANIFEST.MF"); + assertNotNull(manifestEntry); + + Manifest manifest = new Manifest(new ByteArrayInputStream(manifestEntry.read())); + assertEquals(3, manifest.getMainAttributes().size()); + assertEquals("1.0", manifest.getMainAttributes().getValue("Manifest-Version")); + assertEquals("Merry", manifest.getMainAttributes().getValue("Built-By")); + assertEquals("Christmas", manifest.getMainAttributes().getValue("Created-By")); + + Attributes attrs = manifest.getAttributes("directory/file"); + assertNotNull(attrs); + assertEquals(1, attrs.size()); + assertEquals("OOQgIEXBissIvva3ydRoaXk29Rk=", attrs.getValue("SHA1-Digest")); + + StoredEntry signatureEntry = zf3.get("META-INF/CERT.SF"); + assertNotNull(signatureEntry); + + Manifest signature = new Manifest(new ByteArrayInputStream(signatureEntry.read())); + assertEquals(3, signature.getMainAttributes().size()); + assertEquals("1.0", signature.getMainAttributes().getValue("Signature-Version")); + assertEquals("1.0 (Android)", signature.getMainAttributes().getValue("Created-By")); + + byte[] manifestTextBytes = manifestEntry.read(); + byte[] manifestSha1Bytes = Hashing.sha1().hashBytes(manifestTextBytes).asBytes(); + String manifestSha1 = Base64.getEncoder().encodeToString(manifestSha1Bytes); + + assertEquals(manifestSha1, + signature.getMainAttributes().getValue("SHA1-Digest-Manifest")); + + Attributes signAttrs = signature.getAttributes("directory/file"); + assertNotNull(signAttrs); + assertEquals(1, signAttrs.size()); + assertEquals("LGSOwy4uGcUWoc+ZhS8ukzmf0fY=", signAttrs.getValue("SHA1-Digest")); + + StoredEntry rsaEntry = zf3.get("META-INF/CERT.RSA"); + assertNotNull(rsaEntry); + } + } + + @Test + public void signJarWithPrexistingSimpleTextFilePos18() throws Exception { + File zipFile = new File(mTemporaryFolder.getRoot(), "a.zip"); + try (ZFile zf1 = new ZFile(zipFile)) { + ApkZFileTestUtils.addAndroidManifest(zf1); + zf1.add("directory/file", new ByteArrayInputStream("useless text".getBytes( + Charsets.US_ASCII))); + } + + ApkZLibPair p = SignatureTestUtils.generateSignaturePos18(); + + try (ZFile zf2 = new ZFile(zipFile)) { + ManifestGenerationExtension me = new ManifestGenerationExtension("Merry", "Christmas"); + me.register(zf2); + new SigningExtension(21, p.v2, p.v1, true, false).register(zf2); + } + + try (ZFile zf3 = new ZFile(zipFile)) { + StoredEntry manifestEntry = zf3.get("META-INF/MANIFEST.MF"); + assertNotNull(manifestEntry); + + Manifest manifest = new Manifest(new ByteArrayInputStream(manifestEntry.read())); + assertEquals(3, manifest.getMainAttributes().size()); + assertEquals("1.0", manifest.getMainAttributes().getValue("Manifest-Version")); + assertEquals("Merry", manifest.getMainAttributes().getValue("Built-By")); + assertEquals("Christmas", manifest.getMainAttributes().getValue("Created-By")); + + Attributes attrs = manifest.getAttributes("directory/file"); + assertNotNull(attrs); + assertEquals(1, attrs.size()); + assertEquals("QjupZsopQM/01O6+sWHqH64ilMmoBEtljg9VEqN6aI4=", + attrs.getValue("SHA-256-Digest")); + + StoredEntry signatureEntry = zf3.get("META-INF/CERT.SF"); + assertNotNull(signatureEntry); + + Manifest signature = new Manifest(new ByteArrayInputStream(signatureEntry.read())); + assertEquals(3, signature.getMainAttributes().size()); + assertEquals("1.0", signature.getMainAttributes().getValue("Signature-Version")); + assertEquals("1.0 (Android)", signature.getMainAttributes().getValue("Created-By")); + + byte[] manifestTextBytes = manifestEntry.read(); + byte[] manifestSha256Bytes = Hashing.sha256().hashBytes(manifestTextBytes).asBytes(); + String manifestSha256 = Base64.getEncoder().encodeToString(manifestSha256Bytes); + + assertEquals(manifestSha256, signature.getMainAttributes().getValue( + "SHA-256-Digest-Manifest")); + + Attributes signAttrs = signature.getAttributes("directory/file"); + assertNotNull(signAttrs); + assertEquals(1, signAttrs.size()); + assertEquals("dBnaLpqNjmUnLlZF4tNqOcDWL8wy8Tsw1ZYFqTZhjIs=", + signAttrs.getValue("SHA-256-Digest")); + + StoredEntry ecdsaEntry = zf3.get("META-INF/CERT.EC"); + assertNotNull(ecdsaEntry); + } + } + + @Test + public void v2SignAddsApkSigningBlock() throws Exception { + File zipFile = new File(mTemporaryFolder.getRoot(), "a.zip"); + try (ZFile zf = new ZFile(zipFile)) { + ApkZFileTestUtils.addAndroidManifest(zf); + ManifestGenerationExtension manifestExtension = + new ManifestGenerationExtension("Me", "Me"); + manifestExtension.register(zf); + + ApkZLibPair p = SignatureTestUtils.generateSignaturePre18(); + + new SigningExtension(12, p.v2, p.v1, false, true).register(zf); + } + + try (ZFile verifyZFile = new ZFile(zipFile)) { + long centralDirOffset = verifyZFile.getCentralDirectoryOffset(); + byte[] apkSigningBlockMagic = new byte[16]; + verifyZFile.directFullyRead( + centralDirOffset - apkSigningBlockMagic.length, apkSigningBlockMagic); + assertEquals("APK Sig Block 42", new String(apkSigningBlockMagic, "US-ASCII")); + } + } + + @Test + public void v1ReSignOnFileChange() throws Exception { + File zipFile = new File(mTemporaryFolder.getRoot(), "a.zip"); + ApkZLibPair p = SignatureTestUtils.generateSignaturePos18(); + + byte[] file1Contents = "I am a test file".getBytes(Charsets.US_ASCII); + String file1Name = "path/to/file1"; + byte[] file1Sha = Hashing.sha256().hashBytes(file1Contents).asBytes(); + String file1ShaTxt = Base64.getEncoder().encodeToString(file1Sha); + + String builtBy = "Santa Claus"; + String createdBy = "Uses Android"; + + try (ZFile zf1 = new ZFile(zipFile)) { + ApkZFileTestUtils.addAndroidManifest(zf1); + zf1.add(file1Name, new ByteArrayInputStream(file1Contents)); + ManifestGenerationExtension me = new ManifestGenerationExtension(builtBy, createdBy); + me.register(zf1); + new SigningExtension(21, p.v2, p.v1, true, false).register(zf1); + + zf1.update(); + + StoredEntry manifestEntry = zf1.get("META-INF/MANIFEST.MF"); + assertNotNull(manifestEntry); + + try (InputStream manifestIs = manifestEntry.open()) { + Manifest manifest = new Manifest(manifestIs); + + assertEquals(2, manifest.getEntries().size()); + + Attributes file1Attrs = manifest.getEntries().get(file1Name); + assertNotNull(file1Attrs); + assertEquals(file1ShaTxt, file1Attrs.getValue("SHA-256-Digest")); + } + + /* + * Change the file without closing the zip. + */ + file1Contents = "I am a modified test file".getBytes(Charsets.US_ASCII); + file1Sha = Hashing.sha256().hashBytes(file1Contents).asBytes(); + file1ShaTxt = Base64.getEncoder().encodeToString(file1Sha); + + zf1.add(file1Name, new ByteArrayInputStream(file1Contents)); + + zf1.update(); + + manifestEntry = zf1.get("META-INF/MANIFEST.MF"); + assertNotNull(manifestEntry); + + try (InputStream manifestIs = manifestEntry.open()) { + Manifest manifest = new Manifest(manifestIs); + + assertEquals(2, manifest.getEntries().size()); + + Attributes file1Attrs = manifest.getEntries().get(file1Name); + assertNotNull(file1Attrs); + assertEquals(file1ShaTxt, file1Attrs.getValue("SHA-256-Digest")); + } + } + + /* + * Change the file closing the zip. + */ + file1Contents = "I have changed again!".getBytes(Charsets.US_ASCII); + file1Sha = Hashing.sha256().hashBytes(file1Contents).asBytes(); + file1ShaTxt = Base64.getEncoder().encodeToString(file1Sha); + + try (ZFile zf2 = new ZFile(zipFile)) { + ApkZFileTestUtils.addAndroidManifest(zf2); + ManifestGenerationExtension me = new ManifestGenerationExtension(builtBy, createdBy); + me.register(zf2); + new SigningExtension(21, p.v2, p.v1, true, false).register(zf2); + + zf2.add(file1Name, new ByteArrayInputStream(file1Contents)); + + zf2.update(); + + StoredEntry manifestEntry = zf2.get("META-INF/MANIFEST.MF"); + assertNotNull(manifestEntry); + + try (InputStream manifestIs = manifestEntry.open()) { + Manifest manifest = new Manifest(manifestIs); + + assertEquals(2, manifest.getEntries().size()); + + Attributes file1Attrs = manifest.getEntries().get(file1Name); + assertNotNull(file1Attrs); + assertEquals(file1ShaTxt, file1Attrs.getValue("SHA-256-Digest")); + } + } + } + + @Test + public void openSignedJarDoesNotForcesWriteIfSignatureIsNotCorrect() throws Exception { + File zipFile = new File(mTemporaryFolder.getRoot(), "a.zip"); + + ApkZLibPair p = SignatureTestUtils.generateSignaturePos18(); + + String fileName = "file"; + byte[] fileContents = "Very interesting contents".getBytes(Charsets.US_ASCII); + + try (ZFile zf = new ZFile(zipFile)) { + ApkZFileTestUtils.addAndroidManifest(zf); + ManifestGenerationExtension me = new ManifestGenerationExtension("I", "Android"); + me.register(zf); + new SigningExtension(21, p.v2, p.v1, true, false).register(zf); + + zf.add(fileName, new ByteArrayInputStream(fileContents)); + } + + long fileTimestamp = zipFile.lastModified(); + + ApkZFileTestUtils.waitForFileSystemTick(fileTimestamp); + + /* + * Open the zip file, but don't touch it. + */ + try (ZFile zf = new ZFile(zipFile)) { + ManifestGenerationExtension me = new ManifestGenerationExtension("I", "Android"); + me.register(zf); + new SigningExtension(21, p.v2, p.v1, true, false).register(zf); + } + + /* + * Check the file wasn't touched. + */ + assertEquals(fileTimestamp, zipFile.lastModified()); + + /* + * Change the file contents ignoring any signing. + */ + fileContents = "Not so interesting contents".getBytes(Charsets.US_ASCII); + try (ZFile zf = new ZFile(zipFile)) { + zf.add(fileName, new ByteArrayInputStream(fileContents)); + } + + fileTimestamp = zipFile.lastModified(); + + /* + * Wait to make sure the timestamp can increase. + */ + while (true) { + File notUsed = mTemporaryFolder.newFile(); + long notTimestamp = notUsed.lastModified(); + notUsed.delete(); + if (notTimestamp > fileTimestamp) { + break; + } + } + + /* + * Open the zip file, but do any changes. The need to updating the signature should force + * a file update. + */ + try (ZFile zf = new ZFile(zipFile)) { + ManifestGenerationExtension me = new ManifestGenerationExtension("I", "Android"); + me.register(zf); + new SigningExtension(21, p.v2, p.v1, true, false).register(zf); + } + + /* + * Check the file was touched. + */ + assertNotEquals(fileTimestamp, zipFile.lastModified()); + } +} diff --git a/src/test/java/com/android/tools/build/apkzlib/sign/ManifestGenerationTest.java b/src/test/java/com/android/tools/build/apkzlib/sign/ManifestGenerationTest.java new file mode 100644 index 0000000..963903e --- /dev/null +++ b/src/test/java/com/android/tools/build/apkzlib/sign/ManifestGenerationTest.java @@ -0,0 +1,180 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * 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.android.tools.build.apkzlib.sign; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import com.android.tools.build.apkzlib.utils.ApkZFileTestUtils; +import com.android.tools.build.apkzlib.zip.StoredEntry; +import com.android.tools.build.apkzlib.zip.ZFile; +import com.google.common.base.Charsets; +import com.google.common.io.Closer; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.InputStream; +import java.util.HashSet; +import java.util.Set; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +public class ManifestGenerationTest { + + private static final String WIKI_PATH = "/testData/packaging/text-files/wikipedia.html"; + + @Rule + public TemporaryFolder mTemporaryFolder = new TemporaryFolder(); + + @Test + public void elementaryManifestGeneration() throws Exception { + File zip = new File(mTemporaryFolder.getRoot(), "f.zip"); + + try (ZFile zf = new ZFile(zip)) { + zf.add("abc", new ByteArrayInputStream(new byte[]{1})); + zf.add("x/", new ByteArrayInputStream(new byte[0])); + zf.add("x/abc", new ByteArrayInputStream(new byte[]{2})); + + ManifestGenerationExtension extension = + new ManifestGenerationExtension("Me, of course", "Myself"); + extension.register(zf); + + zf.update(); + + StoredEntry se = zf.get("META-INF/MANIFEST.MF"); + assertNotNull(se); + + String text = new String(se.read(), Charsets.US_ASCII); + text = text.trim(); + String lines[] = text.split(System.getProperty("line.separator")); + assertEquals(3, lines.length); + + assertEquals("Manifest-Version: 1.0", lines[0].trim()); + + Set linesSet = new HashSet<>(); + for (String l : lines) { + linesSet.add(l.trim()); + } + + assertTrue(linesSet.contains("Built-By: Me, of course")); + assertTrue(linesSet.contains("Created-By: Myself")); + } + } + + @Test + public void manifestGenerationOnHalfWrittenFile() throws Exception { + File zip = new File(mTemporaryFolder.getRoot(), "f.zip"); + try (Closer closer = Closer.create()) { + ZFile zf = closer.register(new ZFile(zip)); + + try (InputStream wiki = getClass().getResourceAsStream(WIKI_PATH)) { + zf.add("wiki", wiki); + } + + ManifestGenerationExtension extension = + new ManifestGenerationExtension("Me, of course", "Myself"); + extension.register(zf); + + zf.close(); + + StoredEntry se = zf.get("META-INF/MANIFEST.MF"); + assertNotNull(se); + + String text = new String(se.read(), Charsets.US_ASCII); + text = text.trim(); + String lines[] = text.split(System.getProperty("line.separator")); + assertEquals(3, lines.length); + + assertEquals("Manifest-Version: 1.0", lines[0].trim()); + + Set linesSet = new HashSet<>(); + for (String l : lines) { + linesSet.add(l.trim()); + } + + assertTrue(linesSet.contains("Built-By: Me, of course")); + assertTrue(linesSet.contains("Created-By: Myself")); + } + } + + @Test + public void manifestGenerationOnExistingFile() throws Exception { + File zip = new File(mTemporaryFolder.getRoot(), "f.zip"); + try (Closer closer = Closer.create()) { + ZFile zf = closer.register(new ZFile(zip)); + + try (InputStream wiki = getClass().getResourceAsStream(WIKI_PATH)) { + zf.add("wiki", wiki); + } + + zf.close(); + + ManifestGenerationExtension extension = + new ManifestGenerationExtension("Me, of course", "Myself"); + extension.register(zf); + + zf.close(); + + StoredEntry se = zf.get("META-INF/MANIFEST.MF"); + assertNotNull(se); + + String text = new String(se.read(), Charsets.US_ASCII); + text = text.trim(); + String lines[] = text.split(System.getProperty("line.separator")); + assertEquals(3, lines.length); + + assertEquals("Manifest-Version: 1.0", lines[0].trim()); + + Set linesSet = new HashSet<>(); + for (String l : lines) { + linesSet.add(l.trim()); + } + + assertTrue(linesSet.contains("Built-By: Me, of course")); + assertTrue(linesSet.contains("Created-By: Myself")); + } + } + + @Test + public void manifestGenerationOnIncrementalNoChanges() throws Exception { + File zip = new File(mTemporaryFolder.getRoot(), "f.zip"); + try (Closer closer = Closer.create()) { + ZFile zf = closer.register(new ZFile(zip)); + + ManifestGenerationExtension extension = + new ManifestGenerationExtension("Me, of course", "Myself"); + extension.register(zf); + + try (InputStream wiki = getClass().getResourceAsStream(WIKI_PATH)) { + zf.add("wiki", wiki); + } + + zf.close(); + + long timeOfWriting = zip.lastModified(); + + ApkZFileTestUtils.waitForFileSystemTick(timeOfWriting); + + zf = closer.register(new ZFile(zip)); + zf.close(); + + long secondTimeOfWriting = zip.lastModified(); + assertEquals(timeOfWriting, secondTimeOfWriting); + } + } +} diff --git a/src/test/java/com/android/tools/build/apkzlib/sign/SignatureTestUtils.java b/src/test/java/com/android/tools/build/apkzlib/sign/SignatureTestUtils.java new file mode 100644 index 0000000..5f1d7e1 --- /dev/null +++ b/src/test/java/com/android/tools/build/apkzlib/sign/SignatureTestUtils.java @@ -0,0 +1,133 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * 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.android.tools.build.apkzlib.sign; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.fail; + +import com.android.tools.build.apkzlib.utils.ApkZLibPair; +import java.math.BigInteger; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.cert.X509Certificate; +import java.security.interfaces.ECPublicKey; +import java.security.interfaces.RSAPublicKey; +import java.util.Date; +import javax.annotation.Nonnull; +import javax.security.auth.x500.X500Principal; +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo; +import org.bouncycastle.cert.X509CertificateHolder; +import org.bouncycastle.cert.X509v1CertificateBuilder; +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; +import org.bouncycastle.crypto.params.RSAKeyParameters; +import org.bouncycastle.crypto.util.SubjectPublicKeyInfoFactory; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.operator.ContentSigner; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; +import org.junit.Assume; + +/** + * Utilities to use signatures in tests. + */ +public class SignatureTestUtils { + + /** + * Generates a private key / certificate for pre-18 systems. + * + * @return the pair with the private key and certificate + * @throws Exception failed to generate the signature data + */ + @Nonnull + public static ApkZLibPair generateSignaturePre18() + throws Exception { + return generateSignature("RSA", "SHA1withRSA"); + } + + /** + * Generates a private key / certificate for post-18 systems. + * + * @return the pair with the private key and certificate + * @throws Exception failed to generate the signature data + */ + @Nonnull + public static ApkZLibPair generateSignaturePos18() + throws Exception { + return generateSignature("EC", "SHA256withECDSA"); + } + + /** + * Generates a private key / certificate. + * + * @param sign the asymmetric cypher, e.g., {@code RSA} + * @param full the full signature algorithm name, e.g., {@code SHA1withRSA} + * @return the pair with the private key and certificate + * @throws Exception failed to generate the signature data + */ + @Nonnull + public static ApkZLibPair generateSignature( + @Nonnull String sign, + @Nonnull String full) + throws Exception { + // http://stackoverflow.com/questions/28538785/ + // easy-way-to-generate-a-self-signed-certificate-for-java-security-keystore-using + + KeyPairGenerator generator = null; + try { + generator = KeyPairGenerator.getInstance(sign); + } catch (NoSuchAlgorithmException e) { + Assume.assumeNoException("Algorithm " + sign + " not supported.", e); + } + + assertNotNull(generator); + KeyPair keyPair = generator.generateKeyPair(); + + Date notBefore = new Date(System.currentTimeMillis() - 24 * 60 * 60 * 1000); + Date notAfter = new Date(System.currentTimeMillis() + 365L * 24 * 60 * 60 * 1000); + + X500Name issuer = new X500Name(new X500Principal("cn=Myself").getName()); + + SubjectPublicKeyInfo publicKeyInfo; + + if (keyPair.getPublic() instanceof RSAPublicKey) { + RSAPublicKey rsaPublicKey = (RSAPublicKey) keyPair.getPublic(); + publicKeyInfo = SubjectPublicKeyInfoFactory.createSubjectPublicKeyInfo( + new RSAKeyParameters(false, rsaPublicKey.getModulus(), + rsaPublicKey.getPublicExponent())); + } else if (keyPair.getPublic() instanceof ECPublicKey) { + publicKeyInfo = SubjectPublicKeyInfo.getInstance(keyPair.getPublic().getEncoded()); + } else { + fail(); + publicKeyInfo = null; + } + + X509v1CertificateBuilder builder = new X509v1CertificateBuilder(issuer, BigInteger.ONE, + notBefore, notAfter, issuer, publicKeyInfo); + + ContentSigner signer = new JcaContentSignerBuilder(full).setProvider( + new BouncyCastleProvider()).build(keyPair.getPrivate()); + X509CertificateHolder holder = builder.build(signer); + + JcaX509CertificateConverter converter = new JcaX509CertificateConverter() + .setProvider(new BouncyCastleProvider()); + + return new ApkZLibPair(keyPair.getPrivate(), converter.getCertificate(holder)); + } + +} diff --git a/src/test/java/com/android/tools/build/apkzlib/utils/ApkZFileTestUtils.java b/src/test/java/com/android/tools/build/apkzlib/utils/ApkZFileTestUtils.java new file mode 100644 index 0000000..217d3a1 --- /dev/null +++ b/src/test/java/com/android/tools/build/apkzlib/utils/ApkZFileTestUtils.java @@ -0,0 +1,139 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * 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.android.tools.build.apkzlib.utils; + +import static org.junit.Assert.assertTrue; + +import com.android.testutils.TestResources; +import com.android.tools.build.apkzlib.zip.ZFile; +import com.google.common.base.Preconditions; +import com.google.common.io.ByteSource; +import com.google.common.io.Resources; +import java.io.ByteArrayInputStream; +import java.io.EOFException; +import java.io.File; +import java.io.IOException; +import java.io.RandomAccessFile; +import javax.annotation.Nonnull; + +/** + * Utility functions for tests. + */ +public final class ApkZFileTestUtils { + + /** + * Reads a portion of a file to memory. + * + * @param file the file to read data from + * @param start the offset in the file to start reading + * @param length the number of bytes to read + * @return the bytes read + * @throws Exception failed to read the file + */ + @Nonnull + public static byte[] readSegment(@Nonnull File file, long start, int length) throws Exception { + Preconditions.checkArgument(start >= 0, "start < 0"); + Preconditions.checkArgument(length >= 0, "length < 0"); + + byte data[]; + try (RandomAccessFile raf = new RandomAccessFile(file, "r")) { + raf.seek(start); + + data = new byte[length]; + int tot = 0; + while (tot < length) { + int r = raf.read(data, tot, length - tot); + if (r < 0) { + throw new EOFException(); + } + + tot += r; + } + } + + return data; + } + + /** + * Obtains the test resource with the given path. + * + * @param path the path + * @return the test resource + */ + @Nonnull + public static File getResource(@Nonnull String path) { + File resource = TestResources.getFile(ApkZFileTestUtils.class, path); + assertTrue(resource.exists()); + return resource; + } + + /** + * Obtains the test resource with the given path. + * + * @param path the path + * @return the test resource + */ + @Nonnull + public static ByteSource getResourceBytes(@Nonnull String path) { + return Resources.asByteSource(Resources.getResource(ApkZFileTestUtils.class, path)); + } + + /** + * Sleeps the current thread for enough time to ensure that the local file system had enough + * time to notice a "tick". This method is usually called in tests when it is necessary to + * ensure filesystem writes are detected through timestamp modification. + * + * @param currentTimestamp last timestamp read from disk + * @throws InterruptedException waiting interrupted + * @throws IOException issues creating a temporary file + */ + public static void waitForFileSystemTick(long currentTimestamp) + throws InterruptedException, IOException { + while (getFreshTimestamp() <= currentTimestamp) { + Thread.sleep(100); + } + } + + /* + * Adds a basic compiled AndroidManifest to the given ZFile containing minSdkVersion equal 15 + * and targetSdkVersion equal 25. + */ + public static void addAndroidManifest(ZFile zf) throws IOException { + zf.add("AndroidManifest.xml", new ByteArrayInputStream(getAndroidManifest())); + } + + /* + * Provides a basic compiled AndroidManifest containing minSdkVersion equal 15 and + * targetSdkVersion equal 25. + */ + public static byte[] getAndroidManifest() throws IOException { + return ApkZFileTestUtils.getResourceBytes("/testData/packaging/AndroidManifest.xml").read(); + } + + /** + * Obtains the timestamp of a newly-created file. + * + * @return the timestamp + * @throws IOException the I/O Exception + */ + private static long getFreshTimestamp() throws IOException { + File notUsed = File.createTempFile(ApkZFileTestUtils.class.getName(), "waitForFSTick"); + long freshTimestamp = notUsed.lastModified(); + assertTrue(notUsed.delete()); + return freshTimestamp; + } +} diff --git a/src/test/java/com/android/tools/build/apkzlib/utils/CachedFileContentsTest.java b/src/test/java/com/android/tools/build/apkzlib/utils/CachedFileContentsTest.java new file mode 100644 index 0000000..dc28a0f --- /dev/null +++ b/src/test/java/com/android/tools/build/apkzlib/utils/CachedFileContentsTest.java @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * 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.android.tools.build.apkzlib.utils; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +import com.google.common.base.Charsets; +import com.google.common.io.Files; +import java.io.File; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +public class CachedFileContentsTest { + @Rule + public TemporaryFolder mTemporaryFolder = new TemporaryFolder(); + + @Test + public void createFileAndCheckWithNoChanges() throws Exception { + File f = mTemporaryFolder.newFile("test"); + Files.write("abc", f, Charsets.US_ASCII); + + Object cache = new Object(); + + CachedFileContents cachedFile = new CachedFileContents<>(f); + cachedFile.closed(cache); + + assertTrue(cachedFile.isValid()); + assertSame(cache, cachedFile.getCache()); + } + + @Test + public void createFileAndCheckChanges() throws Exception { + File f = mTemporaryFolder.newFile("test"); + Files.write("abc", f, Charsets.US_ASCII); + + Object cache = new Object(); + + CachedFileContents cachedFile = new CachedFileContents<>(f); + cachedFile.closed(cache); + + Files.write("def", f, Charsets.US_ASCII); + + assertFalse(cachedFile.isValid()); + assertNull(cachedFile.getCache()); + } + + @Test + public void createFileUpdateAndCheckChanges() throws Exception { + File f = mTemporaryFolder.newFile("test"); + Files.write("abc", f, Charsets.US_ASCII); + + Object cache = new Object(); + + CachedFileContents cachedFile = new CachedFileContents<>(f); + cachedFile.closed(cache); + + Files.write("def", f, Charsets.US_ASCII); + cachedFile.closed(cache); + + assertTrue(cachedFile.isValid()); + assertSame(cache, cachedFile.getCache()); + } + + @Test + public void immediateChangesDetected() throws Exception { + File f = mTemporaryFolder.newFile("foo"); + Files.write("bar", f, Charsets.US_ASCII); + + CachedFileContents cachedFile = new CachedFileContents<>(f); + cachedFile.closed(null); + + Files.write("xpto", f, Charsets.US_ASCII); + assertFalse(cachedFile.isValid()); + } + + @Test + public void immediateChangesDetectedEvenWithHackedTs() throws Exception { + File f = mTemporaryFolder.newFile("foo"); + Files.write("bar", f, Charsets.US_ASCII); + + CachedFileContents cachedFile = new CachedFileContents<>(f); + cachedFile.closed(null); + long lastTs = f.lastModified(); + + Files.write("xpto", f, Charsets.US_ASCII); + f.setLastModified(lastTs); + assertFalse(cachedFile.isValid()); + } + + @Test + public void immediateChangesWithNoContentChangeNotDetected() throws Exception { + File f = mTemporaryFolder.newFile("foo"); + Files.write("bar", f, Charsets.US_ASCII); + + CachedFileContents cachedFile = new CachedFileContents<>(f); + cachedFile.closed(null); + long lastTs = f.lastModified(); + + Files.write("bar", f, Charsets.US_ASCII); + f.setLastModified(lastTs); + assertTrue(cachedFile.isValid()); + } +} diff --git a/src/test/java/com/android/tools/build/apkzlib/utils/CachedSupplierTest.java b/src/test/java/com/android/tools/build/apkzlib/utils/CachedSupplierTest.java new file mode 100644 index 0000000..cd082dc --- /dev/null +++ b/src/test/java/com/android/tools/build/apkzlib/utils/CachedSupplierTest.java @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * 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.android.tools.build.apkzlib.utils; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.util.function.Supplier; +import org.junit.Test; + +public class CachedSupplierTest { + + @Test + public void testGetsOnlyOnce() { + TestSupplier ts = new TestSupplier(); + CachedSupplier cs = new CachedSupplier<>(ts); + assertFalse(cs.isValid()); + + ts.value = "foo"; + assertEquals(0, ts.invocationCount); + assertEquals("foo", cs.get()); + assertEquals(1, ts.invocationCount); + assertTrue(cs.isValid()); + + ts.value = "bar"; + assertEquals("foo", cs.get()); + assertEquals(1, ts.invocationCount); + assertTrue(cs.isValid()); + } + + @Test + public void cacheCanBePreset() { + TestSupplier ts = new TestSupplier(); + ts.value = "foo"; + CachedSupplier cs = new CachedSupplier<>(ts); + cs.precomputed("bar"); + assertTrue(cs.isValid()); + + assertEquals("bar", cs.get()); + assertEquals(0, ts.invocationCount); + } + + @Test + public void exceptionThrownBySupplier() { + CachedSupplier cs = new CachedSupplier<>(() -> { + throw new RuntimeException("foo"); + }); + assertFalse(cs.isValid()); + + try { + cs.get(); + fail(); + } catch (RuntimeException e) { + assertEquals("foo", e.getMessage()); + } + + assertFalse(cs.isValid()); + + try { + cs.get(); + fail(); + } catch (RuntimeException e) { + assertEquals("foo", e.getMessage()); + } + } + + @Test + public void reset() { + TestSupplier ts = new TestSupplier(); + ts.value = "foo"; + CachedSupplier cs = new CachedSupplier<>(ts); + assertFalse(cs.isValid()); + + assertEquals("foo", cs.get()); + assertEquals(1, ts.invocationCount); + assertTrue(cs.isValid()); + ts.value = "bar"; + + cs.reset(); + assertFalse(cs.isValid()); + assertEquals("bar", cs.get()); + assertEquals(2, ts.invocationCount); + } + + static class TestSupplier implements Supplier { + int invocationCount = 0; + String value; + + @Override + public String get() { + invocationCount++; + return value; + } + } +} diff --git a/src/test/java/com/android/tools/build/apkzlib/zfile/ApkAlignmentTest.java b/src/test/java/com/android/tools/build/apkzlib/zfile/ApkAlignmentTest.java new file mode 100644 index 0000000..eee979a --- /dev/null +++ b/src/test/java/com/android/tools/build/apkzlib/zfile/ApkAlignmentTest.java @@ -0,0 +1,231 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * 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.android.tools.build.apkzlib.zfile; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import com.android.tools.build.apkzlib.zip.CompressionMethod; +import com.android.tools.build.apkzlib.zip.StoredEntry; +import com.android.tools.build.apkzlib.zip.ZFile; +import com.android.tools.build.apkzlib.zip.ZFileOptions; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.nio.file.Files; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +public class ApkAlignmentTest { + @Rule public TemporaryFolder mTemporaryFolder = new TemporaryFolder(); + + @Test + public void soFilesUncompressedAndAligned() throws Exception { + File apk = new File(mTemporaryFolder.getRoot(), "a.apk"); + + File soFile = new File(mTemporaryFolder.getRoot(), "doesnt_work.so"); + Files.write(soFile.toPath(), new byte[500]); + + ApkZFileCreatorFactory cf = new ApkZFileCreatorFactory(new ZFileOptions()); + ApkCreatorFactory.CreationData creationData = + new ApkCreatorFactory.CreationData( + apk, + null, + null, + false, + false, + null, + null, + 20, + NativeLibrariesPackagingMode.UNCOMPRESSED_AND_ALIGNED, + path -> false); + + ApkCreator creator = cf.make(creationData); + + creator.writeFile(soFile, "/doesnt_work.so"); + creator.close(); + + try (ZFile zf = new ZFile(apk)) { + StoredEntry soEntry = zf.get("/doesnt_work.so"); + assertNotNull(soEntry); + assertEquals( + CompressionMethod.STORE, + soEntry.getCentralDirectoryHeader().getCompressionInfoWithWait().getMethod()); + long offset = + soEntry.getCentralDirectoryHeader().getOffset() + soEntry.getLocalHeaderSize(); + assertTrue(offset % 4096 == 0); + } + } + + @Test + public void soFilesMergedFromZipsCanBeUncompressedAndAligned() throws Exception { + + // Create a zip file with a compressed, unaligned so file. + File zipToMerge = new File(mTemporaryFolder.getRoot(), "a.zip"); + try (ZFile zf = new ZFile(zipToMerge)) { + zf.add("/zero.so", new ByteArrayInputStream(new byte[500])); + } + + try (ZFile zf = new ZFile(zipToMerge)) { + StoredEntry zeroSo = zf.get("/zero.so"); + assertNotNull(zeroSo); + assertEquals( + CompressionMethod.DEFLATE, + zeroSo.getCentralDirectoryHeader().getCompressionInfoWithWait().getMethod()); + long offset = + zeroSo.getCentralDirectoryHeader().getOffset() + zeroSo.getLocalHeaderSize(); + assertFalse(offset % 4096 == 0); + } + + // Create an APK and merge the zip file. + File apk = new File(mTemporaryFolder.getRoot(), "b.apk"); + ApkZFileCreatorFactory cf = new ApkZFileCreatorFactory(new ZFileOptions()); + ApkCreatorFactory.CreationData creationData = + new ApkCreatorFactory.CreationData( + apk, + null, + null, + false, + false, + null, + null, + 20, + NativeLibrariesPackagingMode.UNCOMPRESSED_AND_ALIGNED, + path -> false); + + try (ApkCreator creator = cf.make(creationData)) { + creator.writeZip(zipToMerge, null, null); + } + + // Make sure the file is uncompressed and aligned. + try (ZFile zf = new ZFile(apk)) { + StoredEntry soEntry = zf.get("/zero.so"); + assertNotNull(soEntry); + assertEquals( + CompressionMethod.STORE, + soEntry.getCentralDirectoryHeader().getCompressionInfoWithWait().getMethod()); + long offset = + soEntry.getCentralDirectoryHeader().getOffset() + soEntry.getLocalHeaderSize(); + assertTrue(offset % 4096 == 0); + + byte[] data = soEntry.read(); + assertEquals(500, data.length); + for (int i = 0; i < data.length; i++) { + assertEquals(0, data[i]); + } + } + } + + @Test + public void soFilesUncompressedAndNotAligned() throws Exception { + File apk = new File(mTemporaryFolder.getRoot(), "a.apk"); + + File soFile = new File(mTemporaryFolder.getRoot(), "doesnt_work.so"); + Files.write(soFile.toPath(), new byte[500]); + + ApkZFileCreatorFactory cf = new ApkZFileCreatorFactory(new ZFileOptions()); + ApkCreatorFactory.CreationData creationData = + new ApkCreatorFactory.CreationData( + apk, + null, + null, + false, + false, + null, + null, + 20, + NativeLibrariesPackagingMode.COMPRESSED, + path -> false); + + ApkCreator creator = cf.make(creationData); + + creator.writeFile(soFile, "/doesnt_work.so"); + creator.close(); + + try (ZFile zf = new ZFile(apk)) { + StoredEntry soEntry = zf.get("/doesnt_work.so"); + assertNotNull(soEntry); + assertEquals( + CompressionMethod.DEFLATE, + soEntry.getCentralDirectoryHeader().getCompressionInfoWithWait().getMethod()); + long offset = + soEntry.getCentralDirectoryHeader().getOffset() + soEntry.getLocalHeaderSize(); + assertTrue(offset % 4096 != 0); + } + } + + @Test + public void soFilesMergedFromZipsCanBeUncompressedAndNotAligned() throws Exception { + + // Create a zip file with a compressed, unaligned so file. + File zipToMerge = new File(mTemporaryFolder.getRoot(), "a.zip"); + try (ZFile zf = new ZFile(zipToMerge)) { + zf.add("/zero.so", new ByteArrayInputStream(new byte[500])); + } + + try (ZFile zf = new ZFile(zipToMerge)) { + StoredEntry zeroSo = zf.get("/zero.so"); + assertNotNull(zeroSo); + assertEquals( + CompressionMethod.DEFLATE, + zeroSo.getCentralDirectoryHeader().getCompressionInfoWithWait().getMethod()); + long offset = + zeroSo.getCentralDirectoryHeader().getOffset() + zeroSo.getLocalHeaderSize(); + assertFalse(offset % 4096 == 0); + } + + // Create an APK and merge the zip file. + File apk = new File(mTemporaryFolder.getRoot(), "b.apk"); + ApkZFileCreatorFactory cf = new ApkZFileCreatorFactory(new ZFileOptions()); + ApkCreatorFactory.CreationData creationData = + new ApkCreatorFactory.CreationData( + apk, + null, + null, + false, + false, + null, + null, + 20, + NativeLibrariesPackagingMode.COMPRESSED, + path -> false); + + try (ApkCreator creator = cf.make(creationData)) { + creator.writeZip(zipToMerge, null, null); + } + + // Make sure the file is uncompressed and aligned. + try (ZFile zf = new ZFile(apk)) { + StoredEntry soEntry = zf.get("/zero.so"); + assertNotNull(soEntry); + assertEquals( + CompressionMethod.DEFLATE, + soEntry.getCentralDirectoryHeader().getCompressionInfoWithWait().getMethod()); + long offset = + soEntry.getCentralDirectoryHeader().getOffset() + soEntry.getLocalHeaderSize(); + assertTrue(offset % 4096 != 0); + + byte[] data = soEntry.read(); + assertEquals(500, data.length); + for (int i = 0; i < data.length; i++) { + assertEquals(0, data[i]); + } + } + } +} diff --git a/src/test/java/com/android/tools/build/apkzlib/zip/AlignmentTest.java b/src/test/java/com/android/tools/build/apkzlib/zip/AlignmentTest.java new file mode 100644 index 0000000..4f54b3e --- /dev/null +++ b/src/test/java/com/android/tools/build/apkzlib/zip/AlignmentTest.java @@ -0,0 +1,856 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * 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.android.tools.build.apkzlib.zip; + +import static com.android.tools.build.apkzlib.utils.ApkZFileTestUtils.readSegment; +import static junit.framework.TestCase.assertEquals; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import com.google.common.base.Charsets; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.util.Random; +import java.util.Set; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; +import org.junit.Ignore; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +public class AlignmentTest { + + private static final AlignmentRule SUFFIX_ALIGNMENT_RULES = + AlignmentRules.compose( + // Disable 4-aligning of uncompressed *.u files, so we can more easily + // calculate offsets for testing. + AlignmentRules.constantForSuffix(".u", 1), + AlignmentRules.constantForSuffix(".a", 1024)); + @Rule + public TemporaryFolder mTemporaryFolder = new TemporaryFolder(); + + @Test + public void addAlignedFile() throws Exception { + File newZFile = new File(mTemporaryFolder.getRoot(), "test.zip"); + + byte testBytes[] = "This is some text.".getBytes(Charsets.US_ASCII); + + ZFileOptions options = new ZFileOptions(); + options.setAlignmentRule(AlignmentRules.constantForSuffix(".txt", 1024)); + try (ZFile zf = new ZFile(newZFile, options)) { + zf.add("test.txt", new ByteArrayInputStream(testBytes), false); + } + + byte found[] = readSegment(newZFile, 1024, testBytes.length); + assertArrayEquals(testBytes, found); + } + + @Test + public void addNonAlignedFile() throws Exception { + File newZFile = new File(mTemporaryFolder.getRoot(), "test.zip"); + + byte testBytes[] = "This is some text.".getBytes(Charsets.US_ASCII); + + ZFileOptions options = new ZFileOptions(); + options.setAlignmentRule(AlignmentRules.constantForSuffix(".txt", 1024)); + try (ZFile zf = new ZFile(newZFile, options)) { + zf.add("test.txt.foo", new ByteArrayInputStream(testBytes), false); + } + + assertTrue(newZFile.length() < 1024); + } + + @Test + public void realignSingleFile() throws Exception { + File newZFile = new File(mTemporaryFolder.getRoot(), "test.zip"); + + byte testBytes0[] = "Text number 1".getBytes(Charsets.US_ASCII); + byte testBytes1[] = "Text number 2, which is actually 1".getBytes(Charsets.US_ASCII); + + long offset0; + try (ZFile zf = new ZFile(newZFile)) { + zf.add("file1.txt", new ByteArrayInputStream(testBytes1), false); + zf.add("file0.txt", new ByteArrayInputStream(testBytes0), false); + zf.close(); + + StoredEntry se0 = zf.get("file0.txt"); + assertNotNull(se0); + offset0 = se0.getCentralDirectoryHeader().getOffset(); + + StoredEntry se1 = zf.get("file1.txt"); + assertNotNull(se1); + + assertTrue(newZFile.length() < 1024); + } + + ZFileOptions options = new ZFileOptions(); + options.setAlignmentRule(AlignmentRules.constantForSuffix(".txt", 1024)); + try (ZFile zf = new ZFile(newZFile, options)) { + StoredEntry se1 = zf.get("file1.txt"); + assertNotNull(se1); + se1.realign(); + zf.close(); + + StoredEntry se0 = zf.get("file0.txt"); + assertNotNull(se0); + assertEquals(offset0, se0.getCentralDirectoryHeader().getOffset()); + + se1 = zf.get("file1.txt"); + assertNotNull(se1); + assertTrue(se1.getCentralDirectoryHeader().getOffset() > 950); + assertTrue(se1.getCentralDirectoryHeader().getOffset() < 1024); + assertArrayEquals(testBytes1, readSegment(newZFile, 1024, testBytes1.length)); + + assertTrue(newZFile.length() > 1024); + } + } + + @Test + public void realignFile() throws Exception { + File newZFile = new File(mTemporaryFolder.getRoot(), "test.zip"); + + byte testBytes0[] = "Text number 1".getBytes(Charsets.US_ASCII); + byte testBytes1[] = "Text number 2, which is actually 1".getBytes(Charsets.US_ASCII); + + try (ZFile zf = new ZFile(newZFile)) { + zf.add("file0.txt", new ByteArrayInputStream(testBytes0), false); + zf.add("file1.txt", new ByteArrayInputStream(testBytes1), false); + } + + assertTrue(newZFile.length() < 1024); + + ZFileOptions options = new ZFileOptions(); + options.setAlignmentRule(AlignmentRules.constantForSuffix(".txt", 1024)); + try (ZFile zf = new ZFile(newZFile, options)) { + zf.realign(); + zf.update(); + + StoredEntry se0 = zf.get("file0.txt"); + assertNotNull(se0); + long off0 = 1024; + + StoredEntry se1 = zf.get("file1.txt"); + assertNotNull(se1); + long off1 = 2048; + + /* + * ZFile does not guarantee any order. + */ + if (se1.getCentralDirectoryHeader().getOffset() < + se0.getCentralDirectoryHeader().getOffset()) { + off0 = 2048; + off1 = 1024; + } + + assertArrayEquals(testBytes0, readSegment(newZFile, off0, testBytes0.length)); + assertArrayEquals(testBytes1, readSegment(newZFile, off1, testBytes1.length)); + } + } + + @Test + public void realignAlignedEntry() throws Exception { + File newZFile = new File(mTemporaryFolder.getRoot(), "test.zip"); + + byte testBytes[] = "This is some text.".getBytes(Charsets.US_ASCII); + + ZFileOptions options = new ZFileOptions(); + options.setAlignmentRule(AlignmentRules.constantForSuffix(".txt", 1024)); + try (ZFile zf = new ZFile(newZFile, options)) { + zf.add("test.txt", new ByteArrayInputStream(testBytes), false); + } + + assertArrayEquals(testBytes, readSegment(newZFile, 1024, testBytes.length)); + + int flen = (int) newZFile.length(); + + try (ZFile zf = new ZFile(newZFile)) { + StoredEntry entry = zf.get("test.txt"); + assertNotNull(entry); + assertFalse(entry.realign()); + } + + assertEquals(flen, (int) newZFile.length()); + assertArrayEquals(testBytes, readSegment(newZFile, 1024, testBytes.length)); + } + + @Test + public void alignmentRulesDoNotAffectAddedFiles() throws Exception { + File newZFile = new File(mTemporaryFolder.getRoot(), "test.zip"); + + byte testBytes0[] = "Text number 1".getBytes(Charsets.US_ASCII); + byte testBytes1[] = "Text number 2, which is actually 1".getBytes(Charsets.US_ASCII); + + try (ZFile zf = new ZFile(newZFile)) { + zf.add("file0.txt", new ByteArrayInputStream(testBytes0), false); + } + + ZFileOptions options = new ZFileOptions(); + options.setAlignmentRule(AlignmentRules.constantForSuffix(".txt", 1024)); + try (ZFile zf = new ZFile(newZFile, options)) { + zf.add("file1.txt", new ByteArrayInputStream(testBytes1), false); + zf.update(); + + StoredEntry se0 = zf.get("file0.txt"); + assertNotNull(se0); + + StoredEntry se1 = zf.get("file1.txt"); + assertNotNull(se1); + assertArrayEquals(testBytes1, readSegment(newZFile, 1024, testBytes1.length)); + } + } + + @Test + public void realignStreamedZip() throws Exception { + File zipFile = new File(mTemporaryFolder.getRoot(), "test.zip"); + + byte[] pattern = new byte[1024]; + new Random().nextBytes(pattern); + + String name = ""; + try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zipFile))) { + for (int j = 0; j < 10; j++) { + name = name + "a"; + ZipEntry ze = new ZipEntry(name); + zos.putNextEntry(ze); + for (int i = 0; i < 1000; i++) { + zos.write(pattern); + } + } + } + + ZFileOptions options = new ZFileOptions(); + options.setAlignmentRule(AlignmentRules.constant(10)); + try (ZFile zf = new ZFile(zipFile, options)) { + zf.realign(); + } + } + + @Test + public void alignFirstEntryUsingExtraField() throws Exception { + File zipFile = new File(mTemporaryFolder.getRoot(), "test.zip"); + + byte[] recognizable = new byte[] { 1, 2, 3, 4, 4, 3, 2, 1 }; + + ZFileOptions options = new ZFileOptions(); + options.setCoverEmptySpaceUsingExtraField(true); + options.setAlignmentRule(AlignmentRules.constant(1024)); + try (ZFile zf = new ZFile(zipFile, options)) { + zf.add("foo", new ByteArrayInputStream(recognizable), false); + } + + /* + * Contents should be at 1024 bytes. + */ + assertArrayEquals(recognizable, readSegment(zipFile, 1024, recognizable.length)); + + /* + * But local header should be in the beginning. + */ + try (ZFile zf = new ZFile(zipFile)) { + StoredEntry entry = zf.get("foo"); + assertNotNull(entry); + assertEquals(0, entry.getCentralDirectoryHeader().getOffset()); + } + } + + @Test + public void alignFirstEntryUsingOffset() throws Exception { + File zipFile = new File(mTemporaryFolder.getRoot(), "test.zip"); + + byte[] recognizable = new byte[] { 1, 2, 3, 4, 4, 3, 2, 1 }; + + ZFileOptions options = new ZFileOptions(); + options.setCoverEmptySpaceUsingExtraField(false); + options.setAlignmentRule(AlignmentRules.constant(1024)); + try (ZFile zf = new ZFile(zipFile, options)) { + zf.add("foo", new ByteArrayInputStream(recognizable), false); + } + + /* + * Contents should be at 1024 bytes. + */ + assertArrayEquals(recognizable, readSegment(zipFile, 1024, recognizable.length)); + + /* + * Local header should start at 991 (1024 - LOCAL_HEADER_SIZE - 3). + */ + try (ZFile zf = new ZFile(zipFile)) { + StoredEntry entry = zf.get("foo"); + assertNotNull(entry); + assertEquals(991, entry.getCentralDirectoryHeader().getOffset()); + } + } + + @Test + public void alignMiddleEntryUsingExtraField() throws Exception { + File zipFile = new File(mTemporaryFolder.getRoot(), "test.zip"); + + byte[] recognizable = new byte[] { 1, 2, 3, 4, 4, 3, 2, 1 }; + + ZFileOptions options = new ZFileOptions(); + options.setCoverEmptySpaceUsingExtraField(true); + options.setAlignmentRule(SUFFIX_ALIGNMENT_RULES); + try (ZFile zf = new ZFile(zipFile, options)) { + zf.add("first.u", new ByteArrayInputStream(new byte[1024]), false); + zf.add("middle.a", new ByteArrayInputStream(recognizable), false); + zf.add("last.u", new ByteArrayInputStream(new byte[1024]), false); + } + + /* + * Contents should be at 2048 bytes. + */ + assertArrayEquals(recognizable, readSegment(zipFile, 2048, recognizable.length)); + + /* + * But local header should be right after the first entry. + */ + try (ZFile zf = new ZFile(zipFile)) { + StoredEntry middleEntry = zf.get("middle.a"); + assertNotNull(middleEntry); + assertEquals( + ZFileTestConstants.LOCAL_HEADER_SIZE + "first.u".length() + 1024, + middleEntry.getCentralDirectoryHeader().getOffset()); + } + } + + @Test + public void alignMiddleEntryUsingOffset() throws Exception { + File zipFile = new File(mTemporaryFolder.getRoot(), "test.zip"); + + byte[] recognizable = new byte[] { 1, 2, 3, 4, 4, 3, 2, 1 }; + + ZFileOptions options = new ZFileOptions(); + options.setCoverEmptySpaceUsingExtraField(false); + options.setAlignmentRule(AlignmentRules.constantForSuffix(".a", 1024)); + try (ZFile zf = new ZFile(zipFile, options)) { + zf.add("bar1", new ByteArrayInputStream(new byte[1024]), false); + zf.add("foo.a", new ByteArrayInputStream(recognizable), false); + zf.add("bar2", new ByteArrayInputStream(new byte[1024]), false); + } + + /* + * Contents should be at 2048 bytes. + */ + assertArrayEquals(recognizable, readSegment(zipFile, 2048, recognizable.length)); + + /* + * Local header should start at 2015 (2048 - LOCAL_HEADER_SIZE - 5). + */ + try (ZFile zf = new ZFile(zipFile)) { + StoredEntry entry = zf.get("foo.a"); + assertNotNull(entry); + assertEquals(2013, entry.getCentralDirectoryHeader().getOffset()); + } + } + + @Test + public void alignUsingOffsetAllowsSmallSpaces() throws Exception { + File zipFile = new File(mTemporaryFolder.getRoot(), "test.zip"); + + int fixedLh = ZFileTestConstants.LOCAL_HEADER_SIZE + 3; + + byte[] recognizable = new byte[] { 1, 2, 3, 4, 4, 3, 2, 1 }; + + ZFileOptions options = new ZFileOptions(); + options.setCoverEmptySpaceUsingExtraField(false); + options.setAlignmentRule(AlignmentRules.constant(fixedLh)); + try (ZFile zf = new ZFile(zipFile, options)) { + zf.add("f", new ByteArrayInputStream(recognizable), false); + } + + assertArrayEquals(recognizable, readSegment(zipFile, fixedLh, recognizable.length)); + } + + @Test + public void alignUsingExtraFieldDoesNotAllowSmallSpaces() throws Exception { + File zipFile = new File(mTemporaryFolder.getRoot(), "test.zip"); + + int fixedLh = ZFileTestConstants.LOCAL_HEADER_SIZE + 3; + + byte[] recognizable = new byte[] { 1, 2, 3, 4, 4, 3, 2, 1 }; + + ZFileOptions options = new ZFileOptions(); + options.setCoverEmptySpaceUsingExtraField(true); + options.setAlignmentRule(AlignmentRules.constant(fixedLh)); + try (ZFile zf = new ZFile(zipFile, options)) { + zf.add("f", new ByteArrayInputStream(recognizable), false); + } + + assertArrayEquals(recognizable, readSegment(zipFile, fixedLh * 2, recognizable.length)); + } + + @Test + public void extraFieldSpaceUsedForAlignmentCanBeReclaimedBeforeUpdate() throws Exception { + File zipFile = new File(mTemporaryFolder.getRoot(), "test.zip"); + + byte[] recognizable1 = new byte[] { 1, 2, 3, 4, 4, 3, 2, 1 }; + byte[] recognizable2 = new byte[] { 9, 9, 8, 8, 7, 7, 6, 6, 5, 5, 4, 4 }; + + ZFileOptions options = new ZFileOptions(); + options.setCoverEmptySpaceUsingExtraField(true); + options.setAlignmentRule(SUFFIX_ALIGNMENT_RULES); + try (ZFile zf = new ZFile(zipFile, options)) { + zf.add("f.a", new ByteArrayInputStream(recognizable1), false); + zf.add("f.u", new ByteArrayInputStream(recognizable2), false); + } + + assertArrayEquals(recognizable1, readSegment(zipFile, 1024, recognizable1.length)); + assertArrayEquals( + recognizable2, + readSegment( + zipFile, + ZFileTestConstants.LOCAL_HEADER_SIZE + "f.u".length(), + recognizable2.length)); + } + + @Test + @Ignore("See ZFile.readData() contents to understand why this is ignored") + public void extraFieldSpaceUsedForAlignmentCanBeReclaimedAfterUpdate() throws Exception { + File zipFile = new File(mTemporaryFolder.getRoot(), "test.zip"); + + byte[] recognizable1 = new byte[] { 1, 2, 3, 4, 4, 3, 2, 1 }; + byte[] recognizable2 = new byte[] { 9, 9, 8, 8, 7, 7, 6, 6, 5, 5, 4, 4 }; + + ZFileOptions options = new ZFileOptions(); + options.setCoverEmptySpaceUsingExtraField(true); + options.setAlignmentRule(AlignmentRules.constantForSuffix(".a", 1024)); + try (ZFile zf = new ZFile(zipFile, options)) { + zf.add("f.a", new ByteArrayInputStream(recognizable1), false); + } + + try (ZFile zf = new ZFile(zipFile, options)) { + zf.add("f.b", new ByteArrayInputStream(recognizable2), false); + } + + assertArrayEquals(recognizable1, readSegment(zipFile, 1024, recognizable1.length)); + assertArrayEquals( + recognizable2, + readSegment( + zipFile, + ZFileTestConstants.LOCAL_HEADER_SIZE + "f.b".length(), + recognizable2.length)); + } + + @Test + public void fillEmptySpaceWithExtraFieldAfterDelete() throws Exception { + File zipFile = new File(mTemporaryFolder.getRoot(), "large.zip"); + + byte[] recognizable1 = new byte[] { 1, 2, 3, 4, 4, 3, 2, 1 }; + byte[] recognizable2 = new byte[] { 9, 8, 7, 6, 5, 4, 3, 2 }; + + ZFileOptions options = new ZFileOptions(); + options.setCoverEmptySpaceUsingExtraField(true); + options.setAlignmentRule(SUFFIX_ALIGNMENT_RULES); + try (ZFile zf = new ZFile(zipFile, options)) { + zf.add("first.u", new ByteArrayInputStream(recognizable1), false); + zf.add("second.u", new ByteArrayInputStream(recognizable2), false); + + zf.update(); + + StoredEntry firstEntry = zf.get("first.u"); + assertNotNull(firstEntry); + firstEntry.delete(); + } + + try (ZFile zf = new ZFile(zipFile)) { + Set entries = zf.entries(); + assertEquals(1, entries.size()); + + StoredEntry entry = entries.iterator().next(); + assertEquals("second.u", entry.getCentralDirectoryHeader().getName()); + assertEquals(0, entry.getCentralDirectoryHeader().getOffset()); + assertEquals( + ZFileTestConstants.LOCAL_HEADER_SIZE + + "first.u".length() + + recognizable1.length, + entry.getLocalExtra().size()); + } + } + + @Test + public void fillInLargeGapsWithExtraField() throws Exception { + File zipFile = new File(mTemporaryFolder.getRoot(), "large.zip"); + + byte[] recognizable1 = new byte[] { 1, 2, 3, 4, 4, 3, 2, 1 }; + byte[] recognizable2 = new byte[] { 9, 8, 7, 6, 5, 4, 3, 2 }; + byte[] bigEmpty = new byte[10 * 1024]; + + ZFileOptions options = new ZFileOptions(); + options.setCoverEmptySpaceUsingExtraField(true); + options.setAlignmentRule(SUFFIX_ALIGNMENT_RULES); + try (ZFile zf = new ZFile(zipFile, options)) { + zf.add("begin.u", new ByteArrayInputStream(recognizable1), false); + zf.add("middle.u", new ByteArrayInputStream(bigEmpty), false); + zf.add("end.u", new ByteArrayInputStream(recognizable2), false); + + zf.update(); + + StoredEntry middleEntry = zf.get("middle.u"); + assertNotNull(middleEntry); + middleEntry.delete(); + } + + /* + * Find the two recognizable files. + */ + int recognizable1Start = ZFileTestConstants.LOCAL_HEADER_SIZE + "begin.u".length(); + assertArrayEquals( + recognizable1, + readSegment(zipFile, recognizable1Start, recognizable1.length)); + + int recognizable2Start = + 3 * ZFileTestConstants.LOCAL_HEADER_SIZE + + "begin.u".length() + + "middle.u".length() + + "end.u".length() + + recognizable1.length + + bigEmpty.length; + assertArrayEquals( + recognizable2, + readSegment(zipFile, recognizable2Start, recognizable2.length)); + } + + @Test + public void fillHoleWithExactEntry() throws Exception { + File zipFile = new File(mTemporaryFolder.getRoot(), "a.zip"); + + Random random = new Random(); + + byte[] fourtyFour = new byte[44]; + random.nextBytes(fourtyFour); + byte[] recognizable = new byte[] { 1, 5, 5, 1, 5, 1, 1, 5 }; + byte[] twoHundred = new byte[200]; + random.nextBytes(twoHundred); + + /* + * Start | Header End | Name end | Contents End | Name + * 0 | 30 | 59 | 103 | "File taking exactly 103 bytes" + * 103 | 133 | 136 | 144 | "foo" + * 144 | 174 | 196 | 396 | "File taking more space" + */ + try (ZFile zf = new ZFile(zipFile)) { + zf.add("File taking exactly 103 bytes", new ByteArrayInputStream(fourtyFour), false); + zf.add("foo", new ByteArrayInputStream(recognizable), false); + zf.add("File taking more space", new ByteArrayInputStream(twoHundred), false); + } + + assertArrayEquals(fourtyFour, readSegment(zipFile, 59, fourtyFour.length)); + assertArrayEquals(recognizable, readSegment(zipFile, 136, recognizable.length)); + assertArrayEquals(twoHundred, readSegment(zipFile, 196, twoHundred.length)); + + /* + * Remove the middle file. + */ + try (ZFile zf = new ZFile(zipFile)) { + StoredEntry fooEntry = zf.get("foo"); + assertNotNull(fooEntry); + fooEntry.delete(); + } + + /* + * Add the file again with 4-byte alignment. Because the file fits exactly in the hole, it + * is placed there. + */ + byte[] recognizable2 = new byte[] { 2, 6, 6, 2, 6, 2, 2, 6 }; + + ZFileOptions zfo = new ZFileOptions(); + zfo.setCoverEmptySpaceUsingExtraField(true); + zfo.setAlignmentRule(AlignmentRules.constant(4)); + try (ZFile zf = new ZFile(zipFile, zfo)) { + zf.add("bar", new ByteArrayInputStream(recognizable2), false); + } + + assertArrayEquals(fourtyFour, readSegment(zipFile, 59, fourtyFour.length)); + assertArrayEquals(recognizable2, readSegment(zipFile, 136, recognizable2.length)); + assertArrayEquals(twoHundred, readSegment(zipFile, 196, twoHundred.length)); + } + + @Test + public void fillHoleWithSmallEntry() throws Exception { + File zipFile = new File(mTemporaryFolder.getRoot(), "a.zip"); + + Random random = new Random(); + + byte[] fourtyFour = new byte[44]; + random.nextBytes(fourtyFour); + byte[] recognizable = new byte[] { 1, 5, 5, 1, 5, 1, 1, 5, 1, 5, 5, 1, 5, 1, 1, 5 }; + byte[] twoHundred = new byte[200]; + random.nextBytes(twoHundred); + + /* + * Start | Header End | Name end | Contents End | Name + * 0 | 30 | 59 | 103 | "File taking exactly 103 bytes" + * 103 | 133 | 136 | 152 | "foo" + * 152 | 182 | 204 | 404 | "File taking more space" + */ + try (ZFile zf = new ZFile(zipFile)) { + zf.add("File taking exactly 103 bytes", new ByteArrayInputStream(fourtyFour), false); + zf.add("foo", new ByteArrayInputStream(recognizable), false); + zf.add("File taking more space", new ByteArrayInputStream(twoHundred), false); + } + + assertArrayEquals(fourtyFour, readSegment(zipFile, 59, fourtyFour.length)); + assertArrayEquals(recognizable, readSegment(zipFile, 136, recognizable.length)); + assertArrayEquals(twoHundred, readSegment(zipFile, 204, twoHundred.length)); + + /* + * Remove the middle file. + */ + try (ZFile zf = new ZFile(zipFile)) { + StoredEntry fooEntry = zf.get("foo"); + assertNotNull(fooEntry); + fooEntry.delete(); + } + + /* + * Add a smaller file. It should fit nicely as: + * + * Start | Header End | Name end | Contents End | Name + * 0 | 30 | 59 | 103 | "File taking exactly 103 bytes" + * 103 | 133 | 136 | 140 | "bar" + * 140 - 152 (empty) + * 152 | 182 | 204 | 404 | "File taking more space" + */ + byte[] recognizable2 = new byte[] { 7, 7, 7, 7 }; + + ZFileOptions zfo = new ZFileOptions(); + zfo.setCoverEmptySpaceUsingExtraField(true); + zfo.setAlignmentRule(AlignmentRules.constant(4)); + try (ZFile zf = new ZFile(zipFile, zfo)) { + zf.add("bar", new ByteArrayInputStream(recognizable2), false); + } + + assertArrayEquals(fourtyFour, readSegment(zipFile, 59, fourtyFour.length)); + assertArrayEquals(recognizable2, readSegment(zipFile, 136, recognizable2.length)); + assertArrayEquals(twoHundred, readSegment(zipFile, 204, twoHundred.length)); + } + + @Test + public void fillHoleWithSmallerEntryNotEnoughFreeSpace() throws Exception { + File zipFile = new File(mTemporaryFolder.getRoot(), "a.zip"); + + Random random = new Random(); + + byte[] fourtyFour = new byte[44]; + random.nextBytes(fourtyFour); + byte[] recognizable = new byte[] { 1, 5, 5, 1, 5, 1, 1, 5, 1, 5, 5, 1, 5, 1, 1, 5 }; + byte[] twoHundred = new byte[200]; + random.nextBytes(twoHundred); + + /* + * Start | Header End | Name end | Contents End | Name + * 0 | 30 | 59 | 103 | "File taking exactly 103 bytes" + * 103 | 133 | 136 | 152 | "foo" + * 152 | 182 | 204 | 404 | "File taking more space" + */ + try (ZFile zf = new ZFile(zipFile)) { + zf.add("File taking exactly 103 bytes", new ByteArrayInputStream(fourtyFour), false); + zf.add("foo", new ByteArrayInputStream(recognizable), false); + zf.add("File taking more space", new ByteArrayInputStream(twoHundred), false); + } + + assertArrayEquals(fourtyFour, readSegment(zipFile, 59, fourtyFour.length)); + assertArrayEquals(recognizable, readSegment(zipFile, 136, recognizable.length)); + assertArrayEquals(twoHundred, readSegment(zipFile, 204, twoHundred.length)); + + /* + * Remove the middle file. + */ + try (ZFile zf = new ZFile(zipFile)) { + StoredEntry fooEntry = zf.get("foo"); + assertNotNull(fooEntry); + fooEntry.delete(); + } + + /* + * Add a smaller file. But it can't fit because it would leave less than 6 bytes to + * cover in the next file: + * + * Start | Header End | Name end | Contents End | Name + * 0 | 30 | 59 | 103 | "File taking exactly 103 bytes" + * 103 | 133 | 136 | 148 | "foo" + * 148 - 152 (empty) + * 152 | 182 | 204 | 404 | "File taking more space" + * + * So we end up with: + * + * Start | Header End | Name end | Contents End | Name + * 0 | 30 | 59 | 103 | "File taking exactly 103 bytes" + * 152 | 182 | 204 | 404 | "File taking more space" + * 404 | 434 -> 441 | 444 | 456 | "bar" + */ + byte[] recognizable2 = new byte[] { 7, 7, 7, 7, 8, 8, 8, 8, 9, 9, 9, 9 }; + + ZFileOptions zfo = new ZFileOptions(); + zfo.setCoverEmptySpaceUsingExtraField(true); + zfo.setAlignmentRule(AlignmentRules.constant(4)); + try (ZFile zf = new ZFile(zipFile, zfo)) { + zf.add("bar", new ByteArrayInputStream(recognizable2), false); + } + + assertArrayEquals(fourtyFour, readSegment(zipFile, 59, fourtyFour.length)); + assertArrayEquals(recognizable2, readSegment(zipFile, 444, recognizable2.length)); + assertArrayEquals(twoHundred, readSegment(zipFile, 204, twoHundred.length)); + } + + @Test + public void fillHoleWithSmallerEntryEnoughFreeSpaceButRequiresExtraOffset() throws Exception { + File zipFile = new File(mTemporaryFolder.getRoot(), "a.zip"); + + Random random = new Random(); + + byte[] fourtyFour = new byte[44]; + random.nextBytes(fourtyFour); + byte[] recognizable = new byte[] { 1, 5, 5, 1, 5, 1, 1, 5, 1, 5, 5, 1, 5, 1, 1, 5 }; + byte[] twoHundred = new byte[200]; + random.nextBytes(twoHundred); + + /* + * Start | Header End | Name end | Contents End | Name + * 0 | 30 | 59 | 103 | "File taking exactly 103 bytes" + * 103 | 133 | 136 | 152 | "foo" + * 152 | 182 | 204 | 404 | "File taking more space" + */ + try (ZFile zf = new ZFile(zipFile)) { + zf.add("File taking exactly 103 bytes", new ByteArrayInputStream(fourtyFour), false); + zf.add("foo", new ByteArrayInputStream(recognizable), false); + zf.add("File taking more space", new ByteArrayInputStream(twoHundred), false); + } + + assertArrayEquals(fourtyFour, readSegment(zipFile, 59, fourtyFour.length)); + assertArrayEquals(recognizable, readSegment(zipFile, 136, recognizable.length)); + assertArrayEquals(twoHundred, readSegment(zipFile, 204, twoHundred.length)); + + /* + * Remove the middle file. + */ + try (ZFile zf = new ZFile(zipFile)) { + StoredEntry fooEntry = zf.get("foo"); + assertNotNull(fooEntry); + fooEntry.delete(); + } + + /* + * Add a smaller file. It will fit, but not aligned at 140 because that would require + * adding less than 6 bytes in the local header. It has to move to 150. + * + * Start | Header End | Name end | Contents End | Name + * 0 | 30 | 59 | 103 | "File taking exactly 103 bytes" + * 103 | 133 | 150 | 152 | "foo" + * 152 | 182 | 204 | 404 | "File taking more space" + */ + byte[] recognizable2 = new byte[] { 10, 10 }; + + ZFileOptions zfo = new ZFileOptions(); + zfo.setCoverEmptySpaceUsingExtraField(true); + zfo.setAlignmentRule(AlignmentRules.constant(10)); + try (ZFile zf = new ZFile(zipFile, zfo)) { + zf.add("bar", new ByteArrayInputStream(recognizable2), false); + } + + assertArrayEquals(fourtyFour, readSegment(zipFile, 59, fourtyFour.length)); + assertArrayEquals(recognizable2, readSegment(zipFile, 150, recognizable2.length)); + assertArrayEquals(twoHundred, readSegment(zipFile, 204, twoHundred.length)); + } + + @Test + public void alignCoveringEmptySpaceWhenExtraFieldIsInvalid() throws Exception { + File zipFile = new File(mTemporaryFolder.getRoot(), "a.zip"); + ZFileOptions options = new ZFileOptions(); + options.setCoverEmptySpaceUsingExtraField(true); + options.setAlignmentRule(AlignmentRules.constant(100)); + try (ZFile zf = new ZFile(zipFile, options)) { + zf.add("foo", new ByteArrayInputStream(new byte[] { 1, 2, 3, 4 })); + StoredEntry foo = zf.get("foo"); + assertNotNull(foo); + foo.setLocalExtra(new ExtraField(new byte[] { 0, 0 })); + zf.add("bar", new ByteArrayInputStream(new byte[] { 5, 6, 7, 8 })); + } + } + + @Test + public void fourByteAlignment() throws Exception { + // When aligning with 4 bytes, there are are only 3 possible cases: + // - We're 2 bytes short and so need to add +6 bytes (6 bytes for header + no zeroes) + // - We're 3 bytes short and so need to add +7 bytes (6 bytes for header + 1 zero) + // - We're 1 byte short and so need to add +9 bytes (6 bytes for header + 3 zeroes) + + File zipFile = new File(mTemporaryFolder.getRoot(), "a.zip"); + ZFileOptions options = new ZFileOptions(); + options.setCoverEmptySpaceUsingExtraField(true); + options.setAlignmentRule(AlignmentRules.constant(4)); + try (ZFile zf = new ZFile(zipFile, options)) { + // File header starts at 0. + // File name starts at 30 (LOCAL_HEADER_SIZE). + // If unaligned we would have data starting at 33, but with aligned we have data + // starting at 40 (36 isn't enough for the extra data header). + String fooName = "foo"; + byte[] fooData = new byte[] { 1, 2, 3, 4, 5 }; + zf.add(fooName, new ByteArrayInputStream(fooData), false); + zf.update(); + StoredEntry foo = zf.get(fooName); + long fooOffset = ZFileTestConstants.LOCAL_HEADER_SIZE + fooName.length() + 7; + assertEquals(fooOffset, foo.getLocalHeaderSize()); + + // Bar header starts at 45 (foo data starts at 40 and is 5 bytes long). + // Bar header ends at 75. + // If unaligned we would have data starting at 78, but with aligned we have data + // starting at 84 (80 isn't enough for the extra header). + String barName = "bar"; + byte[] barData = new byte[] { 6 }; + zf.add(barName, new ByteArrayInputStream(barData), false); + zf.update(); + + StoredEntry bar = zf.get(barName); + long barStart = bar.getCentralDirectoryHeader().getOffset(); + assertEquals(fooOffset + fooData.length, barStart); + + long barStartOffset = ZFileTestConstants.LOCAL_HEADER_SIZE + barName.length() + 6; + assertEquals(barStartOffset, bar.getLocalHeaderSize()); + + // Xpto header starts at 85 (bar data starts at 84 and is 1 byte long). + // Xpto header ends at 115. + // If unaligned we would have data starting at 119, but with aligned we have data + // starting at 128 (120 & 124 are not enough for the extra header). + String xptoName = "xpto"; + byte[] xptoData = new byte[] { 7, 8, 9, 10 }; + zf.add(xptoName, new ByteArrayInputStream(xptoData), false); + zf.update(); + + StoredEntry xpto = zf.get(xptoName); + long xptoStart = xpto.getCentralDirectoryHeader().getOffset(); + assertEquals(barStart + barStartOffset + barData.length, xptoStart); + + long xptoStartOffset = ZFileTestConstants.LOCAL_HEADER_SIZE + xptoName.length() + 9; + assertEquals(xptoStartOffset, xpto.getLocalHeaderSize()); + + // Dummy header starts at 133 (xpto data starts at 128 and is 6 bytes long). + String dummyName = "dummy"; + byte[] dummyData = new byte[] { 11 }; + zf.add(dummyName, new ByteArrayInputStream(dummyData), false); + zf.update(); + + StoredEntry dummy = zf.get(dummyName); + long dummyStart = dummy.getCentralDirectoryHeader().getOffset(); + assertEquals(xptoStart + xptoStartOffset + xptoData.length, dummyStart); + } + } +} diff --git a/src/test/java/com/android/tools/build/apkzlib/zip/EncodeUtilsTest.java b/src/test/java/com/android/tools/build/apkzlib/zip/EncodeUtilsTest.java new file mode 100644 index 0000000..adae865 --- /dev/null +++ b/src/test/java/com/android/tools/build/apkzlib/zip/EncodeUtilsTest.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * 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.android.tools.build.apkzlib.zip; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +public class EncodeUtilsTest { + @Test + public void canEncodeAsciiWithAsciiString() { + assertTrue(EncodeUtils.canAsciiEncode("foo")); + } + + @Test + public void cannotEncodeAscuuWithUtf8String() { + String greekInGreek ="\u3b53\ubb3b\ub3b7\u3bd3\ub93b\ua3ac"; + assertFalse(EncodeUtils.canAsciiEncode(greekInGreek)); + } + + @Test + public void asciiEncodeAndDecode() { + String text = "foo"; + GPFlags flags = GPFlags.make(false); + + byte[] encoded = EncodeUtils.encode(text, flags); + assertArrayEquals(new byte[] { 0x66, 0x6f, 0x6f }, encoded); + assertEquals(text, EncodeUtils.decode(encoded, flags)); + } + + @Test + public void utf8EncodeAndDecode() { + String kazakhCapital = "\u0410\u0441\u0442\u0430\u043d\u0430"; + GPFlags flags = GPFlags.make(true); + + byte[] encoded = EncodeUtils.encode(kazakhCapital, flags); + assertArrayEquals(new byte[] { (byte) 0xd0, (byte) 0x90, (byte) 0xd1, (byte) 0x81, + (byte) 0xd1, (byte) 0x82, (byte) 0xd0, (byte) 0xb0, (byte) 0xd0, (byte) 0xbd, + (byte) 0xd0, (byte) 0xb0 }, encoded); + assertEquals(kazakhCapital, EncodeUtils.decode(encoded, flags)); + } + + @Test + public void asciiDecodeAsUtf8() { + byte[] greatWallChinese = + new byte[] { + (byte) 0xe9, (byte) 0x95, (byte) 0xB7, (byte) 0xe5, (byte) 0x9F, (byte) 0x8E + }; + + GPFlags flags = GPFlags.make(false); + + String text = EncodeUtils.decode(greatWallChinese, flags); + assertEquals("\u9577\u57ce", text); + } +} diff --git a/src/test/java/com/android/tools/build/apkzlib/zip/ExtraFieldTest.java b/src/test/java/com/android/tools/build/apkzlib/zip/ExtraFieldTest.java new file mode 100644 index 0000000..a173dc4 --- /dev/null +++ b/src/test/java/com/android/tools/build/apkzlib/zip/ExtraFieldTest.java @@ -0,0 +1,359 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * 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.android.tools.build.apkzlib.zip; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.fail; + +import com.google.common.collect.ImmutableList; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.function.BiConsumer; +import java.util.function.Function; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +/** + * Test setting, removing and updating the extra field of zip entries. + */ +@RunWith(Parameterized.class) +public class ExtraFieldTest { + + @Rule + public TemporaryFolder mTemporaryFolder = new TemporaryFolder(); + + private File mZipFile; + + @Parameterized.Parameter + public Function mExtraFieldGetter; + + @Parameterized.Parameter(1) + public BiConsumer mExtraFieldSetter; + + @Before + public final void before() throws Exception { + mZipFile = mTemporaryFolder.newFile(); + mZipFile.delete(); + } + + @Parameterized.Parameters + public static ImmutableList getParameters() { + Function localGet = StoredEntry::getLocalExtra; + BiConsumer localSet = (se, ef) -> { + try { + se.setLocalExtra(ef); + } catch (IOException e) { + throw new AssertionError(e); + } + }; + + Function centralGet = + se -> se.getCentralDirectoryHeader().getExtraField(); + BiConsumer centralSet = (se, ef) -> { + try { + se.getCentralDirectoryHeader().setExtraField(ef); + } catch (Exception e) { + throw new AssertionError(e); + } + }; + + return ImmutableList.of( + new Object[]{ localGet, localSet }, + new Object[]{ centralGet, centralSet }); + } + + @Test + public void readEntryWithNoExtraField() throws Exception { + try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(mZipFile))) { + zos.putNextEntry(new ZipEntry("foo")); + zos.write(new byte[] { 1, 2, 3 }); + } + + try (ZFile zf = new ZFile(mZipFile)) { + StoredEntry foo = zf.get("foo"); + assertNotNull(foo); + assertEquals(3, foo.getCentralDirectoryHeader().getUncompressedSize()); + assertEquals(0, mExtraFieldGetter.apply(foo).size()); + } + } + + @Test + public void readSingleExtraField() throws Exception { + /* + * Header ID: 0x0A0B + * Data Size: 0x0004 + * Data: 0x01 0x02 0x03 0x04 + * + * In little endian is: + * + * 0xCDAB040001020304 + */ + byte[] extraField = new byte[] { 0x0B, 0x0A, 0x04, 0x00, 0x01, 0x02, 0x03, 0x04 }; + + try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(mZipFile))) { + ZipEntry ze = new ZipEntry("foo"); + ze.setExtra(extraField); + zos.putNextEntry(ze); + zos.write(new byte[] { 1, 2, 3 }); + } + + try (ZFile zf = new ZFile(mZipFile)) { + StoredEntry foo = zf.get("foo"); + assertNotNull(foo); + assertEquals(3, foo.getCentralDirectoryHeader().getUncompressedSize()); + assertEquals(8, mExtraFieldGetter.apply(foo).size()); + ImmutableList segments = mExtraFieldGetter.apply(foo).getSegments(); + assertEquals(1, segments.size()); + assertEquals(0x0A0B, segments.get(0).getHeaderId()); + byte[] segData = new byte[8]; + segments.get(0).write(ByteBuffer.wrap(segData)); + assertArrayEquals(extraField, segData); + } + } + + @Test + public void readMultipleExtraFields() throws Exception { + /* + * Header ID: 0x0A01 + * Data Size: 0x0002 + * Data: 0x01 0x02 + * + * Header ID: 0x0A02 + * Data Size: 0x0001 + * Data: 0x03 + * + * Header ID: 0x0A02 + * Data Size: 0x0001 + * Dataa: 0x04 + * + * In little endian is: + * + * 0x010A02000102 020A010003 020A010004 + */ + byte[] extraField = + new byte[] { + 0x01, 0x0A, 0x02, 0x00, 0x01, 0x02, + 0x02, 0x0A, 0x01, 0x00, 0x03, + 0x02, 0x0A, 0x01, 0x00, 0x04 }; + + try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(mZipFile))) { + ZipEntry ze = new ZipEntry("foo"); + + ze.setExtra(extraField); + zos.putNextEntry(ze); + zos.write(new byte[] { 1, 2, 3 }); + } + + try (ZFile zf = new ZFile(mZipFile)) { + StoredEntry foo = zf.get("foo"); + assertNotNull(foo); + assertEquals(3, foo.getCentralDirectoryHeader().getUncompressedSize()); + assertEquals(16, mExtraFieldGetter.apply(foo).size()); + ImmutableList segments = mExtraFieldGetter.apply(foo).getSegments(); + assertEquals(3, segments.size()); + + assertEquals(0x0A01, segments.get(0).getHeaderId()); + byte[] segData = new byte[6]; + segments.get(0).write(ByteBuffer.wrap(segData)); + assertArrayEquals(new byte[] { 0x01, 0x0A, 0x02, 0x00, 0x01, 0x02 }, segData); + + assertEquals(0x0A02, segments.get(1).getHeaderId()); + segData = new byte[5]; + segments.get(1).write(ByteBuffer.wrap(segData)); + assertArrayEquals(new byte[] { 0x02, 0x0A, 0x01, 0x00, 0x03 }, segData); + + assertEquals(0x0A02, segments.get(2).getHeaderId()); + segData = new byte[5]; + segments.get(2).write(ByteBuffer.wrap(segData)); + assertArrayEquals(new byte[] { 0x02, 0x0A, 0x01, 0x00, 0x04 }, segData); + } + } + + @Test + public void addExtraFieldToExistingEntry() throws Exception { + try (ZFile zf = new ZFile(mZipFile)) { + zf.add("before", new ByteArrayInputStream(new byte[] { 0, 1, 2 })); + zf.add("extra", new ByteArrayInputStream(new byte[] { 3, 4, 5 })); + zf.add("after", new ByteArrayInputStream(new byte[] { 6, 7, 8 })); + } + + try (ZFile zf = new ZFile(mZipFile)) { + StoredEntry ex = zf.get("extra"); + assertNotNull(ex); + mExtraFieldSetter.accept(ex, + new ExtraField( + ImmutableList.of( + new ExtraField.RawDataSegment( + 0x7654, + new byte[] { 1, 1, 3, 3 })))); + } + + try (ZFile zf = new ZFile(mZipFile)) { + StoredEntry before = zf.get("before"); + assertNotNull(before); + assertArrayEquals(new byte[] { 0, 1, 2 }, before.read()); + + StoredEntry extra = zf.get("extra"); + assertNotNull(extra); + assertArrayEquals(new byte[] { 3, 4, 5 }, extra.read()); + + StoredEntry after = zf.get("after"); + assertNotNull(after); + assertArrayEquals(new byte[] { 6, 7, 8 }, after.read()); + + ExtraField ef = mExtraFieldGetter.apply(extra); + assertEquals(1, ef.getSegments().size()); + ExtraField.Segment s = ef.getSingleSegment(0x7654); + assertNotNull(s); + byte[] sData = new byte[8]; + s.write(ByteBuffer.wrap(sData)); + assertArrayEquals(new byte[] { 0x54, 0x76, 0x04, 0x00, 1, 1, 3, 3 }, sData); + } + } + + @Test + public void removeExtraFieldFromExistingEntry() throws Exception { + try (ZFile zf = new ZFile(mZipFile)) { + zf.add("before", new ByteArrayInputStream(new byte[] { 0, 1, 2 })); + zf.add("extra", new ByteArrayInputStream(new byte[] { 3, 4, 5 })); + zf.add("after", new ByteArrayInputStream(new byte[] { 6, 7, 8 })); + } + + try (ZFile zf = new ZFile(mZipFile)) { + StoredEntry ex = zf.get("extra"); + assertNotNull(ex); + mExtraFieldSetter.accept(ex, + new ExtraField( + ImmutableList.of( + new ExtraField.RawDataSegment( + 0x7654, + new byte[] { 1, 1, 3, 3 })))); + } + + try (ZFile zf = new ZFile(mZipFile)) { + StoredEntry ex = zf.get("extra"); + assertNotNull(ex); + mExtraFieldSetter.accept(ex, new ExtraField()); + } + + try (ZFile zf = new ZFile(mZipFile)) { + StoredEntry before = zf.get("before"); + assertNotNull(before); + assertArrayEquals(new byte[] { 0, 1, 2 }, before.read()); + + StoredEntry extra = zf.get("extra"); + assertNotNull(extra); + assertArrayEquals(new byte[] { 3, 4, 5 }, extra.read()); + + StoredEntry after = zf.get("after"); + assertNotNull(after); + assertArrayEquals(new byte[] { 6, 7, 8 }, after.read()); + + ExtraField ef = mExtraFieldGetter.apply(extra); + assertEquals(0, ef.getSegments().size()); + } + } + + @Test + public void updateExtraFieldOfExistingEntry() throws Exception { + try (ZFile zf = new ZFile(mZipFile)) { + zf.add("before", new ByteArrayInputStream(new byte[] { 0, 1, 2 })); + zf.add("extra", new ByteArrayInputStream(new byte[] { 3, 4, 5 })); + zf.add("after", new ByteArrayInputStream(new byte[] { 6, 7, 8 })); + } + + try (ZFile zf = new ZFile(mZipFile)) { + StoredEntry ex = zf.get("extra"); + assertNotNull(ex); + mExtraFieldSetter.accept(ex, + new ExtraField( + ImmutableList.of( + new ExtraField.RawDataSegment( + 0x7654, + new byte[] { 1, 1, 3, 3 })))); + } + + try (ZFile zf = new ZFile(mZipFile)) { + StoredEntry ex = zf.get("extra"); + assertNotNull(ex); + mExtraFieldSetter.accept(ex, + new ExtraField( + ImmutableList.of( + new ExtraField.RawDataSegment( + 0x7654, + new byte[] { 2, 4, 2, 4 })))); + } + + try (ZFile zf = new ZFile(mZipFile)) { + StoredEntry before = zf.get("before"); + assertNotNull(before); + assertArrayEquals(new byte[] { 0, 1, 2 }, before.read()); + + StoredEntry extra = zf.get("extra"); + assertNotNull(extra); + assertArrayEquals(new byte[] { 3, 4, 5 }, extra.read()); + + StoredEntry after = zf.get("after"); + assertNotNull(after); + assertArrayEquals(new byte[] { 6, 7, 8 }, after.read()); + + ExtraField ef = mExtraFieldGetter.apply(extra); + assertEquals(1, ef.getSegments().size()); + ExtraField.Segment s = ef.getSingleSegment(0x7654); + assertNotNull(s); + byte[] sData = new byte[8]; + s.write(ByteBuffer.wrap(sData)); + assertArrayEquals(new byte[] { 0x54, 0x76, 0x04, 0x00, 2, 4, 2, 4 }, sData); + } + } + + @Test + public void parseInvalidExtraFieldWithInvalidHeader() throws Exception { + byte[] raw = new byte[1]; + ExtraField ef = new ExtraField(raw); + try { + ef.getSegments(); + fail(); + } catch (IOException e) { + // Expected. + } + } + + @Test + public void parseInvalidExtraFieldWithInsufficientData() throws Exception { + // Remember: 0x05, 0x00 = 5 in little endian! + byte[] raw = new byte[] { /* Header */ 0x01, 0x02, /* Size */ 0x05, 0x00, /* Data */ 0x01 }; + ExtraField ef = new ExtraField(raw); + try { + ef.getSegments(); + fail(); + } catch (IOException e) { + // Expected. + } + } +} diff --git a/src/test/java/com/android/tools/build/apkzlib/zip/FileUseMapTest.java b/src/test/java/com/android/tools/build/apkzlib/zip/FileUseMapTest.java new file mode 100644 index 0000000..363a993 --- /dev/null +++ b/src/test/java/com/android/tools/build/apkzlib/zip/FileUseMapTest.java @@ -0,0 +1,132 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * 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.android.tools.build.apkzlib.zip; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +import com.google.common.base.Stopwatch; +import java.text.DecimalFormat; +import java.util.Random; +import java.util.concurrent.TimeUnit; +import org.junit.Ignore; +import org.junit.Test; + +/** + * Tests for {@link FileUseMap}. + */ +public class FileUseMapTest { + + /** + * Verifies that as elements are added to the map, the performance of adding new elements + * is not significantly downgraded. This test creates a map and does several runs until + * a maximum is reached or a time limit is reached. + * + *

In each run, a random block is requested from the map with a random alignment and offset. + * The time for each run is saved. + * + *

After all runs are completed, the average time of the first runs (the head time) and + * the average time of the last runs (the tail time) is computed, as well as the average + * time. + * + *

The test passes if the average tail set time is (1) at most twice as long as the average + * and (2) is at most three times as long as the head set. This ensures that performance can + * degrade somewhat as the file map size increases, but not too much. + */ + @Test + @Ignore("This test relies on magic ratios to detect when performance is bad.") + public void addPerformanceTest() { + final long MAP_SIZE = 10000000; + final int MAX_RUNS = 10000; + final long MAX_TEST_DURATION_MS = 1000; + final int MAX_RANDOM_BLOCK_SIZE = 1000; + final int MAX_RANDOM_ALIGNMENT = 10; + final int HEAD_SET_SIZE = 1000; + final int TAIL_SET_SIZE = 1000; + final double MAX_TAIL_HEAD_RATIO = 3.0; + final double MAX_TAIL_TOTAL_RATIO = 2.0; + + long mapSize = MAP_SIZE; + FileUseMap map = new FileUseMap(mapSize, 0); + Random rand = new Random(0); + + long[] runs = new long[MAX_RUNS]; + int currentRun = 0; + + Stopwatch testStopwatch = Stopwatch.createStarted(); + while (testStopwatch.elapsed(TimeUnit.MILLISECONDS) < MAX_TEST_DURATION_MS + && currentRun < runs.length) { + Stopwatch runStopwatch = Stopwatch.createStarted(); + + long blockSize = 1 + rand.nextInt(MAX_RANDOM_BLOCK_SIZE); + long start = map.locateFree(blockSize, rand.nextInt(MAX_RANDOM_ALIGNMENT), + rand.nextInt(MAX_RANDOM_ALIGNMENT), FileUseMap.PositionAlgorithm.BEST_FIT); + long end = start + blockSize; + if (end >= mapSize) { + mapSize *= 2; + map.extend(mapSize); + } + + map.add(start, end, new Object()); + + runs[currentRun] = runStopwatch.elapsed(TimeUnit.NANOSECONDS); + currentRun++; + } + + double initialAvg = 0; + for (int i = 0; i < HEAD_SET_SIZE; i++) { + initialAvg += runs[i]; + } + + initialAvg /= HEAD_SET_SIZE; + + double endAvg = 0; + for (int i = currentRun - TAIL_SET_SIZE; i < currentRun; i++) { + endAvg += runs[i]; + } + + endAvg /= TAIL_SET_SIZE; + + double totalAvg = 0; + for (int i = 0; i < runs.length; i++) { + totalAvg += runs[i]; + } + + totalAvg /= currentRun; + + if (endAvg > totalAvg * MAX_TAIL_TOTAL_RATIO || endAvg > initialAvg * MAX_TAIL_HEAD_RATIO) { + DecimalFormat df = new DecimalFormat("#,###"); + + fail("Add performance at end is too bad. Performance in the beginning is " + + df.format(initialAvg) + "ns per insertion and at the end is " + + df.format(endAvg) + "ns. Average over the total of " + currentRun + " runs " + + "is " + df.format(totalAvg) + "ns."); + } + } + + @Test + public void testSizeComputation() { + FileUseMap m = new FileUseMap(200, 0); + + assertEquals(200, m.size()); + assertEquals(0, m.usedSize()); + + m.add(10, 20, new Object()); + assertEquals(200, m.size()); + assertEquals(20, m.usedSize()); + } +} diff --git a/src/test/java/com/android/tools/build/apkzlib/zip/OldApkReadTest.java b/src/test/java/com/android/tools/build/apkzlib/zip/OldApkReadTest.java new file mode 100644 index 0000000..95ccc0e --- /dev/null +++ b/src/test/java/com/android/tools/build/apkzlib/zip/OldApkReadTest.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * 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.android.tools.build.apkzlib.zip; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import com.android.tools.build.apkzlib.utils.ApkZFileTestUtils; +import java.io.File; +import org.junit.Test; + +public class OldApkReadTest { + + @Test + public void testReadOldApk() throws Exception { + File apkFile = ApkZFileTestUtils.getResource("/testData/packaging/test.apk"); + assertTrue(apkFile.exists()); + + try (ZFile zf = new ZFile(apkFile, new ZFileOptions())) { + StoredEntry classesDex = zf.get("classes.dex"); + assertNotNull(classesDex); + } + } +} diff --git a/src/test/java/com/android/tools/build/apkzlib/zip/ReadWithDifferentCompressionLevelsTest.java b/src/test/java/com/android/tools/build/apkzlib/zip/ReadWithDifferentCompressionLevelsTest.java new file mode 100644 index 0000000..48c3e76 --- /dev/null +++ b/src/test/java/com/android/tools/build/apkzlib/zip/ReadWithDifferentCompressionLevelsTest.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * 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.android.tools.build.apkzlib.zip; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import com.android.tools.build.apkzlib.utils.ApkZFileTestUtils; +import java.io.File; +import org.junit.Test; + +public class ReadWithDifferentCompressionLevelsTest { + + @Test + public void readL9() throws Exception { + File l9File = ApkZFileTestUtils.getResource("/testData/packaging/l9.zip"); + assertTrue(l9File.isFile()); + + try (ZFile read = new ZFile(l9File, new ZFileOptions())) { + assertNotNull(read.get("text-files/rfc2460.txt")); + } + } + + @Test + public void readL1() throws Exception { + File l1File = ApkZFileTestUtils.getResource("/testData/packaging/l1.zip"); + assertTrue(l1File.isFile()); + + try (ZFile read = new ZFile(l1File, new ZFileOptions())) { + assertNotNull(read.get("text-files/rfc2460.txt")); + } + } +} diff --git a/src/test/java/com/android/tools/build/apkzlib/zip/ZFileNotificationTest.java b/src/test/java/com/android/tools/build/apkzlib/zip/ZFileNotificationTest.java new file mode 100644 index 0000000..4e08f2c --- /dev/null +++ b/src/test/java/com/android/tools/build/apkzlib/zip/ZFileNotificationTest.java @@ -0,0 +1,420 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * 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.android.tools.build.apkzlib.zip; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; + +import com.android.tools.build.apkzlib.utils.ApkZLibPair; +import com.android.tools.build.apkzlib.utils.IOExceptionRunnable; +import com.google.common.collect.Lists; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.IOException; +import java.util.List; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.mockito.Mockito; + +public class ZFileNotificationTest { + private static class KeepListener extends ZFileExtension { + public int open; + public int beforeUpdated; + public int updated; + public int closed; + public List> added; + public List removed; + public IOExceptionRunnable returnRunnable; + + KeepListener() { + reset(); + } + + @Nullable + @Override + public IOExceptionRunnable open() { + open++; + return returnRunnable; + } + + @Nullable + @Override + public IOExceptionRunnable beforeUpdate() { + beforeUpdated++; + return returnRunnable; + } + + @Override + public void updated() { + updated++; + } + + @Override + public void closed() { + closed++; + } + + @Nullable + @Override + public IOExceptionRunnable added(@Nonnull StoredEntry entry, + @Nullable StoredEntry replaced) { + added.add(new ApkZLibPair<>(entry, replaced)); + return returnRunnable; + } + + @Nullable + @Override + public IOExceptionRunnable removed(@Nonnull StoredEntry entry) { + removed.add(entry); + return returnRunnable; + } + + void reset() { + open = 0; + beforeUpdated = 0; + updated = 0; + closed = 0; + added = Lists.newArrayList(); + removed = Lists.newArrayList(); + } + + void assertClear() { + assertEquals(0, open); + assertEquals(0, beforeUpdated); + assertEquals(0, updated); + assertEquals(0, closed); + assertEquals(0, added.size()); + assertEquals(0, removed.size()); + } + } + + @Rule + public TemporaryFolder mTemporaryFolder = new TemporaryFolder(); + + @Test + public void notifyAddFile() throws Exception { + try (ZFile zf = new ZFile(new File(mTemporaryFolder.getRoot(), "a.zip"))) { + KeepListener kl = new KeepListener(); + zf.addZFileExtension(kl); + + kl.assertClear(); + + zf.add("foo", new ByteArrayInputStream(new byte[] { 1, 2 })); + zf.finishAllBackgroundTasks(); + assertEquals(1, kl.added.size()); + StoredEntry addedSe = kl.added.get(0).v1; + assertNull(kl.added.get(0).v2); + kl.added.clear(); + kl.assertClear(); + + StoredEntry foo = zf.get("foo"); + assertNotNull(foo); + + assertSame(foo, addedSe); + } + } + + @Test + public void notifyRemoveFile() throws Exception { + try (ZFile zf = new ZFile(new File(mTemporaryFolder.getRoot(), "a.zip"))) { + KeepListener kl = new KeepListener(); + zf.addZFileExtension(kl); + + kl.assertClear(); + + zf.add("foo", new ByteArrayInputStream(new byte[] { 1, 2 })); + zf.finishAllBackgroundTasks(); + kl.reset(); + + StoredEntry foo = zf.get("foo"); + assertNotNull(foo); + + foo.delete(); + assertEquals(1, kl.removed.size()); + assertSame(foo, kl.removed.get(0)); + kl.removed.clear(); + kl.assertClear(); + } + } + + @Test + public void notifyUpdateFile() throws Exception { + try (ZFile zf = new ZFile(new File(mTemporaryFolder.getRoot(), "a.zip"))) { + KeepListener kl = new KeepListener(); + zf.addZFileExtension(kl); + + kl.assertClear(); + + zf.add("foo", new ByteArrayInputStream(new byte[] { 1, 2 })); + zf.finishAllBackgroundTasks(); + StoredEntry foo1 = zf.get("foo"); + kl.reset(); + + zf.add("foo", new ByteArrayInputStream(new byte[] { 2, 3 })); + zf.finishAllBackgroundTasks(); + StoredEntry foo2 = zf.get("foo"); + + assertEquals(1, kl.added.size()); + assertSame(foo2, kl.added.get(0).v1); + assertSame(foo1, kl.added.get(0).v2); + + kl.added.clear(); + kl.assertClear(); + } + } + + @Test + public void notifyOpenUpdateClose() throws Exception { + KeepListener kl = new KeepListener(); + try (ZFile zf = new ZFile(new File(mTemporaryFolder.getRoot(), "a.zip"))) { + zf.addZFileExtension(kl); + + kl.assertClear(); + + zf.add("foo", new ByteArrayInputStream(new byte[] { 1, 2 })); + zf.finishAllBackgroundTasks(); + kl.reset(); + } + + assertEquals(1, kl.open); + kl.open = 0; + assertEquals(1, kl.beforeUpdated); + assertEquals(1, kl.updated); + kl.beforeUpdated = 0; + kl.updated = 0; + assertEquals(1, kl.closed); + kl.closed = 0; + kl.assertClear(); + } + + @Test + public void notifyOpenUpdate() throws Exception { + KeepListener kl = new KeepListener(); + try (ZFile zf = new ZFile(new File(mTemporaryFolder.getRoot(), "a.zip"))) { + zf.addZFileExtension(kl); + + kl.assertClear(); + + zf.add("foo", new ByteArrayInputStream(new byte[] { 1, 2 })); + zf.finishAllBackgroundTasks(); + kl.reset(); + zf.update(); + + assertEquals(1, kl.open); + kl.open = 0; + assertEquals(1, kl.beforeUpdated); + assertEquals(1, kl.updated); + kl.beforeUpdated = 0; + kl.updated = 0; + kl.assertClear(); + } + } + + @Test + public void notifyUpdate() throws Exception { + try (ZFile zf = new ZFile(new File(mTemporaryFolder.getRoot(), "a.zip"))) { + KeepListener kl = new KeepListener(); + zf.addZFileExtension(kl); + + kl.assertClear(); + + zf.add("foo", new ByteArrayInputStream(new byte[] { 1, 2 })); + zf.update(); + kl.reset(); + + zf.add("bar", new ByteArrayInputStream(new byte[] { 2, 3 })); + zf.finishAllBackgroundTasks(); + kl.reset(); + + zf.update(); + assertEquals(1, kl.beforeUpdated); + assertEquals(1, kl.updated); + kl.beforeUpdated = 0; + kl.updated = 0; + kl.assertClear(); + } + } + + @Test + public void removedListenersAreNotNotified() throws Exception { + try (ZFile zf = new ZFile(new File(mTemporaryFolder.getRoot(), "a.zip"))) { + KeepListener kl = new KeepListener(); + zf.addZFileExtension(kl); + + kl.assertClear(); + + zf.add("foo", new ByteArrayInputStream(new byte[] { 1, 2 })); + zf.finishAllBackgroundTasks(); + assertEquals(1, kl.added.size()); + kl.added.clear(); + kl.assertClear(); + + zf.removeZFileExtension(kl); + + zf.add("foo", new ByteArrayInputStream(new byte[] { 2, 3 })); + zf.finishAllBackgroundTasks(); + kl.assertClear(); + } + } + + @Test + public void actionsExecutedAtEndOfNotification() throws Exception { + try (ZFile zf = new ZFile(new File(mTemporaryFolder.getRoot(), "a.zip"))) { + + IOException death[] = new IOException[1]; + + KeepListener kl1 = new KeepListener(); + zf.addZFileExtension(kl1); + kl1.returnRunnable = new IOExceptionRunnable() { + private boolean once = false; + + @Override + public void run() { + if (once) { + return; + } + + once = true; + + try { + zf.add("foo", new ByteArrayInputStream(new byte[] { 1, 2 })); + } catch (IOException e) { + death[0] = e; + } + } + }; + + KeepListener kl2 = new KeepListener(); + zf.addZFileExtension(kl2); + kl2.returnRunnable = new IOExceptionRunnable() { + private boolean once = false; + + @Override + public void run() { + if (once) { + return; + } + + once = true; + try { + zf.add("bar", new ByteArrayInputStream(new byte[] { 1, 2 })); + } catch (IOException e) { + death[0] = e; + } + } + }; + + kl1.assertClear(); + kl2.assertClear(); + + zf.add("xpto", new ByteArrayInputStream(new byte[] { 1, 2 })); + zf.finishAllBackgroundTasks(); + + assertEquals(3, kl1.added.size()); + kl1.added.clear(); + kl1.assertClear(); + assertEquals(3, kl2.added.size()); + kl2.added.clear(); + kl2.assertClear(); + + assertNull(death[0]); + } + } + + @Test + public void canAddFilesDuringUpdateNotification() throws Exception { + File zipFile = new File(mTemporaryFolder.getRoot(), "a.zip"); + try (ZFile zf = new ZFile(zipFile)) { + IOException death[] = new IOException[1]; + + KeepListener kl1 = new KeepListener(); + zf.addZFileExtension(kl1); + + zf.add("foo", new ByteArrayInputStream(new byte[] { 1, 2 })); + zf.finishAllBackgroundTasks(); + + kl1.returnRunnable = new IOExceptionRunnable() { + private boolean once = false; + + @Override + public void run() { + if (once) { + return; + } + + once = true; + + try { + zf.add("bar", new ByteArrayInputStream(new byte[] { 1, 2 })); + } catch (IOException e) { + death[0] = e; + } + } + }; + } + + try (ZFile zf2 = new ZFile(zipFile)) { + StoredEntry fooFile = zf2.get("foo"); + assertNotNull(fooFile); + StoredEntry barFile = zf2.get("bar"); + assertNotNull(barFile); + } + } + + @Test + public void notifyOnceEntriesWritten() throws Exception { + File zipFile = new File(mTemporaryFolder.getRoot(), "a.zip"); + ZFileExtension ext = Mockito.mock(ZFileExtension.class); + try (ZFile zf = new ZFile(zipFile)) { + zf.addZFileExtension(ext); + + zf.add("foo", new ByteArrayInputStream(new byte[] { 1, 2 })); + zf.finishAllBackgroundTasks(); + + Mockito.verify(ext, Mockito.times(0)).entriesWritten(); + } + + Mockito.verify(ext, Mockito.times(1)).entriesWritten(); + } + + @Test + public void notifyTwiceEntriesWrittenIfCdChanged() throws Exception { + File zipFile = new File(mTemporaryFolder.getRoot(), "a.zip"); + ZFileExtension ext = Mockito.mock(ZFileExtension.class); + try (ZFile zf = new ZFile(zipFile)) { + Mockito.doAnswer((invocation) -> { + zf.setExtraDirectoryOffset(10); + Mockito.doNothing().when(ext).entriesWritten(); + return null; + }).when(ext).entriesWritten(); + + zf.addZFileExtension(ext); + + zf.add("foo", new ByteArrayInputStream(new byte[] { 1, 2 })); + zf.finishAllBackgroundTasks(); + + Mockito.verify(ext, Mockito.times(0)).entriesWritten(); + } + + Mockito.verify(ext, Mockito.times(2)).entriesWritten(); + } +} diff --git a/src/test/java/com/android/tools/build/apkzlib/zip/ZFileReadOnlyTest.java b/src/test/java/com/android/tools/build/apkzlib/zip/ZFileReadOnlyTest.java new file mode 100644 index 0000000..b0dd5f7 --- /dev/null +++ b/src/test/java/com/android/tools/build/apkzlib/zip/ZFileReadOnlyTest.java @@ -0,0 +1,240 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * 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.android.tools.build.apkzlib.zip; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.fail; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.IOException; +import javax.annotation.Nonnull; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +public class ZFileReadOnlyTest { + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void cannotCreateRoFileOnNonExistingFile() throws Exception { + try { + new ZFile(new File(temporaryFolder.getRoot(), "foo.zip"), new ZFileOptions(), true); + fail(); + } catch (IOException e) { + // Expected. + } + } + + @Nonnull + private File makeTestZip() throws IOException { + File zip = new File(temporaryFolder.getRoot(), "foo.zip"); + try (ZFile zf = new ZFile(zip)) { + zf.add("bar", new ByteArrayInputStream(new byte[] { 0, 1, 2, 3, 4, 5 })); + } + + return zip; + } + + @Test + public void cannotUpdateInRoMode() throws Exception { + try (ZFile zf = new ZFile(makeTestZip(), new ZFileOptions(), true)) { + try { + zf.update(); + fail(); + } catch (IllegalStateException e) { + // Expeted. + } + } + } + + @Test + public void cannotAddFilesInRoMode() throws Exception { + try (ZFile zf = new ZFile(makeTestZip(), new ZFileOptions(), true)) { + try { + zf.add("bar2", new ByteArrayInputStream(new byte[] { 6, 7, })); + fail(); + } catch (IllegalStateException e) { + // Expeted. + } + } + } + + @Test + public void cannotAddRecursivelyInRoMode() throws Exception { + File folder = temporaryFolder.newFolder(); + try (ZFile zf = new ZFile(makeTestZip(), new ZFileOptions(), true)) { + try { + zf.addAllRecursively(folder); + fail(); + } catch (IllegalStateException e) { + // Expeted. + } + } + } + + @Test + public void cannotReplaceFilesInRoMode() throws Exception { + try (ZFile zf = new ZFile(makeTestZip(), new ZFileOptions(), true)) { + try { + zf.add("bar", new ByteArrayInputStream(new byte[] { 6, 7 })); + fail(); + } catch (IllegalStateException e) { + // Expeted. + } + } + } + + @Test + public void cannotDeleteFilesInRoMode() throws Exception { + try (ZFile zf = new ZFile(makeTestZip(), new ZFileOptions(), true)) { + StoredEntry bar = zf.get("bar"); + assertNotNull(bar); + try { + bar.delete(); + fail(); + } catch (IllegalStateException e) { + // Expeted. + } + } + } + + @Test + public void cannotMergeInRoMode() throws Exception { + try (ZFile toMerge = new ZFile(new File(temporaryFolder.getRoot(), "a.zip"))) { + try (ZFile zf = new ZFile(makeTestZip(), new ZFileOptions(), true)) { + try { + zf.mergeFrom(toMerge, s -> false); + fail(); + } catch (IllegalStateException e) { + // Expeted. + } + } + } + } + + @Test + public void cannotTouchInRoMode() throws Exception { + try (ZFile zf = new ZFile(makeTestZip(), new ZFileOptions(), true)) { + try { + zf.touch(); + fail(); + } catch (IllegalStateException e) { + // Expeted. + } + } + } + + @Test + public void cannotRealignInRoMode() throws Exception { + try (ZFile zf = new ZFile(makeTestZip(), new ZFileOptions(), true)) { + try { + zf.realign(); + fail(); + } catch (IllegalStateException e) { + // Expeted. + } + } + } + + @Test + public void cannotAddExtensionInRoMode() throws Exception { + try (ZFile zf = new ZFile(makeTestZip(), new ZFileOptions(), true)) { + try { + zf.addZFileExtension(new ZFileExtension() {}); + fail(); + } catch (IllegalStateException e) { + // Expeted. + } + } + } + + @Test + public void cannotDirectWriteInRoMode() throws Exception { + try (ZFile zf = new ZFile(makeTestZip(), new ZFileOptions(), true)) { + try { + zf.directWrite(0, new byte[1]); + fail(); + } catch (IllegalStateException e) { + // Expeted. + } + } + } + + @Test + public void cannotSetEocdCommentInRoMode() throws Exception { + try (ZFile zf = new ZFile(makeTestZip(), new ZFileOptions(), true)) { + try { + zf.setEocdComment(new byte[2]); + fail(); + } catch (IllegalStateException e) { + // Expeted. + } + } + } + + @Test + public void cannotSetCentralDirectoryOffsetInRoMode() throws Exception { + try (ZFile zf = new ZFile(makeTestZip(), new ZFileOptions(), true)) { + try { + zf.setExtraDirectoryOffset(4); + fail(); + } catch (IllegalStateException e) { + // Expeted. + } + } + } + + @Test + public void cannotSortZipContentsInRoMode() throws Exception { + try (ZFile zf = new ZFile(makeTestZip(), new ZFileOptions(), true)) { + try { + zf.sortZipContents(); + fail(); + } catch (IllegalStateException e) { + // Expeted. + } + } + } + + @Test + public void canOpenAndReadFilesInRoMode() throws Exception { + try (ZFile zf = new ZFile(makeTestZip(), new ZFileOptions(), true)) { + StoredEntry bar = zf.get("bar"); + assertNotNull(bar); + assertArrayEquals(new byte[] { 0, 1, 2, 3, 4, 5 }, bar.read()); + } + } + + @Test + public void canGetDirectoryAndEocdBytesInRoMode() throws Exception { + try (ZFile zf = new ZFile(makeTestZip(), new ZFileOptions(), true)) { + zf.getCentralDirectoryBytes(); + zf.getEocdBytes(); + zf.getEocdComment(); + } + } + + @Test + public void canDirectReadInRoMode() throws Exception { + try (ZFile zf = new ZFile(makeTestZip(), new ZFileOptions(), true)) { + zf.directRead(0, new byte[2]); + } + } +} diff --git a/src/test/java/com/android/tools/build/apkzlib/zip/ZFileSortTest.java b/src/test/java/com/android/tools/build/apkzlib/zip/ZFileSortTest.java new file mode 100644 index 0000000..dd75811 --- /dev/null +++ b/src/test/java/com/android/tools/build/apkzlib/zip/ZFileSortTest.java @@ -0,0 +1,218 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * 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.android.tools.build.apkzlib.zip; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.io.ByteArrayInputStream; +import java.io.File; +import javax.annotation.Nullable; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +public class ZFileSortTest { + @Rule + public TemporaryFolder mTemporaryFolder = new TemporaryFolder(); + private File mFile; + private ZFile mZFile; + private StoredEntry mMaryEntry; + private long mMaryOffset; + private StoredEntry mAndrewEntry; + private long mAndrewOffset; + private StoredEntry mBethEntry; + private long mBethOffset; + private StoredEntry mPeterEntry; + private long mPeterOffset; + + @Before + public final void before() throws Exception { + mFile = new File(mTemporaryFolder.getRoot(), "a.zip"); + setupZFile(null); + } + + @After + public final void after() throws Exception { + mZFile.close(); + } + + /** + * Recreates the zip file, if one already exist. + * + * @param options the options for the file, may be {@code null} in which case the default + * options will be used + * @throws Exception failed to re-create the file + */ + private void setupZFile(@Nullable ZFileOptions options) throws Exception { + if (mZFile != null) { + mZFile.close(); + } + + if (mFile.exists()) { + assertTrue(mFile.delete()); + } + + if (options == null) { + options = new ZFileOptions(); + } + + mZFile = new ZFile(mFile, options); + + mZFile.add("Mary.xml", new ByteArrayInputStream(new byte[] { 1, 2, 3 })); + mZFile.add("Andrew.txt", new ByteArrayInputStream(new byte[] { 4, 5 })); + mZFile.add("Beth.png", new ByteArrayInputStream(new byte[] { 6, 7, 8, 9 })); + mZFile.add("Peter.html", new ByteArrayInputStream(new byte[] { 10 })); + mZFile.finishAllBackgroundTasks(); + } + + private void readEntries() throws Exception { + mMaryEntry = mZFile.get("Mary.xml"); + assertNotNull(mMaryEntry); + mMaryOffset = mMaryEntry.getCentralDirectoryHeader().getOffset(); + assertArrayEquals(new byte[] { 1, 2, 3 }, mMaryEntry.read()); + + mAndrewEntry = mZFile.get("Andrew.txt"); + assertNotNull(mAndrewEntry); + mAndrewOffset = mAndrewEntry.getCentralDirectoryHeader().getOffset(); + assertArrayEquals(new byte[] { 4, 5 }, mAndrewEntry.read()); + + mBethEntry = mZFile.get("Beth.png"); + assertNotNull(mBethEntry); + mBethOffset = mBethEntry.getCentralDirectoryHeader().getOffset(); + assertArrayEquals(new byte[] { 6, 7, 8, 9 }, mBethEntry.read()); + + mPeterEntry = mZFile.get("Peter.html"); + assertNotNull(mPeterEntry); + mPeterOffset = mPeterEntry.getCentralDirectoryHeader().getOffset(); + assertArrayEquals(new byte[] { 10 }, mPeterEntry.read()); + } + + @Test + public void noSort() throws Exception { + readEntries(); + + assertEquals(-1, mMaryOffset); + assertEquals(-1, mAndrewOffset); + assertEquals(-1, mBethOffset); + assertEquals(-1, mPeterOffset); + + mZFile.update(); + + readEntries(); + + assertTrue(mMaryOffset >= 0); + assertTrue(mMaryOffset < mAndrewOffset); + assertTrue(mAndrewOffset < mBethOffset); + assertTrue(mBethOffset < mPeterOffset); + } + + @Test + public void sortFilesBeforeUpdate() throws Exception { + readEntries(); + mZFile.sortZipContents(); + + mZFile.update(); + + readEntries(); + + assertTrue(mAndrewOffset >= 0); + assertTrue(mBethOffset > mAndrewOffset); + assertTrue(mMaryOffset > mBethOffset); + assertTrue(mPeterOffset > mMaryOffset); + } + + @Test + public void autoSort() throws Exception { + ZFileOptions options = new ZFileOptions(); + options.setAutoSortFiles(true); + setupZFile(options); + + readEntries(); + + mZFile.update(); + + readEntries(); + + assertTrue(mAndrewOffset >= 0); + assertTrue(mBethOffset > mAndrewOffset); + assertTrue(mMaryOffset > mBethOffset); + assertTrue(mPeterOffset > mMaryOffset); + } + + @Test + public void sortFilesAfterUpdate() throws Exception { + readEntries(); + + mZFile.update(); + + mZFile.sortZipContents(); + + readEntries(); + + assertEquals(-1, mMaryOffset); + assertEquals(-1, mAndrewOffset); + assertEquals(-1, mBethOffset); + assertEquals(-1, mPeterOffset); + + mZFile.update(); + + readEntries(); + + assertTrue(mAndrewOffset >= 0); + assertTrue(mBethOffset > mAndrewOffset); + assertTrue(mMaryOffset > mBethOffset); + assertTrue(mPeterOffset > mMaryOffset); + } + + @Test + public void sortFilesWithAlignment() throws Exception { + mZFile.close(); + + ZFileOptions options = new ZFileOptions(); + options.setAlignmentRule(AlignmentRules.constantForSuffix(".xml", 1024)); + mZFile = new ZFile(mFile, options); + + mZFile.sortZipContents(); + mZFile.update(); + + readEntries(); + assertTrue(mAndrewOffset >= 0); + assertTrue(mBethOffset > mAndrewOffset); + assertTrue(mPeterOffset > mBethOffset); + assertTrue(mMaryOffset > mPeterOffset); + } + + @Test + public void sortFilesOnClosedFile() throws Exception { + mZFile.close(); + mZFile = new ZFile(mFile); + mZFile.sortZipContents(); + mZFile.update(); + + readEntries(); + + assertTrue(mAndrewOffset >= 0); + assertTrue(mBethOffset > mAndrewOffset); + assertTrue(mMaryOffset > mBethOffset); + assertTrue(mPeterOffset > mMaryOffset); + } +} diff --git a/src/test/java/com/android/tools/build/apkzlib/zip/ZFileTest.java b/src/test/java/com/android/tools/build/apkzlib/zip/ZFileTest.java new file mode 100644 index 0000000..fa69f5d --- /dev/null +++ b/src/test/java/com/android/tools/build/apkzlib/zip/ZFileTest.java @@ -0,0 +1,1821 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * 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.android.tools.build.apkzlib.zip; + +import static com.android.tools.build.apkzlib.utils.ApkZFileTestUtils.readSegment; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import com.android.tools.build.apkzlib.zip.compress.DeflateExecutionCompressor; +import com.android.tools.build.apkzlib.zip.utils.CloseableByteSource; +import com.android.tools.build.apkzlib.zip.utils.RandomAccessFileUtils; +import com.google.common.base.Charsets; +import com.google.common.base.Strings; +import com.google.common.base.Throwables; +import com.google.common.hash.Hashing; +import com.google.common.io.ByteStreams; +import com.google.common.io.Closer; +import com.google.common.io.Files; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.RandomAccessFile; +import java.util.Locale; +import java.util.Random; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.zip.Deflater; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; +import java.util.zip.ZipInputStream; +import java.util.zip.ZipOutputStream; +import javax.annotation.Nonnull; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +public class ZFileTest { + @Rule + public TemporaryFolder mTemporaryFolder = new TemporaryFolder(); + + @Test + public void getZipPath() throws Exception { + File temporaryDir = mTemporaryFolder.getRoot(); + File zpath = new File(temporaryDir, "a"); + try (ZFile zf = new ZFile(zpath)) { + assertEquals(zpath, zf.getFile()); + } + } + + @Test + public void readNonExistingFile() throws Exception { + File temporaryDir = mTemporaryFolder.getRoot(); + File zf = new File(temporaryDir, "a"); + try (ZFile azf = new ZFile(zf)) { + azf.touch(); + } + + assertTrue(zf.exists()); + } + + @Test(expected = IOException.class) + public void readExistingEmptyFile() throws Exception { + File temporaryDir = mTemporaryFolder.getRoot(); + File zf = new File(temporaryDir, "a"); + Files.write(new byte[0], zf); + try (ZFile azf = new ZFile(zf)) { + /* + * Just open and close. + */ + } + } + + @Test + public void readAlmostEmptyZip() throws Exception { + File zf = ZipTestUtils.cloneRsrc("empty-zip.zip", mTemporaryFolder); + + try (ZFile azf = new ZFile(zf)) { + assertEquals(1, azf.entries().size()); + + StoredEntry z = azf.get("z/"); + assertNotNull(z); + assertSame(StoredEntryType.DIRECTORY, z.getType()); + } + } + + @Test + public void readZipWithTwoFilesOneDirectory() throws Exception { + File zf = ZipTestUtils.cloneRsrc("simple-zip.zip", mTemporaryFolder); + + try (ZFile azf = new ZFile(zf)) { + assertEquals(3, azf.entries().size()); + + StoredEntry e0 = azf.get("dir/"); + assertNotNull(e0); + assertSame(StoredEntryType.DIRECTORY, e0.getType()); + + StoredEntry e1 = azf.get("dir/inside"); + assertNotNull(e1); + assertSame(StoredEntryType.FILE, e1.getType()); + ByteArrayOutputStream e1BytesOut = new ByteArrayOutputStream(); + ByteStreams.copy(e1.open(), e1BytesOut); + byte e1Bytes[] = e1BytesOut.toByteArray(); + String e1Txt = new String(e1Bytes, Charsets.US_ASCII); + assertEquals("inside", e1Txt); + + StoredEntry e2 = azf.get("file.txt"); + assertNotNull(e2); + assertSame(StoredEntryType.FILE, e2.getType()); + ByteArrayOutputStream e2BytesOut = new ByteArrayOutputStream(); + ByteStreams.copy(e2.open(), e2BytesOut); + byte e2Bytes[] = e2BytesOut.toByteArray(); + String e2Txt = new String(e2Bytes, Charsets.US_ASCII); + assertEquals("file with more text to allow deflating to be useful", e2Txt); + } + } + + @Test + public void readOnlyZipSupport() throws Exception { + File testZip = ZipTestUtils.cloneRsrc("empty-zip.zip", mTemporaryFolder); + + assertTrue(testZip.setWritable(false)); + + try (ZFile zf = new ZFile(testZip)) { + assertEquals(1, zf.entries().size()); + assertTrue(zf.getCentralDirectoryOffset() > 0); + assertTrue(zf.getEocdOffset() > 0); + } + } + + @Test + public void readOnlyV2SignedApkSupport() throws Exception { + File testZip = ZipTestUtils.cloneRsrc("v2-signed.apk", mTemporaryFolder); + + assertTrue(testZip.setWritable(false)); + + try (ZFile zf = new ZFile(testZip)) { + assertEquals(416, zf.entries().size()); + assertTrue(zf.getCentralDirectoryOffset() > 0); + assertTrue(zf.getEocdOffset() > 0); + } + } + + @Test + public void compressedFilesReadableByJavaZip() throws Exception { + File testZip = new File(mTemporaryFolder.getRoot(), "t.zip"); + + File wiki = ZipTestUtils + .cloneRsrc("text-files/wikipedia.html", mTemporaryFolder, "wiki"); + File rfc = ZipTestUtils.cloneRsrc("text-files/rfc2460.txt", mTemporaryFolder, "rfc"); + File lena = ZipTestUtils.cloneRsrc("images/lena.png", mTemporaryFolder, "lena"); + byte[] wikiData = Files.toByteArray(wiki); + byte[] rfcData = Files.toByteArray(rfc); + byte[] lenaData = Files.toByteArray(lena); + + try (ZFile zf = new ZFile(testZip)) { + zf.add("wiki", new ByteArrayInputStream(wikiData)); + zf.add("rfc", new ByteArrayInputStream(rfcData)); + zf.add("lena", new ByteArrayInputStream(lenaData)); + } + + try(ZipFile jz = new ZipFile(testZip)) { + ZipEntry ze = jz.getEntry("wiki"); + assertNotNull(ze); + assertEquals(ZipEntry.DEFLATED, ze.getMethod()); + assertTrue(ze.getCompressedSize() < wikiData.length); + InputStream zeis = jz.getInputStream(ze); + assertArrayEquals(wikiData, ByteStreams.toByteArray(zeis)); + zeis.close(); + + ze = jz.getEntry("rfc"); + assertNotNull(ze); + assertEquals(ZipEntry.DEFLATED, ze.getMethod()); + assertTrue(ze.getCompressedSize() < rfcData.length); + zeis = jz.getInputStream(ze); + assertArrayEquals(rfcData, ByteStreams.toByteArray(zeis)); + zeis.close(); + + ze = jz.getEntry("lena"); + assertNotNull(ze); + assertEquals(ZipEntry.STORED, ze.getMethod()); + assertTrue(ze.getCompressedSize() == lenaData.length); + zeis = jz.getInputStream(ze); + assertArrayEquals(lenaData, ByteStreams.toByteArray(zeis)); + zeis.close(); + } + } + + @Test + public void removeFileFromZip() throws Exception { + File zipFile = mTemporaryFolder.newFile("test.zip"); + + try(ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zipFile))) { + ZipEntry entry = new ZipEntry("foo/"); + entry.setMethod(ZipEntry.STORED); + entry.setSize(0); + entry.setCompressedSize(0); + entry.setCrc(0); + zos.putNextEntry(entry); + zos.putNextEntry(new ZipEntry("foo/bar")); + zos.write(new byte[] { 1, 2, 3, 4 }); + zos.closeEntry(); + } + + try (ZFile zf = new ZFile(zipFile)) { + assertEquals(2, zf.entries().size()); + for (StoredEntry e : zf.entries()) { + if (e.getType() == StoredEntryType.FILE) { + e.delete(); + } + } + + zf.update(); + + try (ZipInputStream zis = new ZipInputStream(new FileInputStream(zipFile))) { + ZipEntry e1 = zis.getNextEntry(); + assertNotNull(e1); + + assertEquals("foo/", e1.getName()); + + ZipEntry e2 = zis.getNextEntry(); + assertNull(e2); + } + } + } + + @Test + public void addFileToZip() throws Exception { + File zipFile = mTemporaryFolder.newFile("test.zip"); + + try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zipFile))) { + ZipEntry fooDir = new ZipEntry("foo/"); + fooDir.setCrc(0); + fooDir.setCompressedSize(0); + fooDir.setSize(0); + fooDir.setMethod(ZipEntry.STORED); + zos.putNextEntry(fooDir); + zos.closeEntry(); + } + + ZFile zf = new ZFile(zipFile); + assertEquals(1, zf.entries().size()); + + zf.update(); + + try (ZipInputStream zis = new ZipInputStream(new FileInputStream(zipFile))) { + ZipEntry e1 = zis.getNextEntry(); + assertNotNull(e1); + + assertEquals("foo/", e1.getName()); + + ZipEntry e2 = zis.getNextEntry(); + assertNull(e2); + } + } + + @Test + public void createNewZip() throws Exception { + File zipFile = new File(mTemporaryFolder.getRoot(), "test.zip"); + + ZFile zf = new ZFile(zipFile); + zf.add("foo", new ByteArrayInputStream(new byte[] { 0, 1 })); + zf.close(); + + try (ZipFile jzf = new ZipFile(zipFile)) { + assertEquals(1, jzf.size()); + + ZipEntry fooEntry = jzf.getEntry("foo"); + assertNotNull(fooEntry); + assertEquals("foo", fooEntry.getName()); + assertEquals(2, fooEntry.getSize()); + + InputStream is = jzf.getInputStream(fooEntry); + assertEquals(0, is.read()); + assertEquals(1, is.read()); + assertEquals(-1, is.read()); + + is.close(); + } + } + + @Test + public void replaceFileWithSmallerInMiddle() throws Exception { + File zipFile = new File(mTemporaryFolder.getRoot(), "test.zip"); + + try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zipFile))) { + zos.putNextEntry(new ZipEntry("file1")); + zos.write(new byte[] { 1, 2, 3, 4, 5 }); + zos.putNextEntry(new ZipEntry("file2")); + zos.write(new byte[] { 6, 7, 8 }); + zos.putNextEntry(new ZipEntry("file3")); + zos.write(new byte[] { 9, 0, 1, 2, 3, 4 }); + } + + int totalSize = (int) zipFile.length(); + + try (ZFile zf = new ZFile(zipFile)) { + assertEquals(3, zf.entries().size()); + + StoredEntry file2 = zf.get("file2"); + assertNotNull(file2); + assertEquals(3, file2.getCentralDirectoryHeader().getUncompressedSize()); + + assertArrayEquals(new byte[] { 6, 7, 8 }, file2.read()); + + zf.add("file2", new ByteArrayInputStream(new byte[] { 11, 12 })); + + int newTotalSize = (int) zipFile.length(); + assertTrue(newTotalSize + " == " + totalSize, newTotalSize == totalSize); + + file2 = zf.get("file2"); + assertNotNull(file2); + assertArrayEquals(new byte[] { 11, 12 }, file2.read()); + } + + try (ZFile zf2 = new ZFile(zipFile)) { + StoredEntry file2 = zf2.get("file2"); + assertNotNull(file2); + assertArrayEquals(new byte[] { 11, 12 }, file2.read()); + } + } + + @Test + public void replaceFileWithSmallerAtEnd() throws Exception { + File zipFile = new File(mTemporaryFolder.getRoot(), "test.zip"); + try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zipFile))) { + zos.putNextEntry(new ZipEntry("file1")); + zos.write(new byte[]{1, 2, 3, 4, 5}); + zos.putNextEntry(new ZipEntry("file2")); + zos.write(new byte[]{6, 7, 8}); + zos.putNextEntry(new ZipEntry("file3")); + zos.write(new byte[]{9, 0, 1, 2, 3, 4}); + } + + int totalSize = (int) zipFile.length(); + + try (ZFile zf = new ZFile(zipFile)) { + assertEquals(3, zf.entries().size()); + + StoredEntry file3 = zf.get("file3"); + assertNotNull(file3); + assertEquals(6, file3.getCentralDirectoryHeader().getUncompressedSize()); + + assertArrayEquals(new byte[]{9, 0, 1, 2, 3, 4}, file3.read()); + + zf.add("file3", new ByteArrayInputStream(new byte[]{11, 12})); + zf.close(); + + int newTotalSize = (int) zipFile.length(); + assertTrue(newTotalSize + " < " + totalSize, newTotalSize < totalSize); + + file3 = zf.get("file3"); + assertNotNull(file3); + assertArrayEquals(new byte[]{11, 12,}, file3.read()); + } + + try (ZFile zf2 = new ZFile(zipFile)) { + StoredEntry file3 = zf2.get("file3"); + assertNotNull(file3); + assertArrayEquals(new byte[]{11, 12,}, file3.read()); + } + } + + @Test + public void replaceFileWithBiggerAtBegin() throws Exception { + File zipFile = new File(mTemporaryFolder.getRoot(), "test.zip"); + + try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zipFile))) { + zos.putNextEntry(new ZipEntry("file1")); + zos.write(new byte[]{1, 2, 3, 4, 5}); + zos.putNextEntry(new ZipEntry("file2")); + zos.write(new byte[]{6, 7, 8}); + zos.putNextEntry(new ZipEntry("file3")); + zos.write(new byte[]{9, 0, 1, 2, 3, 4}); + } + + int totalSize = (int) zipFile.length(); + byte[] newData = new byte[100]; + + try (ZFile zf = new ZFile(zipFile)) { + assertEquals(3, zf.entries().size()); + + StoredEntry file1 = zf.get("file1"); + assertNotNull(file1); + assertEquals(5, file1.getCentralDirectoryHeader().getUncompressedSize()); + + assertArrayEquals(new byte[]{1, 2, 3, 4, 5}, file1.read()); + + /* + * Need some data because java zip API uses data descriptors which we don't and makes + * the entries bigger (meaning just adding a couple of bytes would still fit in the + * same place). + */ + Random r = new Random(); + r.nextBytes(newData); + + zf.add("file1", new ByteArrayInputStream(newData)); + zf.close(); + + int newTotalSize = (int) zipFile.length(); + assertTrue(newTotalSize + " > " + totalSize, newTotalSize > totalSize); + + file1 = zf.get("file1"); + assertNotNull(file1); + assertArrayEquals(newData, file1.read()); + } + + try (ZFile zf2 = new ZFile(zipFile)) { + StoredEntry file1 = zf2.get("file1"); + assertNotNull(file1); + assertArrayEquals(newData, file1.read()); + } + } + + @Test + public void replaceFileWithBiggerAtEnd() throws Exception { + File zipFile = new File(mTemporaryFolder.getRoot(), "test.zip"); + + try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zipFile))) { + zos.putNextEntry(new ZipEntry("file1")); + zos.write(new byte[]{1, 2, 3, 4, 5}); + zos.putNextEntry(new ZipEntry("file2")); + zos.write(new byte[]{6, 7, 8}); + zos.putNextEntry(new ZipEntry("file3")); + zos.write(new byte[]{9, 0, 1, 2, 3, 4}); + } + + int totalSize = (int) zipFile.length(); + byte[] newData = new byte[100]; + + try (ZFile zf = new ZFile(zipFile)) { + assertEquals(3, zf.entries().size()); + + StoredEntry file3 = zf.get("file3"); + assertNotNull(file3); + assertEquals(6, file3.getCentralDirectoryHeader().getUncompressedSize()); + + assertArrayEquals(new byte[]{9, 0, 1, 2, 3, 4}, file3.read()); + + /* + * Need some data because java zip API uses data descriptors which we don't and makes + * the entries bigger (meaning just adding a couple of bytes would still fit in the + * same place). + */ + Random r = new Random(); + r.nextBytes(newData); + + zf.add("file3", new ByteArrayInputStream(newData)); + zf.close(); + + int newTotalSize = (int) zipFile.length(); + assertTrue(newTotalSize + " > " + totalSize, newTotalSize > totalSize); + + file3 = zf.get("file3"); + assertNotNull(file3); + assertArrayEquals(newData, file3.read()); + } + + try (ZFile zf2 = new ZFile(zipFile)) { + StoredEntry file3 = zf2.get("file3"); + assertNotNull(file3); + assertArrayEquals(newData, file3.read()); + } + } + + @Test + public void ignoredFilesDuringMerge() throws Exception { + File zip1 = mTemporaryFolder.newFile("t1.zip"); + + try (ZipOutputStream zos1 = new ZipOutputStream(new FileOutputStream(zip1))) { + zos1.putNextEntry(new ZipEntry("only_in_1")); + zos1.write(new byte[] { 1, 2 }); + zos1.putNextEntry(new ZipEntry("overridden_by_2")); + zos1.write(new byte[] { 2, 3 }); + zos1.putNextEntry(new ZipEntry("not_overridden_by_2")); + zos1.write(new byte[] { 3, 4 }); + } + + File zip2 = mTemporaryFolder.newFile("t2.zip"); + try (ZipOutputStream zos2 = new ZipOutputStream(new FileOutputStream(zip2))) { + zos2.putNextEntry(new ZipEntry("only_in_2")); + zos2.write(new byte[] { 4, 5 }); + zos2.putNextEntry(new ZipEntry("overridden_by_2")); + zos2.write(new byte[] { 5, 6 }); + zos2.putNextEntry(new ZipEntry("not_overridden_by_2")); + zos2.write(new byte[] { 6, 7 }); + zos2.putNextEntry(new ZipEntry("ignored_in_2")); + zos2.write(new byte[] { 7, 8 }); + } + + try ( + ZFile zf1 = new ZFile(zip1); + ZFile zf2 = new ZFile(zip2)) { + zf1.mergeFrom(zf2, (input) -> input.matches("not.*") || input.matches(".*gnored.*")); + + StoredEntry only_in_1 = zf1.get("only_in_1"); + assertNotNull(only_in_1); + assertArrayEquals(new byte[]{1, 2}, only_in_1.read()); + + StoredEntry overridden_by_2 = zf1.get("overridden_by_2"); + assertNotNull(overridden_by_2); + assertArrayEquals(new byte[]{5, 6}, overridden_by_2.read()); + + StoredEntry not_overridden_by_2 = zf1.get("not_overridden_by_2"); + assertNotNull(not_overridden_by_2); + assertArrayEquals(new byte[]{3, 4}, not_overridden_by_2.read()); + + StoredEntry only_in_2 = zf1.get("only_in_2"); + assertNotNull(only_in_2); + assertArrayEquals(new byte[]{4, 5}, only_in_2.read()); + + StoredEntry ignored_in_2 = zf1.get("ignored_in_2"); + assertNull(ignored_in_2); + } + } + + @Test + public void addingFileDoesNotAddDirectoriesAutomatically() throws Exception { + File zip = new File(mTemporaryFolder.getRoot(), "z.zip"); + try (ZFile zf = new ZFile(zip)) { + zf.add("a/b/c", new ByteArrayInputStream(new byte[]{1, 2, 3})); + zf.update(); + assertEquals(1, zf.entries().size()); + + StoredEntry c = zf.get("a/b/c"); + assertNotNull(c); + assertEquals(3, c.read().length); + } + } + + @Test + public void zipFileWithEocdSignatureInComment() throws Exception { + File zip = mTemporaryFolder.newFile("f.zip"); + try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zip))) { + zos.putNextEntry(new ZipEntry("a")); + zos.write(new byte[] { 1, 2, 3 }); + zos.setComment("Random comment with XXXX weird characters. There must be enough " + + "characters to survive skipping back the EOCD size."); + } + + byte zipBytes[] = Files.toByteArray(zip); + boolean didX4 = false; + for (int i = 0; i < zipBytes.length - 3; i++) { + boolean x4 = true; + for (int j = 0; j < 4; j++) { + if (zipBytes[i + j] != 'X') { + x4 = false; + break; + } + } + + if (x4) { + zipBytes[i] = (byte) 0x50; + zipBytes[i + 1] = (byte) 0x4b; + zipBytes[i + 2] = (byte) 0x05; + zipBytes[i + 3] = (byte) 0x06; + didX4 = true; + break; + } + } + + assertTrue(didX4); + + Files.write(zipBytes, zip); + + try (ZFile zf = new ZFile(zip)) { + assertEquals(1, zf.entries().size()); + StoredEntry a = zf.get("a"); + assertNotNull(a); + assertArrayEquals(new byte[]{1, 2, 3}, a.read()); + } + } + + @Test + public void addFileRecursively() throws Exception { + File tdir = mTemporaryFolder.newFolder(); + File tfile = new File(tdir, "blah-blah"); + Files.write("blah", tfile, Charsets.US_ASCII); + + File zip = new File(tdir, "f.zip"); + try (ZFile zf = new ZFile(zip)) { + zf.addAllRecursively(tfile); + + StoredEntry blahEntry = zf.get("blah-blah"); + assertNotNull(blahEntry); + String contents = new String(blahEntry.read(), Charsets.US_ASCII); + assertEquals("blah", contents); + } + } + + @Test + public void addDirectoryRecursively() throws Exception { + File tdir = mTemporaryFolder.newFolder(); + + String boom = Strings.repeat("BOOM!", 100); + String kaboom = Strings.repeat("KABOOM!", 100); + + Files.write(boom, new File(tdir, "danger"), Charsets.US_ASCII); + Files.write(kaboom, new File(tdir, "do not touch"), Charsets.US_ASCII); + File safeDir = new File(tdir, "safe"); + assertTrue(safeDir.mkdir()); + + String iLoveChocolate = Strings.repeat("I love chocolate! ", 200); + String iLoveOrange = Strings.repeat("I love orange! ", 50); + String loremIpsum = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean vitae " + + "turpis quis justo scelerisque vulputate in et magna. Suspendisse eleifend " + + "ultricies nisi, placerat consequat risus accumsan et. Pellentesque habitant " + + "morbi tristique senectus et netus et malesuada fames ac turpis egestas. " + + "Integer vitae leo purus. Nulla facilisi. Duis ligula libero, lacinia a " + + "malesuada a, viverra tempor sapien. Donec eget consequat sapien, ultrices" + + "interdum diam. Maecenas ipsum erat, suscipit at iaculis a, mollis nec risus. " + + "Quisque tristique ac velit sed auctor. Nulla lacus diam, tristique id sem non, " + + "pellentesque commodo mauris."; + + Files.write(iLoveChocolate, new File(safeDir, "eat.sweet"), Charsets.US_ASCII); + Files.write(iLoveOrange, new File(safeDir, "eat.fruit"), Charsets.US_ASCII); + Files.write(loremIpsum, new File(safeDir, "bedtime.reading.txt"), Charsets.US_ASCII); + + File zip = new File(tdir, "f.zip"); + try (ZFile zf = new ZFile(zip)) { + zf.addAllRecursively(tdir, (f) -> !f.getName().startsWith("eat.")); + + assertEquals(6, zf.entries().size()); + + StoredEntry boomEntry = zf.get("danger"); + assertNotNull(boomEntry); + assertEquals(CompressionMethod.DEFLATE, + boomEntry.getCentralDirectoryHeader().getCompressionInfoWithWait().getMethod()); + assertEquals(boom, new String(boomEntry.read(), Charsets.US_ASCII)); + + StoredEntry kaboomEntry = zf.get("do not touch"); + assertNotNull(kaboomEntry); + assertEquals(CompressionMethod.DEFLATE, + kaboomEntry + .getCentralDirectoryHeader() + .getCompressionInfoWithWait() + .getMethod()); + assertEquals(kaboom, new String(kaboomEntry.read(), Charsets.US_ASCII)); + + StoredEntry safeEntry = zf.get("safe/"); + assertNotNull(safeEntry); + assertEquals(0, safeEntry.read().length); + + StoredEntry choc = zf.get("safe/eat.sweet"); + assertNotNull(choc); + assertEquals(CompressionMethod.STORE, + choc.getCentralDirectoryHeader().getCompressionInfoWithWait().getMethod()); + assertEquals(iLoveChocolate, new String(choc.read(), Charsets.US_ASCII)); + + StoredEntry orangeEntry = zf.get("safe/eat.fruit"); + assertNotNull(orangeEntry); + assertEquals(CompressionMethod.STORE, + orangeEntry + .getCentralDirectoryHeader() + .getCompressionInfoWithWait() + .getMethod()); + assertEquals(iLoveOrange, new String(orangeEntry.read(), Charsets.US_ASCII)); + + StoredEntry loremEntry = zf.get("safe/bedtime.reading.txt"); + assertNotNull(loremEntry); + assertEquals(CompressionMethod.DEFLATE, + loremEntry + .getCentralDirectoryHeader() + .getCompressionInfoWithWait() + .getMethod()); + assertEquals(loremIpsum, new String(loremEntry.read(), Charsets.US_ASCII)); + } + } + + @Test + public void extraDirectoryOffsetEmptyFile() throws Exception { + File zipNoOffsetFile = new File(mTemporaryFolder.getRoot(), "a.zip"); + File zipWithOffsetFile = new File(mTemporaryFolder.getRoot(), "b.zip"); + + int offset = 31; + + long zipNoOffsetSize; + try ( + ZFile zipNoOffset = new ZFile(zipNoOffsetFile); + ZFile zipWithOffset = new ZFile(zipWithOffsetFile)) { + zipWithOffset.setExtraDirectoryOffset(offset); + + zipNoOffset.close(); + zipWithOffset.close(); + + zipNoOffsetSize = zipNoOffsetFile.length(); + long zipWithOffsetSize = zipWithOffsetFile.length(); + + assertEquals(zipNoOffsetSize + offset, zipWithOffsetSize); + + /* + * EOCD with no comment has 22 bytes. + */ + assertEquals(0, zipNoOffset.getCentralDirectoryOffset()); + assertEquals(0, zipNoOffset.getCentralDirectorySize()); + assertEquals(0, zipNoOffset.getEocdOffset()); + assertEquals(ZFileTestConstants.EOCD_SIZE, zipNoOffset.getEocdSize()); + assertEquals(offset, zipWithOffset.getCentralDirectoryOffset()); + assertEquals(0, zipWithOffset.getCentralDirectorySize()); + assertEquals(offset, zipWithOffset.getEocdOffset()); + assertEquals(ZFileTestConstants.EOCD_SIZE, zipWithOffset.getEocdSize()); + } + + /* + * The EOCDs should not differ up until the end of the Central Directory size and should + * not differ after the offset + */ + int p1Start = 0; + int p1Size = Eocd.F_CD_SIZE.endOffset(); + int p2Start = Eocd.F_CD_OFFSET.endOffset(); + int p2Size = (int) zipNoOffsetSize - p2Start; + + byte[] noOffsetData1 = readSegment(zipNoOffsetFile, p1Start, p1Size); + byte[] noOffsetData2 = readSegment(zipNoOffsetFile, p2Start, p2Size); + byte[] withOffsetData1 = readSegment(zipWithOffsetFile, offset, p1Size); + byte[] withOffsetData2 = readSegment(zipWithOffsetFile, offset + p2Start, p2Size); + + assertArrayEquals(noOffsetData1, withOffsetData1); + assertArrayEquals(noOffsetData2, withOffsetData2); + + try (ZFile readWithOffset = new ZFile(zipWithOffsetFile)) { + assertEquals(0, readWithOffset.entries().size()); + } + } + + @Test + public void extraDirectoryOffsetNonEmptyFile() throws Exception { + File zipNoOffsetFile = new File(mTemporaryFolder.getRoot(), "a.zip"); + File zipWithOffsetFile = new File(mTemporaryFolder.getRoot(), "b.zip"); + + int cdSize; + + // The byte arrays below are larger when compressed, so we end up storing them uncompressed, + // which would normally cause them to be 4-aligned. Disable that, to make calculations + // easier. + ZFileOptions options = new ZFileOptions(); + options.setAlignmentRule(AlignmentRules.constant(AlignmentRule.NO_ALIGNMENT)); + + try (ZFile zipNoOffset = new ZFile(zipNoOffsetFile, options); + ZFile zipWithOffset = new ZFile(zipWithOffsetFile, options)) { + zipWithOffset.setExtraDirectoryOffset(37); + + zipNoOffset.add("x", new ByteArrayInputStream(new byte[]{1, 2})); + zipWithOffset.add("x", new ByteArrayInputStream(new byte[]{1, 2})); + + zipNoOffset.close(); + zipWithOffset.close(); + + long zipNoOffsetSize = zipNoOffsetFile.length(); + long zipWithOffsetSize = zipWithOffsetFile.length(); + + assertEquals(zipNoOffsetSize + 37, zipWithOffsetSize); + + /* + * Local file header has 30 bytes + name. + * Central directory entry has 46 bytes + name + * EOCD with no comment has 22 bytes. + */ + assertEquals(ZFileTestConstants.LOCAL_HEADER_SIZE + 1 + 2, + zipNoOffset.getCentralDirectoryOffset()); + cdSize = (int) zipNoOffset.getCentralDirectorySize(); + assertEquals(ZFileTestConstants.CENTRAL_DIRECTORY_ENTRY_SIZE + 1, cdSize); + assertEquals(ZFileTestConstants.LOCAL_HEADER_SIZE + 1 + 2 + cdSize, + zipNoOffset.getEocdOffset()); + assertEquals(ZFileTestConstants.EOCD_SIZE, zipNoOffset.getEocdSize()); + assertEquals(ZFileTestConstants.LOCAL_HEADER_SIZE + 1 + 2 + 37, + zipWithOffset.getCentralDirectoryOffset()); + assertEquals(cdSize, zipWithOffset.getCentralDirectorySize()); + assertEquals(ZFileTestConstants.LOCAL_HEADER_SIZE + 1 + 2 + 37 + cdSize, + zipWithOffset.getEocdOffset()); + assertEquals(ZFileTestConstants.EOCD_SIZE, zipWithOffset.getEocdSize()); + } + + /* + * The files should be equal: until the end of the first entry, from the beginning of the + * central directory until the offset field in the EOCD and after the offset field. + */ + int p1Start = 0; + int p1Size = ZFileTestConstants.LOCAL_HEADER_SIZE + 1 + 2; + int p2Start = ZFileTestConstants.LOCAL_HEADER_SIZE + 1 + 2; + int p2Size = cdSize + Eocd.F_CD_SIZE.endOffset(); + int p3Start = p2Start + cdSize + Eocd.F_CD_OFFSET.endOffset(); + int p3Size = ZFileTestConstants.EOCD_SIZE - Eocd.F_CD_OFFSET.endOffset(); + + byte[] noOffsetData1 = readSegment(zipNoOffsetFile, p1Start, p1Size); + byte[] noOffsetData2 = readSegment(zipNoOffsetFile, p2Start, p2Size); + byte[] noOffsetData3 = readSegment(zipNoOffsetFile, p3Start, p3Size); + byte[] withOffsetData1 = readSegment(zipWithOffsetFile, p1Start, p1Size); + byte[] withOffsetData2 = readSegment(zipWithOffsetFile, 37 + p2Start, p2Size); + byte[] withOffsetData3 = readSegment(zipWithOffsetFile, 37 + p3Start, p3Size); + + assertArrayEquals(noOffsetData1, withOffsetData1); + assertArrayEquals(noOffsetData2, withOffsetData2); + assertArrayEquals(noOffsetData3, withOffsetData3); + + try (ZFile readWithOffset = new ZFile(zipWithOffsetFile)) { + assertEquals(1, readWithOffset.entries().size()); + } + } + + @Test + public void changeExtraDirectoryOffset() throws Exception { + File zipFile = new File(mTemporaryFolder.getRoot(), "a.zip"); + + try (ZFile zip = new ZFile(zipFile)) { + zip.add("x", new ByteArrayInputStream(new byte[]{1, 2})); + zip.close(); + + long noOffsetSize = zipFile.length(); + + zip.setExtraDirectoryOffset(177); + zip.close(); + + long withOffsetSize = zipFile.length(); + + assertEquals(noOffsetSize + 177, withOffsetSize); + } + } + + @Test + public void computeOffsetWhenReadingEmptyFile() throws Exception { + File zipFile = new File(mTemporaryFolder.getRoot(), "a.zip"); + + try (ZFile zip = new ZFile(zipFile)) { + zip.setExtraDirectoryOffset(18); + } + + try (ZFile zip = new ZFile(zipFile)) { + assertEquals(18, zip.getExtraDirectoryOffset()); + } + } + + @Test + public void computeOffsetWhenReadingNonEmptyFile() throws Exception { + File zipFile = new File(mTemporaryFolder.getRoot(), "a.zip"); + + try (ZFile zip = new ZFile(zipFile)) { + zip.setExtraDirectoryOffset(287); + zip.add("x", new ByteArrayInputStream(new byte[]{1, 2})); + } + + try (ZFile zip = new ZFile(zipFile)) { + assertEquals(287, zip.getExtraDirectoryOffset()); + } + } + + @Test + public void obtainingCDAndEocdWhenEntriesWrittenOnEmptyZip() throws Exception { + File zipFile = new File(mTemporaryFolder.getRoot(), "a.zip"); + + byte[][] cd = new byte[1][]; + byte[][] eocd = new byte[1][]; + + try (ZFile zip = new ZFile(zipFile)) { + zip.addZFileExtension(new ZFileExtension() { + @Override + public void entriesWritten() throws IOException { + cd[0] = zip.getCentralDirectoryBytes(); + eocd[0] = zip.getEocdBytes(); + } + }); + } + + assertNotNull(cd[0]); + assertEquals(0, cd[0].length); + assertNotNull(eocd[0]); + assertEquals(22, eocd[0].length); + } + + @Test + public void obtainingCDAndEocdWhenEntriesWrittenOnNonEmptyZip() throws Exception { + File zipFile = new File(mTemporaryFolder.getRoot(), "a.zip"); + + byte[][] cd = new byte[1][]; + byte[][] eocd = new byte[1][]; + + try (ZFile zip = new ZFile(zipFile)) { + zip.add("foo", new ByteArrayInputStream(new byte[0])); + zip.addZFileExtension(new ZFileExtension() { + @Override + public void entriesWritten() throws IOException { + cd[0] = zip.getCentralDirectoryBytes(); + eocd[0] = zip.getEocdBytes(); + } + }); + } + + /* + * Central directory entry has 46 bytes + name + * EOCD with no comment has 22 bytes. + */ + assertNotNull(cd[0]); + assertEquals(46 + 3, cd[0].length); + assertNotNull(eocd[0]); + assertEquals(22, eocd[0].length); + } + + @Test + public void java7JarSupported() throws Exception { + File jar = ZipTestUtils.cloneRsrc("j7.jar", mTemporaryFolder); + + try (ZFile j = new ZFile(jar)) { + assertEquals(8, j.entries().size()); + } + } + + @Test + public void java8JarSupported() throws Exception { + File jar = ZipTestUtils.cloneRsrc("j8.jar", mTemporaryFolder); + + try (ZFile j = new ZFile(jar)) { + assertEquals(8, j.entries().size()); + } + } + + @Test + public void utf8NamesSupportedOnReading() throws Exception { + File zip = ZipTestUtils.cloneRsrc("zip-with-utf8-filename.zip", mTemporaryFolder); + + try (ZFile f = new ZFile(zip)) { + assertEquals(1, f.entries().size()); + + StoredEntry entry = f.entries().iterator().next(); + String filetMignonKorean = "\uc548\uc2eC \uc694\ub9ac"; + String isGoodJapanese = "\u3068\u3066\u3082\u826f\u3044"; + + assertEquals( + filetMignonKorean + " " + isGoodJapanese, + entry.getCentralDirectoryHeader().getName()); + assertArrayEquals( + "Stuff about food is good.\n".getBytes(Charsets.US_ASCII), entry.read()); + } + } + + @Test + public void utf8NamesSupportedOnReadingWithoutUtf8Flag() throws Exception { + File zip = ZipTestUtils.cloneRsrc("zip-with-utf8-filename.zip", mTemporaryFolder); + + // Reset bytes 7 and 122 that have the flag in the local header and central directory. + byte[] data = Files.toByteArray(zip); + data[7] = 0; + data[122] = 0; + Files.write(data, zip); + + try (ZFile f = new ZFile(zip)) { + assertEquals(1, f.entries().size()); + + StoredEntry entry = f.entries().iterator().next(); + String filetMignonKorean = "\uc548\uc2eC \uc694\ub9ac"; + String isGoodJapanese = "\u3068\u3066\u3082\u826f\u3044"; + + assertEquals( + filetMignonKorean + " " + isGoodJapanese, + entry.getCentralDirectoryHeader().getName()); + assertArrayEquals( + "Stuff about food is good.\n".getBytes(Charsets.US_ASCII), + entry.read()); + } + } + + @Test + public void utf8NamesSupportedOnWriting() throws Exception { + File zipFile = new File(mTemporaryFolder.getRoot(), "a.zip"); + String lettuceIsHealthyArmenian = "\u0533\u0561\u0566\u0561\u0580\u0020\u0561\u057C" + + "\u0578\u0572\u057B"; + + try (ZFile zip = new ZFile(zipFile)) { + zip.add(lettuceIsHealthyArmenian, new ByteArrayInputStream(new byte[]{0})); + } + + try (ZFile zip2 = new ZFile(zipFile)) { + assertEquals(1, zip2.entries().size()); + StoredEntry entry = zip2.entries().iterator().next(); + assertEquals(lettuceIsHealthyArmenian, entry.getCentralDirectoryHeader().getName()); + } + } + + @Test + public void zipMemoryUsageIsZeroAfterClose() throws Exception { + File zipFile = new File(mTemporaryFolder.getRoot(), "a.zip"); + + ZFileOptions options = new ZFileOptions(); + long used; + try (ZFile zip = new ZFile(zipFile, options)) { + + assertEquals(0, options.getTracker().getBytesUsed()); + assertEquals(0, options.getTracker().getMaxBytesUsed()); + + zip.add("Blah", new ByteArrayInputStream(new byte[500])); + used = options.getTracker().getBytesUsed(); + assertTrue(used > 500); + assertEquals(used, options.getTracker().getMaxBytesUsed()); + } + + assertEquals(0, options.getTracker().getBytesUsed()); + assertEquals(used, options.getTracker().getMaxBytesUsed()); + } + + @Test + public void unusedZipAreasAreClearedOnWrite() throws Exception { + File zipFile = new File(mTemporaryFolder.getRoot(), "a.zip"); + ZFileOptions options = new ZFileOptions(); + options.setAlignmentRule(AlignmentRules.constantForSuffix(".txt", 1000)); + try (ZFile zf = new ZFile(zipFile, options)) { + zf.add("test1.txt", new ByteArrayInputStream(new byte[]{1}), false); + } + + /* + * Write dummy data in some unused portion of the file. + */ + try (RandomAccessFile raf = new RandomAccessFile(zipFile, "rw")) { + + raf.seek(500); + byte[] dummyData = "Dummy".getBytes(Charsets.US_ASCII); + raf.write(dummyData); + } + + try (ZFile zf = new ZFile(zipFile)) { + zf.touch(); + } + + try (RandomAccessFile raf = new RandomAccessFile(zipFile, "r")) { + + /* + * test1.txt won't take more than 200 bytes. Additionally, the header for + */ + byte[] data = new byte[900]; + RandomAccessFileUtils.fullyRead(raf, data); + + byte[] zeroData = new byte[data.length]; + assertArrayEquals(zeroData, data); + } + } + + @Test + public void deferredCompression() throws Exception { + File zipFile = new File(mTemporaryFolder.getRoot(), "a.zip"); + + ExecutorService executor = Executors.newSingleThreadExecutor(); + + ZFileOptions options = new ZFileOptions(); + boolean[] done = new boolean[1]; + options.setCompressor(new DeflateExecutionCompressor(executor, options.getTracker(), + Deflater.BEST_COMPRESSION) { + @Nonnull + @Override + protected CompressionResult immediateCompress(@Nonnull CloseableByteSource source) + throws Exception { + Thread.sleep(500); + CompressionResult cr = super.immediateCompress(source); + done[0] = true; + return cr; + } + }); + + try (ZFile zip = new ZFile(zipFile, options)) { + byte sequences = 100; + int seqCount = 1000; + byte[] compressableData = new byte[sequences * seqCount]; + for (byte i = 0; i < sequences; i++) { + for (int j = 0; j < seqCount; j++) { + compressableData[i * seqCount + j] = i; + } + } + + zip.add("compressedFile", new ByteArrayInputStream(compressableData)); + assertFalse(done[0]); + + /* + * Even before closing, eventually all the stream will be read. + */ + long tooLong = System.currentTimeMillis() + 10000; + while (!done[0] && System.currentTimeMillis() < tooLong) { + Thread.sleep(10); + } + + assertTrue(done[0]); + } + + executor.shutdownNow(); + } + + @Test + public void zipFileWithEocdMarkerInComment() throws Exception { + File zipFile = new File(mTemporaryFolder.getRoot(), "x"); + + try (Closer closer = Closer.create()) { + ZipOutputStream zos = closer.register( + new ZipOutputStream(new FileOutputStream(zipFile))); + zos.setComment("\u0065\u4b50"); + zos.putNextEntry(new ZipEntry("foo")); + zos.write(new byte[] { 1, 2, 3, 4 }); + zos.close(); + + ZFile zf = closer.register(new ZFile(zipFile)); + StoredEntry entry = zf.get("foo"); + assertNotNull(entry); + assertEquals(4, entry.getCentralDirectoryHeader().getUncompressedSize()); + } + } + + @Test + public void zipFileWithEocdMarkerInFileName() throws Exception { + File zipFile = new File(mTemporaryFolder.getRoot(), "x"); + + String fname = "tricky-\u0050\u004b\u0005\u0006"; + byte[] bytes = new byte[] { 1, 2, 3, 4 }; + + try (Closer closer = Closer.create()) { + ZipOutputStream zos = closer.register( + new ZipOutputStream(new FileOutputStream(zipFile))); + zos.putNextEntry(new ZipEntry(fname)); + zos.write(bytes); + zos.close(); + + ZFile zf = closer.register(new ZFile(zipFile)); + StoredEntry entry = zf.get(fname); + assertNotNull(entry); + assertEquals(4, entry.getCentralDirectoryHeader().getUncompressedSize()); + } + } + + @Test + public void zipFileWithEocdMarkerInFileContents() throws Exception { + File zipFile = new File(mTemporaryFolder.getRoot(), "x"); + + byte[] bytes = new byte[] { 0x50, 0x4b, 0x05, 0x06 }; + + try (Closer closer = Closer.create()) { + ZipOutputStream zos = closer.register( + new ZipOutputStream(new FileOutputStream(zipFile))); + ZipEntry zipEntry = new ZipEntry("file"); + zipEntry.setMethod(ZipEntry.STORED); + zipEntry.setCompressedSize(4); + zipEntry.setSize(4); + zipEntry.setCrc(Hashing.crc32().hashBytes(bytes).padToLong()); + zos.putNextEntry(zipEntry); + zos.write(bytes); + zos.close(); + + ZFile zf = closer.register(new ZFile(zipFile)); + StoredEntry entry = zf.get("file"); + assertNotNull(entry); + assertEquals(4, entry.getCentralDirectoryHeader().getUncompressedSize()); + } + } + + @Test + public void replaceVeryLargeFileWithBiggerInMiddleOfZip() throws Exception { + File zipFile = new File(mTemporaryFolder.getRoot(), "x"); + + long small1Offset; + long small2Offset; + ZFileOptions coverOptions = new ZFileOptions(); + coverOptions.setCoverEmptySpaceUsingExtraField(true); + try (ZFile zf = new ZFile(zipFile, coverOptions)) { + zf.add("small1", new ByteArrayInputStream(new byte[] { 0, 1 })); + } + + try (ZFile zf = new ZFile(zipFile, coverOptions)) { + zf.add("verybig", new ByteArrayInputStream(new byte[100_000]), false); + } + + try (ZFile zf = new ZFile(zipFile, coverOptions)) { + zf.add("small2", new ByteArrayInputStream(new byte[] { 0, 1 })); + } + + try (ZFile zf = new ZFile(zipFile, coverOptions)) { + StoredEntry se = zf.get("small1"); + assertNotNull(se); + small1Offset = se.getCentralDirectoryHeader().getOffset(); + + se = zf.get("small2"); + assertNotNull(se); + small2Offset = se.getCentralDirectoryHeader().getOffset(); + + se = zf.get("verybig"); + assertNotNull(se); + se.delete(); + + zf.add("evenbigger", new ByteArrayInputStream(new byte[110_000]), false); + } + + try (ZFile zf = new ZFile(zipFile, coverOptions)) { + StoredEntry se = zf.get("small1"); + assertNotNull(se); + assertEquals(se.getCentralDirectoryHeader().getOffset(), small1Offset); + + se = zf.get("small2"); + assertNotNull(se); + assertNotEquals(se.getCentralDirectoryHeader().getOffset(), small2Offset); + } + } + + @Test + public void regressionRepackingDoesNotFail() throws Exception { + File zipFile = new File(mTemporaryFolder.getRoot(), "x"); + + ZFileOptions coverOptions = new ZFileOptions(); + coverOptions.setCoverEmptySpaceUsingExtraField(true); + try (ZFile zf = new ZFile(zipFile, coverOptions)) { + zf.add("small_1", new ByteArrayInputStream(new byte[] { 0, 1 })); + zf.add("very_big", new ByteArrayInputStream(new byte[100_000]), false); + zf.add("small_2", new ByteArrayInputStream(new byte[] { 0, 1 })); + zf.add("big", new ByteArrayInputStream(new byte[10_000]), false); + zf.add("small_3", new ByteArrayInputStream(new byte[] { 0, 1 })); + } + + /* + * Regression we're covering is that small_2 cannot be extended to cover up for the space + * taken by very_big and needs to be repositioned. However, the algorithm to reposition + * will put it in the best-fitting block, which is the one in "big", failing to actually + * move it backwards in the file. + */ + try (ZFile zf = new ZFile(zipFile, coverOptions)) { + StoredEntry se = zf.get("big"); + assertNotNull(se); + se.delete(); + + se = zf.get("very_big"); + assertNotNull(se); + se.delete(); + } + } + + @Test + public void cannotAddMoreThan0x7fffExtraField() throws Exception { + File zipFile = new File(mTemporaryFolder.getRoot(), "a.zip"); + + ZFileOptions zfo = new ZFileOptions(); + zfo.setCoverEmptySpaceUsingExtraField(true); + + /* + * Create a zip file with: + * + * [small file][large file with exactly 0x8000 bytes][small file 2] + */ + long smallFile1Offset; + long smallFile2Offset; + long largeFileOffset; + String largeFileName = "Large file"; + try (ZFile zf = new ZFile(zipFile, zfo)) { + zf.add("Small file", new ByteArrayInputStream(new byte[] { 0, 1 })); + + int largeFileTotalSize = 0x8000; + int largeFileContentsSize = + largeFileTotalSize + - ZFileTestConstants.LOCAL_HEADER_SIZE + - largeFileName.length(); + + zf.add(largeFileName, new ByteArrayInputStream(new byte[largeFileContentsSize]), false); + zf.add("Small file 2", new ByteArrayInputStream(new byte[] { 0, 1 })); + + zf.update(); + + StoredEntry sfEntry = zf.get("Small file"); + assertNotNull(sfEntry); + smallFile1Offset = sfEntry.getCentralDirectoryHeader().getOffset(); + assertEquals(0, smallFile1Offset); + + StoredEntry lfEntry = zf.get(largeFileName); + assertNotNull(lfEntry); + largeFileOffset = lfEntry.getCentralDirectoryHeader().getOffset(); + + StoredEntry sf2Entry = zf.get("Small file 2"); + assertNotNull(sf2Entry); + smallFile2Offset = sf2Entry.getCentralDirectoryHeader().getOffset(); + + assertEquals(largeFileTotalSize, smallFile2Offset - largeFileOffset); + } + + /* + * Remove the large file from the zip file and check that small file 2 has been moved, but + * no extra field has been added. + */ + try (ZFile zf = new ZFile(zipFile, zfo)) { + StoredEntry lfEntry = zf.get(largeFileName); + assertNotNull(lfEntry); + lfEntry.delete(); + + zf.update(); + + StoredEntry sfEntry = zf.get("Small file"); + assertNotNull(sfEntry); + smallFile1Offset = sfEntry.getCentralDirectoryHeader().getOffset(); + assertEquals(0, smallFile1Offset); + + StoredEntry sf2Entry = zf.get("Small file 2"); + assertNotNull(sf2Entry); + long newSmallFile2Offset = sf2Entry.getCentralDirectoryHeader().getOffset(); + assertEquals(largeFileOffset, newSmallFile2Offset); + + assertEquals(0, sf2Entry.getLocalExtra().size()); + } + } + + @Test + public void canAddMoreThan0x7fffExtraField() throws Exception { + File zipFile = new File(mTemporaryFolder.getRoot(), "a.zip"); + + ZFileOptions zfo = new ZFileOptions(); + zfo.setCoverEmptySpaceUsingExtraField(true); + + /* + * Create a zip file with: + * + * [small file][large file with exactly 0x7fff bytes][small file 2] + */ + long smallFile1Offset; + long smallFile2Offset; + long largeFileOffset; + String largeFileName = "Large file"; + int largeFileTotalSize = 0x7fff; + try (ZFile zf = new ZFile(zipFile, zfo)) { + zf.add("Small file", new ByteArrayInputStream(new byte[] { 0, 1 })); + + int largeFileContentsSize = + largeFileTotalSize + - ZFileTestConstants.LOCAL_HEADER_SIZE + - largeFileName.length(); + + zf.add(largeFileName, new ByteArrayInputStream(new byte[largeFileContentsSize]), false); + zf.add("Small file 2", new ByteArrayInputStream(new byte[] { 0, 1 })); + + zf.update(); + + StoredEntry sfEntry = zf.get("Small file"); + assertNotNull(sfEntry); + smallFile1Offset = sfEntry.getCentralDirectoryHeader().getOffset(); + assertEquals(0, smallFile1Offset); + + StoredEntry lfEntry = zf.get(largeFileName); + assertNotNull(lfEntry); + largeFileOffset = lfEntry.getCentralDirectoryHeader().getOffset(); + + StoredEntry sf2Entry = zf.get("Small file 2"); + assertNotNull(sf2Entry); + smallFile2Offset = sf2Entry.getCentralDirectoryHeader().getOffset(); + + assertEquals(largeFileTotalSize, smallFile2Offset - largeFileOffset); + } + + /* + * Remove the large file from the zip file and check that small file 2 has been moved back + * but with 0x7fff extra space added. + */ + try (ZFile zf = new ZFile(zipFile, zfo)) { + StoredEntry lfEntry = zf.get(largeFileName); + assertNotNull(lfEntry); + lfEntry.delete(); + + zf.update(); + + StoredEntry sfEntry = zf.get("Small file"); + assertNotNull(sfEntry); + smallFile1Offset = sfEntry.getCentralDirectoryHeader().getOffset(); + assertEquals(0, smallFile1Offset); + + StoredEntry sf2Entry = zf.get("Small file 2"); + assertNotNull(sf2Entry); + long newSmallFile2Offset = sf2Entry.getCentralDirectoryHeader().getOffset(); + + assertEquals(largeFileOffset, newSmallFile2Offset); + assertEquals(largeFileTotalSize, sf2Entry.getLocalExtra().size()); + } + } + + @Test + public void detectIncorrectCRC32InLocalHeader() throws Exception { + File zipFile = new File(mTemporaryFolder.getRoot(), "a.zip"); + + /* + * Zip files created by ZFile never have data descriptors so we need to create one using + * java's zip. + */ + try ( + FileOutputStream fos = new FileOutputStream(zipFile); + ZipOutputStream zos = new ZipOutputStream(fos)) { + ZipEntry ze = new ZipEntry("foo"); + zos.putNextEntry(ze); + byte[] randomBytes = new byte[512]; + new Random().nextBytes(randomBytes); + zos.write(randomBytes); + } + + /* + * Open the zip file and compute where the local header CRC32 is. + */ + long crcOffset; + try (ZFile zf = new ZFile(zipFile)) { + StoredEntry se = zf.get("foo"); + assertNotNull(se); + long cdOffset = zf.getCentralDirectoryOffset(); + + /* + * Twelve bytes from the CD offset, we have the start of the CRC32 of the zip entry. + */ + crcOffset = cdOffset - 12; + } + + /* + * Corrupt the CRC32. + */ + byte[] crc = readSegment(zipFile, crcOffset, 4); + crc[0]++; + try (RandomAccessFile raf = new RandomAccessFile(zipFile, "rw")) { + raf.seek(crcOffset); + raf.write(crc); + } + + /* + * Now open the zip file and it should write a message in the log. + */ + ZFileOptions options = new ZFileOptions(); + options.setVerifyLogFactory(VerifyLogs::unlimited); + try (ZFile zf = new ZFile(zipFile, options)) { + VerifyLog vl = zf.getVerifyLog(); + assertTrue(vl.getLogs().isEmpty()); + StoredEntry fooEntry = zf.get("foo"); + vl = fooEntry.getVerifyLog(); + assertEquals(1, vl.getLogs().size()); + assertTrue(vl.getLogs().get(0).contains("CRC32")); + } + } + + @Test + public void detectIncorrectVersionToExtractInCentralDirectory() throws Exception { + File zipFile = new File(mTemporaryFolder.getRoot(), "a.zip"); + + /* + * Create a valid zip file. + */ + try (ZFile zf = new ZFile(zipFile)) { + zf.add("foo", new ByteArrayInputStream(new byte[0])); + } + + /* + * Change the "version to extract" in the central directory to 0x7777. + */ + int versionToExtractOffset = + ZFileTestConstants.LOCAL_HEADER_SIZE + + 3 + + CentralDirectory.F_VERSION_EXTRACT.offset(); + byte[] allZipBytes = Files.toByteArray(zipFile); + allZipBytes[versionToExtractOffset] = 0x77; + allZipBytes[versionToExtractOffset + 1] = 0x77; + Files.write(allZipBytes, zipFile); + + /* + * Opening the file and it should write a message in the log. The entry has the right + * version to extract (20), but it issues a warning because it is not equal to the one + * in the central directory. + */ + ZFileOptions options = new ZFileOptions(); + options.setVerifyLogFactory(VerifyLogs::unlimited); + try (ZFile zf = new ZFile(zipFile, options)) { + VerifyLog vl = zf.getVerifyLog(); + assertEquals(1, vl.getLogs().size()); + assertTrue(vl.getLogs().get(0).toLowerCase(Locale.US).contains("version")); + assertTrue(vl.getLogs().get(0).toLowerCase(Locale.US).contains("extract")); + StoredEntry fooEntry = zf.get("foo"); + vl = fooEntry.getVerifyLog(); + assertEquals(1, vl.getLogs().size()); + assertTrue(vl.getLogs().get(0).toLowerCase(Locale.US).contains("version")); + assertTrue(vl.getLogs().get(0).toLowerCase(Locale.US).contains("extract")); + } + } + + @Test + public void detectIncorrectVersionToExtractInLocalHeader() throws Exception { + File zipFile = new File(mTemporaryFolder.getRoot(), "a.zip"); + + /* + * Create a valid zip file. + */ + try (ZFile zf = new ZFile(zipFile)) { + zf.add("foo", new ByteArrayInputStream(new byte[0])); + } + + /* + * Change the "version to extract" in the local header to 0x7777. + */ + int versionToExtractOffset = StoredEntry.F_VERSION_EXTRACT.offset(); + byte[] allZipBytes = Files.toByteArray(zipFile); + allZipBytes[versionToExtractOffset] = 0x77; + allZipBytes[versionToExtractOffset + 1] = 0x77; + Files.write(allZipBytes, zipFile); + + /* + * Opening the file should log an error message. + */ + ZFileOptions options = new ZFileOptions(); + options.setVerifyLogFactory(VerifyLogs::unlimited); + try (ZFile zf = new ZFile(zipFile, options)) { + VerifyLog vl = zf.getVerifyLog(); + assertTrue(vl.getLogs().isEmpty()); + StoredEntry fooEntry = zf.get("foo"); + vl = fooEntry.getVerifyLog(); + assertEquals(1, vl.getLogs().size()); + assertTrue(vl.getLogs().get(0).toLowerCase(Locale.US).contains("version")); + assertTrue(vl.getLogs().get(0).toLowerCase(Locale.US).contains("extract")); + } + } + + @Test + public void sortZipContentsWithDeferredCrc32() throws Exception { + /* + * Create a zip file with deferred CRC32 and files in non-alphabetical order. + * ZipOutputStream always creates deferred CRC32 entries. + */ + File zipFile = new File(mTemporaryFolder.getRoot(), "a.zip"); + try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zipFile))) { + zos.putNextEntry(new ZipEntry("b")); + zos.write(new byte[1000]); + zos.putNextEntry(new ZipEntry("a")); + zos.write(new byte[1000]); + } + + /* + * Now open the zip using a ZFile and sort the contents and check that the deferred CRC32 + * bits were reset. + */ + try (ZFile zf = new ZFile(zipFile)) { + StoredEntry a = zf.get("a"); + assertNotNull(a); + assertNotSame(DataDescriptorType.NO_DATA_DESCRIPTOR, a.getDataDescriptorType()); + StoredEntry b = zf.get("b"); + assertNotNull(b); + assertNotSame(DataDescriptorType.NO_DATA_DESCRIPTOR, b.getDataDescriptorType()); + assertTrue( + a.getCentralDirectoryHeader().getOffset() + > b.getCentralDirectoryHeader().getOffset()); + + zf.sortZipContents(); + zf.update(); + + a = zf.get("a"); + assertNotNull(a); + assertSame(DataDescriptorType.NO_DATA_DESCRIPTOR, a.getDataDescriptorType()); + b = zf.get("b"); + assertNotNull(b); + assertSame(DataDescriptorType.NO_DATA_DESCRIPTOR, b.getDataDescriptorType()); + + assertTrue( + a.getCentralDirectoryHeader().getOffset() + < b.getCentralDirectoryHeader().getOffset()); + } + + /* + * Open the file again and check there are no warnings. + */ + try (ZFile zf = new ZFile(zipFile)) { + VerifyLog vl = zf.getVerifyLog(); + assertEquals(0, vl.getLogs().size()); + + StoredEntry a = zf.get("a"); + assertNotNull(a); + vl = a.getVerifyLog(); + assertEquals(0, vl.getLogs().size()); + + StoredEntry b = zf.get("b"); + assertNotNull(b); + vl = b.getVerifyLog(); + assertEquals(0, vl.getLogs().size()); + } + } + + @Test + public void alignZipContentsWithDeferredCrc32() throws Exception { + /* + * Create an unaligned zip file with deferred CRC32 and files in non-alphabetical order. + * We need an uncompressed file to make realigning have any effect. + */ + File zipFile = new File(mTemporaryFolder.getRoot(), "a.zip"); + try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zipFile))) { + zos.putNextEntry(new ZipEntry("x")); + zos.write(new byte[1000]); + zos.putNextEntry(new ZipEntry("y")); + zos.write(new byte[1000]); + ZipEntry zEntry = new ZipEntry("z"); + zEntry.setSize(1000); + zEntry.setMethod(ZipEntry.STORED); + zEntry.setCrc(Hashing.crc32().hashBytes(new byte[1000]).asInt()); + zos.putNextEntry(zEntry); + zos.write(new byte[1000]); + } + + /* + * Now open the zip using a ZFile and realign the contents and check that the deferred CRC32 + * bits were reset. + */ + ZFileOptions options = new ZFileOptions(); + options.setAlignmentRule(AlignmentRules.constant(2000)); + try (ZFile zf = new ZFile(zipFile, options)) { + StoredEntry x = zf.get("x"); + assertNotNull(x); + assertNotSame(DataDescriptorType.NO_DATA_DESCRIPTOR, x.getDataDescriptorType()); + StoredEntry y = zf.get("y"); + assertNotNull(y); + assertNotSame(DataDescriptorType.NO_DATA_DESCRIPTOR, y.getDataDescriptorType()); + StoredEntry z = zf.get("z"); + assertNotNull(z); + assertSame(DataDescriptorType.NO_DATA_DESCRIPTOR, z.getDataDescriptorType()); + + zf.realign(); + zf.update(); + + x = zf.get("x"); + assertNotNull(x); + assertSame(DataDescriptorType.NO_DATA_DESCRIPTOR, x.getDataDescriptorType()); + y = zf.get("y"); + assertNotNull(y); + assertSame(DataDescriptorType.NO_DATA_DESCRIPTOR, y.getDataDescriptorType()); + z = zf.get("z"); + assertNotNull(z); + assertSame(DataDescriptorType.NO_DATA_DESCRIPTOR, z.getDataDescriptorType()); + } + } + + @Test + public void openingZFileDoesNotRemoveDataDescriptors() throws Exception { + /* + * Create a zip file with deferred CRC32. + */ + File zipFile = new File(mTemporaryFolder.getRoot(), "a.zip"); + try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zipFile))) { + zos.putNextEntry(new ZipEntry("a")); + zos.write(new byte[1000]); + } + + /* + * Open using ZFile and check that the deferred CRC32 is there. + */ + try (ZFile zf = new ZFile(zipFile)) { + StoredEntry se = zf.get("a"); + assertNotNull(se); + assertNotEquals(DataDescriptorType.NO_DATA_DESCRIPTOR, se.getDataDescriptorType()); + } + + /* + * Open using ZFile (again) and check that the deferred CRC32 is there. + */ + try (ZFile zf = new ZFile(zipFile)) { + StoredEntry se = zf.get("a"); + assertNotNull(se); + assertNotEquals(DataDescriptorType.NO_DATA_DESCRIPTOR, se.getDataDescriptorType()); + } + } + + @Test + public void zipCommentsAreSaved() throws Exception { + File zipFileWithComments = new File(mTemporaryFolder.getRoot(), "a.zip"); + try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zipFileWithComments))) { + zos.setComment("foo"); + } + + /* + * Open the zip file and check the comment is there. + */ + try (ZFile zf = new ZFile(zipFileWithComments)) { + byte[] comment = zf.getEocdComment(); + assertArrayEquals(new byte[] { 'f', 'o', 'o' }, comment); + + /* + * Modify the comment and write the file. + */ + zf.setEocdComment(new byte[] { 'b', 'a', 'r', 'r' }); + } + + /* + * Open the file and see that the comment is there (both with java and zfile). + */ + try (ZipFile zf2 = new ZipFile(zipFileWithComments)) { + assertEquals("barr", zf2.getComment()); + } + + try (ZFile zf3 = new ZFile(zipFileWithComments)) { + assertArrayEquals(new byte[] { 'b', 'a', 'r', 'r' }, zf3.getEocdComment()); + } + } + + @Test + public void eocdCommentsWithMoreThan64kNotAllowed() throws Exception { + File zipFileWithComments = new File(mTemporaryFolder.getRoot(), "a.zip"); + try (ZFile zf = new ZFile(zipFileWithComments)) { + try { + zf.setEocdComment(new byte[65536]); + fail(); + } catch (IllegalArgumentException e) { + // Expected. + } + + zf.setEocdComment(new byte[65535]); + } + } + + @Test + public void eocdCommentsWithTheEocdMarkerAreAllowed() throws Exception { + File zipFileWithComments = new File(mTemporaryFolder.getRoot(), "a.zip"); + byte[] data = new byte[100]; + data[50] = 0x50; // Signature + data[51] = 0x4b; + data[52] = 0x05; + data[53] = 0x06; + data[54] = 0x00; // Number of disk + data[55] = 0x00; + data[56] = 0x00; // Disk CD start + data[57] = 0x00; + data[54] = 0x01; // Total records 1 + data[55] = 0x00; + data[56] = 0x02; // Total records 2, must be = to total records 1 + data[57] = 0x00; + + try (ZFile zf = new ZFile(zipFileWithComments)) { + zf.setEocdComment(data); + } + + try (ZFile zf = new ZFile(zipFileWithComments)) { + assertArrayEquals(data, zf.getEocdComment()); + } + } + + @Test + public void eocdCommentsWithTheEocdMarkerThatAreInvalidAreNotAllowed() throws Exception { + File zipFileWithComments = new File(mTemporaryFolder.getRoot(), "a.zip"); + byte[] data = new byte[100]; + data[50] = 0x50; + data[51] = 0x4b; + data[52] = 0x05; + data[53] = 0x06; + data[67] = 0x00; + + try (ZFile zf = new ZFile(zipFileWithComments)) { + try { + zf.setEocdComment(data); + fail(); + } catch (IllegalArgumentException e) { + // Expected. + } + } + } + + @Test + public void zipCommentsArePreservedWithFileChanges() throws Exception { + File zipFileWithComments = new File(mTemporaryFolder.getRoot(), "a.zip"); + byte[] comment = new byte[] { 1, 3, 4 }; + try (ZFile zf = new ZFile(zipFileWithComments)) { + zf.add("foo", new ByteArrayInputStream(new byte[50])); + zf.setEocdComment(comment); + } + + try (ZFile zf = new ZFile(zipFileWithComments)) { + assertArrayEquals(comment, zf.getEocdComment()); + zf.add("bar", new ByteArrayInputStream(new byte[100])); + } + + try (ZFile zf = new ZFile(zipFileWithComments)) { + assertArrayEquals(comment, zf.getEocdComment()); + } + } + + @Test + public void overlappingZipEntries() throws Exception { + File myZip = ZipTestUtils.cloneRsrc("overlapping.zip", mTemporaryFolder); + try (ZFile zf = new ZFile(myZip)) { + fail(); + } catch (IOException e) { + assertTrue(Throwables.getStackTraceAsString(e).contains("overlapping/bbb")); + assertTrue(Throwables.getStackTraceAsString(e).contains("overlapping/ddd")); + assertFalse(Throwables.getStackTraceAsString(e).contains("Central Directory")); + } + } + + @Test + public void overlappingZipEntryWithCentralDirectory() throws Exception { + File myZip = ZipTestUtils.cloneRsrc("overlapping2.zip", mTemporaryFolder); + try (ZFile zf = new ZFile(myZip)) { + fail(); + } catch (IOException e) { + assertFalse(Throwables.getStackTraceAsString(e).contains("overlapping/bbb")); + assertTrue(Throwables.getStackTraceAsString(e).contains("overlapping/ddd")); + assertTrue(Throwables.getStackTraceAsString(e).contains("Central Directory")); + } + } + + @Test + public void readFileWithOffsetBeyondFileEnd() throws Exception { + File myZip = ZipTestUtils.cloneRsrc("entry-outside-file.zip", mTemporaryFolder); + try (ZFile zf = new ZFile(myZip)) { + fail(); + } catch (IOException e) { + assertTrue(Throwables.getStackTraceAsString(e).contains("entry-outside-file/foo")); + assertTrue(Throwables.getStackTraceAsString(e).contains("EOF")); + } + } +} diff --git a/src/test/java/com/android/tools/build/apkzlib/zip/ZFileTestConstants.java b/src/test/java/com/android/tools/build/apkzlib/zip/ZFileTestConstants.java new file mode 100644 index 0000000..85b9a76 --- /dev/null +++ b/src/test/java/com/android/tools/build/apkzlib/zip/ZFileTestConstants.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * 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.android.tools.build.apkzlib.zip; + +/** + * Constants used in tests. + */ +public interface ZFileTestConstants { + + /** + * Number of bytes in a zip entry local header, not considering name and comment. + */ + int LOCAL_HEADER_SIZE = 30; + + /** + * Number of bytes in a zip central directory entry, not considering name and comment. + */ + int CENTRAL_DIRECTORY_ENTRY_SIZE = 46; + + /** + * Number of bytes in an EOCD without comment. + */ + int EOCD_SIZE = 22; +} diff --git a/src/test/java/com/android/tools/build/apkzlib/zip/ZipMergeTest.java b/src/test/java/com/android/tools/build/apkzlib/zip/ZipMergeTest.java new file mode 100644 index 0000000..cd1b246 --- /dev/null +++ b/src/test/java/com/android/tools/build/apkzlib/zip/ZipMergeTest.java @@ -0,0 +1,205 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * 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.android.tools.build.apkzlib.zip; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +import com.android.tools.build.apkzlib.utils.CachedFileContents; +import com.google.common.base.Charsets; +import com.google.common.hash.Hashing; +import com.google.common.io.ByteStreams; +import com.google.common.io.Closer; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +public class ZipMergeTest { + @Rule + public TemporaryFolder mTemporaryFolder = new TemporaryFolder(); + + @Test + public void mergeZip() throws Exception { + File aZip = ZipTestUtils.cloneRsrc("simple-zip.zip", mTemporaryFolder, "a.zip"); + + CachedFileContents changeDetector; + File merged = new File(mTemporaryFolder.getRoot(), "r.zip"); + try (ZFile mergedZf = new ZFile(merged)) { + mergedZf.mergeFrom(new ZFile(aZip), f -> false); + mergedZf.close(); + + assertEquals(3, mergedZf.entries().size()); + + StoredEntry e0 = mergedZf.get("dir/"); + assertNotNull(e0); + assertSame(StoredEntryType.DIRECTORY, e0.getType()); + + StoredEntry e1 = mergedZf.get("dir/inside"); + assertNotNull(e1); + assertSame(StoredEntryType.FILE, e1.getType()); + ByteArrayOutputStream e1BytesOut = new ByteArrayOutputStream(); + ByteStreams.copy(e1.open(), e1BytesOut); + byte e1Bytes[] = e1BytesOut.toByteArray(); + String e1Txt = new String(e1Bytes, Charsets.US_ASCII); + assertEquals("inside", e1Txt); + + StoredEntry e2 = mergedZf.get("file.txt"); + assertNotNull(e2); + assertSame(StoredEntryType.FILE, e2.getType()); + ByteArrayOutputStream e2BytesOut = new ByteArrayOutputStream(); + ByteStreams.copy(e2.open(), e2BytesOut); + byte e2Bytes[] = e2BytesOut.toByteArray(); + String e2Txt = new String(e2Bytes, Charsets.US_ASCII); + assertEquals("file with more text to allow deflating to be useful", e2Txt); + + changeDetector = new CachedFileContents<>(merged); + changeDetector.closed(null); + + /* + * Clone aZip into bZip and merge. Should have no effect on the final zip file. + */ + File bZip = ZipTestUtils.cloneRsrc("simple-zip.zip", mTemporaryFolder, "b.zip"); + + mergedZf.mergeFrom(new ZFile(bZip), f -> false); + } + + assertTrue(changeDetector.isValid()); + } + + @Test + public void mergeZipWithDeferredCrc() throws Exception { + File foo = mTemporaryFolder.newFile("foo"); + + byte[] wBytes = ZipTestUtils.rsrcBytes("text-files/wikipedia.html"); + + try (ZipOutputStream fooOut = new ZipOutputStream(new FileOutputStream(foo))) { + fooOut.putNextEntry(new ZipEntry("w")); + fooOut.write(wBytes); + } + + try (Closer closer = Closer.create()) { + ZFile fooZf = closer.register(new ZFile(foo)); + StoredEntry wStored = fooZf.get("w"); + assertNotNull(wStored); + assertTrue(wStored.getCentralDirectoryHeader().getGpBit().isDeferredCrc()); + assertEquals(CompressionMethod.DEFLATE, + wStored.getCentralDirectoryHeader().getCompressionInfoWithWait().getMethod()); + + ZFile merged = closer.register(new ZFile(new File(mTemporaryFolder.getRoot(), "bar"))); + merged.mergeFrom(fooZf, f -> false); + merged.update(); + + StoredEntry wmStored = merged.get("w"); + assertNotNull(wmStored); + assertFalse(wmStored.getCentralDirectoryHeader().getGpBit().isDeferredCrc()); + assertEquals(CompressionMethod.DEFLATE, + wmStored.getCentralDirectoryHeader().getCompressionInfoWithWait().getMethod()); + } + } + + @Test + public void mergeZipKeepsDeflatedAndStored() throws Exception { + File foo = mTemporaryFolder.newFile("foo"); + + byte[] wBytes = ZipTestUtils.rsrcBytes("text-files/wikipedia.html"); + byte[] lBytes = ZipTestUtils.rsrcBytes("images/lena.png"); + + try (ZipOutputStream fooOut = new ZipOutputStream(new FileOutputStream(foo))) { + fooOut.putNextEntry(new ZipEntry("w")); + fooOut.write(wBytes); + ZipEntry le = new ZipEntry("l"); + le.setMethod(ZipEntry.STORED); + le.setSize(lBytes.length); + le.setCrc(Hashing.crc32().hashBytes(lBytes).padToLong()); + fooOut.putNextEntry(le); + fooOut.write(lBytes); + } + + try (Closer closer = Closer.create()) { + ZFile fooZf = closer.register(new ZFile(foo)); + StoredEntry wStored = fooZf.get("w"); + assertNotNull(wStored); + assertEquals(CompressionMethod.DEFLATE, + wStored.getCentralDirectoryHeader().getCompressionInfoWithWait().getMethod()); + StoredEntry lStored = fooZf.get("l"); + assertNotNull(lStored); + assertEquals(CompressionMethod.STORE, + lStored.getCentralDirectoryHeader().getCompressionInfoWithWait().getMethod()); + + ZFile merged = closer.register(new ZFile(new File(mTemporaryFolder.getRoot(), "bar"))); + merged.mergeFrom(fooZf, f -> false); + merged.update(); + + StoredEntry wmStored = merged.get("w"); + assertNotNull(wmStored); + assertFalse(wmStored.getCentralDirectoryHeader().getGpBit().isDeferredCrc()); + assertEquals(CompressionMethod.DEFLATE, + wmStored.getCentralDirectoryHeader().getCompressionInfoWithWait().getMethod()); + assertArrayEquals(wBytes, wmStored.read()); + + StoredEntry lmStored = merged.get("l"); + assertNotNull(lmStored); + assertEquals(CompressionMethod.STORE, + lmStored.getCentralDirectoryHeader().getCompressionInfoWithWait().getMethod()); + assertArrayEquals(lBytes, lmStored.read()); + } + } + + @Test + public void mergeZipWithSorting() throws Exception { + File foo = mTemporaryFolder.newFile("foo"); + + byte[] wBytes = ZipTestUtils.rsrcBytes("text-files/wikipedia.html"); + byte[] lBytes = ZipTestUtils.rsrcBytes("images/lena.png"); + + try (ZipOutputStream fooOut = new ZipOutputStream(new FileOutputStream(foo))) { + fooOut.putNextEntry(new ZipEntry("w")); + fooOut.write(wBytes); + ZipEntry le = new ZipEntry("l"); + le.setMethod(ZipEntry.STORED); + le.setSize(lBytes.length); + le.setCrc(Hashing.crc32().hashBytes(lBytes).padToLong()); + fooOut.putNextEntry(le); + fooOut.write(lBytes); + } + + try ( + ZFile fooZf = new ZFile(foo); + ZFile merged = new ZFile(new File(mTemporaryFolder.getRoot(), "bar"))) { + merged.mergeFrom(fooZf, f -> false); + merged.sortZipContents(); + merged.update(); + + StoredEntry wmStored = merged.get("w"); + assertNotNull(wmStored); + assertArrayEquals(wBytes, wmStored.read()); + + StoredEntry lmStored = merged.get("l"); + assertNotNull(lmStored); + assertArrayEquals(lBytes, lmStored.read()); + } + } +} diff --git a/src/test/java/com/android/tools/build/apkzlib/zip/ZipTestUtils.java b/src/test/java/com/android/tools/build/apkzlib/zip/ZipTestUtils.java new file mode 100644 index 0000000..d6e31a2 --- /dev/null +++ b/src/test/java/com/android/tools/build/apkzlib/zip/ZipTestUtils.java @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * 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.android.tools.build.apkzlib.zip; + +import static org.junit.Assert.assertFalse; + +import com.android.tools.build.apkzlib.utils.ApkZFileTestUtils; +import com.google.common.io.Files; +import java.io.File; +import java.io.IOException; +import javax.annotation.Nonnull; +import org.junit.rules.TemporaryFolder; + +/** + * Utility method for zip tests. + */ +class ZipTestUtils { + + /** + * Obtains the data of a resource with the given name. + * + * @param rsrcName the resource name inside packaging resource folder + * @return the resource data + * @throws IOException I/O failed + */ + @Nonnull + static byte[] rsrcBytes(@Nonnull String rsrcName) throws IOException { + return ApkZFileTestUtils.getResourceBytes("/testData/packaging/" + rsrcName).read(); + } + + /** + * Clones a resource to a temporary folder. Generally, resources do not need to be cloned to + * be used. However, in code where there is danger of changing resource files and corrupting + * the source directory, cloning should be done before accessing the resources. + * + * @param rsrcName the resource name + * @param folder the temporary folder + * @return the file that was created with the resource + * @throws IOException failed to clone the resource + */ + static File cloneRsrc(@Nonnull String rsrcName, @Nonnull TemporaryFolder folder) + throws IOException { + String cloneName; + if (rsrcName.contains("/")) { + cloneName = rsrcName.substring(rsrcName.lastIndexOf('/') + 1); + } else { + cloneName = rsrcName; + } + + return cloneRsrc(rsrcName, folder, cloneName); + } + + /** + * Clones a resource to a temporary folder. Generally, resources do not need to be cloned to + * be used. However, in code where there is danger of changing resource files and corrupting + * the source directory, cloning should be done before accessing the resources. + * + * @param rsrcName the resource name + * @param folder the temporary folder + * @param cloneName the name of the cloned resource that will be created inside the temporary + * folder + * @return the file that was created with the resource + * @throws IOException failed to clone the resource + */ + static File cloneRsrc( + @Nonnull String rsrcName, + @Nonnull TemporaryFolder folder, + @Nonnull String cloneName) + throws IOException { + File result = new File(folder.getRoot(), cloneName); + assertFalse(result.exists()); + + Files.write(rsrcBytes(rsrcName), result); + return result; + } +} diff --git a/src/test/java/com/android/tools/build/apkzlib/zip/ZipToolsTest.java b/src/test/java/com/android/tools/build/apkzlib/zip/ZipToolsTest.java new file mode 100644 index 0000000..d5108fe --- /dev/null +++ b/src/test/java/com/android/tools/build/apkzlib/zip/ZipToolsTest.java @@ -0,0 +1,223 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * 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.android.tools.build.apkzlib.zip; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import com.google.common.base.Charsets; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.google.common.io.ByteStreams; +import com.google.common.io.Files; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.InputStream; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import org.junit.Assume; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +@RunWith(Parameterized.class) +public class ZipToolsTest { + + @Parameterized.Parameter(0) + @Nullable + public String mZipFile; + + @Parameterized.Parameter(1) + @Nullable + public List mUnzipCommand; + + @Parameterized.Parameter(2) + @Nullable + public String mUnzipLineRegex; + + @Parameterized.Parameter(3) + public boolean mToolStoresDirectories; + + @Parameterized.Parameter(4) + public String mName; + + @Rule + @Nonnull + public TemporaryFolder mTemporaryFolder = new TemporaryFolder(); + + @Parameterized.Parameters(name = "{4} {index}") + public static Iterable getConfigurations() { + return Arrays.asList(new Object[][] { + { + "linux-zip.zip", + ImmutableList.of("/usr/bin/unzip", "-v"), + "^\\s*(?\\d+)\\s+(?:Stored|Defl:N).*\\s(?\\S+)\\S*$", + true, + "Linux Zip" + }, + { + "windows-7zip.zip", + ImmutableList.of("c:\\Program Files\\7-Zip\\7z.exe", "l"), + "^(?:\\S+\\s+){3}(?\\d+)\\s+\\d+\\s+(?\\S+)\\s*$", + true, + "Windows 7-Zip" + }, + { + "windows-cf.zip", + ImmutableList.of( + "Cannot use compressed folders from cmd line to list zip contents"), + "", + false, + "Windows Compressed Folders" + } + }); + } + + private File cloneZipFile() throws Exception { + File zfile = mTemporaryFolder.newFile("file.zip"); + Files.write(ZipTestUtils.rsrcBytes(mZipFile), zfile); + return zfile; + } + + private static void assertFileInZip(@Nonnull ZFile zfile, @Nonnull String name) throws Exception { + StoredEntry root = zfile.get(name); + assertNotNull(root); + + InputStream is = root.open(); + byte[] inZipData = ByteStreams.toByteArray(is); + is.close(); + + byte[] inFileData = ZipTestUtils.rsrcBytes(name); + assertArrayEquals(inFileData, inZipData); + } + + @Test + public void zfileReadsZipFile() throws Exception { + try (ZFile zf = new ZFile(cloneZipFile())) { + if (mToolStoresDirectories) { + assertEquals(6, zf.entries().size()); + } else { + assertEquals(4, zf.entries().size()); + } + + assertFileInZip(zf, "root"); + assertFileInZip(zf, "images/lena.png"); + assertFileInZip(zf, "text-files/rfc2460.txt"); + assertFileInZip(zf, "text-files/wikipedia.html"); + } + } + + @Test + public void toolReadsZfFile() throws Exception { + testReadZFile(false); + } + + @Test + public void toolReadsAlignedZfFile() throws Exception { + testReadZFile(true); + } + + private void testReadZFile(boolean align) throws Exception { + String unzipcmd = mUnzipCommand.get(0); + Assume.assumeTrue(new File(unzipcmd).canExecute()); + + ZFileOptions options = new ZFileOptions(); + if (align) { + options.setAlignmentRule(AlignmentRules.constant(500)); + } + + File zfile = new File (mTemporaryFolder.getRoot(), "zfile.zip"); + try (ZFile zf = new ZFile(zfile, options)) { + zf.add("root", new ByteArrayInputStream(ZipTestUtils.rsrcBytes("root"))); + zf.add("images/", new ByteArrayInputStream(new byte[0])); + zf.add( + "images/lena.png", + new ByteArrayInputStream(ZipTestUtils.rsrcBytes("images/lena.png"))); + zf.add("text-files/", new ByteArrayInputStream(new byte[0])); + zf.add( + "text-files/rfc2460.txt", + new ByteArrayInputStream(ZipTestUtils.rsrcBytes("text-files/rfc2460.txt"))); + zf.add( + "text-files/wikipedia.html", + new ByteArrayInputStream(ZipTestUtils.rsrcBytes("text-files/wikipedia.html"))); + } + + List command = Lists.newArrayList(mUnzipCommand); + command.add(zfile.getAbsolutePath()); + ProcessBuilder pb = new ProcessBuilder(command); + Process proc = pb.start(); + InputStream is = proc.getInputStream(); + byte output[] = ByteStreams.toByteArray(is); + String text = new String(output, Charsets.US_ASCII); + String lines[] = text.split("\n"); + Map sizes = Maps.newHashMap(); + for (String l : lines) { + Matcher m = Pattern.compile(mUnzipLineRegex).matcher(l); + if (m.matches()) { + String sizeTxt = m.group("size"); + int size = Integer.parseInt(sizeTxt); + String name = m.group("name"); + sizes.put(name, size); + } + } + + assertEquals(6, sizes.size()); + + /* + * The "images" directory may show up as "images" or "images/". + */ + String imagesKey = "images/"; + if (!sizes.containsKey(imagesKey)) { + imagesKey = "images"; + } + + assertTrue(sizes.containsKey(imagesKey)); + assertEquals(0, sizes.get(imagesKey).intValue()); + + assertSize(new String[] { "images/", "images" }, 0, sizes); + assertSize(new String[] { "text-files/", "text-files"}, 0, sizes); + assertSize(new String[] { "root" }, ZipTestUtils.rsrcBytes("root").length, sizes); + assertSize(new String[] { "images/lena.png", "images\\lena.png" }, + ZipTestUtils.rsrcBytes("images/lena.png").length, sizes); + assertSize(new String[] { "text-files/rfc2460.txt", "text-files\\rfc2460.txt" }, + ZipTestUtils.rsrcBytes("text-files/rfc2460.txt").length, sizes); + assertSize(new String[] { "text-files/wikipedia.html", "text-files\\wikipedia.html" }, + ZipTestUtils.rsrcBytes("text-files/wikipedia.html").length, sizes); + } + + private static void assertSize(String[] names, long size, Map sizes) { + for (String n : names) { + if (sizes.containsKey(n)) { + assertEquals((long) sizes.get(n), size); + return; + } + } + + fail(); + } +} diff --git a/src/test/java/com/android/tools/build/apkzlib/zip/compress/MultiCompressorTest.java b/src/test/java/com/android/tools/build/apkzlib/zip/compress/MultiCompressorTest.java new file mode 100644 index 0000000..ee42eaa --- /dev/null +++ b/src/test/java/com/android/tools/build/apkzlib/zip/compress/MultiCompressorTest.java @@ -0,0 +1,155 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * 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.android.tools.build.apkzlib.zip.compress; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import com.android.tools.build.apkzlib.utils.ApkZFileTestUtils; +import com.android.tools.build.apkzlib.zip.CentralDirectoryHeaderCompressInfo; +import com.android.tools.build.apkzlib.zip.CompressionMethod; +import com.android.tools.build.apkzlib.zip.StoredEntry; +import com.android.tools.build.apkzlib.zip.ZFile; +import com.android.tools.build.apkzlib.zip.ZFileOptions; +import com.google.common.util.concurrent.MoreExecutors; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.util.zip.Deflater; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +public class MultiCompressorTest { + @Rule + public TemporaryFolder mTemporaryFolder = new TemporaryFolder(); + + private static byte[] getCompressibleData() throws Exception { + return ApkZFileTestUtils + .getResourceBytes("/testData/packaging/text-files/wikipedia.html") + .read(); + } + + private static byte[] compress(byte[] data, int level) throws Exception { + Deflater deflater = new Deflater(level); + deflater.setInput(data); + deflater.finish(); + + byte[] resultAll = new byte[data.length * 2]; + int resultAllCount = deflater.deflate(resultAll); + + byte[] result = new byte[resultAllCount]; + System.arraycopy(resultAll, 0, result, 0, resultAllCount); + return result; + } + + @Test + public void storeIsBest() throws Exception { + File zip = new File(mTemporaryFolder.getRoot(), "test.zip"); + + try (ZFile zf = new ZFile(zip)) { + zf.add("file", new ByteArrayInputStream(new byte[0])); + StoredEntry entry = zf.get("file"); + assertNotNull(entry); + + CentralDirectoryHeaderCompressInfo ci = + entry.getCentralDirectoryHeader().getCompressionInfoWithWait(); + + assertEquals(0, ci.getCompressedSize()); + assertEquals(CompressionMethod.STORE, ci.getMethod()); + } + } + + @Test + public void sameCompressionResultButBetterThanStore() throws Exception { + File zip = new File(mTemporaryFolder.getRoot(), "test.zip"); + + byte[] data = new byte[] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; + + try (ZFile zf = new ZFile(zip)) { + zf.add("file", new ByteArrayInputStream(data)); + StoredEntry entry = zf.get("file"); + assertNotNull(entry); + + CentralDirectoryHeaderCompressInfo ci = + entry.getCentralDirectoryHeader().getCompressionInfoWithWait(); + + assertEquals(CompressionMethod.DEFLATE, ci.getMethod()); + assertTrue(ci.getCompressedSize() < data.length); + } + } + + @Test + public void bestBetterThanDefault() throws Exception { + byte[] data = getCompressibleData(); + int bestSize = compress(data, Deflater.BEST_COMPRESSION).length; + int defaultSize = compress(data, Deflater.DEFAULT_COMPRESSION).length; + + double ratio = bestSize / (double) defaultSize; + assertTrue(ratio < 1.0); + + File defaultFile = new File(mTemporaryFolder.getRoot(), "default.zip"); + File resultFile = new File(mTemporaryFolder.getRoot(), "result.zip"); + + ZFileOptions resultOptions = new ZFileOptions(); + resultOptions.setCompressor( + new BestAndDefaultDeflateExecutorCompressor( + MoreExecutors.directExecutor(), resultOptions.getTracker(), ratio + 0.001)); + + try ( + ZFile defaultZFile = new ZFile(defaultFile); + ZFile resultZFile = new ZFile(resultFile, resultOptions)) { + defaultZFile.add("wikipedia.html", new ByteArrayInputStream(data)); + resultZFile.add("wikipedia.html", new ByteArrayInputStream(data)); + } + + long defaultFileSize = defaultFile.length(); + long resultFileSize = resultFile.length(); + + assertTrue(resultFileSize < defaultFileSize); + } + + @Test + public void bestBetterThanDefaultButNotEnough() throws Exception { + byte[] data = getCompressibleData(); + int bestSize = compress(data, Deflater.BEST_COMPRESSION).length; + int defaultSize = compress(data, Deflater.DEFAULT_COMPRESSION).length; + + double ratio = bestSize / (double) defaultSize; + assertTrue(ratio < 1.0); + + File defaultFile = new File(mTemporaryFolder.getRoot(), "default.zip"); + File resultFile = new File(mTemporaryFolder.getRoot(), "result.zip"); + + ZFileOptions resultOptions = new ZFileOptions(); + resultOptions.setCompressor( + new BestAndDefaultDeflateExecutorCompressor( + MoreExecutors.directExecutor(), resultOptions.getTracker(), ratio - 0.001)); + + try ( + ZFile defaultZFile = new ZFile(defaultFile); + ZFile resultZFile = new ZFile(resultFile, resultOptions)) { + defaultZFile.add("wikipedia.html", new ByteArrayInputStream(data)); + resultZFile.add("wikipedia.html", new ByteArrayInputStream(data)); + } + + long defaultFileSize = defaultFile.length(); + long resultFileSize = resultFile.length(); + + assertTrue(resultFileSize == defaultFileSize); + } +} diff --git a/src/test/java/com/android/tools/build/apkzlib/zip/utils/LittleEndianUtilsTest.java b/src/test/java/com/android/tools/build/apkzlib/zip/utils/LittleEndianUtilsTest.java new file mode 100644 index 0000000..3ba856f --- /dev/null +++ b/src/test/java/com/android/tools/build/apkzlib/zip/utils/LittleEndianUtilsTest.java @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * 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.android.tools.build.apkzlib.zip.utils; + +import static junit.framework.TestCase.assertEquals; +import static org.junit.Assert.assertArrayEquals; + +import java.nio.ByteBuffer; +import java.util.Random; +import org.junit.Test; + +public class LittleEndianUtilsTest { + @Test + public void read2Le() throws Exception { + assertEquals(0x0102, LittleEndianUtils.readUnsigned2Le(ByteBuffer.wrap( + new byte[] { 2, 1 }))); + assertEquals(0xfedc, LittleEndianUtils.readUnsigned2Le(ByteBuffer.wrap( + new byte[] { (byte) 0xdc, (byte) 0xfe }))); + } + + @Test + public void write2Le() throws Exception { + ByteBuffer out = ByteBuffer.allocate(2); + LittleEndianUtils.writeUnsigned2Le(out, 0x0102); + assertArrayEquals(new byte[] { 2, 1 }, out.array()); + + out = ByteBuffer.allocate(2); + LittleEndianUtils.writeUnsigned2Le(out, 0xfedc); + assertArrayEquals(new byte[] { (byte) 0xdc, (byte) 0xfe }, out.array()); + } + + @Test + public void readWrite2Le() throws Exception { + Random r = new Random(); + + int range = 0x0000ffff; + + final int COUNT = 1000; + int[] data = new int[COUNT]; + for (int i = 0; i < data.length; i++) { + data[i] = r.nextInt(range); + } + + ByteBuffer out = ByteBuffer.allocate(COUNT * 2); + for (int d : data) { + LittleEndianUtils.writeUnsigned2Le(out, d); + } + + ByteBuffer in = ByteBuffer.wrap(out.array()); + for (int i = 0; i < data.length; i++) { + assertEquals(data[i], LittleEndianUtils.readUnsigned2Le(in)); + } + } + + @Test + public void read4Le() throws Exception { + assertEquals(0x01020304, LittleEndianUtils.readUnsigned4Le(ByteBuffer.wrap( + new byte[] { 4, 3, 2, 1 }))); + assertEquals(0xfedcba98L, LittleEndianUtils.readUnsigned4Le(ByteBuffer.wrap( + new byte[] { (byte) 0x98, (byte) 0xba, (byte) 0xdc, (byte) 0xfe }))); + } + + @Test + public void write4Le() throws Exception { + ByteBuffer out = ByteBuffer.allocate(4); + LittleEndianUtils.writeUnsigned4Le(out, 0x01020304); + assertArrayEquals(new byte[] { 4, 3, 2, 1 }, out.array()); + + out = ByteBuffer.allocate(4); + LittleEndianUtils.writeUnsigned4Le(out, 0xfedcba98L); + assertArrayEquals(new byte[] { (byte) 0x98, (byte) 0xba, (byte) 0xdc, (byte) 0xfe }, + out.array()); + } + + @Test + public void readWrite4Le() throws Exception { + Random r = new Random(); + + final int COUNT = 1000; + long[] data = new long[COUNT]; + for (int i = 0; i < data.length; i++) { + do { + data[i] = r.nextInt() - (long) Integer.MIN_VALUE; + } while (data[i] < 0); + } + + ByteBuffer out = ByteBuffer.allocate(COUNT * 4); + for (long d : data) { + LittleEndianUtils.writeUnsigned4Le(out, d); + } + + ByteBuffer in = ByteBuffer.wrap(out.array()); + for (int i = 0; i < data.length; i++) { + assertEquals(data[i], LittleEndianUtils.readUnsigned4Le(in)); + } + } +} diff --git a/src/test/java/com/android/tools/build/apkzlib/zip/utils/MsDosDateTimeUtilsTest.java b/src/test/java/com/android/tools/build/apkzlib/zip/utils/MsDosDateTimeUtilsTest.java new file mode 100644 index 0000000..953e9de --- /dev/null +++ b/src/test/java/com/android/tools/build/apkzlib/zip/utils/MsDosDateTimeUtilsTest.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * 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.android.tools.build.apkzlib.zip.utils; + +import static org.junit.Assert.assertEquals; + +import java.util.Calendar; +import org.junit.Test; + +public class MsDosDateTimeUtilsTest { + @Test + public void packDate() throws Exception { + Calendar c = Calendar.getInstance(); + + c.set(Calendar.YEAR, 2016); + c.set(Calendar.MONTH, 0); + c.set(Calendar.DAY_OF_MONTH, 5); + + long time = c.getTime().getTime(); + + int packed = MsDosDateTimeUtils.packDate(time); + + // Year = 2016 - 1980 = 36 (0000 0000 0[010 0100]) + // Month = 1 (0000 0000 0000 [0001]) + // Day = 5 (0000 0000 000[0 0101]) + // Packs as 010 0100 | 0001 | 00101 + // Or 0100 1000 0010 0101 + // In hex 4 8 2 5 + + int expectedDateBits = 0x4825; + assertEquals(expectedDateBits, packed); + } + + @Test + public void packTime() throws Exception { + Calendar c = Calendar.getInstance(); + + c.set(Calendar.HOUR_OF_DAY, 8); + c.set(Calendar.MINUTE, 45); + c.set(Calendar.SECOND, 20); + + long time = c.getTime().getTime(); + + int packed = MsDosDateTimeUtils.packTime(time); + + // Hour = 8 (0000 0000 000[0 1000]) + // Minute = 45 (0000 0000 00[10 1101]) + // Second = 20 / 2 = 10 (0000 0000 000[0 1010]) + // Pack as 0 1000 | 10 1101 | 0 1010 + // Or 0100 0101 1010 1010 + // In hex 4 5 A A + + int expectedTimeBits = 0x45AA; + assertEquals(expectedTimeBits, packed); + } +} -- cgit v1.2.3