├── .github ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── Bug_report.md │ ├── Feature_request.md │ └── Support_question.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── CI.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── Package.swift ├── Package@swift-4.0.swift ├── Package@swift-4.1.swift ├── Package@swift-4.2.swift ├── Package@swift-5.9.swift ├── README.md ├── Sources ├── CZLib │ ├── module.modulemap │ └── shim.h └── ZIPFoundation │ ├── Archive+BackingConfiguration.swift │ ├── Archive+Deprecated.swift │ ├── Archive+Helpers.swift │ ├── Archive+MemoryFile.swift │ ├── Archive+Progress.swift │ ├── Archive+Reading.swift │ ├── Archive+ReadingDeprecated.swift │ ├── Archive+Writing.swift │ ├── Archive+WritingDeprecated.swift │ ├── Archive+ZIP64.swift │ ├── Archive.swift │ ├── Data+Compression.swift │ ├── Data+CompressionDeprecated.swift │ ├── Data+Serialization.swift │ ├── Date+ZIP.swift │ ├── Entry+Serialization.swift │ ├── Entry+ZIP64.swift │ ├── Entry.swift │ ├── FileManager+ZIP.swift │ ├── FileManager+ZIPDeprecated.swift │ ├── Resources │ └── PrivacyInfo.xcprivacy │ └── URL+ZIP.swift ├── Tests ├── LinuxMain.swift └── ZIPFoundationTests │ ├── Resources │ ├── testAddDirectoryToArchiveWithZIP64LFHOffset.zip │ ├── testAddEntryToArchiveWithZIP64LFHOffset.zip │ ├── testArchiveAddCompressedEntryProgress.png │ ├── testArchiveAddCompressedEntryProgress.zip │ ├── testArchiveAddEntryErrorConditions.zip │ ├── testArchiveAddUncompressedEntryProgress.png │ ├── testArchiveAddUncompressedEntryProgress.zip │ ├── testArchiveIteratorErrorConditions.zip │ ├── testArchiveReadErrorConditions.zip │ ├── testCRC32Calculation.data │ ├── testCRC32Check.zip │ ├── testCorruptFileErrorConditions.zip │ ├── testCorruptSymbolicLinkErrorConditions.zip │ ├── testCreateArchiveAddCompressedEntry.png │ ├── testCreateArchiveAddCompressedEntryToMemory.png │ ├── testCreateArchiveAddEntryErrorConditions.txt │ ├── testCreateArchiveAddSymbolicLink.png │ ├── testCreateArchiveAddUncompressedEntry.png │ ├── testCreateArchiveAddUncompressedEntryToMemory.png │ ├── testCreateArchiveAddZeroSizeCompressedEntry.txt │ ├── testCreateArchiveAddZeroSizeUncompressedEntry.txt │ ├── testCreateZIP64ArchiveAddUncompressedEntry.png │ ├── testDetectEntryType.zip │ ├── testEntryIsCompressed.zip │ ├── testExtractCompressedDataDescriptorArchive.zip │ ├── testExtractCompressedEntryCancelation.zip │ ├── testExtractCompressedFolderEntries.zip │ ├── testExtractCompressedFolderEntriesFromMemory.zip │ ├── testExtractCompressedZIP64Entries.zip │ ├── testExtractEncryptedArchiveErrorConditions.zip │ ├── testExtractEntryWithZIP64DataDescriptor.zip │ ├── testExtractErrorConditions.zip │ ├── testExtractInvalidBufferSizeErrorConditions.zip │ ├── testExtractMSDOSArchive.zip │ ├── testExtractPreferredEncoding.zip │ ├── testExtractUncompressedDataDescriptorArchive.zip │ ├── testExtractUncompressedEmptyFile.zip │ ├── testExtractUncompressedEntryCancelation.zip │ ├── testExtractUncompressedFolderEntries.zip │ ├── testExtractUncompressedFolderEntriesFromMemory.zip │ ├── testExtractUncompressedZIP64Entries.zip │ ├── testFileModificationDate.png │ ├── testInvalidCompressionMethodErrorConditions.zip │ ├── testMemoryArchiveErrorConditions.zip │ ├── testPOSIXPermissions.png │ ├── testPathDelimiterTraversalAttack.zip │ ├── testProgressHelpers.zip │ ├── testRemoveCompressedEntry.zip │ ├── testRemoveDataDescriptorCompressedEntry.zip │ ├── testRemoveEntryErrorConditions.zip │ ├── testRemoveEntryFromArchiveWithZIP64EOCD.zip │ ├── testRemoveEntryProgress.zip │ ├── testRemoveEntryWithZIP64ExtendedInformation.zip │ ├── testRemoveUncompressedEntry.zip │ ├── testRemoveZIP64EntryFromArchiveWithZIP64EOCD.zip │ ├── testSimpleTraversalAttack.zip │ ├── testSymlinkModificationDateTransferErrorConditions.png │ ├── testSymlinkPermissionsTransferErrorConditions.png │ ├── testUnzipCompressedZIP64Item.zip │ ├── testUnzipItem.zip │ ├── testUnzipItemErrorConditions.png │ ├── testUnzipItemErrorConditions.zip │ ├── testUnzipItemProgress.zip │ ├── testUnzipItemWithPreferredEncoding.zip │ ├── testUnzipItemWithZIP64DataDescriptor.zip │ ├── testUnzipUncompressedZIP64Item.zip │ ├── testUnzipUncontainedSymlink.zip │ ├── testUpdateArchiveRemoveUncompressedEntryFromMemory.zip │ ├── testZIP64ArchiveAddEntryProgress.png │ ├── testZIP64ArchiveAddEntryProgress.zip │ ├── testZipCompressedZIP64Item.png │ ├── testZipItem.png │ ├── testZipItemProgress.png │ └── testZipUncompressedZIP64Item.png │ ├── ZIPFoundationArchiveTests+ZIP64.swift │ ├── ZIPFoundationArchiveTests.swift │ ├── ZIPFoundationDataSerializationTests.swift │ ├── ZIPFoundationEntryTests+ZIP64.swift │ ├── ZIPFoundationEntryTests.swift │ ├── ZIPFoundationErrorConditionTests+ZIP64.swift │ ├── ZIPFoundationErrorConditionTests.swift │ ├── ZIPFoundationFileAttributeTests.swift │ ├── ZIPFoundationFileManagerTests+ZIP64.swift │ ├── ZIPFoundationFileManagerTests.swift │ ├── ZIPFoundationMemoryTests.swift │ ├── ZIPFoundationPerformanceTests.swift │ ├── ZIPFoundationProgressTests.swift │ ├── ZIPFoundationReadingTests+ZIP64.swift │ ├── ZIPFoundationReadingTests.swift │ ├── ZIPFoundationTests.swift │ ├── ZIPFoundationWritingTests+ZIP64.swift │ └── ZIPFoundationWritingTests.swift ├── ZIPFoundation.podspec └── ZIPFoundation.xcodeproj ├── ZIPFoundationTests_Info.plist ├── ZIPFoundation_Info.plist ├── project.pbxproj ├── project.xcworkspace ├── contents.xcworkspacedata └── xcshareddata │ └── IDEWorkspaceChecks.plist └── xcshareddata └── xcschemes ├── ZIPFoundation.xcscheme └── xcschememanagement.plist /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at abuse@peakstep.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | This document contains information about contributing to this project. 4 | Please read it before you start participating. 5 | 6 | **Topics** 7 | 8 | * [Asking Questions](#asking-questions) 9 | * [Reporting Issues](#reporting-issues) 10 | * [Pull Requests](#pull-requests) 11 | 12 | ## Asking Questions 13 | 14 | For questions that are not about specific code or documentation issues, 15 | please ask on [Stack Overflow](https://stackoverflow.com). You can search for existing ZIP Foundation related questions by using the `zipfoundation` tag: 16 | https://stackoverflow.com/questions/tagged/zipfoundation. Please also use this tag when creating new questions. 17 | 18 | ## Reporting Issues 19 | 20 | Prior to opening a new issue, please check that the project issues database doesn't already contain a similar issue. 21 | If you find a match, add a quick "+1" comment. 22 | 23 | When reporting issues, please include the following: 24 | 25 | * The version of Xcode you're using 26 | * The version of macOS, iOS, tvOS or watchOS you're targeting 27 | * If you are experiencing problems on non-Apple platforms, please include information about your environment (Swift version, Linux distribution, etc...) 28 | * For code related problems, please include relevant snippets 29 | * Any other details that would be useful in understanding the issue 30 | 31 | ## Pull Requests 32 | 33 | All contributions must be sent in via GitHub pull request. Before opening a pull request, please make sure that: 34 | 35 | * All units test pass 36 | * Code coverage stays at 100% 37 | * Added code paths are compatible with all supported platforms 38 | * Code style does not trigger any swiftlint issues (Make sure you have the current [swiftlint](https://github.com/realm/SwiftLint) version installed) 39 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [weichsel] 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F41E Bug Report" 3 | about: Create a report to help us to improve ZIP Foundation 4 | 5 | --- 6 | 7 | # Summary 8 | 9 | 10 | # Steps to Reproduce 11 | 12 | 13 | # Expected Results 14 | 15 | 16 | # Actual Results 17 | 18 | 19 | # Regression & Version 20 | 21 | 22 | # Related Link 23 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F528 Feature Request" 3 | about: Suggest an idea for this project 4 | 5 | --- 6 | 7 | **Is your feature request related to a problem? Please describe.** 8 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 9 | 10 | **Describe the solution you'd like** 11 | A clear and concise description of what you want to happen. 12 | 13 | **Describe alternatives you've considered** 14 | A clear and concise description of any alternative solutions or features you've considered. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Support_question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "❓ Support Question" 3 | about: Ask here if you have a question about the usage of ZIP Foundation 4 | 5 | --- 6 | 7 | 11 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Fixes # 2 | 3 | # Changes proposed in this PR 4 | * 5 | * 6 | * 7 | 8 | # Tests performed 9 | 10 | 11 | # Further info for the reviewer 12 | 13 | 14 | # Open Issues 15 | -------------------------------------------------------------------------------- /.github/workflows/CI.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | paths: 6 | - '.github/workflows/swiftlint.yml' 7 | - '.swiftlint.yml' 8 | - '**/*.swift' 9 | 10 | jobs: 11 | SwiftLint: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | - name: SwiftLint 16 | uses: norio-nomura/action-swiftlint@3.2.1 17 | with: 18 | args: --strict 19 | 20 | Xcode: 21 | strategy: 22 | matrix: 23 | xcode_version: ['15.0.1', '15.1', '15.2', '15.3', '15.4', '16.0', '16.1'] 24 | runs-on: macos-latest 25 | env: 26 | DEVELOPER_DIR: /Applications/Xcode_${{ matrix.xcode_version }}.app 27 | steps: 28 | - uses: actions/checkout@v4 29 | - run: swift -version 30 | - run: swift test -c release -Xswiftc -enable-testing 31 | 32 | Linux: 33 | strategy: 34 | matrix: 35 | tag: ['5.7', '5.8', '5.9', '5.10'] 36 | runs-on: ubuntu-latest 37 | container: 38 | image: swift:${{ matrix.tag }} 39 | steps: 40 | - uses: actions/checkout@v4 41 | - run: swift test -c release -Xswiftc -enable-testing 42 | 43 | Coverage: 44 | runs-on: macos-latest 45 | steps: 46 | - uses: actions/checkout@v4 47 | - run: swift test -c release -Xswiftc -enable-testing --enable-code-coverage 48 | - run: find .build/release/codecov/ -name "*.profraw" -print0 | xargs -0 xcrun llvm-profdata merge -sparse -o .build/release/codecov/default.profdata 49 | - run: xcrun llvm-cov export -summary-only -ignore-filename-regex 'ZIPFoundationTests|resource_bundle_accessor.swift|.*Deprecated.*$' .build/release/ZIPFoundationPackageTests.xctest/Contents/MacOS/ZIPFoundationPackageTests -instr-profile .build/release/codecov/default.profdata > .build/coverage.json 50 | - run: (cat .build/coverage.json|jq '.data[0]["totals"]["lines"]["percent"]' | grep -Eq "100") && { exit 0; } || { echo 'Please make sure that the test suite covers all framework code paths.'; exit 1; } 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xccheckout 23 | *.xcscmblueprint 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | *.ipa 28 | *.dSYM.zip 29 | *.dSYM 30 | 31 | ## Playgrounds 32 | timeline.xctimeline 33 | playground.xcworkspace 34 | 35 | # Swift Package Manager 36 | # 37 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 38 | # Packages/ 39 | # Package.pins 40 | .build/ 41 | 42 | # CocoaPods 43 | # 44 | # We recommend against adding the Pods directory to your .gitignore. However 45 | # you should judge for yourself, the pros and cons are mentioned at: 46 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 47 | # 48 | # Pods/ 49 | 50 | # Carthage 51 | # 52 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 53 | # Carthage/Checkouts 54 | 55 | Carthage/Build 56 | 57 | # fastlane 58 | # 59 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 60 | # screenshots whenever they are needed. 61 | # For more information about the recommended setup visit: 62 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 63 | 64 | fastlane/report.xml 65 | fastlane/Preview.html 66 | fastlane/screenshots 67 | fastlane/test_output 68 | 69 | # Swift Package Manager 70 | .swiftpm 71 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [0.9.19](https://github.com/weichsel/ZIPFoundation/releases/tag/0.9.19) 4 | 5 | ### Updated 6 | - Fixed privacy manifest 7 | - Fixed deprecation warning in package manifest 8 | - Fixed resource bundling instruction in the podspec 9 | 10 | ## [0.9.18](https://github.com/weichsel/ZIPFoundation/releases/tag/0.9.18) 11 | 12 | ### Added 13 | - Added ability to enforce symlink containment 14 | 15 | ### Updated 16 | - Fixed path escape vulnerability 17 | - Fixed platform requirement warnings 18 | - Improved error info when encountering permission errors 19 | 20 | ## [0.9.17](https://github.com/weichsel/ZIPFoundation/releases/tag/0.9.17) 21 | 22 | ### Added 23 | - Added visionOS support 24 | - Added Xcode privacy manifest 25 | - Added throwing initializers for `Archive` 26 | 27 | ### Updated 28 | - Improved symlink handling 29 | - Improved forwarding of underlying errors 30 | 31 | ## [0.9.16](https://github.com/weichsel/ZIPFoundation/releases/tag/0.9.16) 32 | 33 | ### Added 34 | - Added `isCompressed` accessor to `Entry` 35 | 36 | ### Updated 37 | - Improved README and documentation 38 | - Fixed deprecation in the random test data generator 39 | 40 | ## [0.9.15](https://github.com/weichsel/ZIPFoundation/releases/tag/0.9.15) 41 | 42 | ### Added 43 | - Added initial support for building for Android 44 | 45 | ### Updated 46 | - Fixed CRC32 calculation for non-final compression streams 47 | - Fixed evaluation of CRC32 checksums when using `FileManager.unzipItem` 48 | 49 | ## [0.9.14](https://github.com/weichsel/ZIPFoundation/releases/tag/0.9.14) 50 | 51 | ### Updated 52 | - Fixed missing extra field data after entry removal 53 | 54 | ## [0.9.13](https://github.com/weichsel/ZIPFoundation/releases/tag/0.9.13) 55 | 56 | ### Added 57 | - Added large file support (ZIP64) 58 | 59 | ### Updated 60 | - Fixed an `UInt16` overflow when calculating the number of entries 61 | - Fixed entry removal for in-memory archives 62 | - Fixed a crash when `fopen()` fails during archive replacement 63 | - Improved CRC32 calculation performance via zlib (when available) 64 | 65 | ## [0.9.12](https://github.com/weichsel/ZIPFoundation/releases/tag/0.9.12) 66 | 67 | ### Added 68 | - Added check to disallow removal of entries from readonly archives 69 | - Added guard against API misuse by providing zero byte buffer sizes 70 | 71 | ### Updated 72 | - Fixed an `UInt16` overflow when calculating the end of the central directory record 73 | - Fixed detection of ZIP version required to extract 74 | - Fixed missing consumer closure call for zero byte entries 75 | - Fixed erroneous application of `.deflate` compression on `.symlink` and `.directory` entries 76 | - Improved detection of `.directory` entries 77 | - Improved performance when looking up entries via subscripting 78 | - Improved consistency of URL format used in the Swift package description 79 | 80 | ## [0.9.11](https://github.com/weichsel/ZIPFoundation/releases/tag/0.9.11) 81 | 82 | ### Added 83 | - Read/Write support for in-memory archives 84 | 85 | ### Updated 86 | - Fixed a memory safety issue during (de)compression 87 | - Fixed dangling pointer warnings when serializing ZIP internal structs to `Data` 88 | - Fixed missing Swift 5 language version when integrating via CocoaPods 89 | - Fixed inconsistent usage of the optional `preferredEncoding` parameter during entry addition 90 | - Improved documentation for compression settings 91 | 92 | ## [0.9.10](https://github.com/weichsel/ZIPFoundation/releases/tag/0.9.10) 93 | 94 | ### Added 95 | - Optional `skipCRC32` parameter to speed up entry reading 96 | 97 | ### Updated 98 | - Fixed a race condition during archive creation or extraction 99 | - Fixed an error when trying to add broken symlinks to an archive 100 | - Fixed an App Store submission issue by updating the product identifier to use reverse DNS notation 101 | - Improved CRC32 calculation performance 102 | - Improved entry replacement performance on separate volumes 103 | - Improved documentation for closure-based writing 104 | 105 | ## [0.9.9](https://github.com/weichsel/ZIPFoundation/releases/tag/0.9.9) 106 | 107 | ### Added 108 | - Swift 5.0 support 109 | - Optional `preferredEncoding` parameter to explicitly configure an encoding for filepaths 110 | 111 | ### Updated 112 | - Fixed a library load error related to dylib versioning 113 | - Fixed a hang during read when decoding small, `.deflate` compressed entries 114 | - Improved Linux support 115 | - Improved test suite on non-Darwin platforms 116 | 117 | ## [0.9.8](https://github.com/weichsel/ZIPFoundation/releases/tag/0.9.8) 118 | 119 | ### Updated 120 | - Disabled symlink resolution during path traversal checking 121 | 122 | ## [0.9.7](https://github.com/weichsel/ZIPFoundation/releases/tag/0.9.7) 123 | 124 | ### Added 125 | - App extension support 126 | - Optional `compressionMethod` parameter for `zipItem:` 127 | 128 | ### Updated 129 | - Fixed a path traversal attack vulnerability 130 | - Fixed a crash due to wrong error handling after failed `fopen` calls 131 | 132 | ### Removed 133 | - Temporarily removed the currently unsupported `.modificationDate` attribute on non-Darwin platforms 134 | 135 | ## [0.9.6](https://github.com/weichsel/ZIPFoundation/releases/tag/0.9.6) 136 | 137 | ### Added 138 | - Swift 4.1 support 139 | 140 | ### Updated 141 | - Fixed default directory permissions 142 | - Fixed a compile issue when targeting Linux 143 | 144 | ## [0.9.5](https://github.com/weichsel/ZIPFoundation/releases/tag/0.9.5) 145 | 146 | ### Added 147 | - Progress tracking support 148 | - Operation cancellation support 149 | 150 | ### Updated 151 | - Improved performance of CRC32 calculations 152 | - Improved Linux support 153 | - Fixed wrong behaviour when using the `shouldKeepParent` flag 154 | - Fixed a linker error during archive builds when integrating via Carthage 155 | 156 | ## [0.9.4](https://github.com/weichsel/ZIPFoundation/releases/tag/0.9.4) 157 | 158 | ### Updated 159 | - Fixed a wrong setting for `FRAMEWORK_SEARCH_PATHS` that interfered with code signing 160 | - Added a proper value for `CURRENT_PROJECT_VERSION` to make the framework App Store compliant when using Carthage 161 | 162 | ## [0.9.3](https://github.com/weichsel/ZIPFoundation/releases/tag/0.9.3) 163 | 164 | ### Added 165 | - Carthage support 166 | 167 | ### Updated 168 | - Improved error handling 169 | - Made consistent use of Swift's `CocoaError` instead of `NSError` 170 | 171 | ## [0.9.2](https://github.com/weichsel/ZIPFoundation/releases/tag/0.9.2) 172 | 173 | ### Updated 174 | - Changed default POSIX permissions when file attributes are missing 175 | - Improved docs 176 | - Fixed a compiler warning when compiling with the latest Xcode 9 beta 177 | 178 | ## [0.9.1](https://github.com/weichsel/ZIPFoundation/releases/tag/0.9.1) 179 | 180 | ### Added 181 | - Optional parameter to skip CRC32 checksum calculation 182 | 183 | ### Updated 184 | - Tweaked POSIX buffer sizes to improve IO and comrpression performance 185 | - Improved source readability 186 | - Refined documentation 187 | 188 | ### Removed 189 | - Optional parameter skip decompression during entry retrieval 190 | 191 | ## [0.9.0](https://github.com/weichsel/ZIPFoundation/releases/tag/0.9.0) 192 | 193 | ### Added 194 | - Initial release of ZIP Foundation. 195 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017-2024 Thomas Zoechling (https://www.peakstep.com) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.0 2 | import PackageDescription 3 | 4 | #if canImport(Compression) 5 | let targets: [Target] = [ 6 | .target(name: "ZIPFoundation"), 7 | .testTarget(name: "ZIPFoundationTests", dependencies: ["ZIPFoundation"]) 8 | ] 9 | #else 10 | let targets: [Target] = [ 11 | .systemLibrary(name: "CZLib", pkgConfig: "zlib", providers: [.brew(["zlib"]), .apt(["zlib"])]), 12 | .target(name: "ZIPFoundation", dependencies: ["CZLib"], cSettings: [.define("_GNU_SOURCE", to: "1")]), 13 | .testTarget(name: "ZIPFoundationTests", dependencies: ["ZIPFoundation"]) 14 | ] 15 | #endif 16 | 17 | let package = Package( 18 | name: "ZIPFoundation", 19 | platforms: [ 20 | .macOS(.v10_11), .iOS(.v9), .tvOS(.v9), .watchOS(.v2) 21 | ], 22 | products: [ 23 | .library(name: "ZIPFoundation", targets: ["ZIPFoundation"]) 24 | ], 25 | targets: targets, 26 | swiftLanguageVersions: [.v4, .v4_2, .v5] 27 | ) 28 | -------------------------------------------------------------------------------- /Package@swift-4.0.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:4.0 2 | import PackageDescription 3 | 4 | #if os(macOS) || os(iOS) || os(tvOS) || os(visionOS) || os(watchOS) 5 | let dependencies: [Package.Dependency] = [] 6 | #else 7 | let dependencies: [Package.Dependency] = [.package(url: "https://github.com/IBM-Swift/CZlib.git", .exact("0.1.2"))] 8 | #endif 9 | 10 | let package = Package( 11 | name: "ZIPFoundation", 12 | products: [ 13 | .library(name: "ZIPFoundation", targets: ["ZIPFoundation"]) 14 | ], 15 | dependencies: dependencies, 16 | targets: [ 17 | .target(name: "ZIPFoundation"), 18 | .testTarget(name: "ZIPFoundationTests", dependencies: ["ZIPFoundation"]) 19 | ] 20 | ) 21 | -------------------------------------------------------------------------------- /Package@swift-4.1.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:4.1 2 | import PackageDescription 3 | 4 | #if canImport(Compression) 5 | let dependencies: [Package.Dependency] = [] 6 | #else 7 | let dependencies: [Package.Dependency] = [.package(url: "https://github.com/IBM-Swift/CZlib.git", .exact("0.1.2"))] 8 | #endif 9 | 10 | let package = Package( 11 | name: "ZIPFoundation", 12 | products: [ 13 | .library(name: "ZIPFoundation", targets: ["ZIPFoundation"]) 14 | ], 15 | dependencies: dependencies, 16 | targets: [ 17 | .target(name: "ZIPFoundation"), 18 | .testTarget(name: "ZIPFoundationTests", dependencies: ["ZIPFoundation"]) 19 | ] 20 | ) 21 | -------------------------------------------------------------------------------- /Package@swift-4.2.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:4.2 2 | import PackageDescription 3 | 4 | #if canImport(Compression) 5 | let dependencies: [Package.Dependency] = [] 6 | #else 7 | let dependencies: [Package.Dependency] = [.package(url: "https://github.com/IBM-Swift/CZlib.git", .exact("0.1.2"))] 8 | #endif 9 | 10 | let package = Package( 11 | name: "ZIPFoundation", 12 | products: [ 13 | .library(name: "ZIPFoundation", targets: ["ZIPFoundation"]) 14 | ], 15 | dependencies: dependencies, 16 | targets: [ 17 | .target(name: "ZIPFoundation"), 18 | .testTarget(name: "ZIPFoundationTests", dependencies: ["ZIPFoundation"]) 19 | ], 20 | swiftLanguageVersions: [.v4, .v4_2] 21 | ) 22 | -------------------------------------------------------------------------------- /Package@swift-5.9.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.9 2 | import PackageDescription 3 | 4 | #if canImport(Compression) 5 | let targets: [Target] = [ 6 | .target(name: "ZIPFoundation", 7 | resources: [ 8 | .copy("Resources/PrivacyInfo.xcprivacy") 9 | ]), 10 | .testTarget(name: "ZIPFoundationTests", dependencies: ["ZIPFoundation"]) 11 | ] 12 | #else 13 | let targets: [Target] = [ 14 | .systemLibrary(name: "CZLib", pkgConfig: "zlib", providers: [.brew(["zlib"]), .apt(["zlib"])]), 15 | .target(name: "ZIPFoundation", dependencies: ["CZLib"], cSettings: [.define("_GNU_SOURCE", to: "1")]), 16 | .testTarget(name: "ZIPFoundationTests", dependencies: ["ZIPFoundation"]) 17 | ] 18 | #endif 19 | 20 | let package = Package( 21 | name: "ZIPFoundation", 22 | platforms: [ 23 | .macOS(.v10_13), .iOS(.v12), .tvOS(.v12), .watchOS(.v4), .visionOS(.v1) 24 | ], 25 | products: [ 26 | .library(name: "ZIPFoundation", targets: ["ZIPFoundation"]) 27 | ], 28 | targets: targets, 29 | swiftLanguageVersions: [.v4, .v4_2, .v5] 30 | ) 31 | -------------------------------------------------------------------------------- /Sources/CZLib/module.modulemap: -------------------------------------------------------------------------------- 1 | // 2 | // module.modulemap 3 | // ZIPFoundation 4 | // 5 | // Copyright © 2017-2023 Thomas Zoechling, https://www.peakstep.com and the ZIP Foundation project authors. 6 | // Released under the MIT License. 7 | // 8 | // See https://github.com/weichsel/ZIPFoundation/blob/master/LICENSE for license information. 9 | // 10 | 11 | module CZlib { 12 | header "shim.h" 13 | link "z" 14 | export * 15 | } 16 | -------------------------------------------------------------------------------- /Sources/CZLib/shim.h: -------------------------------------------------------------------------------- 1 | // 2 | // shim.h 3 | // ZIPFoundation 4 | // 5 | // Copyright © 2017-2023 Thomas Zoechling, https://www.peakstep.com and the ZIP Foundation project authors. 6 | // Released under the MIT License. 7 | // 8 | // See https://github.com/weichsel/ZIPFoundation/blob/master/LICENSE for license information. 9 | // 10 | 11 | #ifndef zlib_shim_h 12 | #define zlib_shim_h 13 | 14 | #import 15 | #import 16 | 17 | // [zlib] provide 64-bit offset functions if _LARGEFILE64_SOURCE defined 18 | #ifndef _LARGEFILE64_SOURCE 19 | # define _LARGEFILE64_SOURCE 1 20 | #endif 21 | // [zlib] change the regular functions to 64 bits if _FILE_OFFSET_BITS is 64 22 | #ifndef _FILE_OFFSET_BITS 23 | # define _FILE_OFFSET_BITS 64 24 | #endif 25 | // [zlib] on systems without large file support, _LFS64_LARGEFILE must also be true 26 | #ifndef _LFS64_LARGEFILE 27 | # define _LFS64_LARGEFILE 1 28 | #endif 29 | 30 | #endif 31 | -------------------------------------------------------------------------------- /Sources/ZIPFoundation/Archive+BackingConfiguration.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Archive+BackingConfiguration.swift 3 | // ZIPFoundation 4 | // 5 | // Copyright © 2017-2024 Thomas Zoechling, https://www.peakstep.com and the ZIP Foundation project authors. 6 | // Released under the MIT License. 7 | // 8 | // See https://github.com/weichsel/ZIPFoundation/blob/master/LICENSE for license information. 9 | // 10 | 11 | import Foundation 12 | 13 | extension Archive { 14 | 15 | struct BackingConfiguration { 16 | let file: FILEPointer 17 | let endOfCentralDirectoryRecord: EndOfCentralDirectoryRecord 18 | let zip64EndOfCentralDirectory: ZIP64EndOfCentralDirectory? 19 | #if swift(>=5.0) 20 | let memoryFile: MemoryFile? 21 | 22 | init(file: FILEPointer, 23 | endOfCentralDirectoryRecord: EndOfCentralDirectoryRecord, 24 | zip64EndOfCentralDirectory: ZIP64EndOfCentralDirectory? = nil, 25 | memoryFile: MemoryFile? = nil) { 26 | self.file = file 27 | self.endOfCentralDirectoryRecord = endOfCentralDirectoryRecord 28 | self.zip64EndOfCentralDirectory = zip64EndOfCentralDirectory 29 | self.memoryFile = memoryFile 30 | } 31 | #else 32 | 33 | init(file: FILEPointer, 34 | endOfCentralDirectoryRecord: EndOfCentralDirectoryRecord, 35 | zip64EndOfCentralDirectory: ZIP64EndOfCentralDirectory?) { 36 | self.file = file 37 | self.endOfCentralDirectoryRecord = endOfCentralDirectoryRecord 38 | self.zip64EndOfCentralDirectory = zip64EndOfCentralDirectory 39 | } 40 | #endif 41 | } 42 | 43 | static func makeBackingConfiguration(for url: URL, mode: AccessMode) throws 44 | -> BackingConfiguration { 45 | let fileManager = FileManager() 46 | switch mode { 47 | case .read: 48 | let fileSystemRepresentation = fileManager.fileSystemRepresentation(withPath: url.path) 49 | guard let archiveFile = fopen(fileSystemRepresentation, "rb") else { 50 | throw POSIXError(errno, path: url.path) 51 | } 52 | guard let (eocdRecord, zip64EOCD) = Archive.scanForEndOfCentralDirectoryRecord(in: archiveFile) else { 53 | fclose(archiveFile) 54 | throw ArchiveError.missingEndOfCentralDirectoryRecord 55 | } 56 | return BackingConfiguration(file: archiveFile, 57 | endOfCentralDirectoryRecord: eocdRecord, 58 | zip64EndOfCentralDirectory: zip64EOCD) 59 | case .create: 60 | let endOfCentralDirectoryRecord = EndOfCentralDirectoryRecord(numberOfDisk: 0, numberOfDiskStart: 0, 61 | totalNumberOfEntriesOnDisk: 0, 62 | totalNumberOfEntriesInCentralDirectory: 0, 63 | sizeOfCentralDirectory: 0, 64 | offsetToStartOfCentralDirectory: 0, 65 | zipFileCommentLength: 0, 66 | zipFileCommentData: Data()) 67 | try endOfCentralDirectoryRecord.data.write(to: url, options: .withoutOverwriting) 68 | fallthrough 69 | case .update: 70 | let fileSystemRepresentation = fileManager.fileSystemRepresentation(withPath: url.path) 71 | guard let archiveFile = fopen(fileSystemRepresentation, "rb+") else { 72 | throw POSIXError(errno, path: url.path) 73 | } 74 | guard let (eocdRecord, zip64EOCD) = Archive.scanForEndOfCentralDirectoryRecord(in: archiveFile) else { 75 | fclose(archiveFile) 76 | throw ArchiveError.missingEndOfCentralDirectoryRecord 77 | } 78 | fseeko(archiveFile, 0, SEEK_SET) 79 | return BackingConfiguration(file: archiveFile, 80 | endOfCentralDirectoryRecord: eocdRecord, 81 | zip64EndOfCentralDirectory: zip64EOCD) 82 | } 83 | } 84 | 85 | #if swift(>=5.0) 86 | static func makeBackingConfiguration(for data: Data, mode: AccessMode) throws 87 | -> BackingConfiguration { 88 | let memoryFile = MemoryFile(data: data) 89 | let archiveFile = memoryFile.open(mode: mode) 90 | switch mode { 91 | case .read: 92 | guard let (eocdRecord, zip64EOCD) = Archive.scanForEndOfCentralDirectoryRecord(in: archiveFile) else { 93 | throw ArchiveError.missingEndOfCentralDirectoryRecord 94 | } 95 | 96 | return BackingConfiguration(file: archiveFile, 97 | endOfCentralDirectoryRecord: eocdRecord, 98 | zip64EndOfCentralDirectory: zip64EOCD, 99 | memoryFile: memoryFile) 100 | case .create: 101 | let endOfCentralDirectoryRecord = EndOfCentralDirectoryRecord(numberOfDisk: 0, numberOfDiskStart: 0, 102 | totalNumberOfEntriesOnDisk: 0, 103 | totalNumberOfEntriesInCentralDirectory: 0, 104 | sizeOfCentralDirectory: 0, 105 | offsetToStartOfCentralDirectory: 0, 106 | zipFileCommentLength: 0, 107 | zipFileCommentData: Data()) 108 | _ = endOfCentralDirectoryRecord.data.withUnsafeBytes { (buffer: UnsafeRawBufferPointer) in 109 | fwrite(buffer.baseAddress, buffer.count, 1, archiveFile) // Errors handled during read 110 | } 111 | fallthrough 112 | case .update: 113 | guard let (eocdRecord, zip64EOCD) = Archive.scanForEndOfCentralDirectoryRecord(in: archiveFile) else { 114 | throw ArchiveError.missingEndOfCentralDirectoryRecord 115 | } 116 | 117 | fseeko(archiveFile, 0, SEEK_SET) 118 | return BackingConfiguration(file: archiveFile, 119 | endOfCentralDirectoryRecord: eocdRecord, 120 | zip64EndOfCentralDirectory: zip64EOCD, 121 | memoryFile: memoryFile) 122 | } 123 | } 124 | #endif 125 | } 126 | -------------------------------------------------------------------------------- /Sources/ZIPFoundation/Archive+Deprecated.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Archive+Deprecated.swift 3 | // ZIPFoundation 4 | // 5 | // Created by Thomas Zoechling on 06.02.23. 6 | // 7 | 8 | import Foundation 9 | 10 | public extension Archive { 11 | 12 | @available(*, deprecated, message: "Please use the throwing initializer.") 13 | convenience init?(url: URL, accessMode mode: AccessMode, preferredEncoding: String.Encoding? = nil) { 14 | try? self.init(url: url, accessMode: mode, pathEncoding: preferredEncoding) 15 | } 16 | 17 | #if swift(>=5.0) 18 | @available(*, deprecated, message: "Please use the throwing initializer.") 19 | convenience init?(data: Data = Data(), accessMode mode: AccessMode, preferredEncoding: String.Encoding? = nil) { 20 | try? self.init(data: data, accessMode: mode, pathEncoding: preferredEncoding) 21 | } 22 | #endif 23 | } 24 | -------------------------------------------------------------------------------- /Sources/ZIPFoundation/Archive+MemoryFile.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Archive+MemoryFile.swift 3 | // ZIPFoundation 4 | // 5 | // Copyright © 2017-2024 Thomas Zoechling, https://www.peakstep.com and the ZIP Foundation project authors. 6 | // Released under the MIT License. 7 | // 8 | // See https://github.com/weichsel/ZIPFoundation/blob/master/LICENSE for license information. 9 | // 10 | 11 | import Foundation 12 | 13 | extension Archive { 14 | var isMemoryArchive: Bool { return self.url.scheme == memoryURLScheme } 15 | } 16 | 17 | #if swift(>=5.0) 18 | 19 | extension Archive { 20 | 21 | class MemoryFile { 22 | 23 | private(set) var data: Data 24 | private var offset = 0 25 | 26 | init(data: Data = Data()) { 27 | self.data = data 28 | } 29 | 30 | func open(mode: AccessMode) -> FILEPointer { 31 | let cookie = Unmanaged.passRetained(self) 32 | #if os(macOS) || os(iOS) || os(tvOS) || os(visionOS) || os(watchOS) || os(Android) 33 | let result = mode.isWritable 34 | ? funopen(cookie.toOpaque(), readStub, writeStub, seekStub, closeStub)! 35 | : funopen(cookie.toOpaque(), readStub, nil, seekStub, closeStub)! 36 | #else 37 | let stubs = cookie_io_functions_t(read: readStub, write: writeStub, seek: seekStub, close: closeStub) 38 | let result = fopencookie(cookie.toOpaque(), mode.posixMode, stubs)! 39 | #endif 40 | return result 41 | } 42 | } 43 | 44 | /// Returns a `Data` object containing a representation of the receiver. 45 | public var data: Data? { return self.memoryFile?.data } 46 | } 47 | 48 | private extension Archive.MemoryFile { 49 | 50 | func readData(buffer: UnsafeMutableRawBufferPointer) -> Int { 51 | let size = min(buffer.count, data.count-offset) 52 | let start = data.startIndex 53 | self.data.copyBytes(to: buffer.bindMemory(to: UInt8.self), from: start+offset.. Int { 59 | let start = self.data.startIndex 60 | if self.offset < self.data.count && self.offset+buffer.count > self.data.count { 61 | self.data.removeSubrange(start+self.offset.. data.count { 63 | self.data.append(Data(count: self.offset-self.data.count)) 64 | } 65 | if self.offset == self.data.count { 66 | self.data.append(buffer.bindMemory(to: UInt8.self)) 67 | } else { 68 | let start = self.data.startIndex // May have changed in earlier mutation 69 | self.data.replaceSubrange( 70 | start+self.offset.. Int { 79 | var result = -1 80 | if whence == SEEK_SET { 81 | result = offset 82 | } else if whence == SEEK_CUR { 83 | result = self.offset + offset 84 | } else if whence == SEEK_END { 85 | result = data.count + offset 86 | } 87 | self.offset = result 88 | return self.offset 89 | } 90 | } 91 | 92 | private func fileFromCookie(cookie: UnsafeRawPointer) -> Archive.MemoryFile { 93 | return Unmanaged.fromOpaque(cookie).takeUnretainedValue() 94 | } 95 | 96 | private func closeStub(_ cookie: UnsafeMutableRawPointer?) -> Int32 { 97 | if let cookie = cookie { 98 | Unmanaged.fromOpaque(cookie).release() 99 | } 100 | return 0 101 | } 102 | 103 | #if os(macOS) || os(iOS) || os(tvOS) || os(visionOS) || os(watchOS) || os(Android) 104 | 105 | private func readStub(_ cookie: UnsafeMutableRawPointer?, 106 | _ bytePtr: UnsafeMutablePointer?, 107 | _ count: Int32) -> Int32 { 108 | guard let cookie = cookie, let bytePtr = bytePtr else { return 0 } 109 | return Int32(fileFromCookie(cookie: cookie).readData( 110 | buffer: UnsafeMutableRawBufferPointer(start: bytePtr, count: Int(count)))) 111 | } 112 | 113 | private func writeStub(_ cookie: UnsafeMutableRawPointer?, 114 | _ bytePtr: UnsafePointer?, 115 | _ count: Int32) -> Int32 { 116 | guard let cookie = cookie, let bytePtr = bytePtr else { return 0 } 117 | return Int32(fileFromCookie(cookie: cookie).writeData( 118 | buffer: UnsafeRawBufferPointer(start: bytePtr, count: Int(count)))) 119 | } 120 | 121 | private func seekStub(_ cookie: UnsafeMutableRawPointer?, 122 | _ offset: fpos_t, 123 | _ whence: Int32) -> fpos_t { 124 | guard let cookie = cookie else { return 0 } 125 | return fpos_t(fileFromCookie(cookie: cookie).seek(offset: Int(offset), whence: whence)) 126 | } 127 | 128 | #else 129 | 130 | extension Archive.AccessMode { 131 | 132 | var posixMode: String { 133 | switch self { 134 | case .read: return "rb" 135 | case .create: return "wb+" 136 | case .update: return "rb+" 137 | } 138 | } 139 | } 140 | 141 | private func readStub(_ cookie: UnsafeMutableRawPointer?, 142 | _ bytePtr: UnsafeMutablePointer?, 143 | _ count: Int) -> Int { 144 | guard let cookie = cookie, let bytePtr = bytePtr else { return 0 } 145 | return fileFromCookie(cookie: cookie).readData( 146 | buffer: UnsafeMutableRawBufferPointer(start: bytePtr, count: count)) 147 | } 148 | 149 | private func writeStub(_ cookie: UnsafeMutableRawPointer?, 150 | _ bytePtr: UnsafePointer?, 151 | _ count: Int) -> Int { 152 | guard let cookie = cookie, let bytePtr = bytePtr else { return 0 } 153 | return fileFromCookie(cookie: cookie).writeData( 154 | buffer: UnsafeRawBufferPointer(start: bytePtr, count: count)) 155 | } 156 | 157 | private func seekStub(_ cookie: UnsafeMutableRawPointer?, 158 | _ offset: UnsafeMutablePointer?, 159 | _ whence: Int32) -> Int32 { 160 | guard let cookie = cookie, let offset = offset else { return 0 } 161 | let result = fileFromCookie(cookie: cookie).seek(offset: Int(offset.pointee), whence: whence) 162 | if result >= 0 { 163 | offset.pointee = result 164 | return 0 165 | } else { 166 | return -1 167 | } 168 | } 169 | #endif 170 | #endif 171 | -------------------------------------------------------------------------------- /Sources/ZIPFoundation/Archive+Progress.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Archive+Progress.swift 3 | // ZIPFoundation 4 | // 5 | // Copyright © 2017-2024 Thomas Zoechling, https://www.peakstep.com and the ZIP Foundation project authors. 6 | // Released under the MIT License. 7 | // 8 | // See https://github.com/weichsel/ZIPFoundation/blob/master/LICENSE for license information. 9 | // 10 | 11 | import Foundation 12 | 13 | extension Archive { 14 | /// The number of the work units that have to be performed when 15 | /// removing `entry` from the receiver. 16 | /// 17 | /// - Parameter entry: The entry that will be removed. 18 | /// - Returns: The number of the work units. 19 | public func totalUnitCountForRemoving(_ entry: Entry) -> Int64 { 20 | return Int64(self.offsetToStartOfCentralDirectory - entry.localSize) 21 | } 22 | 23 | func makeProgressForRemoving(_ entry: Entry) -> Progress { 24 | return Progress(totalUnitCount: self.totalUnitCountForRemoving(entry)) 25 | } 26 | 27 | /// The number of the work units that have to be performed when 28 | /// reading `entry` from the receiver. 29 | /// 30 | /// - Parameter entry: The entry that will be read. 31 | /// - Returns: The number of the work units. 32 | public func totalUnitCountForReading(_ entry: Entry) -> Int64 { 33 | switch entry.type { 34 | case .file, .symlink: 35 | return Int64(entry.uncompressedSize) 36 | case .directory: 37 | return defaultDirectoryUnitCount 38 | } 39 | } 40 | 41 | func makeProgressForReading(_ entry: Entry) -> Progress { 42 | return Progress(totalUnitCount: self.totalUnitCountForReading(entry)) 43 | } 44 | 45 | /// The number of the work units that have to be performed when 46 | /// adding the file at `url` to the receiver. 47 | /// - Parameter entry: The entry that will be removed. 48 | /// - Returns: The number of the work units. 49 | public func totalUnitCountForAddingItem(at url: URL) -> Int64 { 50 | var count = Int64(0) 51 | do { 52 | let type = try FileManager.typeForItem(at: url) 53 | switch type { 54 | case .file, .symlink: 55 | count = Int64(try FileManager.fileSizeForItem(at: url)) 56 | case .directory: 57 | count = defaultDirectoryUnitCount 58 | } 59 | } catch { count = -1 } 60 | return count 61 | } 62 | 63 | func makeProgressForAddingItem(at url: URL) -> Progress { 64 | return Progress(totalUnitCount: self.totalUnitCountForAddingItem(at: url)) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Sources/ZIPFoundation/Archive+Reading.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Archive+Reading.swift 3 | // ZIPFoundation 4 | // 5 | // Copyright © 2017-2024 Thomas Zoechling, https://www.peakstep.com and the ZIP Foundation project authors. 6 | // Released under the MIT License. 7 | // 8 | // See https://github.com/weichsel/ZIPFoundation/blob/master/LICENSE for license information. 9 | // 10 | 11 | import Foundation 12 | 13 | extension Archive { 14 | /// Read a ZIP `Entry` from the receiver and write it to `url`. 15 | /// 16 | /// - Parameters: 17 | /// - entry: The ZIP `Entry` to read. 18 | /// - url: The destination file URL. 19 | /// - bufferSize: The maximum size of the read buffer and the decompression buffer (if needed). 20 | /// - skipCRC32: Optional flag to skip calculation of the CRC32 checksum to improve performance. 21 | /// - allowUncontainedSymlinks: Optional flag to allow symlinks that point to paths outside the destination. 22 | /// - progress: A progress object that can be used to track or cancel the extract operation. 23 | /// - Returns: The checksum of the processed content or 0 if the `skipCRC32` flag was set to `true`. 24 | /// - Throws: An error if the destination file cannot be written or the entry contains malformed content. 25 | public func extract(_ entry: Entry, to url: URL, bufferSize: Int = defaultReadChunkSize, 26 | skipCRC32: Bool = false, allowUncontainedSymlinks: Bool = false, 27 | progress: Progress? = nil) throws -> CRC32 { 28 | guard bufferSize > 0 else { 29 | throw ArchiveError.invalidBufferSize 30 | } 31 | let fileManager = FileManager() 32 | var checksum = CRC32(0) 33 | switch entry.type { 34 | case .file: 35 | guard fileManager.itemExists(at: url) == false else { 36 | throw CocoaError(.fileWriteFileExists, userInfo: [NSFilePathErrorKey: url.path]) 37 | } 38 | try fileManager.createParentDirectoryStructure(for: url) 39 | let destinationRepresentation = fileManager.fileSystemRepresentation(withPath: url.path) 40 | guard let destinationFile: FILEPointer = fopen(destinationRepresentation, "wb+") else { 41 | throw POSIXError(errno, path: url.path) 42 | } 43 | defer { fclose(destinationFile) } 44 | let consumer = { _ = try Data.write(chunk: $0, to: destinationFile) } 45 | checksum = try self.extract(entry, bufferSize: bufferSize, skipCRC32: skipCRC32, 46 | progress: progress, consumer: consumer) 47 | case .directory: 48 | let consumer = { (_: Data) in 49 | try fileManager.createDirectory(at: url, withIntermediateDirectories: true, attributes: nil) 50 | } 51 | checksum = try self.extract(entry, bufferSize: bufferSize, skipCRC32: skipCRC32, 52 | progress: progress, consumer: consumer) 53 | case .symlink: 54 | guard fileManager.itemExists(at: url) == false else { 55 | throw CocoaError(.fileWriteFileExists, userInfo: [NSFilePathErrorKey: url.path]) 56 | } 57 | let consumer = { (data: Data) in 58 | guard let linkPath = String(data: data, encoding: .utf8) else { throw ArchiveError.invalidEntryPath } 59 | 60 | let parentURL = url.deletingLastPathComponent() 61 | let isAbsolutePath = (linkPath as NSString).isAbsolutePath 62 | let linkURL = URL(fileURLWithPath: linkPath, relativeTo: isAbsolutePath ? nil : parentURL) 63 | let isContained = allowUncontainedSymlinks || linkURL.isContained(in: parentURL) 64 | guard isContained else { throw ArchiveError.uncontainedSymlink } 65 | 66 | try fileManager.createParentDirectoryStructure(for: url) 67 | try fileManager.createSymbolicLink(atPath: url.path, withDestinationPath: linkPath) 68 | } 69 | checksum = try self.extract(entry, bufferSize: bufferSize, skipCRC32: skipCRC32, 70 | progress: progress, consumer: consumer) 71 | } 72 | try fileManager.transferAttributes(from: entry, toItemAtURL: url) 73 | return checksum 74 | } 75 | 76 | /// Read a ZIP `Entry` from the receiver and forward its contents to a `Consumer` closure. 77 | /// 78 | /// - Parameters: 79 | /// - entry: The ZIP `Entry` to read. 80 | /// - bufferSize: The maximum size of the read buffer and the decompression buffer (if needed). 81 | /// - skipCRC32: Optional flag to skip calculation of the CRC32 checksum to improve performance. 82 | /// - progress: A progress object that can be used to track or cancel the extract operation. 83 | /// - consumer: A closure that consumes contents of `Entry` as `Data` chunks. 84 | /// - Returns: The checksum of the processed content or 0 if the `skipCRC32` flag was set to `true`.. 85 | /// - Throws: An error if the destination file cannot be written or the entry contains malformed content. 86 | public func extract(_ entry: Entry, bufferSize: Int = defaultReadChunkSize, skipCRC32: Bool = false, 87 | progress: Progress? = nil, consumer: Consumer) throws -> CRC32 { 88 | guard bufferSize > 0 else { 89 | throw ArchiveError.invalidBufferSize 90 | } 91 | var checksum = CRC32(0) 92 | let localFileHeader = entry.localFileHeader 93 | guard entry.dataOffset <= .max else { throw ArchiveError.invalidLocalHeaderDataOffset } 94 | fseeko(self.archiveFile, off_t(entry.dataOffset), SEEK_SET) 95 | progress?.totalUnitCount = self.totalUnitCountForReading(entry) 96 | switch entry.type { 97 | case .file: 98 | guard let compressionMethod = CompressionMethod(rawValue: localFileHeader.compressionMethod) else { 99 | throw ArchiveError.invalidCompressionMethod 100 | } 101 | switch compressionMethod { 102 | case .none: checksum = try self.readUncompressed(entry: entry, bufferSize: bufferSize, 103 | skipCRC32: skipCRC32, progress: progress, with: consumer) 104 | case .deflate: checksum = try self.readCompressed(entry: entry, bufferSize: bufferSize, 105 | skipCRC32: skipCRC32, progress: progress, with: consumer) 106 | } 107 | case .directory: 108 | try consumer(Data()) 109 | progress?.completedUnitCount = self.totalUnitCountForReading(entry) 110 | case .symlink: 111 | let localFileHeader = entry.localFileHeader 112 | let size = Int(localFileHeader.compressedSize) 113 | let data = try Data.readChunk(of: size, from: self.archiveFile) 114 | checksum = data.crc32(checksum: 0) 115 | try consumer(data) 116 | progress?.completedUnitCount = self.totalUnitCountForReading(entry) 117 | } 118 | return checksum 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /Sources/ZIPFoundation/Archive+ReadingDeprecated.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Archive+ReadingDeprecated.swift 3 | // ZIPFoundation 4 | // 5 | // Copyright © 2017-2024 Thomas Zoechling, https://www.peakstep.com and the ZIP Foundation project authors. 6 | // Released under the MIT License. 7 | // 8 | // See https://github.com/weichsel/ZIPFoundation/blob/master/LICENSE for license information. 9 | // 10 | 11 | import Foundation 12 | 13 | public extension Archive { 14 | 15 | @available(*, deprecated, 16 | message: "Please use `Int` for `bufferSize`.") 17 | func extract(_ entry: Entry, to url: URL, bufferSize: UInt32, skipCRC32: Bool = false, 18 | progress: Progress? = nil) throws -> CRC32 { 19 | try self.extract(entry, to: url, bufferSize: Int(bufferSize), skipCRC32: skipCRC32, progress: progress) 20 | } 21 | 22 | @available(*, deprecated, 23 | message: "Please use `Int` for `bufferSize`.") 24 | func extract(_ entry: Entry, bufferSize: UInt32, skipCRC32: Bool = false, 25 | progress: Progress? = nil, consumer: Consumer) throws -> CRC32 { 26 | try self.extract(entry, bufferSize: Int(bufferSize), skipCRC32: skipCRC32, 27 | progress: progress, consumer: consumer) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Sources/ZIPFoundation/Archive+WritingDeprecated.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Archive+WritingDeprecated.swift 3 | // ZIPFoundation 4 | // 5 | // Copyright © 2017-2024 Thomas Zoechling, https://www.peakstep.com and the ZIP Foundation project authors. 6 | // Released under the MIT License. 7 | // 8 | // See https://github.com/weichsel/ZIPFoundation/blob/master/LICENSE for license information. 9 | // 10 | 11 | import Foundation 12 | 13 | public extension Archive { 14 | 15 | @available(*, deprecated, 16 | message: "Please use `Int` for `bufferSize`.") 17 | func addEntry(with path: String, relativeTo baseURL: URL, 18 | compressionMethod: CompressionMethod = .none, 19 | bufferSize: UInt32, progress: Progress? = nil) throws { 20 | try self.addEntry(with: path, relativeTo: baseURL, compressionMethod: compressionMethod, 21 | bufferSize: Int(bufferSize), progress: progress) 22 | } 23 | 24 | @available(*, deprecated, 25 | message: "Please use `Int` for `bufferSize`.") 26 | func addEntry(with path: String, fileURL: URL, compressionMethod: CompressionMethod = .none, 27 | bufferSize: UInt32, progress: Progress? = nil) throws { 28 | try self.addEntry(with: path, fileURL: fileURL, compressionMethod: compressionMethod, 29 | bufferSize: Int(bufferSize), progress: progress) 30 | } 31 | 32 | @available(*, deprecated, 33 | message: "Please use `Int64` for `uncompressedSize` and provider `position`. `Int` for `bufferSize`.") 34 | func addEntry(with path: String, type: Entry.EntryType, uncompressedSize: UInt32, 35 | modificationDate: Date = Date(), permissions: UInt16? = nil, 36 | compressionMethod: CompressionMethod = .none, bufferSize: Int = defaultWriteChunkSize, 37 | progress: Progress? = nil, provider: (_ position: Int, _ size: Int) throws -> Data) throws { 38 | let newProvider: Provider = { try provider(Int($0), $1) } 39 | try self.addEntry(with: path, type: type, uncompressedSize: Int64(uncompressedSize), 40 | modificationDate: modificationDate, permissions: permissions, 41 | compressionMethod: compressionMethod, bufferSize: bufferSize, 42 | progress: progress, provider: newProvider) 43 | } 44 | 45 | @available(*, deprecated, 46 | message: "Please use `Int` for `bufferSize`.") 47 | func remove(_ entry: Entry, bufferSize: UInt32, progress: Progress? = nil) throws { 48 | try self.remove(entry, bufferSize: Int(bufferSize), progress: progress) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Sources/ZIPFoundation/Archive+ZIP64.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Archive+ZIP64.swift 3 | // ZIPFoundation 4 | // 5 | // Copyright © 2017-2024 Thomas Zoechling, https://www.peakstep.com and the ZIP Foundation project authors. 6 | // Released under the MIT License. 7 | // 8 | // See https://github.com/weichsel/ZIPFoundation/blob/master/LICENSE for license information. 9 | // 10 | 11 | import Foundation 12 | 13 | let zip64EOCDRecordStructSignature = 0x06064b50 14 | let zip64EOCDLocatorStructSignature = 0x07064b50 15 | 16 | enum ExtraFieldHeaderID: UInt16 { 17 | case zip64ExtendedInformation = 0x0001 18 | } 19 | 20 | extension Archive { 21 | struct ZIP64EndOfCentralDirectory { 22 | let record: ZIP64EndOfCentralDirectoryRecord 23 | let locator: ZIP64EndOfCentralDirectoryLocator 24 | } 25 | 26 | struct ZIP64EndOfCentralDirectoryRecord: DataSerializable { 27 | let zip64EOCDRecordSignature = UInt32(zip64EOCDRecordStructSignature) 28 | let sizeOfZIP64EndOfCentralDirectoryRecord: UInt64 29 | let versionMadeBy: UInt16 30 | let versionNeededToExtract: UInt16 31 | let numberOfDisk: UInt32 32 | let numberOfDiskStart: UInt32 33 | let totalNumberOfEntriesOnDisk: UInt64 34 | let totalNumberOfEntriesInCentralDirectory: UInt64 35 | let sizeOfCentralDirectory: UInt64 36 | let offsetToStartOfCentralDirectory: UInt64 37 | let zip64ExtensibleDataSector: Data 38 | static let size = 56 39 | } 40 | 41 | struct ZIP64EndOfCentralDirectoryLocator: DataSerializable { 42 | let zip64EOCDLocatorSignature = UInt32(zip64EOCDLocatorStructSignature) 43 | let numberOfDiskWithZIP64EOCDRecordStart: UInt32 44 | let relativeOffsetOfZIP64EOCDRecord: UInt64 45 | let totalNumberOfDisk: UInt32 46 | static let size = 20 47 | } 48 | } 49 | 50 | extension Archive.ZIP64EndOfCentralDirectoryRecord { 51 | var data: Data { 52 | var zip64EOCDRecordSignature = self.zip64EOCDRecordSignature 53 | var sizeOfZIP64EOCDRecord = self.sizeOfZIP64EndOfCentralDirectoryRecord 54 | var versionMadeBy = self.versionMadeBy 55 | var versionNeededToExtract = self.versionNeededToExtract 56 | var numberOfDisk = self.numberOfDisk 57 | var numberOfDiskStart = self.numberOfDiskStart 58 | var totalNumberOfEntriesOnDisk = self.totalNumberOfEntriesOnDisk 59 | var totalNumberOfEntriesInCD = self.totalNumberOfEntriesInCentralDirectory 60 | var sizeOfCD = self.sizeOfCentralDirectory 61 | var offsetToStartOfCD = self.offsetToStartOfCentralDirectory 62 | var data = Data() 63 | withUnsafePointer(to: &zip64EOCDRecordSignature, { data.append(UnsafeBufferPointer(start: $0, count: 1))}) 64 | withUnsafePointer(to: &sizeOfZIP64EOCDRecord, { data.append(UnsafeBufferPointer(start: $0, count: 1))}) 65 | withUnsafePointer(to: &versionMadeBy, { data.append(UnsafeBufferPointer(start: $0, count: 1))}) 66 | withUnsafePointer(to: &versionNeededToExtract, { data.append(UnsafeBufferPointer(start: $0, count: 1))}) 67 | withUnsafePointer(to: &numberOfDisk, { data.append(UnsafeBufferPointer(start: $0, count: 1))}) 68 | withUnsafePointer(to: &numberOfDiskStart, { data.append(UnsafeBufferPointer(start: $0, count: 1))}) 69 | withUnsafePointer(to: &totalNumberOfEntriesOnDisk, { data.append(UnsafeBufferPointer(start: $0, count: 1))}) 70 | withUnsafePointer(to: &totalNumberOfEntriesInCD, { data.append(UnsafeBufferPointer(start: $0, count: 1))}) 71 | withUnsafePointer(to: &sizeOfCD, { data.append(UnsafeBufferPointer(start: $0, count: 1))}) 72 | withUnsafePointer(to: &offsetToStartOfCD, { data.append(UnsafeBufferPointer(start: $0, count: 1))}) 73 | data.append(self.zip64ExtensibleDataSector) 74 | return data 75 | } 76 | 77 | init?(data: Data, additionalDataProvider provider: (Int) throws -> Data) { 78 | guard data.count == Archive.ZIP64EndOfCentralDirectoryRecord.size else { return nil } 79 | guard data.scanValue(start: 0) == zip64EOCDRecordSignature else { return nil } 80 | self.sizeOfZIP64EndOfCentralDirectoryRecord = data.scanValue(start: 4) 81 | self.versionMadeBy = data.scanValue(start: 12) 82 | self.versionNeededToExtract = data.scanValue(start: 14) 83 | // Version Needed to Extract: 4.5 - File uses ZIP64 format extensions 84 | guard self.versionNeededToExtract >= Archive.Version.v45.rawValue else { return nil } 85 | self.numberOfDisk = data.scanValue(start: 16) 86 | self.numberOfDiskStart = data.scanValue(start: 20) 87 | self.totalNumberOfEntriesOnDisk = data.scanValue(start: 24) 88 | self.totalNumberOfEntriesInCentralDirectory = data.scanValue(start: 32) 89 | self.sizeOfCentralDirectory = data.scanValue(start: 40) 90 | self.offsetToStartOfCentralDirectory = data.scanValue(start: 48) 91 | self.zip64ExtensibleDataSector = Data() 92 | } 93 | 94 | init(record: Archive.ZIP64EndOfCentralDirectoryRecord, 95 | numberOfEntriesOnDisk: UInt64, 96 | numberOfEntriesInCD: UInt64, 97 | sizeOfCentralDirectory: UInt64, 98 | offsetToStartOfCD: UInt64) { 99 | self.sizeOfZIP64EndOfCentralDirectoryRecord = record.sizeOfZIP64EndOfCentralDirectoryRecord 100 | self.versionMadeBy = record.versionMadeBy 101 | self.versionNeededToExtract = record.versionNeededToExtract 102 | self.numberOfDisk = record.numberOfDisk 103 | self.numberOfDiskStart = record.numberOfDiskStart 104 | self.totalNumberOfEntriesOnDisk = numberOfEntriesOnDisk 105 | self.totalNumberOfEntriesInCentralDirectory = numberOfEntriesInCD 106 | self.sizeOfCentralDirectory = sizeOfCentralDirectory 107 | self.offsetToStartOfCentralDirectory = offsetToStartOfCD 108 | self.zip64ExtensibleDataSector = record.zip64ExtensibleDataSector 109 | } 110 | } 111 | 112 | extension Archive.ZIP64EndOfCentralDirectoryLocator { 113 | var data: Data { 114 | var zip64EOCDLocatorSignature = self.zip64EOCDLocatorSignature 115 | var numberOfDiskWithZIP64EOCD = self.numberOfDiskWithZIP64EOCDRecordStart 116 | var offsetOfZIP64EOCDRecord = self.relativeOffsetOfZIP64EOCDRecord 117 | var totalNumberOfDisk = self.totalNumberOfDisk 118 | var data = Data() 119 | withUnsafePointer(to: &zip64EOCDLocatorSignature, { data.append(UnsafeBufferPointer(start: $0, count: 1))}) 120 | withUnsafePointer(to: &numberOfDiskWithZIP64EOCD, { data.append(UnsafeBufferPointer(start: $0, count: 1))}) 121 | withUnsafePointer(to: &offsetOfZIP64EOCDRecord, { data.append(UnsafeBufferPointer(start: $0, count: 1))}) 122 | withUnsafePointer(to: &totalNumberOfDisk, { data.append(UnsafeBufferPointer(start: $0, count: 1))}) 123 | return data 124 | } 125 | 126 | init?(data: Data, additionalDataProvider provider: (Int) throws -> Data) { 127 | guard data.count == Archive.ZIP64EndOfCentralDirectoryLocator.size else { return nil } 128 | guard data.scanValue(start: 0) == zip64EOCDLocatorSignature else { return nil } 129 | self.numberOfDiskWithZIP64EOCDRecordStart = data.scanValue(start: 4) 130 | self.relativeOffsetOfZIP64EOCDRecord = data.scanValue(start: 8) 131 | self.totalNumberOfDisk = data.scanValue(start: 16) 132 | } 133 | 134 | init(locator: Archive.ZIP64EndOfCentralDirectoryLocator, offsetOfZIP64EOCDRecord: UInt64) { 135 | self.numberOfDiskWithZIP64EOCDRecordStart = locator.numberOfDiskWithZIP64EOCDRecordStart 136 | self.relativeOffsetOfZIP64EOCDRecord = offsetOfZIP64EOCDRecord 137 | self.totalNumberOfDisk = locator.totalNumberOfDisk 138 | } 139 | } 140 | 141 | extension Archive.ZIP64EndOfCentralDirectory { 142 | var data: Data { record.data + locator.data } 143 | } 144 | 145 | /// Properties that represent the maximum value of each field 146 | var maxUInt32 = UInt32.max 147 | var maxUInt16 = UInt16.max 148 | 149 | var maxCompressedSize: UInt32 { maxUInt32 } 150 | var maxUncompressedSize: UInt32 { maxUInt32 } 151 | var maxOffsetOfLocalFileHeader: UInt32 { maxUInt32 } 152 | var maxOffsetOfCentralDirectory: UInt32 { maxUInt32 } 153 | var maxSizeOfCentralDirectory: UInt32 { maxUInt32 } 154 | var maxTotalNumberOfEntries: UInt16 { maxUInt16 } 155 | -------------------------------------------------------------------------------- /Sources/ZIPFoundation/Data+CompressionDeprecated.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Data+CompressionDeprecated.swift 3 | // ZIPFoundation 4 | // 5 | // Copyright © 2017-2024 Thomas Zoechling, https://www.peakstep.com and the ZIP Foundation project authors. 6 | // Released under the MIT License. 7 | // 8 | // See https://github.com/weichsel/ZIPFoundation/blob/master/LICENSE for license information. 9 | // 10 | 11 | import Foundation 12 | 13 | public extension Data { 14 | 15 | @available(*, deprecated, message: "Please use `Int64` for `size` and provider `position`.") 16 | static func compress(size: Int, bufferSize: Int, 17 | provider: (_ position: Int, _ size: Int) throws -> Data, 18 | consumer: Consumer) throws -> CRC32 { 19 | let newProvider: Provider = { try provider(Int($0), $1) } 20 | return try self.compress(size: Int64(size), bufferSize: bufferSize, provider: newProvider, consumer: consumer) 21 | } 22 | 23 | @available(*, deprecated, message: "Please use `Int64` for `size` and provider `position`.") 24 | static func decompress(size: Int, bufferSize: Int, skipCRC32: Bool, 25 | provider: (_ position: Int, _ size: Int) throws -> Data, 26 | consumer: Consumer) throws -> CRC32 { 27 | let newProvider: Provider = { try provider(Int($0), $1) } 28 | return try self.decompress(size: Int64(size), bufferSize: bufferSize, skipCRC32: skipCRC32, 29 | provider: newProvider, consumer: consumer) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Sources/ZIPFoundation/Data+Serialization.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Data+Serialization.swift 3 | // ZIPFoundation 4 | // 5 | // Copyright © 2017-2024 Thomas Zoechling, https://www.peakstep.com and the ZIP Foundation project authors. 6 | // Released under the MIT License. 7 | // 8 | // See https://github.com/weichsel/ZIPFoundation/blob/master/LICENSE for license information. 9 | // 10 | 11 | import Foundation 12 | 13 | #if os(Android) 14 | public typealias FILEPointer = OpaquePointer 15 | #else 16 | public typealias FILEPointer = UnsafeMutablePointer 17 | #endif 18 | 19 | protocol DataSerializable { 20 | static var size: Int { get } 21 | init?(data: Data, additionalDataProvider: (Int) throws -> Data) 22 | var data: Data { get } 23 | } 24 | 25 | extension Data { 26 | public enum DataError: Error { 27 | case unreadableFile 28 | case unwritableFile 29 | } 30 | 31 | func scanValue(start: Int) -> T { 32 | return self.withUnsafeBytes { 33 | $0.loadUnaligned(fromByteOffset: start, as: T.self) 34 | } 35 | } 36 | 37 | static func readStruct(from file: FILEPointer, at offset: UInt64) 38 | -> T? where T: DataSerializable { 39 | guard offset <= .max else { return nil } 40 | fseeko(file, off_t(offset), SEEK_SET) 41 | guard let data = try? self.readChunk(of: T.size, from: file) else { 42 | return nil 43 | } 44 | let structure = T(data: data, additionalDataProvider: { (additionalDataSize) -> Data in 45 | return try self.readChunk(of: additionalDataSize, from: file) 46 | }) 47 | return structure 48 | } 49 | 50 | static func consumePart(of size: Int64, chunkSize: Int, skipCRC32: Bool = false, 51 | provider: Provider, consumer: Consumer) throws -> CRC32 { 52 | var checksum = CRC32(0) 53 | guard size > 0 else { 54 | try consumer(Data()) 55 | return checksum 56 | } 57 | 58 | let readInOneChunk = (size < chunkSize) 59 | var chunkSize = readInOneChunk ? Int(size) : chunkSize 60 | var bytesRead: Int64 = 0 61 | while bytesRead < size { 62 | let remainingSize = size - bytesRead 63 | chunkSize = remainingSize < chunkSize ? Int(remainingSize) : chunkSize 64 | let data = try provider(bytesRead, chunkSize) 65 | try consumer(data) 66 | if !skipCRC32 { 67 | checksum = data.crc32(checksum: checksum) 68 | } 69 | bytesRead += Int64(chunkSize) 70 | } 71 | return checksum 72 | } 73 | 74 | static func readChunk(of size: Int, from file: FILEPointer) throws -> Data { 75 | let alignment = MemoryLayout.alignment 76 | #if swift(>=4.1) 77 | let bytes = UnsafeMutableRawPointer.allocate(byteCount: size, alignment: alignment) 78 | #else 79 | let bytes = UnsafeMutableRawPointer.allocate(bytes: size, alignedTo: alignment) 80 | #endif 81 | let bytesRead = fread(bytes, 1, size, file) 82 | let error = ferror(file) 83 | if error > 0 { 84 | bytes.deallocate() 85 | throw DataError.unreadableFile 86 | } 87 | #if swift(>=4.1) 88 | return Data(bytesNoCopy: bytes, count: bytesRead, deallocator: .custom({ buf, _ in buf.deallocate() })) 89 | #else 90 | let deallocator = Deallocator.custom({ buf, _ in buf.deallocate(bytes: size, alignedTo: 1) }) 91 | return Data(bytesNoCopy: bytes, count: bytesRead, deallocator: deallocator) 92 | #endif 93 | } 94 | 95 | static func write(chunk: Data, to file: FILEPointer) throws -> Int { 96 | var sizeWritten: Int = 0 97 | chunk.withUnsafeBytes { (rawBufferPointer) in 98 | if let baseAddress = rawBufferPointer.baseAddress, rawBufferPointer.count > 0 { 99 | let pointer = baseAddress.assumingMemoryBound(to: UInt8.self) 100 | sizeWritten = fwrite(pointer, 1, chunk.count, file) 101 | } 102 | } 103 | let error = ferror(file) 104 | if error > 0 { 105 | throw DataError.unwritableFile 106 | } 107 | return sizeWritten 108 | } 109 | 110 | static func writeLargeChunk(_ chunk: Data, size: UInt64, bufferSize: Int, 111 | to file: FILEPointer) throws -> UInt64 { 112 | var sizeWritten: UInt64 = 0 113 | chunk.withUnsafeBytes { (rawBufferPointer) in 114 | if let baseAddress = rawBufferPointer.baseAddress, rawBufferPointer.count > 0 { 115 | let pointer = baseAddress.assumingMemoryBound(to: UInt8.self) 116 | 117 | while sizeWritten < size { 118 | let remainingSize = size - sizeWritten 119 | let chunkSize = Swift.min(Int(remainingSize), bufferSize) 120 | let curPointer = pointer.advanced(by: Int(sizeWritten)) 121 | fwrite(curPointer, 1, chunkSize, file) 122 | sizeWritten += UInt64(chunkSize) 123 | } 124 | } 125 | } 126 | let error = ferror(file) 127 | if error > 0 { 128 | throw DataError.unwritableFile 129 | } 130 | return sizeWritten 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /Sources/ZIPFoundation/Date+ZIP.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Date+ZIP.swift 3 | // ZIPFoundation 4 | // 5 | // Created by Thomas Zoechling on 20.12.22. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Date { 11 | 12 | var fileModificationDateTime: (UInt16, UInt16) { 13 | return (self.fileModificationDate, self.fileModificationTime) 14 | } 15 | 16 | var fileModificationDate: UInt16 { 17 | var time = time_t(self.timeIntervalSince1970) 18 | guard let unixTime = gmtime(&time) else { 19 | return 0 20 | } 21 | var year = unixTime.pointee.tm_year + 1900 // UNIX time structs count in "years since 1900". 22 | // ZIP uses the MSDOS date format which has a valid range of 1980 - 2099. 23 | year = year >= 1980 ? year : 1980 24 | year = year <= 2099 ? year : 2099 25 | let month = unixTime.pointee.tm_mon + 1 // UNIX time struct month entries are zero based. 26 | let day = unixTime.pointee.tm_mday 27 | return (UInt16)(day + ((month) * 32) + ((year - 1980) * 512)) 28 | } 29 | 30 | var fileModificationTime: UInt16 { 31 | var time = time_t(self.timeIntervalSince1970) 32 | guard let unixTime = gmtime(&time) else { 33 | return 0 34 | } 35 | let hour = unixTime.pointee.tm_hour 36 | let minute = unixTime.pointee.tm_min 37 | let second = unixTime.pointee.tm_sec 38 | return (UInt16)((second/2) + (minute * 32) + (hour * 2048)) 39 | } 40 | 41 | init(dateTime: (UInt16, UInt16)) { 42 | var msdosDateTime = Int(dateTime.0) 43 | msdosDateTime <<= 16 44 | msdosDateTime |= Int(dateTime.1) 45 | var unixTime = tm() 46 | unixTime.tm_sec = Int32((msdosDateTime&31)*2) 47 | unixTime.tm_min = Int32((msdosDateTime>>5)&63) 48 | unixTime.tm_hour = Int32((Int(dateTime.1)>>11)&31) 49 | unixTime.tm_mday = Int32((msdosDateTime>>16)&31) 50 | unixTime.tm_mon = Int32((msdosDateTime>>21)&15) 51 | unixTime.tm_mon -= 1 // UNIX time struct month entries are zero based. 52 | unixTime.tm_year = Int32(1980+(msdosDateTime>>25)) 53 | unixTime.tm_year -= 1900 // UNIX time structs count in "years since 1900". 54 | let time = timegm(&unixTime) 55 | self = Date(timeIntervalSince1970: TimeInterval(time)) 56 | } 57 | 58 | init(timespec: timespec) { 59 | let seconds = 1.0e-9 * Double(timespec.tv_nsec) 60 | let timeIntervalSince1970 = TimeInterval(timespec.tv_sec) 61 | let absoluteTimeIntervalSince1970 = Constants.absoluteTimeIntervalSince1970 62 | self.init(timeIntervalSinceReferenceDate: (timeIntervalSince1970 - absoluteTimeIntervalSince1970) + seconds) 63 | } 64 | } 65 | 66 | private extension Date { 67 | 68 | enum Constants { 69 | #if os(macOS) || os(iOS) || os(tvOS) || os(visionOS) || os(watchOS) 70 | static let absoluteTimeIntervalSince1970 = kCFAbsoluteTimeIntervalSince1970 71 | #else 72 | static let absoluteTimeIntervalSince1970: Double = 978307200.0 73 | #endif 74 | } 75 | } 76 | 77 | extension stat { 78 | 79 | var lastAccessDate: Date { 80 | #if os(macOS) || os(iOS) || os(tvOS) || os(visionOS) || os(watchOS) 81 | return Date(timespec: st_atimespec) 82 | #else 83 | return Date(timespec: st_atim) 84 | #endif 85 | } 86 | } 87 | 88 | extension timeval { 89 | 90 | init(timeIntervalSince1970: TimeInterval) { 91 | let (integral, fractional) = modf(timeIntervalSince1970) 92 | self.init(tv_sec: time_t(integral), tv_usec: suseconds_t(1.0e6 * fractional)) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /Sources/ZIPFoundation/Entry+Serialization.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Entry+Serialization.swift 3 | // ZIPFoundation 4 | // 5 | // Copyright © 2017-2024 Thomas Zoechling, https://www.peakstep.com and the ZIP Foundation project authors. 6 | // Released under the MIT License. 7 | // 8 | // See https://github.com/weichsel/ZIPFoundation/blob/master/LICENSE for license information. 9 | // 10 | 11 | import Foundation 12 | 13 | extension Entry.LocalFileHeader { 14 | var data: Data { 15 | var localFileHeaderSignature = self.localFileHeaderSignature 16 | var versionNeededToExtract = self.versionNeededToExtract 17 | var generalPurposeBitFlag = self.generalPurposeBitFlag 18 | var compressionMethod = self.compressionMethod 19 | var lastModFileTime = self.lastModFileTime 20 | var lastModFileDate = self.lastModFileDate 21 | var crc32 = self.crc32 22 | var compressedSize = self.compressedSize 23 | var uncompressedSize = self.uncompressedSize 24 | var fileNameLength = self.fileNameLength 25 | var extraFieldLength = self.extraFieldLength 26 | var data = Data() 27 | withUnsafePointer(to: &localFileHeaderSignature, { data.append(UnsafeBufferPointer(start: $0, count: 1))}) 28 | withUnsafePointer(to: &versionNeededToExtract, { data.append(UnsafeBufferPointer(start: $0, count: 1))}) 29 | withUnsafePointer(to: &generalPurposeBitFlag, { data.append(UnsafeBufferPointer(start: $0, count: 1))}) 30 | withUnsafePointer(to: &compressionMethod, { data.append(UnsafeBufferPointer(start: $0, count: 1))}) 31 | withUnsafePointer(to: &lastModFileTime, { data.append(UnsafeBufferPointer(start: $0, count: 1))}) 32 | withUnsafePointer(to: &lastModFileDate, { data.append(UnsafeBufferPointer(start: $0, count: 1))}) 33 | withUnsafePointer(to: &crc32, { data.append(UnsafeBufferPointer(start: $0, count: 1))}) 34 | withUnsafePointer(to: &compressedSize, { data.append(UnsafeBufferPointer(start: $0, count: 1))}) 35 | withUnsafePointer(to: &uncompressedSize, { data.append(UnsafeBufferPointer(start: $0, count: 1))}) 36 | withUnsafePointer(to: &fileNameLength, { data.append(UnsafeBufferPointer(start: $0, count: 1))}) 37 | withUnsafePointer(to: &extraFieldLength, { data.append(UnsafeBufferPointer(start: $0, count: 1))}) 38 | data.append(self.fileNameData) 39 | data.append(self.extraFieldData) 40 | return data 41 | } 42 | 43 | init?(data: Data, additionalDataProvider provider: (Int) throws -> Data) { 44 | guard data.count == Entry.LocalFileHeader.size else { return nil } 45 | guard data.scanValue(start: 0) == localFileHeaderSignature else { return nil } 46 | self.versionNeededToExtract = data.scanValue(start: 4) 47 | self.generalPurposeBitFlag = data.scanValue(start: 6) 48 | self.compressionMethod = data.scanValue(start: 8) 49 | self.lastModFileTime = data.scanValue(start: 10) 50 | self.lastModFileDate = data.scanValue(start: 12) 51 | self.crc32 = data.scanValue(start: 14) 52 | self.compressedSize = data.scanValue(start: 18) 53 | self.uncompressedSize = data.scanValue(start: 22) 54 | self.fileNameLength = data.scanValue(start: 26) 55 | self.extraFieldLength = data.scanValue(start: 28) 56 | let additionalDataLength = Int(self.fileNameLength) + Int(self.extraFieldLength) 57 | guard let additionalData = try? provider(additionalDataLength) else { return nil } 58 | guard additionalData.count == additionalDataLength else { return nil } 59 | var subRangeStart = 0 60 | var subRangeEnd = Int(self.fileNameLength) 61 | self.fileNameData = additionalData.subdata(in: subRangeStart.. Data) { 116 | guard data.count == Entry.CentralDirectoryStructure.size else { return nil } 117 | guard data.scanValue(start: 0) == centralDirectorySignature else { return nil } 118 | self.versionMadeBy = data.scanValue(start: 4) 119 | self.versionNeededToExtract = data.scanValue(start: 6) 120 | self.generalPurposeBitFlag = data.scanValue(start: 8) 121 | self.compressionMethod = data.scanValue(start: 10) 122 | self.lastModFileTime = data.scanValue(start: 12) 123 | self.lastModFileDate = data.scanValue(start: 14) 124 | self.crc32 = data.scanValue(start: 16) 125 | self.compressedSize = data.scanValue(start: 20) 126 | self.uncompressedSize = data.scanValue(start: 24) 127 | self.fileNameLength = data.scanValue(start: 28) 128 | self.extraFieldLength = data.scanValue(start: 30) 129 | self.fileCommentLength = data.scanValue(start: 32) 130 | self.diskNumberStart = data.scanValue(start: 34) 131 | self.internalFileAttributes = data.scanValue(start: 36) 132 | self.externalFileAttributes = data.scanValue(start: 38) 133 | self.relativeOffsetOfLocalHeader = data.scanValue(start: 42) 134 | let additionalDataLength = Int(self.fileNameLength) + Int(self.extraFieldLength) + Int(self.fileCommentLength) 135 | guard let additionalData = try? provider(additionalDataLength) else { return nil } 136 | guard additionalData.count == additionalDataLength else { return nil } 137 | var subRangeStart = 0 138 | var subRangeEnd = Int(self.fileNameLength) 139 | self.fileNameData = additionalData.subdata(in: subRangeStart.. Data) { 155 | guard data.count == Self.size else { return nil } 156 | let signature: UInt32 = data.scanValue(start: 0) 157 | // The DataDescriptor signature is not mandatory so we have to re-arrange the input data if it is missing. 158 | var readOffset = 0 159 | if signature == self.dataDescriptorSignature { readOffset = 4 } 160 | self.crc32 = data.scanValue(start: readOffset) 161 | readOffset += MemoryLayout.size 162 | self.compressedSize = data.scanValue(start: readOffset) 163 | readOffset += Self.memoryLengthOfSize 164 | self.uncompressedSize = data.scanValue(start: readOffset) 165 | // Our add(_ entry:) methods always maintain compressed & uncompressed 166 | // sizes and so we don't need a data descriptor for newly added entries. 167 | // Data descriptors of already existing entries are manually preserved 168 | // when copying those entries to the tempArchive during remove(_ entry:). 169 | self.data = Data() 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /Sources/ZIPFoundation/Entry+ZIP64.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Entry+ZIP64.swift 3 | // ZIPFoundation 4 | // 5 | // Copyright © 2017-2024 Thomas Zoechling, https://www.peakstep.com and the ZIP Foundation project authors. 6 | // Released under the MIT License. 7 | // 8 | // See https://github.com/weichsel/ZIPFoundation/blob/master/LICENSE for license information. 9 | // 10 | 11 | import Foundation 12 | 13 | protocol ExtensibleDataField { 14 | var headerID: UInt16 { get } 15 | var dataSize: UInt16 { get } 16 | } 17 | 18 | extension Entry { 19 | 20 | enum EntryError: Error { 21 | case missingPermissionsAttributeError 22 | case missingModificationDateAttributeError 23 | } 24 | 25 | struct ZIP64ExtendedInformation: ExtensibleDataField { 26 | let headerID: UInt16 = ExtraFieldHeaderID.zip64ExtendedInformation.rawValue 27 | let dataSize: UInt16 28 | static let headerSize: UInt16 = 4 29 | let uncompressedSize: UInt64 30 | let compressedSize: UInt64 31 | let relativeOffsetOfLocalHeader: UInt64 32 | let diskNumberStart: UInt32 33 | } 34 | 35 | var zip64ExtendedInformation: ZIP64ExtendedInformation? { 36 | self.centralDirectoryStructure.zip64ExtendedInformation 37 | } 38 | } 39 | 40 | typealias Field = Entry.ZIP64ExtendedInformation.Field 41 | 42 | extension Entry.LocalFileHeader { 43 | var validFields: [Field] { 44 | var fields: [Field] = [] 45 | if self.uncompressedSize == .max { fields.append(.uncompressedSize) } 46 | if self.compressedSize == .max { fields.append(.compressedSize) } 47 | return fields 48 | } 49 | } 50 | 51 | extension Entry.CentralDirectoryStructure { 52 | var validFields: [Field] { 53 | var fields: [Field] = [] 54 | if self.uncompressedSize == .max { fields.append(.uncompressedSize) } 55 | if self.compressedSize == .max { fields.append(.compressedSize) } 56 | if self.relativeOffsetOfLocalHeader == .max { fields.append(.relativeOffsetOfLocalHeader) } 57 | if self.diskNumberStart == .max { fields.append(.diskNumberStart) } 58 | return fields 59 | } 60 | var zip64ExtendedInformation: Entry.ZIP64ExtendedInformation? { 61 | self.extraFields?.compactMap { $0 as? Entry.ZIP64ExtendedInformation }.first 62 | } 63 | } 64 | 65 | extension Entry.ZIP64ExtendedInformation { 66 | 67 | enum Field { 68 | case uncompressedSize 69 | case compressedSize 70 | case relativeOffsetOfLocalHeader 71 | case diskNumberStart 72 | 73 | var size: Int { 74 | switch self { 75 | case .uncompressedSize, .compressedSize, .relativeOffsetOfLocalHeader: 76 | return 8 77 | case .diskNumberStart: 78 | return 4 79 | } 80 | } 81 | } 82 | 83 | var data: Data { 84 | var headerID = self.headerID 85 | var dataSize = self.dataSize 86 | var uncompressedSize = self.uncompressedSize 87 | var compressedSize = self.compressedSize 88 | var relativeOffsetOfLFH = self.relativeOffsetOfLocalHeader 89 | var diskNumberStart = self.diskNumberStart 90 | var data = Data() 91 | withUnsafePointer(to: &headerID, { data.append(UnsafeBufferPointer(start: $0, count: 1))}) 92 | withUnsafePointer(to: &dataSize, { data.append(UnsafeBufferPointer(start: $0, count: 1))}) 93 | if uncompressedSize != 0 || compressedSize != 0 { 94 | withUnsafePointer(to: &uncompressedSize, { data.append(UnsafeBufferPointer(start: $0, count: 1))}) 95 | withUnsafePointer(to: &compressedSize, { data.append(UnsafeBufferPointer(start: $0, count: 1))}) 96 | } 97 | if relativeOffsetOfLocalHeader != 0 { 98 | withUnsafePointer(to: &relativeOffsetOfLFH, { data.append(UnsafeBufferPointer(start: $0, count: 1))}) 99 | } 100 | if diskNumberStart != 0 { 101 | withUnsafePointer(to: &diskNumberStart, { data.append(UnsafeBufferPointer(start: $0, count: 1))}) 102 | } 103 | return data 104 | } 105 | 106 | init?(data: Data, fields: [Field]) { 107 | let headerLength = 4 108 | guard fields.reduce(0, { $0 + $1.size }) + headerLength == data.count else { return nil } 109 | 110 | var readOffset = headerLength 111 | func value(of field: Field) -> T where T: BinaryInteger { 112 | if fields.contains(field), readOffset + field.size <= data.count { 113 | defer { readOffset += MemoryLayout.size } 114 | 115 | return data.scanValue(start: readOffset) 116 | } else { 117 | return 0 118 | } 119 | } 120 | 121 | self.dataSize = data.scanValue(start: 2) 122 | self.uncompressedSize = value(of: .uncompressedSize) 123 | self.compressedSize = value(of: .compressedSize) 124 | self.relativeOffsetOfLocalHeader = value(of: .relativeOffsetOfLocalHeader) 125 | self.diskNumberStart = value(of: .diskNumberStart) 126 | } 127 | 128 | init?(zip64ExtendedInformation: Entry.ZIP64ExtendedInformation?, offset: UInt64) { 129 | // Only used when removing entry, if no ZIP64 extended information exists, 130 | // then this information will not be newly added either 131 | guard let existingInfo = zip64ExtendedInformation else { return nil } 132 | relativeOffsetOfLocalHeader = offset >= maxOffsetOfLocalFileHeader ? offset : 0 133 | uncompressedSize = existingInfo.uncompressedSize 134 | compressedSize = existingInfo.compressedSize 135 | diskNumberStart = existingInfo.diskNumberStart 136 | let tempDataSize = [relativeOffsetOfLocalHeader, uncompressedSize, compressedSize] 137 | .filter { $0 != 0 } 138 | .reduce(UInt16(0), { $0 + UInt16(MemoryLayout.size(ofValue: $1))}) 139 | dataSize = tempDataSize + (diskNumberStart > 0 ? UInt16(MemoryLayout.size(ofValue: diskNumberStart)) : 0) 140 | if dataSize == 0 { return nil } 141 | } 142 | 143 | static func scanForZIP64Field(in data: Data, fields: [Field]) -> Entry.ZIP64ExtendedInformation? { 144 | guard data.isEmpty == false else { return nil } 145 | var offset = 0 146 | var headerID: UInt16 147 | var dataSize: UInt16 148 | let extraFieldLength = data.count 149 | let headerSize = Int(Entry.ZIP64ExtendedInformation.headerSize) 150 | while offset < extraFieldLength - headerSize { 151 | headerID = data.scanValue(start: offset) 152 | dataSize = data.scanValue(start: offset + 2) 153 | let nextOffset = offset + headerSize + Int(dataSize) 154 | guard nextOffset <= extraFieldLength else { return nil } 155 | if headerID == ExtraFieldHeaderID.zip64ExtendedInformation.rawValue { 156 | return Entry.ZIP64ExtendedInformation(data: data.subdata(in: offset.. 2 | 3 | 4 | 5 | NSPrivacyTracking 6 | 7 | NSPrivacyCollectedDataTypes 8 | 9 | NSPrivacyTrackingDomains 10 | 11 | NSPrivacyAccessedAPITypes 12 | 13 | 14 | NSPrivacyAccessedAPIType 15 | NSPrivacyAccessedAPICategoryFileTimestamp 16 | NSPrivacyAccessedAPITypeReasons 17 | 18 | 0A2A.1 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /Sources/ZIPFoundation/URL+ZIP.swift: -------------------------------------------------------------------------------- 1 | // 2 | // URL+ZIP.swift 3 | // ZIPFoundation 4 | // 5 | // Copyright © 2017-2024 Thomas Zoechling, https://www.peakstep.com and the ZIP Foundation project authors. 6 | // Released under the MIT License. 7 | // 8 | // See https://github.com/weichsel/ZIPFoundation/blob/master/LICENSE for license information. 9 | // 10 | 11 | import Foundation 12 | 13 | extension URL { 14 | 15 | static func temporaryReplacementDirectoryURL(for archive: Archive) -> URL { 16 | #if swift(>=5.0) || os(macOS) || os(iOS) || os(tvOS) || os(visionOS) || os(watchOS) 17 | if archive.url.isFileURL, 18 | let tempDir = try? FileManager().url(for: .itemReplacementDirectory, in: .userDomainMask, 19 | appropriateFor: archive.url, create: true) { 20 | return tempDir 21 | } 22 | #endif 23 | 24 | return URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent( 25 | ProcessInfo.processInfo.globallyUniqueString) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import ZIPFoundationTests 3 | 4 | XCTMain([ 5 | testCase(ZIPFoundationTests.allTests) 6 | ]) 7 | -------------------------------------------------------------------------------- /Tests/ZIPFoundationTests/Resources/testAddDirectoryToArchiveWithZIP64LFHOffset.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weichsel/ZIPFoundation/e9b1917bd4d7d050e0ff4ec157b5d6e253c84385/Tests/ZIPFoundationTests/Resources/testAddDirectoryToArchiveWithZIP64LFHOffset.zip -------------------------------------------------------------------------------- /Tests/ZIPFoundationTests/Resources/testAddEntryToArchiveWithZIP64LFHOffset.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weichsel/ZIPFoundation/e9b1917bd4d7d050e0ff4ec157b5d6e253c84385/Tests/ZIPFoundationTests/Resources/testAddEntryToArchiveWithZIP64LFHOffset.zip -------------------------------------------------------------------------------- /Tests/ZIPFoundationTests/Resources/testArchiveAddCompressedEntryProgress.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weichsel/ZIPFoundation/e9b1917bd4d7d050e0ff4ec157b5d6e253c84385/Tests/ZIPFoundationTests/Resources/testArchiveAddCompressedEntryProgress.png -------------------------------------------------------------------------------- /Tests/ZIPFoundationTests/Resources/testArchiveAddCompressedEntryProgress.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weichsel/ZIPFoundation/e9b1917bd4d7d050e0ff4ec157b5d6e253c84385/Tests/ZIPFoundationTests/Resources/testArchiveAddCompressedEntryProgress.zip -------------------------------------------------------------------------------- /Tests/ZIPFoundationTests/Resources/testArchiveAddEntryErrorConditions.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weichsel/ZIPFoundation/e9b1917bd4d7d050e0ff4ec157b5d6e253c84385/Tests/ZIPFoundationTests/Resources/testArchiveAddEntryErrorConditions.zip -------------------------------------------------------------------------------- /Tests/ZIPFoundationTests/Resources/testArchiveAddUncompressedEntryProgress.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weichsel/ZIPFoundation/e9b1917bd4d7d050e0ff4ec157b5d6e253c84385/Tests/ZIPFoundationTests/Resources/testArchiveAddUncompressedEntryProgress.png -------------------------------------------------------------------------------- /Tests/ZIPFoundationTests/Resources/testArchiveAddUncompressedEntryProgress.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weichsel/ZIPFoundation/e9b1917bd4d7d050e0ff4ec157b5d6e253c84385/Tests/ZIPFoundationTests/Resources/testArchiveAddUncompressedEntryProgress.zip -------------------------------------------------------------------------------- /Tests/ZIPFoundationTests/Resources/testArchiveIteratorErrorConditions.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weichsel/ZIPFoundation/e9b1917bd4d7d050e0ff4ec157b5d6e253c84385/Tests/ZIPFoundationTests/Resources/testArchiveIteratorErrorConditions.zip -------------------------------------------------------------------------------- /Tests/ZIPFoundationTests/Resources/testArchiveReadErrorConditions.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weichsel/ZIPFoundation/e9b1917bd4d7d050e0ff4ec157b5d6e253c84385/Tests/ZIPFoundationTests/Resources/testArchiveReadErrorConditions.zip -------------------------------------------------------------------------------- /Tests/ZIPFoundationTests/Resources/testCRC32Calculation.data: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weichsel/ZIPFoundation/e9b1917bd4d7d050e0ff4ec157b5d6e253c84385/Tests/ZIPFoundationTests/Resources/testCRC32Calculation.data -------------------------------------------------------------------------------- /Tests/ZIPFoundationTests/Resources/testCRC32Check.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weichsel/ZIPFoundation/e9b1917bd4d7d050e0ff4ec157b5d6e253c84385/Tests/ZIPFoundationTests/Resources/testCRC32Check.zip -------------------------------------------------------------------------------- /Tests/ZIPFoundationTests/Resources/testCorruptFileErrorConditions.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weichsel/ZIPFoundation/e9b1917bd4d7d050e0ff4ec157b5d6e253c84385/Tests/ZIPFoundationTests/Resources/testCorruptFileErrorConditions.zip -------------------------------------------------------------------------------- /Tests/ZIPFoundationTests/Resources/testCorruptSymbolicLinkErrorConditions.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weichsel/ZIPFoundation/e9b1917bd4d7d050e0ff4ec157b5d6e253c84385/Tests/ZIPFoundationTests/Resources/testCorruptSymbolicLinkErrorConditions.zip -------------------------------------------------------------------------------- /Tests/ZIPFoundationTests/Resources/testCreateArchiveAddCompressedEntry.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weichsel/ZIPFoundation/e9b1917bd4d7d050e0ff4ec157b5d6e253c84385/Tests/ZIPFoundationTests/Resources/testCreateArchiveAddCompressedEntry.png -------------------------------------------------------------------------------- /Tests/ZIPFoundationTests/Resources/testCreateArchiveAddCompressedEntryToMemory.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weichsel/ZIPFoundation/e9b1917bd4d7d050e0ff4ec157b5d6e253c84385/Tests/ZIPFoundationTests/Resources/testCreateArchiveAddCompressedEntryToMemory.png -------------------------------------------------------------------------------- /Tests/ZIPFoundationTests/Resources/testCreateArchiveAddEntryErrorConditions.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weichsel/ZIPFoundation/e9b1917bd4d7d050e0ff4ec157b5d6e253c84385/Tests/ZIPFoundationTests/Resources/testCreateArchiveAddEntryErrorConditions.txt -------------------------------------------------------------------------------- /Tests/ZIPFoundationTests/Resources/testCreateArchiveAddSymbolicLink.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weichsel/ZIPFoundation/e9b1917bd4d7d050e0ff4ec157b5d6e253c84385/Tests/ZIPFoundationTests/Resources/testCreateArchiveAddSymbolicLink.png -------------------------------------------------------------------------------- /Tests/ZIPFoundationTests/Resources/testCreateArchiveAddUncompressedEntry.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weichsel/ZIPFoundation/e9b1917bd4d7d050e0ff4ec157b5d6e253c84385/Tests/ZIPFoundationTests/Resources/testCreateArchiveAddUncompressedEntry.png -------------------------------------------------------------------------------- /Tests/ZIPFoundationTests/Resources/testCreateArchiveAddUncompressedEntryToMemory.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weichsel/ZIPFoundation/e9b1917bd4d7d050e0ff4ec157b5d6e253c84385/Tests/ZIPFoundationTests/Resources/testCreateArchiveAddUncompressedEntryToMemory.png -------------------------------------------------------------------------------- /Tests/ZIPFoundationTests/Resources/testCreateArchiveAddZeroSizeCompressedEntry.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weichsel/ZIPFoundation/e9b1917bd4d7d050e0ff4ec157b5d6e253c84385/Tests/ZIPFoundationTests/Resources/testCreateArchiveAddZeroSizeCompressedEntry.txt -------------------------------------------------------------------------------- /Tests/ZIPFoundationTests/Resources/testCreateArchiveAddZeroSizeUncompressedEntry.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weichsel/ZIPFoundation/e9b1917bd4d7d050e0ff4ec157b5d6e253c84385/Tests/ZIPFoundationTests/Resources/testCreateArchiveAddZeroSizeUncompressedEntry.txt -------------------------------------------------------------------------------- /Tests/ZIPFoundationTests/Resources/testCreateZIP64ArchiveAddUncompressedEntry.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weichsel/ZIPFoundation/e9b1917bd4d7d050e0ff4ec157b5d6e253c84385/Tests/ZIPFoundationTests/Resources/testCreateZIP64ArchiveAddUncompressedEntry.png -------------------------------------------------------------------------------- /Tests/ZIPFoundationTests/Resources/testDetectEntryType.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weichsel/ZIPFoundation/e9b1917bd4d7d050e0ff4ec157b5d6e253c84385/Tests/ZIPFoundationTests/Resources/testDetectEntryType.zip -------------------------------------------------------------------------------- /Tests/ZIPFoundationTests/Resources/testEntryIsCompressed.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weichsel/ZIPFoundation/e9b1917bd4d7d050e0ff4ec157b5d6e253c84385/Tests/ZIPFoundationTests/Resources/testEntryIsCompressed.zip -------------------------------------------------------------------------------- /Tests/ZIPFoundationTests/Resources/testExtractCompressedDataDescriptorArchive.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weichsel/ZIPFoundation/e9b1917bd4d7d050e0ff4ec157b5d6e253c84385/Tests/ZIPFoundationTests/Resources/testExtractCompressedDataDescriptorArchive.zip -------------------------------------------------------------------------------- /Tests/ZIPFoundationTests/Resources/testExtractCompressedEntryCancelation.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weichsel/ZIPFoundation/e9b1917bd4d7d050e0ff4ec157b5d6e253c84385/Tests/ZIPFoundationTests/Resources/testExtractCompressedEntryCancelation.zip -------------------------------------------------------------------------------- /Tests/ZIPFoundationTests/Resources/testExtractCompressedFolderEntries.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weichsel/ZIPFoundation/e9b1917bd4d7d050e0ff4ec157b5d6e253c84385/Tests/ZIPFoundationTests/Resources/testExtractCompressedFolderEntries.zip -------------------------------------------------------------------------------- /Tests/ZIPFoundationTests/Resources/testExtractCompressedFolderEntriesFromMemory.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weichsel/ZIPFoundation/e9b1917bd4d7d050e0ff4ec157b5d6e253c84385/Tests/ZIPFoundationTests/Resources/testExtractCompressedFolderEntriesFromMemory.zip -------------------------------------------------------------------------------- /Tests/ZIPFoundationTests/Resources/testExtractCompressedZIP64Entries.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weichsel/ZIPFoundation/e9b1917bd4d7d050e0ff4ec157b5d6e253c84385/Tests/ZIPFoundationTests/Resources/testExtractCompressedZIP64Entries.zip -------------------------------------------------------------------------------- /Tests/ZIPFoundationTests/Resources/testExtractEncryptedArchiveErrorConditions.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weichsel/ZIPFoundation/e9b1917bd4d7d050e0ff4ec157b5d6e253c84385/Tests/ZIPFoundationTests/Resources/testExtractEncryptedArchiveErrorConditions.zip -------------------------------------------------------------------------------- /Tests/ZIPFoundationTests/Resources/testExtractEntryWithZIP64DataDescriptor.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weichsel/ZIPFoundation/e9b1917bd4d7d050e0ff4ec157b5d6e253c84385/Tests/ZIPFoundationTests/Resources/testExtractEntryWithZIP64DataDescriptor.zip -------------------------------------------------------------------------------- /Tests/ZIPFoundationTests/Resources/testExtractErrorConditions.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weichsel/ZIPFoundation/e9b1917bd4d7d050e0ff4ec157b5d6e253c84385/Tests/ZIPFoundationTests/Resources/testExtractErrorConditions.zip -------------------------------------------------------------------------------- /Tests/ZIPFoundationTests/Resources/testExtractInvalidBufferSizeErrorConditions.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weichsel/ZIPFoundation/e9b1917bd4d7d050e0ff4ec157b5d6e253c84385/Tests/ZIPFoundationTests/Resources/testExtractInvalidBufferSizeErrorConditions.zip -------------------------------------------------------------------------------- /Tests/ZIPFoundationTests/Resources/testExtractMSDOSArchive.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weichsel/ZIPFoundation/e9b1917bd4d7d050e0ff4ec157b5d6e253c84385/Tests/ZIPFoundationTests/Resources/testExtractMSDOSArchive.zip -------------------------------------------------------------------------------- /Tests/ZIPFoundationTests/Resources/testExtractPreferredEncoding.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weichsel/ZIPFoundation/e9b1917bd4d7d050e0ff4ec157b5d6e253c84385/Tests/ZIPFoundationTests/Resources/testExtractPreferredEncoding.zip -------------------------------------------------------------------------------- /Tests/ZIPFoundationTests/Resources/testExtractUncompressedDataDescriptorArchive.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weichsel/ZIPFoundation/e9b1917bd4d7d050e0ff4ec157b5d6e253c84385/Tests/ZIPFoundationTests/Resources/testExtractUncompressedDataDescriptorArchive.zip -------------------------------------------------------------------------------- /Tests/ZIPFoundationTests/Resources/testExtractUncompressedEmptyFile.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weichsel/ZIPFoundation/e9b1917bd4d7d050e0ff4ec157b5d6e253c84385/Tests/ZIPFoundationTests/Resources/testExtractUncompressedEmptyFile.zip -------------------------------------------------------------------------------- /Tests/ZIPFoundationTests/Resources/testExtractUncompressedEntryCancelation.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weichsel/ZIPFoundation/e9b1917bd4d7d050e0ff4ec157b5d6e253c84385/Tests/ZIPFoundationTests/Resources/testExtractUncompressedEntryCancelation.zip -------------------------------------------------------------------------------- /Tests/ZIPFoundationTests/Resources/testExtractUncompressedFolderEntries.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weichsel/ZIPFoundation/e9b1917bd4d7d050e0ff4ec157b5d6e253c84385/Tests/ZIPFoundationTests/Resources/testExtractUncompressedFolderEntries.zip -------------------------------------------------------------------------------- /Tests/ZIPFoundationTests/Resources/testExtractUncompressedFolderEntriesFromMemory.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weichsel/ZIPFoundation/e9b1917bd4d7d050e0ff4ec157b5d6e253c84385/Tests/ZIPFoundationTests/Resources/testExtractUncompressedFolderEntriesFromMemory.zip -------------------------------------------------------------------------------- /Tests/ZIPFoundationTests/Resources/testExtractUncompressedZIP64Entries.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weichsel/ZIPFoundation/e9b1917bd4d7d050e0ff4ec157b5d6e253c84385/Tests/ZIPFoundationTests/Resources/testExtractUncompressedZIP64Entries.zip -------------------------------------------------------------------------------- /Tests/ZIPFoundationTests/Resources/testFileModificationDate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weichsel/ZIPFoundation/e9b1917bd4d7d050e0ff4ec157b5d6e253c84385/Tests/ZIPFoundationTests/Resources/testFileModificationDate.png -------------------------------------------------------------------------------- /Tests/ZIPFoundationTests/Resources/testInvalidCompressionMethodErrorConditions.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weichsel/ZIPFoundation/e9b1917bd4d7d050e0ff4ec157b5d6e253c84385/Tests/ZIPFoundationTests/Resources/testInvalidCompressionMethodErrorConditions.zip -------------------------------------------------------------------------------- /Tests/ZIPFoundationTests/Resources/testMemoryArchiveErrorConditions.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weichsel/ZIPFoundation/e9b1917bd4d7d050e0ff4ec157b5d6e253c84385/Tests/ZIPFoundationTests/Resources/testMemoryArchiveErrorConditions.zip -------------------------------------------------------------------------------- /Tests/ZIPFoundationTests/Resources/testPOSIXPermissions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weichsel/ZIPFoundation/e9b1917bd4d7d050e0ff4ec157b5d6e253c84385/Tests/ZIPFoundationTests/Resources/testPOSIXPermissions.png -------------------------------------------------------------------------------- /Tests/ZIPFoundationTests/Resources/testPathDelimiterTraversalAttack.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weichsel/ZIPFoundation/e9b1917bd4d7d050e0ff4ec157b5d6e253c84385/Tests/ZIPFoundationTests/Resources/testPathDelimiterTraversalAttack.zip -------------------------------------------------------------------------------- /Tests/ZIPFoundationTests/Resources/testProgressHelpers.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weichsel/ZIPFoundation/e9b1917bd4d7d050e0ff4ec157b5d6e253c84385/Tests/ZIPFoundationTests/Resources/testProgressHelpers.zip -------------------------------------------------------------------------------- /Tests/ZIPFoundationTests/Resources/testRemoveCompressedEntry.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weichsel/ZIPFoundation/e9b1917bd4d7d050e0ff4ec157b5d6e253c84385/Tests/ZIPFoundationTests/Resources/testRemoveCompressedEntry.zip -------------------------------------------------------------------------------- /Tests/ZIPFoundationTests/Resources/testRemoveDataDescriptorCompressedEntry.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weichsel/ZIPFoundation/e9b1917bd4d7d050e0ff4ec157b5d6e253c84385/Tests/ZIPFoundationTests/Resources/testRemoveDataDescriptorCompressedEntry.zip -------------------------------------------------------------------------------- /Tests/ZIPFoundationTests/Resources/testRemoveEntryErrorConditions.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weichsel/ZIPFoundation/e9b1917bd4d7d050e0ff4ec157b5d6e253c84385/Tests/ZIPFoundationTests/Resources/testRemoveEntryErrorConditions.zip -------------------------------------------------------------------------------- /Tests/ZIPFoundationTests/Resources/testRemoveEntryFromArchiveWithZIP64EOCD.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weichsel/ZIPFoundation/e9b1917bd4d7d050e0ff4ec157b5d6e253c84385/Tests/ZIPFoundationTests/Resources/testRemoveEntryFromArchiveWithZIP64EOCD.zip -------------------------------------------------------------------------------- /Tests/ZIPFoundationTests/Resources/testRemoveEntryProgress.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weichsel/ZIPFoundation/e9b1917bd4d7d050e0ff4ec157b5d6e253c84385/Tests/ZIPFoundationTests/Resources/testRemoveEntryProgress.zip -------------------------------------------------------------------------------- /Tests/ZIPFoundationTests/Resources/testRemoveEntryWithZIP64ExtendedInformation.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weichsel/ZIPFoundation/e9b1917bd4d7d050e0ff4ec157b5d6e253c84385/Tests/ZIPFoundationTests/Resources/testRemoveEntryWithZIP64ExtendedInformation.zip -------------------------------------------------------------------------------- /Tests/ZIPFoundationTests/Resources/testRemoveUncompressedEntry.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weichsel/ZIPFoundation/e9b1917bd4d7d050e0ff4ec157b5d6e253c84385/Tests/ZIPFoundationTests/Resources/testRemoveUncompressedEntry.zip -------------------------------------------------------------------------------- /Tests/ZIPFoundationTests/Resources/testRemoveZIP64EntryFromArchiveWithZIP64EOCD.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weichsel/ZIPFoundation/e9b1917bd4d7d050e0ff4ec157b5d6e253c84385/Tests/ZIPFoundationTests/Resources/testRemoveZIP64EntryFromArchiveWithZIP64EOCD.zip -------------------------------------------------------------------------------- /Tests/ZIPFoundationTests/Resources/testSimpleTraversalAttack.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weichsel/ZIPFoundation/e9b1917bd4d7d050e0ff4ec157b5d6e253c84385/Tests/ZIPFoundationTests/Resources/testSimpleTraversalAttack.zip -------------------------------------------------------------------------------- /Tests/ZIPFoundationTests/Resources/testSymlinkModificationDateTransferErrorConditions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weichsel/ZIPFoundation/e9b1917bd4d7d050e0ff4ec157b5d6e253c84385/Tests/ZIPFoundationTests/Resources/testSymlinkModificationDateTransferErrorConditions.png -------------------------------------------------------------------------------- /Tests/ZIPFoundationTests/Resources/testSymlinkPermissionsTransferErrorConditions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weichsel/ZIPFoundation/e9b1917bd4d7d050e0ff4ec157b5d6e253c84385/Tests/ZIPFoundationTests/Resources/testSymlinkPermissionsTransferErrorConditions.png -------------------------------------------------------------------------------- /Tests/ZIPFoundationTests/Resources/testUnzipCompressedZIP64Item.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weichsel/ZIPFoundation/e9b1917bd4d7d050e0ff4ec157b5d6e253c84385/Tests/ZIPFoundationTests/Resources/testUnzipCompressedZIP64Item.zip -------------------------------------------------------------------------------- /Tests/ZIPFoundationTests/Resources/testUnzipItem.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weichsel/ZIPFoundation/e9b1917bd4d7d050e0ff4ec157b5d6e253c84385/Tests/ZIPFoundationTests/Resources/testUnzipItem.zip -------------------------------------------------------------------------------- /Tests/ZIPFoundationTests/Resources/testUnzipItemErrorConditions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weichsel/ZIPFoundation/e9b1917bd4d7d050e0ff4ec157b5d6e253c84385/Tests/ZIPFoundationTests/Resources/testUnzipItemErrorConditions.png -------------------------------------------------------------------------------- /Tests/ZIPFoundationTests/Resources/testUnzipItemErrorConditions.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weichsel/ZIPFoundation/e9b1917bd4d7d050e0ff4ec157b5d6e253c84385/Tests/ZIPFoundationTests/Resources/testUnzipItemErrorConditions.zip -------------------------------------------------------------------------------- /Tests/ZIPFoundationTests/Resources/testUnzipItemProgress.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weichsel/ZIPFoundation/e9b1917bd4d7d050e0ff4ec157b5d6e253c84385/Tests/ZIPFoundationTests/Resources/testUnzipItemProgress.zip -------------------------------------------------------------------------------- /Tests/ZIPFoundationTests/Resources/testUnzipItemWithPreferredEncoding.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weichsel/ZIPFoundation/e9b1917bd4d7d050e0ff4ec157b5d6e253c84385/Tests/ZIPFoundationTests/Resources/testUnzipItemWithPreferredEncoding.zip -------------------------------------------------------------------------------- /Tests/ZIPFoundationTests/Resources/testUnzipItemWithZIP64DataDescriptor.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weichsel/ZIPFoundation/e9b1917bd4d7d050e0ff4ec157b5d6e253c84385/Tests/ZIPFoundationTests/Resources/testUnzipItemWithZIP64DataDescriptor.zip -------------------------------------------------------------------------------- /Tests/ZIPFoundationTests/Resources/testUnzipUncompressedZIP64Item.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weichsel/ZIPFoundation/e9b1917bd4d7d050e0ff4ec157b5d6e253c84385/Tests/ZIPFoundationTests/Resources/testUnzipUncompressedZIP64Item.zip -------------------------------------------------------------------------------- /Tests/ZIPFoundationTests/Resources/testUnzipUncontainedSymlink.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weichsel/ZIPFoundation/e9b1917bd4d7d050e0ff4ec157b5d6e253c84385/Tests/ZIPFoundationTests/Resources/testUnzipUncontainedSymlink.zip -------------------------------------------------------------------------------- /Tests/ZIPFoundationTests/Resources/testUpdateArchiveRemoveUncompressedEntryFromMemory.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weichsel/ZIPFoundation/e9b1917bd4d7d050e0ff4ec157b5d6e253c84385/Tests/ZIPFoundationTests/Resources/testUpdateArchiveRemoveUncompressedEntryFromMemory.zip -------------------------------------------------------------------------------- /Tests/ZIPFoundationTests/Resources/testZIP64ArchiveAddEntryProgress.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weichsel/ZIPFoundation/e9b1917bd4d7d050e0ff4ec157b5d6e253c84385/Tests/ZIPFoundationTests/Resources/testZIP64ArchiveAddEntryProgress.png -------------------------------------------------------------------------------- /Tests/ZIPFoundationTests/Resources/testZIP64ArchiveAddEntryProgress.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weichsel/ZIPFoundation/e9b1917bd4d7d050e0ff4ec157b5d6e253c84385/Tests/ZIPFoundationTests/Resources/testZIP64ArchiveAddEntryProgress.zip -------------------------------------------------------------------------------- /Tests/ZIPFoundationTests/Resources/testZipCompressedZIP64Item.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weichsel/ZIPFoundation/e9b1917bd4d7d050e0ff4ec157b5d6e253c84385/Tests/ZIPFoundationTests/Resources/testZipCompressedZIP64Item.png -------------------------------------------------------------------------------- /Tests/ZIPFoundationTests/Resources/testZipItem.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weichsel/ZIPFoundation/e9b1917bd4d7d050e0ff4ec157b5d6e253c84385/Tests/ZIPFoundationTests/Resources/testZipItem.png -------------------------------------------------------------------------------- /Tests/ZIPFoundationTests/Resources/testZipItemProgress.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weichsel/ZIPFoundation/e9b1917bd4d7d050e0ff4ec157b5d6e253c84385/Tests/ZIPFoundationTests/Resources/testZipItemProgress.png -------------------------------------------------------------------------------- /Tests/ZIPFoundationTests/Resources/testZipUncompressedZIP64Item.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weichsel/ZIPFoundation/e9b1917bd4d7d050e0ff4ec157b5d6e253c84385/Tests/ZIPFoundationTests/Resources/testZipUncompressedZIP64Item.png -------------------------------------------------------------------------------- /Tests/ZIPFoundationTests/ZIPFoundationArchiveTests+ZIP64.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ZIPFoundationArchiveTests+ZIP64.swift 3 | // ZIPFoundation 4 | // 5 | // Copyright © 2017-2024 Thomas Zoechling, https://www.peakstep.com and the ZIP Foundation project authors. 6 | // Released under the MIT License. 7 | // 8 | // See https://github.com/weichsel/ZIPFoundation/blob/master/LICENSE for license information. 9 | // 10 | 11 | import XCTest 12 | @testable import ZIPFoundation 13 | 14 | extension ZIPFoundationTests { 15 | 16 | func testArchiveZIP64EOCDRecord() { 17 | let eocdRecordBytes: [UInt8] = [0x50, 0x4b, 0x06, 0x06, 0x2c, 0x00, 0x00, 0x00, 18 | 0x00, 0x00, 0x00, 0x00, 0x2d, 0x00, 0x03, 0x15, 19 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 20 | 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 21 | 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 22 | 0x4c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 23 | 0x5a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00] 24 | let zip64EOCDRecord = Archive.ZIP64EndOfCentralDirectoryRecord(data: Data(eocdRecordBytes), 25 | additionalDataProvider: {_ -> Data in 26 | return Data() }) 27 | XCTAssertNotNil(zip64EOCDRecord) 28 | } 29 | 30 | func testArchiveInvalidZIP64EOCERecordConditions() { 31 | let emptyEOCDRecord = Archive.ZIP64EndOfCentralDirectoryRecord(data: Data(), 32 | additionalDataProvider: {_ -> Data in 33 | return Data() }) 34 | XCTAssertNil(emptyEOCDRecord) 35 | let eocdRecordIncludingExtraByte: [UInt8] = [0x50, 0x4b, 0x06, 0x06, 0x2c, 0x00, 0x00, 0x00, 36 | 0x00, 0x00, 0x00, 0x00, 0x2d, 0x00, 0x03, 0x15, 37 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 38 | 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 39 | 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 40 | 0x4c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 41 | 0x5a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 42 | 0x00, 0x00] 43 | let invalidEOCDRecord = Archive.ZIP64EndOfCentralDirectoryRecord(data: Data(eocdRecordIncludingExtraByte), 44 | additionalDataProvider: {_ -> Data in 45 | return Data() }) 46 | XCTAssertNil(invalidEOCDRecord) 47 | let eocdRecordMissingByte: [UInt8] = [0x50, 0x4b, 0x06, 0x06, 0x2c, 0x00, 0x00, 0x00, 48 | 0x00, 0x00, 0x00, 0x00, 0x2d, 0x00, 0x03, 0x15, 49 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 50 | 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00] 51 | let invalidEOCDRecord2 = Archive.ZIP64EndOfCentralDirectoryRecord(data: Data(eocdRecordMissingByte), 52 | additionalDataProvider: {_ -> Data in 53 | return Data() }) 54 | XCTAssertNil(invalidEOCDRecord2) 55 | let eocdRecordWithWrongVersion: [UInt8] = [0x50, 0x4b, 0x06, 0x06, 0x2c, 0x00, 0x00, 0x00, 56 | 0x00, 0x00, 0x00, 0x00, 0x1e, 0x03, 0x14, 0x00, 57 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 58 | 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 59 | 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 60 | 0x4c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 61 | 0x5a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00] 62 | let invalidEOCDRecord3 = Archive.ZIP64EndOfCentralDirectoryRecord(data: Data(eocdRecordWithWrongVersion), 63 | additionalDataProvider: {_ -> Data in 64 | return Data() }) 65 | XCTAssertNil(invalidEOCDRecord3) 66 | } 67 | 68 | func testArchiveZIP64EOCDLocator() { 69 | let eocdLocatorBytes: [UInt8] = [0x50, 0x4b, 0x06, 0x07, 0x00, 0x00, 0x00, 0x00, 70 | 0x9a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 71 | 0x01, 0x00, 0x00, 0x00] 72 | let zip64EOCDRecord = Archive.ZIP64EndOfCentralDirectoryLocator(data: Data(eocdLocatorBytes), 73 | additionalDataProvider: {_ -> Data in 74 | return Data() }) 75 | XCTAssertNotNil(zip64EOCDRecord) 76 | } 77 | 78 | func testArchiveInvalidZIP64EOCDLocatorConditions() { 79 | let emptyEOCDLocator = Archive.ZIP64EndOfCentralDirectoryLocator(data: Data(), 80 | additionalDataProvider: {_ -> Data in 81 | return Data() }) 82 | XCTAssertNil(emptyEOCDLocator) 83 | let eocdLocatorIncludingExtraByte: [UInt8] = [0x50, 0x4b, 0x06, 0x07, 0x00, 0x00, 0x00, 0x00, 84 | 0x9a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 85 | 0x01, 0x00, 0x00, 0x00, 0x00] 86 | let invalidEOCDLocator = Archive.ZIP64EndOfCentralDirectoryLocator(data: Data(eocdLocatorIncludingExtraByte), 87 | additionalDataProvider: {_ -> Data in 88 | return Data() }) 89 | XCTAssertNil(invalidEOCDLocator) 90 | let eocdLocatorMissingByte: [UInt8] = [0x50, 0x4b, 0x06, 0x07, 0x00, 0x00, 0x00, 0x00, 91 | 0x9a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00] 92 | let invalidEOCDLocator2 = Archive.ZIP64EndOfCentralDirectoryLocator(data: Data(eocdLocatorMissingByte), 93 | additionalDataProvider: {_ -> Data in 94 | return Data() }) 95 | XCTAssertNil(invalidEOCDLocator2) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /Tests/ZIPFoundationTests/ZIPFoundationArchiveTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ZIPFoundationErrorConditionTests.swift 3 | // ZIPFoundation 4 | // 5 | // Copyright © 2017-2024 Thomas Zoechling, https://www.peakstep.com and the ZIP Foundation project authors. 6 | // Released under the MIT License. 7 | // 8 | // See https://github.com/weichsel/ZIPFoundation/blob/master/LICENSE for license information. 9 | // 10 | 11 | import XCTest 12 | @testable import ZIPFoundation 13 | 14 | extension ZIPFoundationTests { 15 | 16 | func testArchiveInvalidEOCDRecordConditions() { 17 | let emptyECDR = Archive.EndOfCentralDirectoryRecord(data: Data(), 18 | additionalDataProvider: {_ -> Data in 19 | return Data() }) 20 | XCTAssertNil(emptyECDR) 21 | let invalidECDRData = Data(count: 22) 22 | let invalidECDR = Archive.EndOfCentralDirectoryRecord(data: invalidECDRData, 23 | additionalDataProvider: {_ -> Data in 24 | return Data() }) 25 | XCTAssertNil(invalidECDR) 26 | } 27 | 28 | func testDirectoryCreationHelperMethods() { 29 | let processInfo = ProcessInfo.processInfo 30 | var nestedURL = ZIPFoundationTests.tempZipDirectoryURL 31 | nestedURL.appendPathComponent(processInfo.globallyUniqueString) 32 | nestedURL.appendPathComponent(processInfo.globallyUniqueString) 33 | do { 34 | try FileManager().createParentDirectoryStructure(for: nestedURL) 35 | } catch { XCTFail("Failed to create parent directory.") } 36 | } 37 | 38 | func testTemporaryReplacementDirectoryURL() throws { 39 | let archive = self.archive(for: #function, mode: .create) 40 | var tempURLs = Set() 41 | defer { for url in tempURLs { try? FileManager.default.removeItem(at: url) } } 42 | // We choose 2000 temp directories to test workaround for http://openradar.appspot.com/50553219 43 | for _ in 1...2000 { 44 | let tempDir = URL.temporaryReplacementDirectoryURL(for: archive) 45 | XCTAssertFalse(tempURLs.contains(tempDir), "Temp directory URL should be unique. \(tempDir)") 46 | tempURLs.insert(tempDir) 47 | } 48 | 49 | #if swift(>=5.0) 50 | // Also cover the fallback codepath in the helper method to generate a unique temp URL. 51 | // In-memory archives have no filesystem representation and therefore don't need a per-volume 52 | // temp URL. 53 | let memoryArchive = try Archive(data: Data(), accessMode: .create) 54 | let memoryTempURL = URL.temporaryReplacementDirectoryURL(for: memoryArchive) 55 | XCTAssertNotNil(memoryTempURL, "Temporary URL creation for in-memory archive failed.") 56 | #endif 57 | } 58 | } 59 | 60 | extension XCTestCase { 61 | 62 | func XCTAssertSwiftError(_ expression: @autoclosure () throws -> T, 63 | throws error: E, 64 | in file: StaticString = #file, 65 | line: UInt = #line) { 66 | var thrownError: Error? 67 | XCTAssertThrowsError(try expression(), file: file, line: line) { thrownError = $0} 68 | XCTAssertTrue(thrownError is E, "Unexpected error type: \(type(of: thrownError))", file: file, line: line) 69 | XCTAssertEqual(thrownError as? E, error, file: file, line: line) 70 | } 71 | 72 | func XCTAssertPOSIXError(_ expression: @autoclosure () throws -> T, 73 | throwsErrorWithCode code: POSIXError.Code, 74 | in file: StaticString = #file, 75 | line: UInt = #line) { 76 | var thrownError: POSIXError? 77 | XCTAssertThrowsError(try expression(), file: file, line: line) { thrownError = $0 as? POSIXError } 78 | XCTAssertNotNil(thrownError, file: file, line: line) 79 | XCTAssertTrue(thrownError?.code == code, file: file, line: line) 80 | } 81 | 82 | func XCTAssertCocoaError(_ expression: @autoclosure () throws -> T, 83 | throwsErrorWithCode code: CocoaError.Code, 84 | in file: StaticString = #file, 85 | line: UInt = #line) { 86 | var thrownError: CocoaError? 87 | #if os(macOS) || os(iOS) || os(tvOS) || os(visionOS) || os(watchOS) 88 | XCTAssertThrowsError(try expression(), file: file, line: line) { thrownError = $0 as? CocoaError} 89 | #else 90 | XCTAssertThrowsError(try expression(), file: file, line: line) { 91 | // For unknown reasons, some errors in the `NSCocoaErrorDomain` can't be cast to `CocoaError` on Linux. 92 | // We manually re-create them here as `CocoaError` to work around this. 93 | thrownError = CocoaError(.init(rawValue: ($0 as NSError).code)) 94 | } 95 | #endif 96 | XCTAssertNotNil(thrownError, file: file, line: line) 97 | XCTAssertTrue(thrownError?.code == code, file: file, line: line) 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /Tests/ZIPFoundationTests/ZIPFoundationDataSerializationTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ZIPFoundationDataSerializationTests.swift 3 | // ZIPFoundation 4 | // 5 | // Copyright © 2017-2024 Thomas Zoechling, https://www.peakstep.com and the ZIP Foundation project authors. 6 | // Released under the MIT License. 7 | // 8 | // See https://github.com/weichsel/ZIPFoundation/blob/master/LICENSE for license information. 9 | // 10 | 11 | import XCTest 12 | @testable import ZIPFoundation 13 | 14 | extension ZIPFoundationTests { 15 | 16 | func testReadStructureErrorConditions() { 17 | let processInfo = ProcessInfo.processInfo 18 | let fileManager = FileManager() 19 | var fileURL = ZIPFoundationTests.tempZipDirectoryURL 20 | fileURL.appendPathComponent(processInfo.globallyUniqueString) 21 | let result = fileManager.createFile(atPath: fileURL.path, contents: Data(), 22 | attributes: nil) 23 | XCTAssert(result == true) 24 | let fileSystemRepresentation = fileManager.fileSystemRepresentation(withPath: fileURL.path) 25 | let file: FILEPointer = fopen(fileSystemRepresentation, "rb") 26 | // Close the file to exercise the error path during readStructure that deals with 27 | // unreadable file data. 28 | fclose(file) 29 | let centralDirectoryStructure: Entry.CentralDirectoryStructure? = Data.readStruct(from: file, at: 0) 30 | XCTAssertNil(centralDirectoryStructure) 31 | } 32 | 33 | func testReadChunkErrorConditions() { 34 | let processInfo = ProcessInfo.processInfo 35 | let fileManager = FileManager() 36 | var fileURL = ZIPFoundationTests.tempZipDirectoryURL 37 | fileURL.appendPathComponent(processInfo.globallyUniqueString) 38 | let result = fileManager.createFile(atPath: fileURL.path, contents: Data(), 39 | attributes: nil) 40 | XCTAssert(result == true) 41 | let fileSystemRepresentation = fileManager.fileSystemRepresentation(withPath: fileURL.path) 42 | let file: FILEPointer = fopen(fileSystemRepresentation, "rb") 43 | // Close the file to exercise the error path during readChunk that deals with 44 | // unreadable file data. 45 | fclose(file) 46 | XCTAssertSwiftError(try Data.readChunk(of: 10, from: file), 47 | throws: Data.DataError.unreadableFile) 48 | } 49 | 50 | func testWriteChunkErrorConditions() { 51 | let processInfo = ProcessInfo.processInfo 52 | let fileManager = FileManager() 53 | var fileURL = ZIPFoundationTests.tempZipDirectoryURL 54 | fileURL.appendPathComponent(processInfo.globallyUniqueString) 55 | let result = fileManager.createFile(atPath: fileURL.path, contents: Data(), 56 | attributes: nil) 57 | XCTAssert(result == true) 58 | let fileSystemRepresentation = fileManager.fileSystemRepresentation(withPath: fileURL.path) 59 | let file: FILEPointer = fopen(fileSystemRepresentation, "rb") 60 | // Close the file to exercise the error path during writeChunk that deals with 61 | // unwritable files. 62 | fclose(file) 63 | XCTAssertSwiftError(try Data.write(chunk: Data(count: 10), to: file), 64 | throws: Data.DataError.unwritableFile) 65 | } 66 | 67 | func testCRC32Calculation() { 68 | let dataURL = self.resourceURL(for: #function, pathExtension: "data") 69 | let data = (try? Data.init(contentsOf: dataURL)) ?? Data() 70 | XCTAssertEqual(data.crc32(checksum: 0), 1400077496) 71 | #if canImport(zlib) 72 | XCTAssertEqual(data.crc32(checksum: 0), data.builtInCRC32(checksum: 0)) 73 | #endif 74 | } 75 | 76 | func testWriteLargeChunk() { 77 | let processInfo = ProcessInfo.processInfo 78 | let fileManager = FileManager() 79 | var fileURL = ZIPFoundationTests.tempZipDirectoryURL 80 | fileURL.appendPathComponent(processInfo.globallyUniqueString) 81 | let result = fileManager.createFile(atPath: fileURL.path, contents: Data(), 82 | attributes: nil) 83 | XCTAssert(result == true) 84 | let fileSystemRepresentation = fileManager.fileSystemRepresentation(withPath: fileURL.path) 85 | let file: FILEPointer = fopen(fileSystemRepresentation, "rb+") 86 | let data = Data.makeRandomData(size: 1024) 87 | do { 88 | let writtenSize = try Data.writeLargeChunk(data, size: 1024, bufferSize: 256, to: file) 89 | XCTAssertEqual(writtenSize, 1024) 90 | fseeko(file, 0, SEEK_SET) 91 | let writtenData = try Data.readChunk(of: Int(writtenSize), from: file) 92 | XCTAssertEqual(writtenData, data) 93 | } catch { 94 | XCTFail("Unexpected error while testing to write into a closed file.") 95 | } 96 | } 97 | 98 | func testWriteLargeChunkErrorConditions() { 99 | let processInfo = ProcessInfo.processInfo 100 | let fileManager = FileManager() 101 | var fileURL = ZIPFoundationTests.tempZipDirectoryURL 102 | fileURL.appendPathComponent(processInfo.globallyUniqueString) 103 | let result = fileManager.createFile(atPath: fileURL.path, contents: Data(), 104 | attributes: nil) 105 | XCTAssert(result == true) 106 | let fileSystemRepresentation = fileManager.fileSystemRepresentation(withPath: fileURL.path) 107 | let file: FILEPointer = fopen(fileSystemRepresentation, "rb") 108 | let data = Data.makeRandomData(size: 1024) 109 | // Close the file to exercise the error path during writeChunk that deals with 110 | // unwritable files. 111 | fclose(file) 112 | XCTAssertSwiftError(try Data.writeLargeChunk(data, size: 1024, bufferSize: 256, to: file), 113 | throws: Data.DataError.unwritableFile) 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /Tests/ZIPFoundationTests/ZIPFoundationEntryTests+ZIP64.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ZIPFoundationEntryTests+ZIP64.swift 3 | // ZIPFoundation 4 | // 5 | // Copyright © 2017-2024 Thomas Zoechling, https://www.peakstep.com and the ZIP Foundation project authors. 6 | // Released under the MIT License. 7 | // 8 | // See https://github.com/weichsel/ZIPFoundation/blob/master/LICENSE for license information. 9 | // 10 | 11 | import XCTest 12 | @testable import ZIPFoundation 13 | 14 | extension ZIPFoundationTests { 15 | 16 | typealias ZIP64ExtendedInformation = Entry.ZIP64ExtendedInformation 17 | 18 | func testEntryZIP64ExtraField() { 19 | let extraFieldBytesIncludingSizeFields: [UInt8] = [0x01, 0x00, 0x10, 0x00, 0x0a, 0x00, 0x00, 0x00, 20 | 0x00, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x00, 0x00, 21 | 0x00, 0x00, 0x00, 0x00] 22 | let zip64ExtraField1 = ZIP64ExtendedInformation(data: Data(extraFieldBytesIncludingSizeFields), 23 | fields: [.compressedSize, .uncompressedSize]) 24 | XCTAssertNotNil(zip64ExtraField1) 25 | let extraFieldBytesIncludingOtherFields: [UInt8] = [0x01, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 26 | 0x00, 0x00, 0x00, 0x0a, 0x0, 0x00, 0x00, 0x0a] 27 | let zip64ExtraField2 = ZIP64ExtendedInformation(data: Data(extraFieldBytesIncludingOtherFields), 28 | fields: [.relativeOffsetOfLocalHeader, .diskNumberStart]) 29 | XCTAssertNotNil(zip64ExtraField2) 30 | 31 | let updatedZIP64ExtraField1 = ZIP64ExtendedInformation(zip64ExtendedInformation: zip64ExtraField1, offset: 10) 32 | XCTAssertEqual(updatedZIP64ExtraField1?.relativeOffsetOfLocalHeader, 33 | zip64ExtraField1?.relativeOffsetOfLocalHeader) 34 | XCTAssertEqual(updatedZIP64ExtraField1?.dataSize, zip64ExtraField1?.dataSize) 35 | let updatedZIP64ExtraField2 = ZIP64ExtendedInformation(zip64ExtendedInformation: zip64ExtraField2, 36 | offset: UInt64(UInt32.max) + 1) 37 | XCTAssertEqual(updatedZIP64ExtraField2?.relativeOffsetOfLocalHeader, UInt64(UInt32.max) + 1) 38 | XCTAssertEqual(updatedZIP64ExtraField2?.dataSize, zip64ExtraField2?.dataSize) 39 | } 40 | 41 | func testEntryZIP64FieldOnlyHasUncompressedSize() { 42 | // Including both original and compressed file size fields. (at least in the Local header) 43 | let expectedZIP64ExtraFieldBytes: [UInt8] = [0x01, 0x00, 0x10, 0x00, 0x0a, 0x00, 0x00, 0x00, 44 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 45 | 0x00, 0x00, 0x00, 0x00] 46 | let zip64Field = ZIP64ExtendedInformation(dataSize: 16, 47 | uncompressedSize: 10, 48 | compressedSize: 0, 49 | relativeOffsetOfLocalHeader: 0, 50 | diskNumberStart: 0) 51 | XCTAssertEqual(zip64Field.data, Data(expectedZIP64ExtraFieldBytes)) 52 | } 53 | 54 | func testEntryZIP64FieldIncludingDiskNumberStart() { 55 | let expectedZIP64ExtraFieldBytes: [UInt8] = [0x01, 0x00, 0x04, 0x00, 0x0a, 0x00, 0x00, 0x00] 56 | let zip64Field = ZIP64ExtendedInformation(dataSize: 4, 57 | uncompressedSize: 0, 58 | compressedSize: 0, 59 | relativeOffsetOfLocalHeader: 0, 60 | diskNumberStart: 10) 61 | XCTAssertEqual(zip64Field.data, Data(expectedZIP64ExtraFieldBytes)) 62 | } 63 | 64 | func testEntryValidZIP64DataDescriptor() { 65 | let zip64DDBytes: [UInt8] = [0x50, 0x4b, 0x07, 0x08, 0x00, 0x00, 0x00, 0x00, 66 | 0x0a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 67 | 0x0a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00] 68 | let zip64DataDescriptor = Entry.ZIP64DataDescriptor(data: Data(zip64DDBytes), 69 | additionalDataProvider: {_ -> Data in 70 | return Data() }) 71 | XCTAssertEqual(zip64DataDescriptor?.uncompressedSize, 10) 72 | XCTAssertEqual(zip64DataDescriptor?.compressedSize, 10) 73 | } 74 | 75 | func testEntryWithZIP64ExtraField() { 76 | // Central Directory 77 | let extraFieldBytesIncludingSizeFields: [UInt8] = [0x01, 0x00, 0x10, 0x00, 0x0a, 0x00, 0x00, 0x00, 78 | 0x00, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x00, 0x00, 79 | 0x00, 0x00, 0x00, 0x00] 80 | let cdsBytes: [UInt8] = [0x50, 0x4b, 0x01, 0x02, 0x1e, 0x15, 0x2d, 0x00, 81 | 0x08, 0x08, 0x08, 0x00, 0xab, 0x85, 0x77, 0x47, 82 | 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 83 | 0xff, 0xff, 0xff, 0xff, 0x01, 0x00, 0x14, 0x00, 84 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 85 | 0xb0, 0x11, 0x00, 0x00, 0x00, 0x00] 86 | guard let cds = Entry.CentralDirectoryStructure(data: Data(cdsBytes), 87 | additionalDataProvider: { count -> Data in 88 | let name = Data("/".utf8) 89 | let extra = name + Data(extraFieldBytesIncludingSizeFields) 90 | XCTAssert(count == extra.count) 91 | return extra 92 | }) else { 93 | XCTFail("Failed to read central directory structure."); return 94 | } 95 | XCTAssertNotNil(cds.extraFields) 96 | XCTAssertEqual(cds.effectiveCompressedSize, 10) 97 | XCTAssertEqual(cds.effectiveUncompressedSize, 10) 98 | // Entry 99 | let lfhBytes: [UInt8] = [0x50, 0x4b, 0x03, 0x04, 0x14, 0x00, 0x08, 0x08, 100 | 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 101 | 0x00, 0x00, 0x0a, 0x00, 0x00, 0x00, 0x0a, 0x00, 102 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00] 103 | guard let lfh = Entry.LocalFileHeader(data: Data(lfhBytes), 104 | additionalDataProvider: { _ -> Data in 105 | return Data() 106 | }) else { 107 | XCTFail("Failed to read local file header."); return 108 | } 109 | guard let entry = Entry(centralDirectoryStructure: cds, localFileHeader: lfh) else { 110 | XCTFail("Failed to create test entry."); return 111 | } 112 | XCTAssertNotNil(entry.zip64ExtendedInformation) 113 | XCTAssertEqual(entry.compressedSize, 10) 114 | XCTAssertEqual(entry.uncompressedSize, 10) 115 | } 116 | 117 | func testEntryInvalidZIP64ExtraFieldErrorConditions() { 118 | let emptyExtraField = ZIP64ExtendedInformation(data: Data(), 119 | fields: [.compressedSize]) 120 | XCTAssertNil(emptyExtraField) 121 | let extraFieldBytesIncludingExtraByte: [UInt8] = [0x01, 0x00, 0x10, 0x00, 0x0a, 0x00, 0x00, 0x00, 122 | 0x00, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x00, 0x00, 123 | 0x00, 0x00, 0x00, 0x00, 0x00] 124 | let invalidExtraField1 = ZIP64ExtendedInformation(data: Data(extraFieldBytesIncludingExtraByte), 125 | fields: [.compressedSize, .uncompressedSize]) 126 | XCTAssertNil(invalidExtraField1) 127 | let extraFieldBytesMissingByte: [UInt8] = [0x01, 0x00, 0x10, 0x00, 0x0a, 0x00, 0x00, 0x00, 128 | 0x00, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x00, 0x00, 129 | 0x00, 0x00, 0x00] 130 | let invalidExtraField2 = ZIP64ExtendedInformation(data: Data(extraFieldBytesMissingByte), 131 | fields: [.compressedSize, .uncompressedSize]) 132 | XCTAssertNil(invalidExtraField2) 133 | let extraFieldBytesWithWrongFields: [UInt8] = [0x01, 0x00, 0x10, 0x00, 0x0a, 0x00, 0x00, 0x00, 134 | 0x00, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x00, 0x00, 135 | 0x00, 0x00, 0x00, 0x00] 136 | let invalidExtraField3 = ZIP64ExtendedInformation(data: Data(extraFieldBytesWithWrongFields), 137 | fields: [.compressedSize]) 138 | XCTAssertNil(invalidExtraField3) 139 | let extraFieldBytesWithWrongFieldLength: [UInt8] = [0x01, 0x00, 0x10, 0x00, 0x0a, 0x00, 0x00, 0x00, 140 | 0x00, 0x00, 0x00, 0x00] 141 | let invalidExtraField4 = ZIP64ExtendedInformation(data: Data(extraFieldBytesWithWrongFieldLength), 142 | fields: [.diskNumberStart]) 143 | XCTAssertNil(invalidExtraField4) 144 | } 145 | 146 | func testEntryScanForZIP64Field() { 147 | let extraFieldBytesWithZIP64Field: [UInt8] = [0x01, 0x00, 0x10, 0x00, 0x0a, 0x00, 0x00, 0x00, 148 | 0x00, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x00, 0x00, 149 | 0x00, 0x00, 0x00, 0x00] 150 | let zip64Field1 = ZIP64ExtendedInformation.scanForZIP64Field(in: Data(extraFieldBytesWithZIP64Field), 151 | fields: [.uncompressedSize, .compressedSize]) 152 | XCTAssertNotNil(zip64Field1) 153 | let extraFieldBytesWithZIP64Field2: [UInt8] = [0x09, 0x00, 0x04, 0x00, 0x0a, 0x00, 0x00, 0x00, 154 | 0x01, 0x00, 0x10, 0x00, 0x0a, 0x00, 0x00, 0x00, 155 | 0x00, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x00, 0x00, 156 | 0x00, 0x00, 0x00, 0x00] 157 | let zip64Field2 = ZIP64ExtendedInformation.scanForZIP64Field(in: Data(extraFieldBytesWithZIP64Field2), 158 | fields: [.uncompressedSize, .compressedSize]) 159 | XCTAssertNotNil(zip64Field2) 160 | } 161 | 162 | func testEntryScanForZIP64FieldErrorConditions() { 163 | let emptyExtraField = ZIP64ExtendedInformation.scanForZIP64Field(in: Data(), fields: []) 164 | XCTAssertNil(emptyExtraField) 165 | let extraFieldBytesWithoutZIP64Field: [UInt8] = [0x09, 0x00, 0x10, 0x00, 0x0a, 0x00, 0x00, 0x00, 166 | 0x00, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x00, 0x00, 167 | 0x00, 0x00, 0x00, 0x00] 168 | let noZIP64Field = ZIP64ExtendedInformation.scanForZIP64Field(in: Data(extraFieldBytesWithoutZIP64Field), 169 | fields: []) 170 | XCTAssertNil(noZIP64Field) 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /Tests/ZIPFoundationTests/ZIPFoundationEntryTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ZIPFoundationEntryTests.swift 3 | // ZIPFoundation 4 | // 5 | // Copyright © 2017-2024 Thomas Zoechling, https://www.peakstep.com and the ZIP Foundation project authors. 6 | // Released under the MIT License. 7 | // 8 | // See https://github.com/weichsel/ZIPFoundation/blob/master/LICENSE for license information. 9 | // 10 | 11 | import XCTest 12 | @testable import ZIPFoundation 13 | 14 | extension ZIPFoundationTests { 15 | 16 | func testEntryWrongDataLengthErrorConditions() { 17 | let emptyCDS = Entry.CentralDirectoryStructure(data: Data(), 18 | additionalDataProvider: {_ -> Data in 19 | return Data() }) 20 | XCTAssertNil(emptyCDS) 21 | let emptyLFH = Entry.LocalFileHeader(data: Data(), 22 | additionalDataProvider: {_ -> Data in 23 | return Data() }) 24 | XCTAssertNil(emptyLFH) 25 | let emptyDD = Entry.DefaultDataDescriptor(data: Data(), 26 | additionalDataProvider: {_ -> Data in 27 | return Data() }) 28 | XCTAssertNil(emptyDD) 29 | let emptyZIP64DD = Entry.ZIP64DataDescriptor(data: Data(), 30 | additionalDataProvider: {_ -> Data in 31 | return Data() }) 32 | XCTAssertNil(emptyZIP64DD) 33 | } 34 | 35 | func testEntryInvalidSignatureErrorConditions() { 36 | let invalidCDS = Entry.CentralDirectoryStructure(data: Data(count: Entry.CentralDirectoryStructure.size), 37 | additionalDataProvider: {_ -> Data in 38 | return Data() }) 39 | XCTAssertNil(invalidCDS) 40 | let invalidLFH = Entry.LocalFileHeader(data: Data(count: Entry.LocalFileHeader.size), 41 | additionalDataProvider: {_ -> Data in 42 | return Data() }) 43 | XCTAssertNil(invalidLFH) 44 | } 45 | 46 | func testEntryInvalidAdditionalDataErrorConditions() { 47 | let cdsBytes: [UInt8] = [0x50, 0x4b, 0x01, 0x02, 0x1e, 0x03, 0x14, 0x00, 48 | 0x08, 0x00, 0x08, 0x00, 0xab, 0x85, 0x77, 0x47, 49 | 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 50 | 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 51 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 52 | 0xb0, 0x11, 0x00, 0x00, 0x00, 0x00] 53 | let invalidAddtionalDataCDS = Entry.CentralDirectoryStructure(data: Data(cdsBytes)) { _ -> Data in 54 | return Data() 55 | } 56 | XCTAssertNil(invalidAddtionalDataCDS) 57 | let lfhBytes: [UInt8] = [0x50, 0x4b, 0x03, 0x04, 0x14, 0x00, 0x08, 0x00, 58 | 0x08, 0x00, 0xab, 0x85, 0x77, 0x47, 0x00, 0x00, 59 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 60 | 0x00, 0x00, 0x01, 0x00, 0x00, 0x00] 61 | let invalidAddtionalDataLFH = Entry.LocalFileHeader(data: Data(lfhBytes)) { _ -> Data in 62 | return Data() 63 | } 64 | XCTAssertNil(invalidAddtionalDataLFH) 65 | let cds2Bytes: [UInt8] = [0x50, 0x4b, 0x01, 0x02, 0x1e, 0x03, 0x14, 0x00, 66 | 0x08, 0x08, 0x08, 0x00, 0xab, 0x85, 0x77, 0x47, 67 | 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 68 | 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 69 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 70 | 0xb0, 0x11, 0x00, 0x00, 0x00, 0x00] 71 | let cds2 = Entry.CentralDirectoryStructure(data: Data(cds2Bytes)) { _ -> Data in 72 | throw AdditionalDataError.encodingError 73 | } 74 | XCTAssertNil(cds2) 75 | let lfhBytes2: [UInt8] = [0x50, 0x4b, 0x03, 0x04, 0x14, 0x00, 0x08, 0x08, 76 | 0x08, 0x00, 0xab, 0x85, 0x77, 0x47, 0x00, 0x00, 77 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 78 | 0x00, 0x00, 0x01, 0x00, 0x00, 0x00] 79 | let lfh2 = Entry.LocalFileHeader(data: Data(lfhBytes2)) { _ -> Data in 80 | throw AdditionalDataError.encodingError 81 | } 82 | XCTAssertNil(lfh2) 83 | } 84 | 85 | func testEntryInvalidPathEncodingErrorConditions() { 86 | // Use bytes that are invalid code units in UTF-8 to trigger failed initialization 87 | // of the path String. 88 | let invalidPathBytes: [UInt8] = [0xFF] 89 | let cdsBytes: [UInt8] = [0x50, 0x4b, 0x01, 0x02, 0x1e, 0x03, 0x14, 0x00, 90 | 0x08, 0x08, 0x08, 0x00, 0xab, 0x85, 0x77, 0x47, 91 | 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 92 | 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 93 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 94 | 0xb0, 0x11, 0x00, 0x00, 0x00, 0x00] 95 | let cds = Entry.CentralDirectoryStructure(data: Data(cdsBytes)) { _ -> Data in 96 | return Data(invalidPathBytes) 97 | } 98 | let lfhBytes: [UInt8] = [0x50, 0x4b, 0x03, 0x04, 0x14, 0x00, 0x08, 0x08, 99 | 0x08, 0x00, 0xab, 0x85, 0x77, 0x47, 0x00, 0x00, 100 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 101 | 0x00, 0x00, 0x01, 0x00, 0x00, 0x00] 102 | let lfh = Entry.LocalFileHeader(data: Data(lfhBytes)) { _ -> Data in 103 | return Data(invalidPathBytes) 104 | } 105 | guard let central = cds else { 106 | XCTFail("Failed to read central directory structure.") 107 | return 108 | } 109 | guard let local = lfh else { 110 | XCTFail("Failed to read local file header.") 111 | return 112 | } 113 | guard let entry = Entry(centralDirectoryStructure: central, localFileHeader: local) else { 114 | XCTFail("Failed to read entry.") 115 | return 116 | } 117 | XCTAssertTrue(entry.path == "") 118 | } 119 | 120 | func testEntryMissingDataDescriptorErrorCondition() { 121 | let cdsBytes: [UInt8] = [0x50, 0x4b, 0x01, 0x02, 0x1e, 0x03, 0x14, 0x00, 122 | 0x08, 0x08, 0x08, 0x00, 0xab, 0x85, 0x77, 0x47, 123 | 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 124 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 125 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 126 | 0xb0, 0x11, 0x00, 0x00, 0x00, 0x00] 127 | let cds = Entry.CentralDirectoryStructure(data: Data(cdsBytes)) { _ -> Data in 128 | return Data() 129 | } 130 | let lfhBytes: [UInt8] = [0x50, 0x4b, 0x03, 0x04, 0x14, 0x00, 0x08, 0x08, 131 | 0x08, 0x00, 0xab, 0x85, 0x77, 0x47, 0x00, 0x00, 132 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 133 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00] 134 | let lfh = Entry.LocalFileHeader(data: Data(lfhBytes)) { _ -> Data in 135 | return Data() 136 | } 137 | guard let central = cds else { 138 | XCTFail("Failed to read central directory structure.") 139 | return 140 | } 141 | guard let local = lfh else { 142 | XCTFail("Failed to read local file header.") 143 | return 144 | } 145 | guard let entry = Entry(centralDirectoryStructure: central, localFileHeader: local) else { 146 | XCTFail("Failed to read entry.") 147 | return 148 | } 149 | XCTAssertTrue(entry.checksum == 0) 150 | } 151 | 152 | func testEntryTypeDetectionHeuristics() { 153 | // Set the upper byte of .versionMadeBy to 0x15. 154 | // This exercises the code path that deals with invalid OSTypes. 155 | let cdsBytes: [UInt8] = [0x50, 0x4b, 0x01, 0x02, 0x1e, 0x15, 0x14, 0x00, 156 | 0x08, 0x08, 0x08, 0x00, 0xab, 0x85, 0x77, 0x47, 157 | 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 158 | 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 159 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 160 | 0xb0, 0x11, 0x00, 0x00, 0x00, 0x00] 161 | let cds = Entry.CentralDirectoryStructure(data: Data(cdsBytes)) { _ -> Data in 162 | return Data("/".utf8) 163 | } 164 | let lfhBytes: [UInt8] = [0x50, 0x4b, 0x03, 0x04, 0x14, 0x00, 0x08, 0x08, 165 | 0x08, 0x00, 0xab, 0x85, 0x77, 0x47, 0x00, 0x00, 166 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 167 | 0x00, 0x00, 0x01, 0x00, 0x00, 0x00] 168 | let lfh = Entry.LocalFileHeader(data: Data(lfhBytes)) { _ -> Data in 169 | return Data("/".utf8) 170 | } 171 | guard let central = cds else { 172 | XCTFail("Failed to read central directory structure.") 173 | return 174 | } 175 | guard let local = lfh else { 176 | XCTFail("Failed to read local file header.") 177 | return 178 | } 179 | guard let entry = Entry(centralDirectoryStructure: central, localFileHeader: local) else { 180 | XCTFail("Failed to read entry.") 181 | return 182 | } 183 | XCTAssertTrue(entry.type == .directory) 184 | } 185 | 186 | func testEntryValidDataDescriptor() { 187 | let ddBytes: [UInt8] = [0x50, 0x4b, 0x07, 0x08, 0x00, 0x00, 0x00, 0x00, 188 | 0x0a, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x00, 0x00] 189 | let dataDescriptor = Entry.DefaultDataDescriptor(data: Data(ddBytes), 190 | additionalDataProvider: {_ -> Data in 191 | return Data() }) 192 | XCTAssertEqual(dataDescriptor?.uncompressedSize, 10) 193 | XCTAssertEqual(dataDescriptor?.compressedSize, 10) 194 | // The DataDescriptor signature is not mandatory. 195 | let ddBytesWithoutSignature: [UInt8] = [0x00, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x00, 0x00, 196 | 0x0a, 0x00, 0x00, 0x00, 0x50, 0x4b, 0x07, 0x08] 197 | let dataDescriptorWithoutSignature = Entry.DefaultDataDescriptor(data: Data(ddBytesWithoutSignature), 198 | additionalDataProvider: {_ -> Data in 199 | return Data() }) 200 | XCTAssertEqual(dataDescriptorWithoutSignature?.uncompressedSize, 10) 201 | XCTAssertEqual(dataDescriptorWithoutSignature?.compressedSize, 10) 202 | } 203 | 204 | func testEntryIsCompressed() throws { 205 | let archive = self.archive(for: #function, mode: .read) 206 | XCTAssert(archive["compressed"]?.isCompressed == true) 207 | XCTAssert(archive["uncompressed"]?.isCompressed == false) 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /Tests/ZIPFoundationTests/ZIPFoundationErrorConditionTests+ZIP64.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ZIPFoundationErrorConditionTests+ZIP64.swift 3 | // ZIPFoundation 4 | // 5 | // Copyright © 2017-2024 Thomas Zoechling, https://www.peakstep.com and the ZIP Foundation project authors. 6 | // Released under the MIT License. 7 | // 8 | // See https://github.com/weichsel/ZIPFoundation/blob/master/LICENSE for license information. 9 | // 10 | 11 | import XCTest 12 | @testable import ZIPFoundation 13 | 14 | extension ZIPFoundationTests { 15 | 16 | func testWriteEOCDWithTooLargeSizeOfCentralDirectory() { 17 | let archive = self.archive(for: #function, mode: .create) 18 | archive.zip64EndOfCentralDirectory = makeMockZIP64EndOfCentralDirectory(sizeOfCentralDirectory: .max, 19 | numberOfEntries: 0) 20 | XCTAssertSwiftError( 21 | try archive.writeEndOfCentralDirectory(centralDirectoryStructure: makeMockCentralDirectory()!, 22 | startOfCentralDirectory: 0, 23 | startOfEndOfCentralDirectory: 0, 24 | operation: .add), 25 | throws: Archive.ArchiveError.invalidCentralDirectorySize) 26 | } 27 | 28 | func testWriteEOCDWithTooLargeCentralDirectoryOffset() { 29 | let archive = self.archive(for: #function, mode: .create) 30 | archive.zip64EndOfCentralDirectory = makeMockZIP64EndOfCentralDirectory(sizeOfCentralDirectory: 0, 31 | numberOfEntries: .max) 32 | XCTAssertSwiftError( 33 | try archive.writeEndOfCentralDirectory(centralDirectoryStructure: makeMockCentralDirectory()!, 34 | startOfCentralDirectory: 0, 35 | startOfEndOfCentralDirectory: 0, 36 | operation: .add), 37 | throws: Archive.ArchiveError.invalidCentralDirectoryEntryCount) 38 | } 39 | 40 | // MARK: - Helper 41 | 42 | private func makeMockZIP64EndOfCentralDirectory(sizeOfCentralDirectory: UInt64, numberOfEntries: UInt64) 43 | -> Archive.ZIP64EndOfCentralDirectory { 44 | let record = Archive.ZIP64EndOfCentralDirectoryRecord(sizeOfZIP64EndOfCentralDirectoryRecord: UInt64(44), 45 | versionMadeBy: UInt16(789), 46 | versionNeededToExtract: Archive.Version.v45.rawValue, 47 | numberOfDisk: 0, numberOfDiskStart: 0, 48 | totalNumberOfEntriesOnDisk: 0, 49 | totalNumberOfEntriesInCentralDirectory: numberOfEntries, 50 | sizeOfCentralDirectory: sizeOfCentralDirectory, 51 | offsetToStartOfCentralDirectory: 0, 52 | zip64ExtensibleDataSector: Data()) 53 | let locator = Archive.ZIP64EndOfCentralDirectoryLocator(numberOfDiskWithZIP64EOCDRecordStart: 0, 54 | relativeOffsetOfZIP64EOCDRecord: 0, 55 | totalNumberOfDisk: 1) 56 | return Archive.ZIP64EndOfCentralDirectory(record: record, locator: locator) 57 | } 58 | 59 | private func makeMockCentralDirectory() -> Entry.CentralDirectoryStructure? { 60 | let cdsBytes: [UInt8] = [0x50, 0x4b, 0x01, 0x02, 0x1e, 0x15, 0x14, 0x00, 61 | 0x08, 0x08, 0x08, 0x00, 0xab, 0x85, 0x77, 0x47, 62 | 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 63 | 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 64 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 65 | 0xb0, 0x11, 0x00, 0x00, 0x00, 0x00] 66 | guard let cds = Entry.CentralDirectoryStructure(data: Data(cdsBytes), 67 | additionalDataProvider: { count -> Data in 68 | let pathData = Data("/".utf8) 69 | XCTAssert(count == pathData.count) 70 | return pathData 71 | }) else { 72 | XCTFail("Failed to read central directory structure.") 73 | return nil 74 | } 75 | return cds 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Tests/ZIPFoundationTests/ZIPFoundationErrorConditionTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ZIPFoundationErrorConditionTests.swift 3 | // ZIPFoundation 4 | // 5 | // Copyright © 2017-2024 Thomas Zoechling, https://www.peakstep.com and the ZIP Foundation project authors. 6 | // Released under the MIT License. 7 | // 8 | // See https://github.com/weichsel/ZIPFoundation/blob/master/LICENSE for license information. 9 | // 10 | import XCTest 11 | @testable import ZIPFoundation 12 | 13 | extension ZIPFoundationTests { 14 | 15 | func testArchiveReadErrorConditions() { 16 | let nonExistantURL = URL(fileURLWithPath: "/nothing") 17 | XCTAssertPOSIXError(try Archive(url: nonExistantURL, accessMode: .read), throwsErrorWithCode: .ENOENT) 18 | XCTAssertPOSIXError(try Archive(url: nonExistantURL, accessMode: .update), throwsErrorWithCode: .ENOENT) 19 | let processInfo = ProcessInfo.processInfo 20 | let fileManager = FileManager() 21 | var result = false 22 | var noEndOfCentralDirectoryArchiveURL = ZIPFoundationTests.tempZipDirectoryURL 23 | noEndOfCentralDirectoryArchiveURL.appendPathComponent(processInfo.globallyUniqueString) 24 | let fullPermissionAttributes = [FileAttributeKey.posixPermissions: NSNumber(value: defaultFilePermissions)] 25 | result = fileManager.createFile(atPath: noEndOfCentralDirectoryArchiveURL.path, contents: nil, 26 | attributes: fullPermissionAttributes) 27 | XCTAssert(result == true) 28 | XCTAssertSwiftError(try Archive(url: noEndOfCentralDirectoryArchiveURL, accessMode: .read), 29 | throws: Archive.ArchiveError.missingEndOfCentralDirectoryRecord) 30 | let invalidEndOfCentralDirectoryArchiveURL = self.resourceURL(for: #function, pathExtension: "zip") 31 | XCTAssertSwiftError(try Archive(url: invalidEndOfCentralDirectoryArchiveURL, accessMode: .read), 32 | throws: Archive.ArchiveError.missingEndOfCentralDirectoryRecord) 33 | self.runWithUnprivilegedGroup { 34 | var unreadableArchiveURL = ZIPFoundationTests.tempZipDirectoryURL 35 | unreadableArchiveURL.appendPathComponent(processInfo.globallyUniqueString) 36 | let noPermissionAttributes = [FileAttributeKey.posixPermissions: NSNumber(value: Int16(0o000))] 37 | result = fileManager.createFile(atPath: unreadableArchiveURL.path, contents: nil, 38 | attributes: noPermissionAttributes) 39 | XCTAssert(result == true) 40 | XCTAssertPOSIXError(try Archive(url: unreadableArchiveURL, accessMode: .update), 41 | throwsErrorWithCode: .EACCES) 42 | } 43 | } 44 | 45 | func testArchiveIteratorErrorConditions() throws { 46 | var didFailToMakeIteratorAsExpected = true 47 | // Construct an archive that only contains an EndOfCentralDirectoryRecord 48 | // with a number of entries > 0. 49 | // While the initializer is expected to work for such archives, iterator creation 50 | // should fail. 51 | let invalidCentralDirECDS: [UInt8] = [0x50, 0x4B, 0x05, 0x06, 0x00, 0x00, 0x00, 0x00, 52 | 0x01, 0x00, 0x01, 0x00, 0x5A, 0x00, 0x00, 0x00, 53 | 0x2A, 0x00, 0x00, 0x00, 0x00, 0x00] 54 | let invalidCentralDirECDSData = Data(invalidCentralDirECDS) 55 | let processInfo = ProcessInfo.processInfo 56 | var invalidCentralDirArchiveURL = ZIPFoundationTests.tempZipDirectoryURL 57 | invalidCentralDirArchiveURL.appendPathComponent(processInfo.globallyUniqueString) 58 | let fileManager = FileManager() 59 | let result = fileManager.createFile(atPath: invalidCentralDirArchiveURL.path, 60 | contents: invalidCentralDirECDSData, 61 | attributes: nil) 62 | XCTAssert(result == true) 63 | let invalidCentralDirArchive = try Archive(url: invalidCentralDirArchiveURL, 64 | accessMode: .read) 65 | for _ in invalidCentralDirArchive { 66 | didFailToMakeIteratorAsExpected = false 67 | } 68 | XCTAssertTrue(didFailToMakeIteratorAsExpected) 69 | let archive = self.archive(for: #function, mode: .read) 70 | do { 71 | var invalidLocalFHArchiveURL = ZIPFoundationTests.tempZipDirectoryURL 72 | invalidLocalFHArchiveURL.appendPathComponent(processInfo.globallyUniqueString) 73 | var invalidLocalFHArchiveData = try Data(contentsOf: archive.url) 74 | // Construct an archive with a corrupt LocalFileHeader. 75 | // While the initializer is expected to work for such archives, iterator creation 76 | // should fail. 77 | invalidLocalFHArchiveData[26] = 0xFF 78 | try invalidLocalFHArchiveData.write(to: invalidLocalFHArchiveURL) 79 | let invalidLocalFHArchive = try Archive(url: invalidLocalFHArchiveURL, 80 | accessMode: .read) 81 | for _ in invalidLocalFHArchive { 82 | didFailToMakeIteratorAsExpected = false 83 | } 84 | } catch { 85 | XCTFail("Unexpected error while testing iterator error conditions.") 86 | } 87 | XCTAssertTrue(didFailToMakeIteratorAsExpected) 88 | } 89 | 90 | func testArchiveInvalidDataErrorConditions() { 91 | let ecdrInvalidCommentBytes: [UInt8] = [0x50, 0x4B, 0x05, 0x06, 0x00, 0x00, 0x00, 0x00, 92 | 0x01, 0x00, 0x01, 0x00, 0x5A, 0x00, 0x00, 0x00, 93 | 0x2A, 0x00, 0x00, 0x00, 0x00, 0x00] 94 | let invalidECDRCommentData = Data(ecdrInvalidCommentBytes) 95 | let invalidECDRComment = Archive.EndOfCentralDirectoryRecord(data: invalidECDRCommentData, 96 | additionalDataProvider: {_ -> Data in 97 | throw AdditionalDataError.invalidDataError }) 98 | XCTAssertNil(invalidECDRComment) 99 | let ecdrInvalidCommentLengthBytes: [UInt8] = [0x50, 0x4B, 0x05, 0x06, 0x00, 0x00, 0x00, 0x00, 100 | 0x01, 0x00, 0x01, 0x00, 0x5A, 0x00, 0x00, 0x00, 101 | 0x2A, 0x00, 0x00, 0x00, 0x00, 0x01] 102 | let invalidECDRCommentLengthData = Data(ecdrInvalidCommentLengthBytes) 103 | let invalidECDRCommentLength = Archive.EndOfCentralDirectoryRecord(data: invalidECDRCommentLengthData, 104 | additionalDataProvider: {_ -> Data in 105 | return Data() }) 106 | XCTAssertNil(invalidECDRCommentLength) 107 | } 108 | 109 | func testInvalidPOSIXError() { 110 | let invalidPOSIXError = POSIXError(Int32.max, path: "/") 111 | XCTAssert(invalidPOSIXError.code == .EPERM) 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /Tests/ZIPFoundationTests/ZIPFoundationFileAttributeTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ZIPFoundationFileAttributeTests.swift 3 | // ZIPFoundation 4 | // 5 | // Copyright © 2017-2024 Thomas Zoechling, https://www.peakstep.com and the ZIP Foundation project authors. 6 | // Released under the MIT License. 7 | // 8 | // See https://github.com/weichsel/ZIPFoundation/blob/master/LICENSE for license information. 9 | // 10 | 11 | import XCTest 12 | @testable import ZIPFoundation 13 | 14 | extension ZIPFoundationTests { 15 | 16 | func testFileAttributeHelperMethods() { 17 | let cdsBytes: [UInt8] = [0x50, 0x4b, 0x01, 0x02, 0x1e, 0x15, 0x14, 0x00, 18 | 0x08, 0x08, 0x08, 0x00, 0xab, 0x85, 0x77, 0x47, 19 | 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 20 | 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 21 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 22 | 0xb0, 0x11, 0x00, 0x00, 0x00, 0x00] 23 | guard let cds = Entry.CentralDirectoryStructure(data: Data(cdsBytes), 24 | additionalDataProvider: { count -> Data in 25 | let pathData = Data("/".utf8) 26 | XCTAssert(count == pathData.count) 27 | return pathData 28 | }) else { 29 | XCTFail("Failed to read central directory structure."); return 30 | } 31 | let lfhBytes: [UInt8] = [0x50, 0x4b, 0x03, 0x04, 0x14, 0x00, 0x08, 0x08, 32 | 0x08, 0x00, 0xab, 0x85, 0x77, 0x47, 0x00, 0x00, 33 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 34 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00] 35 | guard let lfh = Entry.LocalFileHeader(data: Data(lfhBytes), 36 | additionalDataProvider: { _ -> Data in return Data() }) 37 | else { 38 | XCTFail("Failed to read local file header."); return 39 | } 40 | guard let entry = Entry(centralDirectoryStructure: cds, localFileHeader: lfh) else { 41 | XCTFail("Failed to create test entry."); return 42 | } 43 | let attributes = FileManager.attributes(from: entry) 44 | guard let permissions = attributes[.posixPermissions] as? UInt16 else { 45 | XCTFail("Failed to read file attributes."); return 46 | } 47 | XCTAssert(permissions == defaultDirectoryPermissions) 48 | } 49 | 50 | func testSymlinkPermissionsTransferErrorConditions() { 51 | let fileManager = FileManager() 52 | let assetURL = self.resourceURL(for: #function, pathExtension: "png") 53 | XCTAssertSwiftError(try fileManager.setAttributes([:], ofItemAtURL: assetURL, traverseLink: false), 54 | throws: Entry.EntryError.missingPermissionsAttributeError) 55 | let permissions = NSNumber(value: Int16(0o753)) 56 | let tempPath = NSTemporaryDirectory() 57 | var nonExistantURL = URL(fileURLWithPath: tempPath) 58 | nonExistantURL.appendPathComponent("invalid.path") 59 | XCTAssertPOSIXError(try fileManager.setAttributes([.posixPermissions: permissions], 60 | ofItemAtURL: nonExistantURL, traverseLink: false), 61 | throwsErrorWithCode: .ENOENT) 62 | XCTAssertSwiftError(try fileManager.setAttributes([.posixPermissions: permissions], 63 | ofItemAtURL: assetURL, traverseLink: false), 64 | throws: Entry.EntryError.missingModificationDateAttributeError) 65 | XCTAssertPOSIXError( try fileManager.setAttributes([.posixPermissions: permissions, .modificationDate: Date()], 66 | ofItemAtURL: nonExistantURL, traverseLink: false), 67 | throwsErrorWithCode: .ENOENT) 68 | } 69 | 70 | func testSymlinkModificationDateTransferErrorConditions() { 71 | let fileManager = FileManager() 72 | var assetURL = self.resourceURL(for: #function, pathExtension: "png") 73 | let tempPath = NSTemporaryDirectory() 74 | var nonExistantURL = URL(fileURLWithPath: tempPath) 75 | nonExistantURL.appendPathComponent("invalid.path") 76 | XCTAssertPOSIXError(try fileManager.setSymlinkModificationDate(Date(), ofItemAtURL: nonExistantURL), 77 | throwsErrorWithCode: .ENOENT) 78 | #if os(macOS) || os(iOS) || os(tvOS) || os(visionOS) || os(watchOS) 79 | var resourceValues = URLResourceValues() 80 | resourceValues.isUserImmutable = true 81 | try? assetURL.setResourceValues(resourceValues) 82 | defer { 83 | resourceValues.isUserImmutable = false 84 | try? assetURL.setResourceValues(resourceValues) 85 | } 86 | XCTAssertPOSIXError(try fileManager.setSymlinkModificationDate(Date(), ofItemAtURL: assetURL), 87 | throwsErrorWithCode: .EPERM) 88 | #endif 89 | } 90 | 91 | func testFilePermissionHelperMethods() { 92 | var permissions = FileManager.permissions(for: UInt32(777), osType: .unix, entryType: .file) 93 | XCTAssert(permissions == defaultFilePermissions) 94 | permissions = FileManager.permissions(for: UInt32(0), osType: .msdos, entryType: .file) 95 | XCTAssert(permissions == defaultFilePermissions) 96 | permissions = FileManager.permissions(for: UInt32(0), osType: .msdos, entryType: .directory) 97 | XCTAssert(permissions == defaultDirectoryPermissions) 98 | } 99 | 100 | func testFileModificationDateHelperMethods() { 101 | guard let nonFileURL = URL(string: "https://www.peakstep.com/") else { 102 | XCTFail("Failed to create file URL."); return 103 | } 104 | 105 | XCTAssertCocoaError(try FileManager.fileModificationDateTimeForItem(at: nonFileURL), 106 | throwsErrorWithCode: CocoaError.fileReadNoSuchFile) 107 | let nonExistantURL = URL(fileURLWithPath: "/nonexistant") 108 | XCTAssertCocoaError(try FileManager.fileModificationDateTimeForItem(at: nonExistantURL), 109 | throwsErrorWithCode: CocoaError.fileReadNoSuchFile) 110 | let msDOSDate = Date(timeIntervalSince1970: TimeInterval(Int.min)).fileModificationDate 111 | XCTAssert(msDOSDate == 0) 112 | let msDOSTime = Date(timeIntervalSince1970: TimeInterval(Int.min)).fileModificationTime 113 | XCTAssert(msDOSTime == 0) 114 | let invalidEarlyMSDOSDate = Date(timeIntervalSince1970: 0).fileModificationDate 115 | XCTAssert(invalidEarlyMSDOSDate == 33) 116 | let invalidLateMSDOSDate = Date(timeIntervalSince1970: 4102444800).fileModificationDate 117 | XCTAssert(invalidLateMSDOSDate == 60961) 118 | } 119 | 120 | func testFileSizeHelperMethods() { 121 | let nonExistantURL = URL(fileURLWithPath: "/nonexistant") 122 | XCTAssertCocoaError(try FileManager.fileSizeForItem(at: nonExistantURL), 123 | throwsErrorWithCode: CocoaError.fileReadNoSuchFile) 124 | } 125 | 126 | func testFileTypeHelperMethods() { 127 | let nonExistantURL = URL(fileURLWithPath: "/nonexistant") 128 | XCTAssertCocoaError(try FileManager.typeForItem(at: nonExistantURL), 129 | throwsErrorWithCode: CocoaError.fileReadNoSuchFile) 130 | guard let nonFileURL = URL(string: "https://www.peakstep.com") else { 131 | XCTFail("Failed to create test URL."); return 132 | } 133 | 134 | XCTAssertCocoaError(try FileManager.typeForItem(at: nonFileURL), 135 | throwsErrorWithCode: CocoaError.fileReadNoSuchFile) 136 | } 137 | 138 | func testFileModificationDate() { 139 | var testDateComponents = DateComponents() 140 | testDateComponents.calendar = Calendar(identifier: Calendar.Identifier.gregorian) 141 | testDateComponents.timeZone = TimeZone(identifier: "UTC") 142 | testDateComponents.year = 2000 143 | testDateComponents.month = 1 144 | testDateComponents.day = 1 145 | testDateComponents.hour = 12 146 | testDateComponents.minute = 30 147 | testDateComponents.second = 10 148 | guard let testDate = testDateComponents.date else { 149 | XCTFail("Failed to create test date/timestamp"); return 150 | } 151 | let assetURL = self.resourceURL(for: #function, pathExtension: "png") 152 | let fileManager = FileManager() 153 | let archive = self.archive(for: #function, mode: .create) 154 | do { 155 | try fileManager.setAttributes([.modificationDate: testDate], ofItemAtPath: assetURL.path) 156 | let relativePath = assetURL.lastPathComponent 157 | let baseURL = assetURL.deletingLastPathComponent() 158 | try archive.addEntry(with: relativePath, relativeTo: baseURL) 159 | guard let entry = archive["\(assetURL.lastPathComponent)"] else { 160 | throw Archive.ArchiveError.unreadableArchive 161 | } 162 | guard let fileDate = entry.fileAttributes[.modificationDate] as? Date else { 163 | throw CocoaError(CocoaError.fileReadUnknown) 164 | } 165 | let currentTimeInterval = testDate.timeIntervalSinceReferenceDate 166 | let fileTimeInterval = fileDate.timeIntervalSinceReferenceDate 167 | // ZIP uses MSDOS timestamps, which provide very poor accuracy 168 | // https://blogs.msdn.microsoft.com/oldnewthing/20151030-00/?p=91881 169 | XCTAssertEqual(currentTimeInterval, fileTimeInterval, accuracy: 2.0) 170 | } catch { XCTFail("Failed to test last file modification date") } 171 | } 172 | 173 | func testPOSIXPermissions() { 174 | let permissions = NSNumber(value: Int16(0o753)) 175 | let assetURL = self.resourceURL(for: #function, pathExtension: "png") 176 | let fileManager = FileManager() 177 | let archive = self.archive(for: #function, mode: .create) 178 | do { 179 | try fileManager.setAttributes([.posixPermissions: permissions], ofItemAtPath: assetURL.path) 180 | let relativePath = assetURL.lastPathComponent 181 | let baseURL = assetURL.deletingLastPathComponent() 182 | try archive.addEntry(with: relativePath, relativeTo: baseURL) 183 | guard let entry = archive["\(assetURL.lastPathComponent)"] else { 184 | throw Archive.ArchiveError.unreadableArchive 185 | } 186 | guard let filePermissions = entry.fileAttributes[.posixPermissions] as? NSNumber else { 187 | throw CocoaError(CocoaError.fileReadUnknown) 188 | } 189 | XCTAssert(permissions.int16Value == filePermissions.int16Value) 190 | } catch { XCTFail("Failed to test POSIX permissions") } 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /Tests/ZIPFoundationTests/ZIPFoundationFileManagerTests+ZIP64.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ZIPFoundationFileManagerTests+ZIP64.swift 3 | // ZIPFoundation 4 | // 5 | // Copyright © 2017-2024 Thomas Zoechling, https://www.peakstep.com and the ZIP Foundation project authors. 6 | // Released under the MIT License. 7 | // 8 | // See https://github.com/weichsel/ZIPFoundation/blob/master/LICENSE for license information. 9 | // 10 | 11 | import XCTest 12 | @testable import ZIPFoundation 13 | 14 | extension ZIPFoundationTests { 15 | 16 | private enum ZIP64FileManagerTestsError: Error, CustomStringConvertible { 17 | case failedToZipItem(url: URL) 18 | case failedToReadArchive(url: URL) 19 | case failedToUnzipItem 20 | 21 | var description: String { 22 | switch self { 23 | case .failedToZipItem(let assetURL): 24 | return "Failed to zip item at URL: \(assetURL)." 25 | case .failedToReadArchive(let fileArchiveURL): 26 | return "Failed to read archive at URL: \(fileArchiveURL)." 27 | case .failedToUnzipItem: 28 | return "Failed to unzip item." 29 | } 30 | } 31 | } 32 | 33 | func testZipCompressedZIP64Item() { 34 | do { 35 | try archiveZIP64Item(for: #function, compressionMethod: .deflate) 36 | } catch { 37 | XCTFail("\(error)") 38 | } 39 | } 40 | 41 | func testZipUncompressedZIP64Item() { 42 | do { 43 | try archiveZIP64Item(for: #function, compressionMethod: .none) 44 | } catch { 45 | XCTFail("\(error)") 46 | } 47 | } 48 | 49 | func testUnzipCompressedZIP64Item() { 50 | // stored by zip 3.0 via command line: zip -0 -fz 51 | // 52 | // testUnzipCompressedZIP64Item.zip/ 53 | // ├─ directory 54 | // ├─ testLink 55 | // ├─ nested 56 | // ├─ nestedLink 57 | // ├─ faust copy.txt 58 | // ├─ deep 59 | // ├─ another.random 60 | // ├─ faust.txt 61 | // ├─ empty 62 | // ├─ data.random 63 | // ├─ random.data 64 | do { 65 | try unarchiveZIP64Item(for: #function) 66 | } catch { 67 | XCTFail("\(error)") 68 | } 69 | } 70 | 71 | func testUnzipUncompressedZIP64Item() { 72 | // stored by zip 3.0 via command line: zip -0 -fz 73 | // 74 | // testUnzipCompressedZIP64Item.zip/ 75 | // ├─ directory 76 | // ├─ testLink 77 | // ├─ nested 78 | // ├─ nestedLink 79 | // ├─ faust copy.txt 80 | // ├─ deep 81 | // ├─ another.random 82 | // ├─ faust.txt 83 | // ├─ empty 84 | // ├─ data.random 85 | // ├─ random.data 86 | do { 87 | try unarchiveZIP64Item(for: #function) 88 | } catch { 89 | XCTFail("\(error)") 90 | } 91 | } 92 | 93 | func testUnzipItemWithZIP64DataDescriptor() { 94 | // testUnzipCompressedZIP64Item.zip 95 | // ├─ simple.data 96 | do { 97 | try unarchiveZIP64Item(for: #function) 98 | } catch { 99 | XCTFail("\(error)") 100 | } 101 | } 102 | 103 | // MARK: - Helpers 104 | 105 | private func archiveZIP64Item(for testFunction: String, compressionMethod: CompressionMethod) throws { 106 | self.mockIntMaxValues(int32Factor: 16, int16Factor: 16) 107 | defer { self.resetIntMaxValues() } 108 | let assetURL = self.resourceURL(for: testFunction, pathExtension: "png") 109 | var fileArchiveURL = ZIPFoundationTests.tempZipDirectoryURL 110 | fileArchiveURL.appendPathComponent(self.archiveName(for: testFunction)) 111 | do { 112 | try FileManager().zipItem(at: assetURL, to: fileArchiveURL, compressionMethod: compressionMethod) 113 | } catch { 114 | throw ZIP64FileManagerTestsError.failedToZipItem(url: assetURL) 115 | } 116 | let archive = try Archive(url: fileArchiveURL, accessMode: .read) 117 | XCTAssertNotNil(archive[assetURL.lastPathComponent]) 118 | XCTAssert(archive.checkIntegrity()) 119 | } 120 | 121 | private func unarchiveZIP64Item(for testFunction: String) throws { 122 | let fileManager = FileManager() 123 | let archive = self.archive(for: testFunction, mode: .read) 124 | let destinationURL = self.createDirectory(for: testFunction) 125 | do { 126 | try fileManager.unzipItem(at: archive.url, to: destinationURL) 127 | } catch { 128 | throw ZIP64FileManagerTestsError.failedToUnzipItem 129 | } 130 | var itemsExist = false 131 | for entry in archive { 132 | let directoryURL = destinationURL.appendingPathComponent(entry.path) 133 | itemsExist = fileManager.itemExists(at: directoryURL) 134 | if !itemsExist { break } 135 | } 136 | XCTAssert(itemsExist) 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /Tests/ZIPFoundationTests/ZIPFoundationMemoryTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ZIPFoundationMemoryTests.swift 3 | // ZIPFoundation 4 | // 5 | // Copyright © 2017-2024 Thomas Zoechling, https://www.peakstep.com and the ZIP Foundation project authors. 6 | // Released under the MIT License. 7 | // 8 | // See https://github.com/weichsel/ZIPFoundation/blob/master/LICENSE for license information. 9 | // 10 | 11 | import Foundation 12 | 13 | import XCTest 14 | @testable import ZIPFoundation 15 | 16 | #if swift(>=5.0) 17 | 18 | extension ZIPFoundationTests { 19 | 20 | func testExtractUncompressedFolderEntriesFromMemory() { 21 | let archive = self.memoryArchive(for: #function, mode: .read) 22 | for entry in archive { 23 | do { 24 | // Test extracting to memory 25 | var checksum = try archive.extract(entry, bufferSize: 32, consumer: { _ in }) 26 | XCTAssert(entry.checksum == checksum) 27 | // Test extracting to file 28 | var fileURL = self.createDirectory(for: #function) 29 | fileURL.appendPathComponent(entry.path) 30 | checksum = try archive.extract(entry, to: fileURL) 31 | XCTAssert(entry.checksum == checksum) 32 | let fileManager = FileManager() 33 | XCTAssertTrue(fileManager.fileExists(atPath: fileURL.path)) 34 | if entry.type == .file { 35 | let fileData = try Data(contentsOf: fileURL) 36 | let checksum = fileData.crc32(checksum: 0) 37 | XCTAssert(checksum == entry.checksum) 38 | } 39 | } catch { 40 | XCTFail("Failed to unzip uncompressed folder entries") 41 | } 42 | } 43 | XCTAssert(archive.data != nil) 44 | } 45 | 46 | func testExtractCompressedFolderEntriesFromMemory() { 47 | let archive = self.memoryArchive(for: #function, mode: .read) 48 | for entry in archive { 49 | do { 50 | // Test extracting to memory 51 | var checksum = try archive.extract(entry, bufferSize: 128, consumer: { _ in }) 52 | XCTAssert(entry.checksum == checksum) 53 | // Test extracting to file 54 | var fileURL = self.createDirectory(for: #function) 55 | fileURL.appendPathComponent(entry.path) 56 | checksum = try archive.extract(entry, to: fileURL) 57 | XCTAssert(entry.checksum == checksum) 58 | let fileManager = FileManager() 59 | XCTAssertTrue(fileManager.fileExists(atPath: fileURL.path)) 60 | if entry.type != .directory { 61 | let fileData = try Data(contentsOf: fileURL) 62 | let checksum = fileData.crc32(checksum: 0) 63 | XCTAssert(checksum == entry.checksum) 64 | } 65 | } catch { 66 | XCTFail("Failed to unzip compressed folder entries") 67 | } 68 | } 69 | } 70 | 71 | func testCreateArchiveAddUncompressedEntryToMemory() { 72 | let archive = self.memoryArchive(for: #function, mode: .create) 73 | let assetURL = self.resourceURL(for: #function, pathExtension: "png") 74 | do { 75 | let relativePath = assetURL.lastPathComponent 76 | let baseURL = assetURL.deletingLastPathComponent() 77 | try archive.addEntry(with: relativePath, relativeTo: baseURL) 78 | } catch { 79 | XCTFail("Failed to add entry to uncompressed folder archive with error : \(error)") 80 | } 81 | XCTAssert(archive.checkIntegrity()) 82 | } 83 | 84 | func testCreateArchiveAddCompressedEntryToMemory() { 85 | let archive = self.memoryArchive(for: #function, mode: .create) 86 | let assetURL = self.resourceURL(for: #function, pathExtension: "png") 87 | do { 88 | let relativePath = assetURL.lastPathComponent 89 | let baseURL = assetURL.deletingLastPathComponent() 90 | try archive.addEntry(with: relativePath, relativeTo: baseURL, compressionMethod: .deflate) 91 | } catch { 92 | XCTFail("Failed to add entry to compressed folder archive with error : \(error)") 93 | } 94 | let entry = archive[assetURL.lastPathComponent] 95 | XCTAssertNotNil(entry) 96 | XCTAssert(archive.checkIntegrity()) 97 | } 98 | 99 | func testUpdateArchiveRemoveUncompressedEntryFromMemory() throws { 100 | let archive = self.memoryArchive(for: #function, mode: .update) 101 | XCTAssert(archive.checkIntegrity()) 102 | guard let entryToRemove = archive["original"] else { 103 | XCTFail("Failed to find entry to remove from memory archive"); return 104 | } 105 | do { 106 | try archive.remove(entryToRemove) 107 | } catch { 108 | XCTFail("Failed to remove entry from memory archive with error : \(error)") 109 | } 110 | XCTAssert(archive.checkIntegrity()) 111 | } 112 | 113 | func testMemoryArchiveErrorConditions() throws { 114 | let data = Data.makeRandomData(size: 1024) 115 | XCTAssertSwiftError(try Archive(data: data, accessMode: .read), 116 | throws: Archive.ArchiveError.missingEndOfCentralDirectoryRecord) 117 | let archive = self.memoryArchive(for: #function, mode: .create) 118 | let replacementArchive = self.memoryArchive(for: #function, mode: .read) 119 | replacementArchive.memoryFile = nil 120 | XCTAssertSwiftError( 121 | try archive.replaceCurrentArchive(with: replacementArchive), 122 | throws: Archive.ArchiveError.unwritableArchive 123 | ) 124 | 125 | var noEndOfCentralDirectoryArchiveURL = ZIPFoundationTests.tempZipDirectoryURL 126 | noEndOfCentralDirectoryArchiveURL.appendPathComponent(ProcessInfo.processInfo.globallyUniqueString) 127 | let fullPermissionAttributes = [FileAttributeKey.posixPermissions: NSNumber(value: defaultFilePermissions)] 128 | FileManager().createFile(atPath: noEndOfCentralDirectoryArchiveURL.path, contents: nil, 129 | attributes: fullPermissionAttributes) 130 | let noEOCDArchiveData = try Data(contentsOf: noEndOfCentralDirectoryArchiveURL) 131 | XCTAssertSwiftError(try Archive(data: noEOCDArchiveData, accessMode: .update), 132 | throws: Archive.ArchiveError.missingEndOfCentralDirectoryRecord) 133 | } 134 | 135 | func testReadOnlyFile() { 136 | let file = Archive.MemoryFile(data: Data("ABCDEabcde".utf8)).open(mode: .read) 137 | var chars: [UInt8] = [0, 0, 0] 138 | XCTAssertEqual(fread(&chars, 1, 2, file), 2) 139 | XCTAssertEqual(String(Unicode.Scalar(chars[0])), "A") 140 | XCTAssertEqual(String(Unicode.Scalar(chars[1])), "B") 141 | XCTAssertNotEqual(fwrite("x", 1, 1, file), 1) 142 | XCTAssertEqual(fseek(file, 3, SEEK_CUR), 0) 143 | XCTAssertEqual(fread(&chars, 1, 2, file), 2) 144 | XCTAssertEqual(String(Unicode.Scalar(chars[0])), "a") 145 | XCTAssertEqual(String(Unicode.Scalar(chars[1])), "b") 146 | XCTAssertEqual(fseek(file, 9, SEEK_SET), 0) 147 | XCTAssertEqual(fread(&chars, 1, 2, file), 1) 148 | XCTAssertEqual(String(Unicode.Scalar(chars[0])), "e") 149 | XCTAssertEqual(String(Unicode.Scalar(chars[1])), "b") 150 | XCTAssertEqual(fclose(file), 0) 151 | } 152 | 153 | func testReadOnlySlicedFile() { 154 | let originalData = Data("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ".utf8) 155 | let slice = originalData[10.. Archive { 199 | var sourceArchiveURL = ZIPFoundationTests.resourceDirectoryURL 200 | sourceArchiveURL.appendPathComponent(testFunction.replacingOccurrences(of: "()", with: "")) 201 | sourceArchiveURL.appendPathExtension("zip") 202 | do { 203 | let data = mode == .create ? Data() : try Data(contentsOf: sourceArchiveURL) 204 | return try Archive(data: data, accessMode: mode, 205 | pathEncoding: pathEncoding) 206 | } catch { 207 | XCTFail("Failed to open memory archive for '\(sourceArchiveURL.lastPathComponent)'") 208 | type(of: self).tearDown() 209 | preconditionFailure() 210 | } 211 | } 212 | } 213 | 214 | #endif 215 | -------------------------------------------------------------------------------- /Tests/ZIPFoundationTests/ZIPFoundationPerformanceTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ZIPFoundationPerformanceTests.swift 3 | // ZIPFoundation 4 | // 5 | // Copyright © 2017-2024 Thomas Zoechling, https://www.peakstep.com and the ZIP Foundation project authors. 6 | // Released under the MIT License. 7 | // 8 | // See https://github.com/weichsel/ZIPFoundation/blob/master/LICENSE for license information. 9 | // 10 | 11 | import XCTest 12 | @testable import ZIPFoundation 13 | 14 | extension ZIPFoundationTests { 15 | 16 | func testPerformanceWriteUncompressed() { 17 | let archive = self.archive(for: #function, mode: .create) 18 | let size = 1024*1024*20 19 | let data = Data.makeRandomData(size: size) 20 | let entryName = ProcessInfo.processInfo.globallyUniqueString 21 | measure { 22 | do { 23 | try archive.addEntry(with: entryName, type: .file, 24 | uncompressedSize: Int64(size), 25 | compressionMethod: .none, 26 | provider: { (position, bufferSize) -> Data in 27 | let upperBound = Swift.min(size, Int(position) + bufferSize) 28 | let range = Range(uncheckedBounds: (lower: Int(position), upper: upperBound)) 29 | return data.subdata(in: range) 30 | }) 31 | } catch { 32 | XCTFail("Failed to add large entry to uncompressed archive with error : \(error)") 33 | } 34 | } 35 | } 36 | 37 | func testPerformanceReadUncompressed() { 38 | let archive = self.archive(for: #function, mode: .create) 39 | let size = 1024*1024*20 40 | let data = Data.makeRandomData(size: size) 41 | let entryName = ProcessInfo.processInfo.globallyUniqueString 42 | do { 43 | try archive.addEntry(with: entryName, type: .file, 44 | uncompressedSize: Int64(size), 45 | compressionMethod: .none, 46 | provider: { (position, bufferSize) -> Data in 47 | let upperBound = Swift.min(size, Int(position) + bufferSize) 48 | let range = Range(uncheckedBounds: (lower: Int(position), upper: upperBound)) 49 | return data.subdata(in: range) 50 | }) 51 | } catch { 52 | XCTFail("Failed to add large entry to uncompressed archive with error : \(error)") 53 | } 54 | measure { 55 | do { 56 | guard let entry = archive[entryName] else { 57 | XCTFail("Failed to read entry.") 58 | return 59 | } 60 | _ = try archive.extract(entry, consumer: {_ in }) 61 | } catch { 62 | XCTFail("Failed to read large entry from uncompressed archive") 63 | } 64 | } 65 | } 66 | 67 | func testPerformanceWriteCompressed() { 68 | let archive = self.archive(for: #function, mode: .create) 69 | let size = 1024*1024*20 70 | let data = Data.makeRandomData(size: size) 71 | let entryName = ProcessInfo.processInfo.globallyUniqueString 72 | measure { 73 | do { 74 | try archive.addEntry(with: entryName, type: .file, 75 | uncompressedSize: Int64(size), 76 | compressionMethod: .deflate, 77 | provider: { (position, bufferSize) -> Data in 78 | let upperBound = Swift.min(size, Int(position) + bufferSize) 79 | let range = Range(uncheckedBounds: (lower: Int(position), upper: upperBound)) 80 | return data.subdata(in: range) 81 | }) 82 | } catch { 83 | XCTFail("Failed to add large entry to compressed archive with error : \(error)") 84 | } 85 | } 86 | } 87 | 88 | func testPerformanceReadCompressed() { 89 | let archive = self.archive(for: #function, mode: .create) 90 | let size = 1024*1024*20 91 | let data = Data.makeRandomData(size: size) 92 | let entryName = ProcessInfo.processInfo.globallyUniqueString 93 | do { 94 | try archive.addEntry(with: entryName, type: .file, 95 | uncompressedSize: Int64(size), 96 | compressionMethod: .deflate, 97 | provider: { (position, bufferSize) -> Data in 98 | let upperBound = Swift.min(size, Int(position) + bufferSize) 99 | let range = Range(uncheckedBounds: (lower: Int(position), upper: upperBound)) 100 | return data.subdata(in: range) 101 | }) 102 | } catch { 103 | XCTFail("Failed to add large entry to compressed archive with error : \(error)") 104 | } 105 | measure { 106 | do { 107 | guard let entry = archive[entryName] else { 108 | XCTFail("Failed to read entry.") 109 | return 110 | } 111 | _ = try archive.extract(entry, consumer: {_ in }) 112 | } catch { 113 | XCTFail("Failed to read large entry from compressed archive") 114 | } 115 | } 116 | } 117 | 118 | func testPerformanceCRC32() { 119 | let size = 1024*1024*20 120 | let data = Data.makeRandomData(size: size) 121 | measure { 122 | _ = data.crc32(checksum: 0) 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /Tests/ZIPFoundationTests/ZIPFoundationProgressTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ZIPFoundationProgressTests.swift 3 | // ZIPFoundation 4 | // 5 | // Copyright © 2017-2024 Thomas Zoechling, https://www.peakstep.com and the ZIP Foundation project authors. 6 | // Released under the MIT License. 7 | // 8 | // See https://github.com/weichsel/ZIPFoundation/blob/master/LICENSE for license information. 9 | // 10 | import XCTest 11 | @testable import ZIPFoundation 12 | 13 | #if os(macOS) || os(iOS) || os(tvOS) || os(visionOS) || os(watchOS) 14 | extension ZIPFoundationTests { 15 | 16 | func testArchiveAddUncompressedEntryProgress() { 17 | let archive = self.archive(for: #function, mode: .update) 18 | let assetURL = self.resourceURL(for: #function, pathExtension: "png") 19 | let progress = archive.makeProgressForAddingItem(at: assetURL) 20 | let handler: XCTKVOExpectation.Handler = { (_, _) -> Bool in 21 | if progress.fractionCompleted > 0.5 { 22 | progress.cancel() 23 | return true 24 | } 25 | return false 26 | } 27 | let cancel = self.keyValueObservingExpectation(for: progress, keyPath: #keyPath(Progress.fractionCompleted), 28 | handler: handler) 29 | let zipQueue = DispatchQueue(label: "ZIPFoundationTests") 30 | zipQueue.async { 31 | do { 32 | let relativePath = assetURL.lastPathComponent 33 | let baseURL = assetURL.deletingLastPathComponent() 34 | try archive.addEntry(with: relativePath, relativeTo: baseURL, bufferSize: 1, progress: progress) 35 | } catch let error as Archive.ArchiveError { 36 | XCTAssert(error == Archive.ArchiveError.cancelledOperation) 37 | } catch { 38 | XCTFail("Failed to add entry to uncompressed folder archive with error : \(error)") 39 | } 40 | } 41 | self.wait(for: [cancel], timeout: 20.0) 42 | zipQueue.sync { 43 | XCTAssert(progress.fractionCompleted > 0.5) 44 | XCTAssert(archive.checkIntegrity()) 45 | } 46 | } 47 | 48 | func testArchiveAddCompressedEntryProgress() { 49 | let archive = self.archive(for: #function, mode: .update) 50 | let assetURL = self.resourceURL(for: #function, pathExtension: "png") 51 | let progress = archive.makeProgressForAddingItem(at: assetURL) 52 | let handler: XCTKVOExpectation.Handler = { (_, _) -> Bool in 53 | if progress.fractionCompleted > 0.5 { 54 | progress.cancel() 55 | return true 56 | } 57 | return false 58 | } 59 | let cancel = self.keyValueObservingExpectation(for: progress, keyPath: #keyPath(Progress.fractionCompleted), 60 | handler: handler) 61 | let zipQueue = DispatchQueue(label: "ZIPFoundationTests") 62 | zipQueue.async { 63 | do { 64 | let relativePath = assetURL.lastPathComponent 65 | let baseURL = assetURL.deletingLastPathComponent() 66 | try archive.addEntry(with: relativePath, relativeTo: baseURL, 67 | compressionMethod: .deflate, bufferSize: 1, progress: progress) 68 | } catch let error as Archive.ArchiveError { 69 | XCTAssert(error == Archive.ArchiveError.cancelledOperation) 70 | } catch { 71 | XCTFail("Failed to add entry to uncompressed folder archive with error : \(error)") 72 | } 73 | } 74 | self.wait(for: [cancel], timeout: 20.0) 75 | zipQueue.sync { 76 | XCTAssert(progress.fractionCompleted > 0.5) 77 | XCTAssert(archive.checkIntegrity()) 78 | } 79 | } 80 | 81 | func testRemoveEntryProgress() { 82 | let archive = self.archive(for: #function, mode: .update) 83 | guard let entryToRemove = archive["test/data.random"] else { 84 | XCTFail("Failed to find entry to remove in uncompressed folder") 85 | return 86 | } 87 | let progress = archive.makeProgressForRemoving(entryToRemove) 88 | let handler: XCTKVOExpectation.Handler = { (_, _) -> Bool in 89 | if progress.fractionCompleted > 0.5 { 90 | progress.cancel() 91 | return true 92 | } 93 | return false 94 | } 95 | let cancel = self.keyValueObservingExpectation(for: progress, keyPath: #keyPath(Progress.fractionCompleted), 96 | handler: handler) 97 | let zipQueue = DispatchQueue(label: "ZIPFoundationTests") 98 | zipQueue.async { 99 | do { 100 | try archive.remove(entryToRemove, progress: progress) 101 | } catch let error as Archive.ArchiveError { 102 | XCTAssert(error == Archive.ArchiveError.cancelledOperation) 103 | } catch { 104 | XCTFail("Failed to remove entry from uncompressed folder archive with error : \(error)") 105 | } 106 | } 107 | self.wait(for: [cancel], timeout: 20.0) 108 | zipQueue.sync { 109 | XCTAssert(progress.fractionCompleted > 0.5) 110 | XCTAssert(archive.checkIntegrity()) 111 | } 112 | } 113 | 114 | func testZipItemProgress() throws { 115 | let fileManager = FileManager() 116 | let assetURL = self.resourceURL(for: #function, pathExtension: "png") 117 | var fileArchiveURL = ZIPFoundationTests.tempZipDirectoryURL 118 | fileArchiveURL.appendPathComponent(self.archiveName(for: #function)) 119 | let fileProgress = Progress() 120 | let fileExpectation = self.keyValueObservingExpectation(for: fileProgress, 121 | keyPath: #keyPath(Progress.fractionCompleted), 122 | expectedValue: 1.0) 123 | var didSucceed = true 124 | let testQueue = DispatchQueue.global() 125 | testQueue.async { 126 | do { 127 | try fileManager.zipItem(at: assetURL, to: fileArchiveURL, progress: fileProgress) 128 | } catch { didSucceed = false } 129 | } 130 | var directoryURL = ZIPFoundationTests.tempZipDirectoryURL 131 | directoryURL.appendPathComponent(ProcessInfo.processInfo.globallyUniqueString) 132 | var directoryArchiveURL = ZIPFoundationTests.tempZipDirectoryURL 133 | directoryArchiveURL.appendPathComponent(self.archiveName(for: #function, suffix: "Directory")) 134 | let newAssetURL = directoryURL.appendingPathComponent(assetURL.lastPathComponent) 135 | let directoryProgress = Progress() 136 | let directoryExpectation = self.keyValueObservingExpectation(for: directoryProgress, 137 | keyPath: #keyPath(Progress.fractionCompleted), 138 | expectedValue: 1.0) 139 | testQueue.async { 140 | do { 141 | try fileManager.createDirectory(at: directoryURL, withIntermediateDirectories: true, attributes: nil) 142 | try fileManager.createDirectory(at: directoryURL.appendingPathComponent("nested"), 143 | withIntermediateDirectories: true, attributes: nil) 144 | try fileManager.copyItem(at: assetURL, to: newAssetURL) 145 | try fileManager.createSymbolicLink(at: directoryURL.appendingPathComponent("link"), 146 | withDestinationURL: newAssetURL) 147 | try fileManager.zipItem(at: directoryURL, to: directoryArchiveURL, progress: directoryProgress) 148 | } catch { didSucceed = false } 149 | } 150 | self.wait(for: [fileExpectation, directoryExpectation], timeout: 20.0) 151 | XCTAssert(didSucceed) 152 | let archive = try Archive(url: fileArchiveURL, accessMode: .read) 153 | XCTAssert(archive.checkIntegrity()) 154 | let directoryArchive = try Archive(url: directoryArchiveURL, accessMode: .read) 155 | XCTAssert(directoryArchive.checkIntegrity()) 156 | } 157 | 158 | func testUnzipItemProgress() { 159 | let fileManager = FileManager() 160 | let archive = self.archive(for: #function, mode: .read) 161 | let destinationURL = self.createDirectory(for: #function) 162 | let progress = Progress() 163 | let expectation = self.keyValueObservingExpectation(for: progress, 164 | keyPath: #keyPath(Progress.fractionCompleted), 165 | expectedValue: 1.0) 166 | DispatchQueue.global().async { 167 | do { 168 | try fileManager.unzipItem(at: archive.url, to: destinationURL, progress: progress) 169 | } catch { 170 | XCTFail("Failed to extract item."); return 171 | } 172 | var itemsExist = false 173 | for entry in archive { 174 | let directoryURL = destinationURL.appendingPathComponent(entry.path) 175 | itemsExist = fileManager.itemExists(at: directoryURL) 176 | if !itemsExist { break } 177 | } 178 | XCTAssert(itemsExist) 179 | } 180 | self.wait(for: [expectation], timeout: 10.0) 181 | } 182 | 183 | func testZIP64ArchiveAddEntryProgress() { 184 | self.mockIntMaxValues() 185 | defer { self.resetIntMaxValues() } 186 | let archive = self.archive(for: #function, mode: .update) 187 | let assetURL = self.resourceURL(for: #function, pathExtension: "png") 188 | let progress = archive.makeProgressForAddingItem(at: assetURL) 189 | let handler: XCTKVOExpectation.Handler = { (_, _) -> Bool in 190 | if progress.fractionCompleted > 0.5 { 191 | progress.cancel() 192 | return true 193 | } 194 | return false 195 | } 196 | let cancel = self.keyValueObservingExpectation(for: progress, keyPath: #keyPath(Progress.fractionCompleted), 197 | handler: handler) 198 | let zipQueue = DispatchQueue(label: "ZIPFoundationTests") 199 | zipQueue.async { 200 | do { 201 | let relativePath = assetURL.lastPathComponent 202 | let baseURL = assetURL.deletingLastPathComponent() 203 | try archive.addEntry(with: relativePath, relativeTo: baseURL, 204 | compressionMethod: .deflate, bufferSize: 1, progress: progress) 205 | } catch let error as Archive.ArchiveError { 206 | XCTAssert(error == Archive.ArchiveError.cancelledOperation) 207 | } catch { 208 | XCTFail("Failed to add entry to uncompressed folder archive with error : \(error)") 209 | } 210 | } 211 | self.wait(for: [cancel], timeout: 20.0) 212 | zipQueue.sync { 213 | XCTAssert(progress.fractionCompleted > 0.5) 214 | XCTAssert(archive.checkIntegrity()) 215 | } 216 | } 217 | } 218 | #endif 219 | -------------------------------------------------------------------------------- /Tests/ZIPFoundationTests/ZIPFoundationReadingTests+ZIP64.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ZIPFoundationReadingTests+ZIP64.swift 3 | // ZIPFoundation 4 | // 5 | // Copyright © 2017-2024 Thomas Zoechling, https://www.peakstep.com and the ZIP Foundation project authors. 6 | // Released under the MIT License. 7 | // 8 | // See https://github.com/weichsel/ZIPFoundation/blob/master/LICENSE for license information. 9 | // 10 | 11 | import XCTest 12 | @testable import ZIPFoundation 13 | 14 | extension ZIPFoundationTests { 15 | 16 | private enum StoreType { 17 | case memory 18 | case file 19 | } 20 | 21 | private enum ZIP64ReadingTestsError: Error, CustomStringConvertible { 22 | case failedToReadEntry(name: String) 23 | case failedToExtractEntry(type: StoreType) 24 | 25 | var description: String { 26 | switch self { 27 | case .failedToReadEntry(let name): 28 | return "Failed to read entry: \(name)." 29 | case .failedToExtractEntry(let type): 30 | return "Failed to extract item to \(type)" 31 | } 32 | } 33 | } 34 | 35 | func testExtractUncompressedZIP64Entries() { 36 | do { 37 | try extractEntryFromZIP64Archive(for: #function) 38 | } catch { 39 | XCTFail("\(error)") 40 | } 41 | } 42 | 43 | func testExtractCompressedZIP64Entries() { 44 | do { 45 | try extractEntryFromZIP64Archive(for: #function) 46 | } catch { 47 | XCTFail("\(error)") 48 | } 49 | } 50 | 51 | func testExtractEntryWithZIP64DataDescriptor() { 52 | do { 53 | try extractEntryFromZIP64Archive(for: #function, reservedFileName: "simple.data") 54 | } catch { 55 | XCTFail("\(error)") 56 | } 57 | } 58 | 59 | // MARK: - Helpers 60 | 61 | private func extractEntryFromZIP64Archive(for testFunction: String, reservedFileName: String? = nil) throws { 62 | let archive = self.archive(for: testFunction, mode: .read) 63 | let fileName = reservedFileName ?? testFunction.replacingOccurrences(of: "()", with: ".png") 64 | guard let entry = archive[fileName] else { 65 | throw ZIP64ReadingTestsError.failedToReadEntry(name: fileName) 66 | } 67 | do { 68 | // Test extracting to memory 69 | let checksum = try archive.extract(entry, bufferSize: 32, consumer: { _ in }) 70 | XCTAssert(entry.checksum == checksum) 71 | } catch { 72 | throw ZIP64ReadingTestsError.failedToExtractEntry(type: .memory) 73 | } 74 | do { 75 | // Test extracting to file 76 | var fileURL = self.createDirectory(for: testFunction) 77 | fileURL.appendPathComponent(entry.path) 78 | let checksum = try archive.extract(entry, to: fileURL) 79 | XCTAssert(entry.checksum == checksum) 80 | let fileManager = FileManager() 81 | XCTAssertTrue(fileManager.itemExists(at: fileURL)) 82 | if entry.type == .file { 83 | let fileData = try Data(contentsOf: fileURL) 84 | let checksum = fileData.crc32(checksum: 0) 85 | XCTAssert(checksum == entry.checksum) 86 | } 87 | } catch { 88 | throw ZIP64ReadingTestsError.failedToExtractEntry(type: .file) 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /ZIPFoundation.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = 'ZIPFoundation' 3 | s.version = '0.9.19' 4 | s.license = 'MIT' 5 | s.summary = 'Effortless ZIP Handling in Swift' 6 | s.homepage = 'https://github.com/weichsel/ZIPFoundation' 7 | s.social_media_url = 'http://twitter.com/weichsel' 8 | s.authors = { 'Thomas Zoechling' => 'thomas@peakstep.com' } 9 | s.source = { :git => 'https://github.com/weichsel/ZIPFoundation.git', :tag => s.version } 10 | s.swift_versions = ['4.0', '4.2', '5.0'] 11 | 12 | s.ios.deployment_target = '12.0' 13 | s.osx.deployment_target = '10.11' 14 | s.tvos.deployment_target = '12.0' 15 | s.watchos.deployment_target = '2.0' 16 | s.visionos.deployment_target = '1.0' 17 | 18 | s.source_files = 'Sources/ZIPFoundation/*.swift' 19 | s.resource_bundles = {'ZIPFoundation_Privacy' => ['Sources/ZIPFoundation/Resources/PrivacyInfo.xcprivacy']} 20 | end 21 | -------------------------------------------------------------------------------- /ZIPFoundation.xcodeproj/ZIPFoundationTests_Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | CFBundleDevelopmentRegion 5 | en 6 | CFBundleExecutable 7 | $(EXECUTABLE_NAME) 8 | CFBundleIdentifier 9 | $(PRODUCT_BUNDLE_IDENTIFIER) 10 | CFBundleInfoDictionaryVersion 11 | 6.0 12 | CFBundleName 13 | $(PRODUCT_NAME) 14 | CFBundlePackageType 15 | BNDL 16 | CFBundleShortVersionString 17 | 1.0 18 | CFBundleSignature 19 | ???? 20 | CFBundleVersion 21 | $(CURRENT_PROJECT_VERSION) 22 | NSPrincipalClass 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /ZIPFoundation.xcodeproj/ZIPFoundation_Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | CFBundleDevelopmentRegion 5 | en 6 | CFBundleExecutable 7 | $(EXECUTABLE_NAME) 8 | CFBundleIdentifier 9 | $(PRODUCT_BUNDLE_IDENTIFIER) 10 | CFBundleInfoDictionaryVersion 11 | 6.0 12 | CFBundleName 13 | $(PRODUCT_NAME) 14 | CFBundlePackageType 15 | FMWK 16 | CFBundleShortVersionString 17 | 1.0 18 | CFBundleSignature 19 | ???? 20 | CFBundleVersion 21 | $(CURRENT_PROJECT_VERSION) 22 | NSPrincipalClass 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /ZIPFoundation.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ZIPFoundation.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ZIPFoundation.xcodeproj/xcshareddata/xcschemes/ZIPFoundation.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 38 | 39 | 40 | 41 | 43 | 49 | 50 | 51 | 52 | 53 | 63 | 64 | 70 | 71 | 72 | 73 | 79 | 80 | 86 | 87 | 88 | 89 | 91 | 92 | 95 | 96 | 97 | -------------------------------------------------------------------------------- /ZIPFoundation.xcodeproj/xcshareddata/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | SchemeUserState 5 | 6 | ZIPFoundation-Package.xcscheme 7 | 8 | 9 | SuppressBuildableAutocreation 10 | 11 | 12 | 13 | --------------------------------------------------------------------------------