From f38e9a9fb218fc15afe4e0381aea35311395c376 Mon Sep 17 00:00:00 2001 From: Francesco Paolo Severino <96546612+fpseverino@users.noreply.github.com> Date: Thu, 7 Nov 2024 12:36:18 +0100 Subject: [PATCH] Windows support and other various changes (#10) ### Issue #9 Originally authored by @compnerd in https://github.com/marmelroy/Zip/pull/246 - Add support for Windows - Add Windows CI tests - Drop support for Swift 5.8 - Adopt `swift-format` - Add CI for iOS and MUSL - Various bug fixes --- .gitattributes | 2 +- .github/CODEOWNERS | 1 + .github/workflows/test.yml | 44 +++ .swift-format | 70 ++++ Package.swift | 60 ++- README.md | 26 +- Sources/{Minizip => CMinizip}/crypt.c | 0 Sources/CMinizip/include/CMinizip.h | 17 + Sources/{Minizip => CMinizip}/include/crypt.h | 0 Sources/{Minizip => CMinizip}/include/ioapi.h | 0 Sources/CMinizip/include/module.modulemap | 4 + Sources/{Minizip => CMinizip}/include/unzip.h | 0 Sources/{Minizip => CMinizip}/include/zip.h | 0 Sources/{Minizip => CMinizip}/ioapi.c | 0 Sources/{Minizip => CMinizip}/unzip.c | 0 Sources/{Minizip => CMinizip}/zip.c | 0 Sources/Minizip/include/Minizip.h | 17 - Sources/Minizip/module/module.modulemap | 5 - Sources/Zip/ArchiveFile.swift | 47 +-- Sources/Zip/Date+dosDate.swift | 9 +- .../Zip/FileManager+ProcessedFilePath.swift | 64 ++++ Sources/Zip/QuickZip.swift | 120 +++--- Sources/Zip/URL+nativePath.swift | 11 + Sources/Zip/Zip.docc/Advanced.md | 2 +- Sources/Zip/Zip.docc/Documentation.md | 18 + Sources/Zip/Zip.swift | 358 ++++++++++-------- Sources/Zip/ZipCompression.swift | 4 +- Sources/Zip/ZipUtilities.swift | 104 ----- .../{Resources => TestResources}/3crBXeO.gif | Bin .../ZipTests/TestResources/PassKitTest.order | Bin 0 -> 14684 bytes .../ZipTests/TestResources/PassKitTest.pkpass | Bin 0 -> 29749 bytes .../{Resources => TestResources}/bb8.zip | Bin .../{Resources => TestResources}/kYkLkPf.gif | Bin .../pathTraversal.zip | Bin .../permissions.zip | Bin ...ple-swift-metrics-main-e6a00d36-finder.zip | Bin ...apple-swift-metrics-main-e6a00d36-test.zip | Bin ...prod-apple-swift-metrics-main-e6a00d36.zip | Bin .../unsupported_permissions.zip | Bin Tests/ZipTests/ZipTests.swift | 187 +++++++-- 40 files changed, 727 insertions(+), 443 deletions(-) create mode 100644 .github/CODEOWNERS create mode 100644 .swift-format rename Sources/{Minizip => CMinizip}/crypt.c (100%) create mode 100644 Sources/CMinizip/include/CMinizip.h rename Sources/{Minizip => CMinizip}/include/crypt.h (100%) rename Sources/{Minizip => CMinizip}/include/ioapi.h (100%) create mode 100644 Sources/CMinizip/include/module.modulemap rename Sources/{Minizip => CMinizip}/include/unzip.h (100%) rename Sources/{Minizip => CMinizip}/include/zip.h (100%) rename Sources/{Minizip => CMinizip}/ioapi.c (100%) rename Sources/{Minizip => CMinizip}/unzip.c (100%) rename Sources/{Minizip => CMinizip}/zip.c (100%) delete mode 100644 Sources/Minizip/include/Minizip.h delete mode 100644 Sources/Minizip/module/module.modulemap create mode 100644 Sources/Zip/FileManager+ProcessedFilePath.swift create mode 100644 Sources/Zip/URL+nativePath.swift delete mode 100644 Sources/Zip/ZipUtilities.swift rename Tests/ZipTests/{Resources => TestResources}/3crBXeO.gif (100%) create mode 100644 Tests/ZipTests/TestResources/PassKitTest.order create mode 100644 Tests/ZipTests/TestResources/PassKitTest.pkpass rename Tests/ZipTests/{Resources => TestResources}/bb8.zip (100%) rename Tests/ZipTests/{Resources => TestResources}/kYkLkPf.gif (100%) rename Tests/ZipTests/{Resources => TestResources}/pathTraversal.zip (100%) rename Tests/ZipTests/{Resources => TestResources}/permissions.zip (100%) rename Tests/ZipTests/{Resources => TestResources}/prod-apple-swift-metrics-main-e6a00d36-finder.zip (100%) rename Tests/ZipTests/{Resources => TestResources}/prod-apple-swift-metrics-main-e6a00d36-test.zip (100%) rename Tests/ZipTests/{Resources => TestResources}/prod-apple-swift-metrics-main-e6a00d36.zip (100%) rename Tests/ZipTests/{Resources => TestResources}/unsupported_permissions.zip (100%) diff --git a/.gitattributes b/.gitattributes index 29ae0767..fc54bed6 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1 @@ -Sources/Minizip/* linguist-vendored +Sources/CMinizip/* linguist-vendored diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 00000000..2f6e4e0e --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @fpseverino \ No newline at end of file diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3dbf46d0..424eda66 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -9,5 +9,49 @@ on: jobs: unit-tests: uses: vapor/ci/.github/workflows/run-unit-tests.yml@main + with: + with_linting: true + with_musl: true + ios_scheme_name: Zip secrets: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + + windows-unit: + if: ${{ !(github.event.pull_request.draft || false) }} + strategy: + fail-fast: false + matrix: + swift-version: + - 5.9 + - 5.10 + - 6.0 + include: + - { swift-version: 5.9, swift-branch: swift-5.9.2-release, swift-tag: 5.9.2-RELEASE } + - { swift-version: 5.10, swift-branch: swift-5.10.1-release, swift-tag: 5.10.1-RELEASE } + - { swift-version: 6.0, swift-branch: swift-6.0.1-release, swift-tag: 6.0.1-RELEASE } + runs-on: windows-latest + timeout-minutes: 60 + steps: + - name: Install Windows Swift toolchain + uses: compnerd/gha-setup-swift@main + with: + branch: ${{ matrix.swift-branch }} + tag: ${{ matrix.swift-tag }} + - name: Download zlib + run: | + curl -L -o zlib.zip https://www.zlib.net/zlib131.zip + mkdir zlib-131 + tar -xf zlib.zip -C zlib-131 --strip-components=1 + - name: Build and install zlib + run: | + cd zlib-131 + mkdir build + cd build + cmake .. + cmake --build . --config Release + cmake --install . --prefix ../install + - name: Check out code + uses: actions/checkout@v4 + - name: Run unit tests + run: | + swift test -Xcc -I'C:/Program Files (x86)/zlib/include' -Xlinker -L'C:/Program Files (x86)/zlib/lib' diff --git a/.swift-format b/.swift-format new file mode 100644 index 00000000..dea65687 --- /dev/null +++ b/.swift-format @@ -0,0 +1,70 @@ +{ + "fileScopedDeclarationPrivacy": { + "accessLevel": "private" + }, + "indentation": { + "spaces": 4 + }, + "indentConditionalCompilationBlocks": true, + "indentSwitchCaseLabels": false, + "lineBreakAroundMultilineExpressionChainComponents": false, + "lineBreakBeforeControlFlowKeywords": false, + "lineBreakBeforeEachArgument": false, + "lineBreakBeforeEachGenericRequirement": false, + "lineLength": 140, + "maximumBlankLines": 1, + "multiElementCollectionTrailingCommas": true, + "noAssignmentInExpressions": { + "allowedFunctions": [ + "XCTAssertNoThrow" + ] + }, + "prioritizeKeepingFunctionOutputTogether": false, + "respectsExistingLineBreaks": true, + "rules": { + "AllPublicDeclarationsHaveDocumentation": false, + "AlwaysUseLiteralForEmptyCollectionInit": false, + "AlwaysUseLowerCamelCase": false, + "AmbiguousTrailingClosureOverload": true, + "BeginDocumentationCommentWithOneLineSummary": false, + "DoNotUseSemicolons": true, + "DontRepeatTypeInStaticProperties": true, + "FileScopedDeclarationPrivacy": true, + "FullyIndirectEnum": true, + "GroupNumericLiterals": true, + "IdentifiersMustBeASCII": true, + "NeverForceUnwrap": false, + "NeverUseForceTry": false, + "NeverUseImplicitlyUnwrappedOptionals": false, + "NoAccessLevelOnExtensionDeclaration": true, + "NoAssignmentInExpressions": true, + "NoBlockComments": true, + "NoCasesWithOnlyFallthrough": true, + "NoEmptyTrailingClosureParentheses": true, + "NoLabelsInCasePatterns": true, + "NoLeadingUnderscores": false, + "NoParensAroundConditions": true, + "NoPlaygroundLiterals": true, + "NoVoidReturnOnFunctionSignature": true, + "OmitExplicitReturns": false, + "OneCasePerLine": true, + "OneVariableDeclarationPerLine": true, + "OnlyOneTrailingClosureArgument": true, + "OrderedImports": true, + "ReplaceForEachWithForLoop": true, + "ReturnVoidInsteadOfEmptyTuple": true, + "TypeNamesShouldBeCapitalized": true, + "UseEarlyExits": false, + "UseExplicitNilCheckInConditions": true, + "UseLetInEveryBoundCaseVariable": true, + "UseShorthandTypeNames": true, + "UseSingleLinePropertyGetter": true, + "UseSynthesizedInitializer": true, + "UseTripleSlashForDocumentationComments": true, + "UseWhereClausesInForLoops": false, + "ValidateDocumentationComments": false + }, + "spacesAroundRangeFormationOperators": false, + "tabWidth": 8, + "version": 1 +} \ No newline at end of file diff --git a/Package.swift b/Package.swift index db8f5915..54481327 100644 --- a/Package.swift +++ b/Package.swift @@ -1,6 +1,12 @@ -// swift-tools-version:5.8 +// swift-tools-version:5.9 import PackageDescription +#if canImport(Darwin) || compiler(<6.0) + import Foundation +#else + import FoundationEssentials +#endif + let package = Package( name: "Zip", products: [ @@ -8,35 +14,55 @@ let package = Package( ], targets: [ .target( - name: "Minizip", - exclude: ["module"], - swiftSettings: [ - .enableUpcomingFeature("ConciseMagicFile"), + name: "CMinizip", + cSettings: [ + .define("_CRT_SECURE_NO_WARNINGS", .when(platforms: [.windows])) ], - linkerSettings: [ - .linkedLibrary("z") - ] + swiftSettings: swiftSettings ), .target( name: "Zip", dependencies: [ - .target(name: "Minizip"), + .target(name: "CMinizip") + ], + cSettings: [ + .define("_CRT_SECURE_NO_WARNINGS", .when(platforms: [.windows])) ], - swiftSettings: [ - .enableUpcomingFeature("ConciseMagicFile"), - ] + swiftSettings: swiftSettings ), .testTarget( name: "ZipTests", dependencies: [ - .target(name: "Zip"), + .target(name: "Zip") ], resources: [ - .copy("Resources"), + .copy("TestResources") ], - swiftSettings: [ - .enableUpcomingFeature("ConciseMagicFile"), - ] + swiftSettings: swiftSettings ), ] ) + +var swiftSettings: [SwiftSetting] { + [ + .enableUpcomingFeature("ExistentialAny"), + .enableUpcomingFeature("ConciseMagicFile"), + .enableUpcomingFeature("ForwardTrailingClosures"), + .enableUpcomingFeature("DisableOutwardActorInference"), + .enableUpcomingFeature("StrictConcurrency"), + .enableExperimentalFeature("StrictConcurrency=complete"), + ] +} + +if let target = package.targets.filter({ $0.name == "CMinizip" }).first { + #if os(Windows) + if ProcessInfo.processInfo.environment["ZIP_USE_DYNAMIC_ZLIB"] == nil { + target.cSettings?.append(contentsOf: [.define("ZLIB_STATIC")]) + target.linkerSettings = [.linkedLibrary("zlibstatic")] + } else { + target.linkerSettings = [.linkedLibrary("zlib")] + } + #else + target.linkerSettings = [.linkedLibrary("z")] + #endif +} diff --git a/README.md b/README.md index 216b7802..086ca6d6 100644 --- a/README.md +++ b/README.md @@ -12,23 +12,41 @@ - Swift 5.8+ + Swift 5.9+
-A framework for zipping and unzipping files in Swift. +📂 A framework for zipping and unzipping files in Swift. Simple and quick to use. Built on top of [Minizip 1.2](https://github.com/zlib-ng/minizip-ng/tree/1.2). +## Overview + +### Getting Started + Use the SPM string to easily include the dependendency in your `Package.swift` file. ```swift .package(url: "https://github.com/vapor-community/Zip.git", from: "2.2.0") ``` -## Usage +and add it to your target's dependencies: + +```swift +.product(name: "Zip", package: "zip") +``` + +### Supported Platforms + +Zip supports all platforms supported by Swift 5.9 and later. + +To use Zip on Windows, you need to pass an available build of `zlib` to the build via extended flags. For example: + +```shell +swift build -Xcc -I'C:/pathTo/zlib/include' -Xlinker -L'C:/pathTo/zlib/lib' +``` ### Quick Functions @@ -56,7 +74,7 @@ import Zip do { let filePath = Bundle.main.url(forResource: "file", withExtension: "zip")! let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] - try Zip.unzipFile(filePath, destination: documentsDirectory, overwrite: true, password: "password") { progress in + try Zip.unzipFile(filePath, destination: documentsDirectory, password: "password") { progress in print(progress) } diff --git a/Sources/Minizip/crypt.c b/Sources/CMinizip/crypt.c similarity index 100% rename from Sources/Minizip/crypt.c rename to Sources/CMinizip/crypt.c diff --git a/Sources/CMinizip/include/CMinizip.h b/Sources/CMinizip/include/CMinizip.h new file mode 100644 index 00000000..f65ac0de --- /dev/null +++ b/Sources/CMinizip/include/CMinizip.h @@ -0,0 +1,17 @@ +// +// CMinizip.h +// Zip +// +// Created by Florian Friedrich on 3/27/19. +// Copyright © 2019 Roy Marmelstein. All rights reserved. +// + +#ifndef CMinizip_h +#define CMinizip_h + +#include "ioapi.h" +#include "crypt.h" +#include "unzip.h" +#include "zip.h" + +#endif /* CMinizip_h */ diff --git a/Sources/Minizip/include/crypt.h b/Sources/CMinizip/include/crypt.h similarity index 100% rename from Sources/Minizip/include/crypt.h rename to Sources/CMinizip/include/crypt.h diff --git a/Sources/Minizip/include/ioapi.h b/Sources/CMinizip/include/ioapi.h similarity index 100% rename from Sources/Minizip/include/ioapi.h rename to Sources/CMinizip/include/ioapi.h diff --git a/Sources/CMinizip/include/module.modulemap b/Sources/CMinizip/include/module.modulemap new file mode 100644 index 00000000..eb1878bf --- /dev/null +++ b/Sources/CMinizip/include/module.modulemap @@ -0,0 +1,4 @@ +module CMinizip [system][extern_c] { + header "CMinizip.h" + export * +} diff --git a/Sources/Minizip/include/unzip.h b/Sources/CMinizip/include/unzip.h similarity index 100% rename from Sources/Minizip/include/unzip.h rename to Sources/CMinizip/include/unzip.h diff --git a/Sources/Minizip/include/zip.h b/Sources/CMinizip/include/zip.h similarity index 100% rename from Sources/Minizip/include/zip.h rename to Sources/CMinizip/include/zip.h diff --git a/Sources/Minizip/ioapi.c b/Sources/CMinizip/ioapi.c similarity index 100% rename from Sources/Minizip/ioapi.c rename to Sources/CMinizip/ioapi.c diff --git a/Sources/Minizip/unzip.c b/Sources/CMinizip/unzip.c similarity index 100% rename from Sources/Minizip/unzip.c rename to Sources/CMinizip/unzip.c diff --git a/Sources/Minizip/zip.c b/Sources/CMinizip/zip.c similarity index 100% rename from Sources/Minizip/zip.c rename to Sources/CMinizip/zip.c diff --git a/Sources/Minizip/include/Minizip.h b/Sources/Minizip/include/Minizip.h deleted file mode 100644 index ef753eb9..00000000 --- a/Sources/Minizip/include/Minizip.h +++ /dev/null @@ -1,17 +0,0 @@ -// -// Minizip.h -// Zip -// -// Created by Florian Friedrich on 3/27/19. -// Copyright © 2019 Roy Marmelstein. All rights reserved. -// - -#ifndef Minizip_h -#define Minizip_h - -#import "ioapi.h" -#import "crypt.h" -#import "unzip.h" -#import "zip.h" - -#endif /* Minizip_h */ diff --git a/Sources/Minizip/module/module.modulemap b/Sources/Minizip/module/module.modulemap deleted file mode 100644 index 59eaacd7..00000000 --- a/Sources/Minizip/module/module.modulemap +++ /dev/null @@ -1,5 +0,0 @@ -module Minizip [system][extern_c] { - header "../include/Minizip.h" - link "z" - export * -} diff --git a/Sources/Zip/ArchiveFile.swift b/Sources/Zip/ArchiveFile.swift index 9b3335b3..2b032b5c 100644 --- a/Sources/Zip/ArchiveFile.swift +++ b/Sources/Zip/ArchiveFile.swift @@ -1,5 +1,5 @@ +@_implementationOnly import CMinizip import Foundation -@_implementationOnly import Minizip /// Defines data saved in memory that will be archived as a file. public struct ArchiveFile { @@ -34,29 +34,25 @@ public struct ArchiveFile { } extension Zip { - /** - Creates a zip file from an array of ``ArchiveFile``s - - - Parameters: - - archiveFiles: Array of ``ArchiveFile``. - - zipFilePath: Destination `URL`, should lead to a `.zip` filepath. - - password: The optional password string. - - compression: The compression strategy to use. - - progress: A progress closure called after unzipping each file in the archive. Double value betweem 0 and 1. - - - Throws: `ZipError.zipFail` if zipping fails. - - > Note: Supports implicit progress composition. - */ + /// Creates a zip file from an array of ``ArchiveFile``s. + /// + /// - Parameters: + /// - archiveFiles: Array of ``ArchiveFile``. + /// - zipFilePath: Destination `URL`, should lead to a `.zip` filepath. + /// - password: The optional password string. + /// - compression: The compression strategy to use. + /// - progress: A progress closure called after zipping each file in the archive. A `Double` value between 0 and 1. + /// + /// - Throws: ``ZipError/zipFail`` if zipping fails. + /// + /// > Note: Supports implicit progress composition. public class func zipData( archiveFiles: [ArchiveFile], zipFilePath: URL, password: String? = nil, compression: ZipCompression = .DefaultCompression, - progress: ((_ progress: Double) -> ())? = nil + progress: ((_ progress: Double) -> Void)? = nil ) throws { - let destinationPath = zipFilePath.path - // Progress handler set up var currentPosition: Int = 0 var totalSize: Int = 0 @@ -71,7 +67,7 @@ extension Zip { progressTracker.kind = ProgressKind.file // Begin Zipping - let zip = zipOpen(destinationPath, APPEND_STATUS_CREATE) + let zip = zipOpen(zipFilePath.nativePath, APPEND_STATUS_CREATE) for archiveFile in archiveFiles { // Skip empty data @@ -111,21 +107,18 @@ extension Zip { // Update progress handler currentPosition += archiveFile.data.count - - if let progressHandler = progress { - progressHandler((Double(currentPosition/totalSize))) + if let progress { + progress(Double(currentPosition / totalSize)) } - progressTracker.completedUnitCount = Int64(currentPosition) } zipClose(zip, nil) // Completed. Update progress handler. - if let progressHandler = progress { - progressHandler(1.0) + if let progress { + progress(1.0) } - progressTracker.completedUnitCount = Int64(totalSize) } -} \ No newline at end of file +} diff --git a/Sources/Zip/Date+dosDate.swift b/Sources/Zip/Date+dosDate.swift index ab5a467a..d4834282 100644 --- a/Sources/Zip/Date+dosDate.swift +++ b/Sources/Zip/Date+dosDate.swift @@ -1,8 +1,13 @@ -import Foundation +#if canImport(Darwin) || compiler(<6.0) + import Foundation +#else + import FoundationEssentials +#endif extension Date { var dosDate: UInt32 { let components = Calendar.current.dateComponents([.year, .month, .day, .hour, .minute, .second], from: self) + let year = UInt32(components.year! - 1980) << 25 let month = UInt32(components.month!) << 21 let day = UInt32(components.day!) << 16 @@ -12,4 +17,4 @@ extension Date { return year | month | day | hour | minute | second } -} \ No newline at end of file +} diff --git a/Sources/Zip/FileManager+ProcessedFilePath.swift b/Sources/Zip/FileManager+ProcessedFilePath.swift new file mode 100644 index 00000000..0b399ec6 --- /dev/null +++ b/Sources/Zip/FileManager+ProcessedFilePath.swift @@ -0,0 +1,64 @@ +import Foundation + +extension FileManager { + struct ProcessedFilePath { + let filePathURL: URL + let fileName: String? + + var filePath: String { + filePathURL.nativePath + } + } + + /// Process zip paths. + /// + /// - Parameter roots: Paths as `URL`. + /// + /// - Returns: Array of ``ProcessedFilePath`` structs. + static func fileSubPaths(from roots: [URL]) -> [ProcessedFilePath] { + var processedFilePaths = [ProcessedFilePath]() + for pathURL in roots { + var isDirectory: ObjCBool = false + _ = FileManager.default.fileExists( + atPath: pathURL.nativePath, + isDirectory: &isDirectory + ) + + if !isDirectory.boolValue { + let processedPath = ProcessedFilePath(filePathURL: pathURL, fileName: pathURL.lastPathComponent) + processedFilePaths.append(processedPath) + } else { + let directoryContents = Self.expandDirectoryFilePath(pathURL) + processedFilePaths.append(contentsOf: directoryContents) + } + } + return processedFilePaths + } + + /// Expand directory contents and parse them into ``ProcessedFilePath`` structs. + /// + /// - Parameter directory: Path of folder as `URL`. + /// + /// - Returns: Array of ``ProcessedFilePath`` structs. + private static func expandDirectoryFilePath(_ directory: URL) -> [ProcessedFilePath] { + var processedFilePaths = [ProcessedFilePath]() + if let enumerator = FileManager.default.enumerator(atPath: directory.nativePath) { + while let filePathComponent = enumerator.nextObject() as? String { + let pathURL = directory.appendingPathComponent(filePathComponent) + + var isDirectory: ObjCBool = false + _ = FileManager.default.fileExists( + atPath: pathURL.nativePath, + isDirectory: &isDirectory + ) + + if !isDirectory.boolValue { + let fileName = (directory.lastPathComponent as NSString).appendingPathComponent(filePathComponent) + let processedPath = ProcessedFilePath(filePathURL: pathURL, fileName: fileName) + processedFilePaths.append(processedPath) + } + } + } + return processedFilePaths + } +} diff --git a/Sources/Zip/QuickZip.swift b/Sources/Zip/QuickZip.swift index 3870a327..5d7fdc00 100644 --- a/Sources/Zip/QuickZip.swift +++ b/Sources/Zip/QuickZip.swift @@ -6,85 +6,77 @@ // Copyright © 2016 Roy Marmelstein. All rights reserved. // -import Foundation +#if canImport(Darwin) || compiler(<6.0) + import Foundation +#else + import FoundationEssentials +#endif -extension Zip { - /** - Quickly unzips a file. - - Unzips to a new folder inside the app's documents folder with the zip file's name. - - - Parameter path: Path of zipped file. - - - Throws: `ZipError.unzipFail` if unzipping fails or `ZipError.fileNotFound` if file is not found. - - - Returns: `URL` of the destination folder. - */ +extension Zip { + /// Unzips a file with less configuration. + /// + /// Unzips to a new folder inside the temporary directory with the zip file's name. + /// + /// - Parameter path: Path of zipped file. + /// + /// - Throws: ``ZipError/unzipFail`` if unzipping fails or ``ZipError/fileNotFound`` if file is not found. + /// + /// - Returns: `URL` of the destination folder. public class func quickUnzipFile(_ path: URL) throws -> URL { return try quickUnzipFile(path, progress: nil) } - - /** - Quickly unzips a file. - - Unzips to a new folder inside the app's documents folder with the zip file's name. - - - Parameters: - - path: Path of zipped file. - - progress: An optional progress closure called after unzipping each file in the archive. A `Double` value between 0 and 1. - - - Throws: `ZipError.unzipFail` if unzipping fails or `ZipError.fileNotFound` if file is not found. - - > Note: Supports implicit progress composition. - - - Returns: `URL` of the destination folder. - */ - public class func quickUnzipFile(_ path: URL, progress: ((_ progress: Double) -> ())?) throws -> URL { - let destinationUrl = FileManager.default.temporaryDirectory - .appendingPathComponent(path.deletingPathExtension().lastPathComponent, isDirectory: true) + + /// Unzips a file with less configuration. + /// + /// Unzips to a new folder inside the temporary directory with the zip file's name. + /// + /// - Parameters: + /// - path: Path of zipped file. + /// - progress: An optional progress closure called after unzipping each file in the archive. A `Double` value between 0 and 1. + /// + /// - Throws: ``ZipError/unzipFail`` if unzipping fails or ``ZipError/fileNotFound`` if file is not found. + /// + /// > Note: Supports implicit progress composition. + /// + /// - Returns: `URL` of the destination folder. + public class func quickUnzipFile(_ path: URL, progress: ((_ progress: Double) -> Void)?) throws -> URL { + let destinationUrl = FileManager.default.temporaryDirectory.appendingPathComponent( + path.deletingPathExtension().lastPathComponent, isDirectory: true + ) try self.unzipFile(path, destination: destinationUrl, progress: progress) return destinationUrl } - /** - Quickly zips files. - - - Parameters: - - paths: Array of `URL` filepaths. - - fileName: File name for the resulting zip file. - - - Throws: `ZipError.zipFail` if zipping fails. - - > Note: Supports implicit progress composition. - - - Returns: `URL` of the destination folder. - */ + /// Zips files with less configuration. + /// + /// - Parameters: + /// - paths: Array of `URL` filepaths. + /// - fileName: File name for the resulting zip file. + /// + /// - Throws: ``ZipError/zipFail`` if zipping fails. + /// + /// - Returns: `URL` of the destination folder. public class func quickZipFiles(_ paths: [URL], fileName: String) throws -> URL { return try quickZipFiles(paths, fileName: fileName, progress: nil) } - - /** - Quickly zips files. - - - Parameters: - - paths: Array of `URL` filepaths. - - fileName: File name for the resulting zip file. - - progress: An optional progress closure called after unzipping each file in the archive. A `Double` value between 0 and 1. - - - Throws: `ZipError.zipFail` if zipping fails. - - > Note: Supports implicit progress composition. - - - Returns: `URL` of the destination folder. - */ - public class func quickZipFiles(_ paths: [URL], fileName: String, progress: ((_ progress: Double) -> ())?) throws -> URL { + + /// Zips files with less configuration. + /// + /// - Parameters: + /// - paths: Array of `URL` filepaths. + /// - fileName: File name for the resulting zip file. + /// - progress: An optional progress closure called after unzipping each file in the archive. A `Double` value between 0 and 1. + /// + /// - Throws: ``ZipError/zipFail`` if zipping fails. + /// + /// > Note: Supports implicit progress composition. + /// + /// - Returns: `URL` of the destination folder. + public class func quickZipFiles(_ paths: [URL], fileName: String, progress: ((_ progress: Double) -> Void)?) throws -> URL { var fileNameWithExtension = fileName if !fileName.hasSuffix(".zip") { fileNameWithExtension += ".zip" } - - print("fileNameWithExtension: \(fileNameWithExtension)") - let destinationUrl = FileManager.default.temporaryDirectory.appendingPathComponent(fileNameWithExtension) try self.zipFiles(paths: paths, zipFilePath: destinationUrl, progress: progress) return destinationUrl diff --git a/Sources/Zip/URL+nativePath.swift b/Sources/Zip/URL+nativePath.swift new file mode 100644 index 00000000..f15e2a5d --- /dev/null +++ b/Sources/Zip/URL+nativePath.swift @@ -0,0 +1,11 @@ +#if canImport(Darwin) || compiler(<6.0) + import Foundation +#else + import FoundationEssentials +#endif + +extension URL { + var nativePath: String { + return withUnsafeFileSystemRepresentation { String(cString: $0!) } + } +} diff --git a/Sources/Zip/Zip.docc/Advanced.md b/Sources/Zip/Zip.docc/Advanced.md index 59e77c04..6dbadce0 100644 --- a/Sources/Zip/Zip.docc/Advanced.md +++ b/Sources/Zip/Zip.docc/Advanced.md @@ -15,7 +15,7 @@ import Zip do { let filePath = Bundle.main.url(forResource: "file", withExtension: "zip")! let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] - try Zip.unzipFile(filePath, destination: documentsDirectory, overwrite: true, password: "password") { progress in + try Zip.unzipFile(filePath, destination: documentsDirectory, password: "password") { progress in print(progress) } diff --git a/Sources/Zip/Zip.docc/Documentation.md b/Sources/Zip/Zip.docc/Documentation.md index bc6b32e5..8a0f49dd 100644 --- a/Sources/Zip/Zip.docc/Documentation.md +++ b/Sources/Zip/Zip.docc/Documentation.md @@ -15,12 +15,30 @@ A framework for zipping and unzipping files in Swift. Simple and quick to use. Built on top of [Minizip 1.2](https://github.com/zlib-ng/minizip-ng/tree/1.2). +### Getting Started + Use the SPM string to easily include the dependendency in your `Package.swift` file. ```swift .package(url: "https://github.com/vapor-community/Zip.git", from: "2.2.0") ``` +and add it to your target's dependencies: + +```swift +.product(name: "Zip", package: "zip") +``` + +### Supported Platforms + +Zip supports all platforms supported by Swift 5.9 and later. + +To use Zip on Windows, you need to pass an available build of `zlib` to the build via extended flags. For example: + +```shell +swift build -Xcc -I'C:/pathTo/zlib/include' -Xlinker -L'C:/pathTo/zlib/lib' +``` + ## Topics ### Essentials diff --git a/Sources/Zip/Zip.swift b/Sources/Zip/Zip.swift index 16b90807..b6cf91ba 100644 --- a/Sources/Zip/Zip.swift +++ b/Sources/Zip/Zip.swift @@ -6,149 +6,180 @@ // Copyright © 2015 Roy Marmelstein. All rights reserved. // +@_implementationOnly import CMinizip import Foundation -@_implementationOnly import Minizip /// Main class that handles zipping and unzipping of files. public class Zip { // Set of vaild file extensions - internal static var customFileExtensions: Set = [] + #if compiler(>=5.10) + nonisolated(unsafe) private static var customFileExtensions: Set = [] + #else + private static var customFileExtensions: Set = [] + #endif + private static let lock = NSLock() @available(*, deprecated, message: "Do not use this initializer. Zip is a utility class and should not be instantiated.") - public init () {} - - /** - Unzips a file. - - - Parameters: - - zipFilePath: Local file path of zipped file. - - destination: Local file path to unzip to. - - overwrite: Indicates whether or not to overwrite files at the destination path. - - password: Optional password if file is protected. - - progress: A progress closure called after unzipping each file in the archive. A `Double` value between 0 and 1. - - fileOutputHandler: A closure called after each file is unzipped. A `URL` value of the unzipped file. - - - Throws: `ZipError.unzipFail` if unzipping fails or if fail is not found. - - > Note: Supports implicit progress composition - */ + public init() {} + + /// Unzips a file. + /// + /// - Parameters: + /// - zipFilePath: Local file path of zipped file. + /// - destination: Local file path to unzip to. + /// - overwrite: Indicates whether or not to overwrite files at the destination path. + /// - password: Optional password if file is protected. + /// - progress: A progress closure called after unzipping each file in the archive. A `Double` value between 0 and 1. + /// - fileOutputHandler: A closure called after each file is unzipped. A `URL` value of the unzipped file. + /// + /// - Throws: ``ZipError/unzipFail`` if unzipping fails or if fail is not found. + /// + /// > Note: Supports implicit progress composition. public class func unzipFile( _ zipFilePath: URL, destination: URL, overwrite: Bool = true, password: String? = nil, - progress: ((_ progress: Double) -> ())? = nil, + progress: ((_ progress: Double) -> Void)? = nil, fileOutputHandler: ((_ unzippedFile: URL) -> Void)? = nil ) throws { - let fileManager = FileManager.default - // Check whether a zip file exists at path. - let path = zipFilePath.path - if fileManager.fileExists(atPath: path) == false || !isValidFileExtension(zipFilePath.pathExtension) { + let path = zipFilePath.nativePath + if !FileManager.default.fileExists(atPath: path) || !isValidFileExtension(zipFilePath.pathExtension) { throw ZipError.fileNotFound } - - // Unzip set up - var ret: Int32 = 0 - var crc_ret: Int32 = 0 - let bufferSize: UInt32 = 4096 - var buffer = Array(repeating: 0, count: Int(bufferSize)) - + // Progress handler set up var totalSize: Double = 0.0 var currentPosition: Double = 0.0 - let fileAttributes = try fileManager.attributesOfItem(atPath: path) + let fileAttributes = try FileManager.default.attributesOfItem(atPath: path) if let attributeFileSize = fileAttributes[FileAttributeKey.size] as? Double { totalSize += attributeFileSize } - + let progressTracker = Progress(totalUnitCount: Int64(totalSize)) progressTracker.isCancellable = false progressTracker.isPausable = false progressTracker.kind = ProgressKind.file - + // Begin unzipping let zip = unzOpen64(path) defer { unzClose(zip) } if unzGoToFirstFile(zip) != UNZ_OK { throw ZipError.unzipFail } + + #if os(Windows) + var fileNames = Set() + #endif + + var buffer = [CUnsignedChar](repeating: 0, count: 4096) + var result: Int32 + repeat { if let cPassword = password?.cString(using: String.Encoding.ascii) { - ret = unzOpenCurrentFilePassword(zip, cPassword) + guard unzOpenCurrentFilePassword(zip, cPassword) == UNZ_OK else { + throw ZipError.unzipFail + } } else { - ret = unzOpenCurrentFile(zip); - } - if ret != UNZ_OK { - throw ZipError.unzipFail + guard unzOpenCurrentFile(zip) == UNZ_OK else { + throw ZipError.unzipFail + } } + var fileInfo = unz_file_info64() - memset(&fileInfo, 0, MemoryLayout.size) - ret = unzGetCurrentFileInfo64(zip, &fileInfo, nil, 0, nil, 0, nil, 0) - if ret != UNZ_OK { + guard unzGetCurrentFileInfo64(zip, &fileInfo, nil, 0, nil, 0, nil, 0) == UNZ_OK else { unzCloseCurrentFile(zip) throw ZipError.unzipFail } + currentPosition += Double(fileInfo.compressed_size) + let fileNameSize = Int(fileInfo.size_filename) + 1 let fileName = UnsafeMutablePointer.allocate(capacity: fileNameSize) + defer { fileName.deallocate() } unzGetCurrentFileInfo64(zip, &fileInfo, fileName, UInt16(fileNameSize), nil, 0, nil, 0) fileName[Int(fileInfo.size_filename)] = 0 var pathString = String(cString: fileName) - guard pathString.count > 0 else { + + #if os(Windows) + // Windows Reserved Characters + let reservedCharacters: CharacterSet = ["<", ">", ":", "\"", "|", "?", "*"] + + if pathString.rangeOfCharacter(from: reservedCharacters) != nil { + pathString = pathString.components(separatedBy: reservedCharacters).joined(separator: "_") + + let pathExtension = (pathString as NSString).pathExtension + let pathWithoutExtension = (pathString as NSString).deletingPathExtension + var counter = 1 + while fileNames.contains(pathString) { + let newFileName = "\(pathWithoutExtension) (\(counter))" + pathString = pathExtension.isEmpty ? newFileName : newFileName.appendingPathExtension(pathExtension) ?? newFileName + counter += 1 + } + } + + fileNames.insert(pathString) + #endif + + guard !pathString.isEmpty else { throw ZipError.unzipFail } - var isDirectory = false - let fileInfoSizeFileName = Int(fileInfo.size_filename-1) - if (fileName[fileInfoSizeFileName] == "/".cString(using: String.Encoding.utf8)?.first || fileName[fileInfoSizeFileName] == "\\".cString(using: String.Encoding.utf8)?.first) { - isDirectory = true; - } - free(fileName) if pathString.rangeOfCharacter(from: CharacterSet(charactersIn: "/\\")) != nil { pathString = pathString.replacingOccurrences(of: "\\", with: "/") } - let fullPath = destination.appendingPathComponent(pathString).standardized.path - // `.standardized` removes any `..` to move a level up. + let fullPath = destination.appendingPathComponent(pathString).standardizedFileURL.nativePath + + // `.standardizedFileURL` removes any `..` to move a level up. // If we then check that the `fullPath` starts with the destination directory we know we are not extracting "outside" the destination. - guard fullPath.starts(with: destination.standardized.path) else { + guard fullPath.starts(with: destination.standardizedFileURL.nativePath) else { throw ZipError.unzipFail } - let creationDate = Date() let directoryAttributes: [FileAttributeKey: Any]? - #if os(Linux) && swift(<6.0) - // On Linux, setting attributes is not yet really implemented. - // In Swift 4.2, the only settable attribute is `.posixPermissions`. - // See https://github.com/apple/swift-corelibs-foundation/blob/swift-4.2-branch/Foundation/FileManager.swift#L182-L196 + #if (os(Linux) || os(Windows)) && compiler(<6.0) directoryAttributes = nil #else + let creationDate = Date() directoryAttributes = [ .creationDate: creationDate, - .modificationDate: creationDate + .modificationDate: creationDate, ] #endif + let isDirectory = + fileName[Int(fileInfo.size_filename - 1)] == "/".cString(using: String.Encoding.utf8)?.first + || fileName[Int(fileInfo.size_filename - 1)] == "\\".cString(using: String.Encoding.utf8)?.first + do { + try FileManager.default.createDirectory( + atPath: (fullPath as NSString).deletingLastPathComponent, + withIntermediateDirectories: true, + attributes: directoryAttributes + ) + if isDirectory { - try fileManager.createDirectory(atPath: fullPath, withIntermediateDirectories: true, attributes: directoryAttributes) - } else { - let parentDirectory = (fullPath as NSString).deletingLastPathComponent - try fileManager.createDirectory(atPath: parentDirectory, withIntermediateDirectories: true, attributes: directoryAttributes) + try FileManager.default.createDirectory( + atPath: fullPath, + withIntermediateDirectories: false, + attributes: directoryAttributes + ) } } catch {} - if fileManager.fileExists(atPath: fullPath) && !isDirectory && !overwrite { + + if FileManager.default.fileExists(atPath: fullPath) && !isDirectory && !overwrite { unzCloseCurrentFile(zip) - ret = unzGoToNextFile(zip) + unzGoToNextFile(zip) } var writeBytes: UInt64 = 0 let filePointer: UnsafeMutablePointer? = fopen(fullPath, "wb") while let filePointer { - let readBytes = unzReadCurrentFile(zip, &buffer, bufferSize) + let readBytes = unzReadCurrentFile(zip, &buffer, UInt32(buffer.count)) guard readBytes > 0 else { break } guard fwrite(buffer, Int(readBytes), 1, filePointer) == 1 else { throw ZipError.unzipFail @@ -158,10 +189,10 @@ public class Zip { if let filePointer { fclose(filePointer) } - crc_ret = unzCloseCurrentFile(zip) - if crc_ret == UNZ_CRCERROR { + guard unzCloseCurrentFile(zip) != UNZ_CRCERROR else { throw ZipError.unzipFail } + guard writeBytes == fileInfo.uncompressed_size else { throw ZipError.unzipFail } @@ -169,108 +200,96 @@ public class Zip { // Set file permissions from current `fileInfo` if fileInfo.external_fa != 0 { let permissions = (fileInfo.external_fa >> 16) & 0x1FF - // We will devifne a valid permission range between Owner read only to full access + // We will define a valid permission range between Owner read only to full access if permissions >= 0o400 && permissions <= 0o777 { do { - try fileManager.setAttributes([.posixPermissions : permissions], ofItemAtPath: fullPath) + try FileManager.default.setAttributes([.posixPermissions: permissions], ofItemAtPath: fullPath) } catch { print("Failed to set permissions to file \(fullPath), error: \(error)") } } } - ret = unzGoToNextFile(zip) - + result = unzGoToNextFile(zip) + // Update progress handler - if let progressHandler = progress { - progressHandler((currentPosition / totalSize)) + if let progress { + progress(currentPosition / totalSize) } - - if let fileHandler = fileOutputHandler, - let encodedString = fullPath.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed), - let fileUrl = URL(string: encodedString) { - fileHandler(fileUrl) + + if let fileOutputHandler { + fileOutputHandler(URL(fileURLWithPath: fullPath, isDirectory: false)) } - + progressTracker.completedUnitCount = Int64(currentPosition) - - } while (ret == UNZ_OK && ret != UNZ_END_OF_LIST_OF_FILE) - + } while result == UNZ_OK && result != UNZ_END_OF_LIST_OF_FILE + // Completed. Update progress handler. - if let progressHandler = progress { - progressHandler(1.0) + if let progress { + progress(1.0) } - progressTracker.completedUnitCount = Int64(totalSize) } - - /** - Zips a group of files. - - - Parameters: - - paths: Array of `URL` filepaths. - - zipFilePath: Destination `URL`, should lead to a `.zip` filepath. - - password: The optional password string. - - compression: The compression strategy to use. - - progress: A progress closure called after unzipping each file in the archive. A `Double` value between 0 and 1. - - - Throws: `ZipError.zipFail` if zipping fails. - - > Note: Supports implicit progress composition - */ + + /// Zips a group of files. + /// + /// - Parameters: + /// - paths: Array of `URL` filepaths. + /// - zipFilePath: Destination `URL`, should lead to a `.zip` filepath. + /// - password: The optional password string. + /// - compression: The compression strategy to use. + /// - progress: A progress closure called after unzipping each file in the archive. A `Double` value between 0 and 1. + /// + /// - Throws: ``ZipError/zipFail`` if zipping fails. + /// + /// > Note: Supports implicit progress composition. public class func zipFiles( paths: [URL], zipFilePath: URL, password: String? = nil, compression: ZipCompression = .DefaultCompression, - progress: ((_ progress: Double) -> ())? = nil + progress: ((_ progress: Double) -> Void)? = nil ) throws { - let fileManager = FileManager.default - - let processedPaths = ZipUtilities().processZipPaths(paths) - - // Zip set up - let chunkSize: Int = 16384 - + let processedPaths = FileManager.fileSubPaths(from: paths) + + let chunkSize = 16384 + // Progress handler set up - var currentPosition: Double = 0.0 - var totalSize: Double = 0.0 + var currentPosition = 0.0 + var totalSize = 0.0 // Get `totalSize` for progress handler for path in processedPaths { do { - let filePath = path.filePath() - let fileAttributes = try fileManager.attributesOfItem(atPath: filePath) - let fileSize = fileAttributes[FileAttributeKey.size] as? Double - if let fileSize { + let fileAttributes = try FileManager.default.attributesOfItem(atPath: path.filePath) + if let fileSize = fileAttributes[FileAttributeKey.size] as? Double { totalSize += fileSize } } catch {} } - + let progressTracker = Progress(totalUnitCount: Int64(totalSize)) progressTracker.isCancellable = false progressTracker.isPausable = false progressTracker.kind = ProgressKind.file - + // Begin Zipping - let zip = zipOpen(zipFilePath.path, APPEND_STATUS_CREATE) + let zip = zipOpen(zipFilePath.nativePath, APPEND_STATUS_CREATE) + for path in processedPaths { - let filePath = path.filePath() + let filePath = path.filePath + var isDirectory: ObjCBool = false - _ = fileManager.fileExists(atPath: filePath, isDirectory: &isDirectory) + _ = FileManager.default.fileExists(atPath: filePath, isDirectory: &isDirectory) if !isDirectory.boolValue { guard let input = fopen(filePath, "r") else { throw ZipError.zipFail } defer { fclose(input) } - let fileName = path.fileName - var zipInfo: zip_fileinfo = zip_fileinfo( - dos_date: 0, - internal_fa: 0, - external_fa: 0 - ) + + var zipInfo: zip_fileinfo = zip_fileinfo(dos_date: 0, internal_fa: 0, external_fa: 0) + do { - let fileAttributes = try fileManager.attributesOfItem(atPath: filePath) + let fileAttributes = try FileManager.default.attributesOfItem(atPath: filePath) if let fileDate = fileAttributes[FileAttributeKey.modificationDate] as? Date { zipInfo.dos_date = fileDate.dosDate } @@ -278,72 +297,77 @@ public class Zip { currentPosition += fileSize } } catch {} - guard let buffer = malloc(chunkSize) else { - throw ZipError.zipFail - } - if let password, let fileName { - zipOpenNewFileInZip3(zip, fileName, &zipInfo, nil, 0, nil, 0, nil, UInt16(Z_DEFLATED), compression.minizipCompression, 0, -MAX_WBITS, DEF_MEM_LEVEL, Z_DEFAULT_STRATEGY, password, 0) - } else if let fileName { - zipOpenNewFileInZip3(zip, fileName, &zipInfo, nil, 0, nil, 0, nil, UInt16(Z_DEFLATED), compression.minizipCompression, 0, -MAX_WBITS, DEF_MEM_LEVEL, Z_DEFAULT_STRATEGY, nil, 0) + + let buffer = UnsafeMutableRawPointer.allocate(byteCount: chunkSize, alignment: 1) + defer { buffer.deallocate() } + + if let fileName = path.fileName { + zipOpenNewFileInZip3( + zip, fileName, &zipInfo, + nil, 0, nil, 0, nil, + UInt16(Z_DEFLATED), compression.minizipCompression, 0, -MAX_WBITS, DEF_MEM_LEVEL, Z_DEFAULT_STRATEGY, + password, 0 + ) } else { throw ZipError.zipFail } - var length: Int = 0 + while feof(input) == 0 { - length = fread(buffer, 1, chunkSize, input) - zipWriteInFileInZip(zip, buffer, UInt32(length)) + zipWriteInFileInZip( + zip, + buffer, + UInt32(fread(buffer, 1, chunkSize, input)) + ) } - + // Update progress handler, only if progress is not 1, // because if we call it when progress == 1, // the user will receive a progress handler call with value 1.0 twice. - if let progressHandler = progress, currentPosition / totalSize != 1 { - progressHandler(currentPosition / totalSize) + if let progress, currentPosition / totalSize != 1 { + progress(currentPosition / totalSize) } - progressTracker.completedUnitCount = Int64(currentPosition) - + zipCloseFileInZip(zip) - free(buffer) } } + zipClose(zip, nil) - + // Completed. Update progress handler. - if let progressHandler = progress{ - progressHandler(1.0) + if let progress { + progress(1.0) } - progressTracker.completedUnitCount = Int64(totalSize) } - - /** - Adds a file extension to the set of custom file extensions. - - - Parameter fileExtension: A file extension. - */ + + /// Adds a file extension to the set of custom file extensions. + /// + /// - Parameter fileExtension: A file extension. public class func addCustomFileExtension(_ fileExtension: String) { + lock.lock() customFileExtensions.insert(fileExtension) + lock.unlock() } - - /** - Removes a file extension from the set of custom file extensions. - - - Parameter fileExtension: A file extension. - */ + + /// Removes a file extension from the set of custom file extensions. + /// + /// - Parameter fileExtension: A file extension. public class func removeCustomFileExtension(_ fileExtension: String) { + lock.lock() customFileExtensions.remove(fileExtension) + lock.unlock() } - - /** - Checks if a specific file extension is valid. - - - Parameter fileExtension: A file extension to check. - - - Returns: `true` if the extension is valid, otherwise `false`. - */ + + /// Checks if a specific file extension is valid. + /// + /// - Parameter fileExtension: A file extension to check. + /// + /// - Returns: `true` if the extension is valid, otherwise `false`. public class func isValidFileExtension(_ fileExtension: String) -> Bool { - let validFileExtensions: Set = customFileExtensions.union(["zip", "cbz"]) + lock.lock() + let validFileExtensions = customFileExtensions.union(["zip", "cbz"]) + lock.unlock() return validFileExtensions.contains(fileExtension) } } diff --git a/Sources/Zip/ZipCompression.swift b/Sources/Zip/ZipCompression.swift index bfa54a2f..24c09a6a 100644 --- a/Sources/Zip/ZipCompression.swift +++ b/Sources/Zip/ZipCompression.swift @@ -1,4 +1,4 @@ -@_implementationOnly import Minizip +@_implementationOnly import CMinizip /// Zip compression strategies. public enum ZipCompression: Int { @@ -19,4 +19,4 @@ public enum ZipCompression: Int { return Z_BEST_COMPRESSION } } -} \ No newline at end of file +} diff --git a/Sources/Zip/ZipUtilities.swift b/Sources/Zip/ZipUtilities.swift deleted file mode 100644 index 1763b4dc..00000000 --- a/Sources/Zip/ZipUtilities.swift +++ /dev/null @@ -1,104 +0,0 @@ -// -// ZipUtilities.swift -// Zip -// -// Created by Roy Marmelstein on 26/01/2016. -// Copyright © 2016 Roy Marmelstein. All rights reserved. -// - -import Foundation - -internal class ZipUtilities { - /* - Include root directory. - Default is true. - - e.g. The Test directory contains two files A.txt and B.txt. - - As true: - $ zip -r Test.zip Test/ - $ unzip -l Test.zip - Test/ - Test/A.txt - Test/B.txt - - As false: - $ zip -r Test.zip Test/ - $ unzip -l Test.zip - A.txt - B.txt - */ - let includeRootDirectory = true - - // File manager - let fileManager = FileManager.default - - /** - * ProcessedFilePath struct - */ - internal struct ProcessedFilePath { - let filePathURL: URL - let fileName: String? - - func filePath() -> String { - return filePathURL.path - } - } - - //MARK: Path processing - - /** - Process zip paths - - - parameter paths: Paths as `URL`. - - - returns: Array of `ProcessedFilePath` structs. - */ - internal func processZipPaths(_ paths: [URL]) -> [ProcessedFilePath] { - var processedFilePaths = [ProcessedFilePath]() - for path in paths { - let filePath = path.path - var isDirectory: ObjCBool = false - _ = fileManager.fileExists(atPath: filePath, isDirectory: &isDirectory) - if !isDirectory.boolValue { - let processedPath = ProcessedFilePath(filePathURL: path, fileName: path.lastPathComponent) - processedFilePaths.append(processedPath) - } else { - let directoryContents = expandDirectoryFilePath(path) - processedFilePaths.append(contentsOf: directoryContents) - } - } - return processedFilePaths - } - - /** - Expand directory contents and parse them into ProcessedFilePath structs. - - - parameter directory: Path of folder as `URL`. - - - returns: Array of `ProcessedFilePath` structs. - */ - internal func expandDirectoryFilePath(_ directory: URL) -> [ProcessedFilePath] { - var processedFilePaths = [ProcessedFilePath]() - let directoryPath = directory.path - if let enumerator = fileManager.enumerator(atPath: directoryPath) { - while let filePathComponent = enumerator.nextObject() as? String { - let path = directory.appendingPathComponent(filePathComponent) - let filePath = path.path - - var isDirectory: ObjCBool = false - _ = fileManager.fileExists(atPath: filePath, isDirectory: &isDirectory) - if !isDirectory.boolValue { - var fileName = filePathComponent - if includeRootDirectory { - let directoryName = directory.lastPathComponent - fileName = (directoryName as NSString).appendingPathComponent(filePathComponent) - } - let processedPath = ProcessedFilePath(filePathURL: path, fileName: fileName) - processedFilePaths.append(processedPath) - } - } - } - return processedFilePaths - } -} diff --git a/Tests/ZipTests/Resources/3crBXeO.gif b/Tests/ZipTests/TestResources/3crBXeO.gif similarity index 100% rename from Tests/ZipTests/Resources/3crBXeO.gif rename to Tests/ZipTests/TestResources/3crBXeO.gif diff --git a/Tests/ZipTests/TestResources/PassKitTest.order b/Tests/ZipTests/TestResources/PassKitTest.order new file mode 100644 index 0000000000000000000000000000000000000000..184772b8cf10ad18f1507b4f243393f7141e2e43 GIT binary patch literal 14684 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 LZcY8|pVj{W8>&is literal 0 HcmV?d00001 diff --git a/Tests/ZipTests/TestResources/PassKitTest.pkpass b/Tests/ZipTests/TestResources/PassKitTest.pkpass new file mode 100644 index 0000000000000000000000000000000000000000..91aadbfb07adb6530b092ef46b30c3e98ff4e974 GIT binary patch literal 29749 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 URL? { - #if swift(>=6.0) - let filePath = URL(fileURLWithPath: #file) - #else - let filePath = URL(fileURLWithPath: #filePath) - #endif - let resourcePath = filePath + let resourcePath = URL(fileURLWithPath: #filePath) .deletingLastPathComponent() - .appendingPathComponent("Resources") + .appendingPathComponent("TestResources") .appendingPathComponent(resource) return ext.map { resourcePath.appendingPathExtension($0) } ?? resourcePath } @@ -48,17 +44,17 @@ final class ZipTests: XCTestCase { try XCTAssertGreaterThan(Data(contentsOf: destinationURL.appendingPathComponent("3crBXeO.gif")).count, 0) try XCTAssertGreaterThan(Data(contentsOf: destinationURL.appendingPathComponent("kYkLkPf.gif")).count, 0) } - + func testQuickUnzipNonExistingPath() { let filePath = URL(fileURLWithPath: "/some/path/to/nowhere/bb9.zip") XCTAssertThrowsError(try Zip.quickUnzipFile(filePath)) } - + func testQuickUnzipNonZipPath() { let filePath = url(forResource: "3crBXeO", withExtension: "gif")! XCTAssertThrowsError(try Zip.quickUnzipFile(filePath)) } - + func testQuickUnzipProgress() throws { let filePath = url(forResource: "bb8", withExtension: "zip")! let destinationURL = try Zip.quickUnzipFile(filePath) { progress in @@ -71,12 +67,12 @@ final class ZipTests: XCTestCase { try XCTAssertGreaterThan(Data(contentsOf: destinationURL.appendingPathComponent("3crBXeO.gif")).count, 0) try XCTAssertGreaterThan(Data(contentsOf: destinationURL.appendingPathComponent("kYkLkPf.gif")).count, 0) } - + func testQuickUnzipOnlineURL() { let filePath = URL(string: "http://www.google.com/google.zip")! XCTAssertThrowsError(try Zip.quickUnzipFile(filePath)) } - + func testUnzip() throws { let filePath = url(forResource: "bb8", withExtension: "zip")! let destinationPath = try autoRemovingSandbox() @@ -87,7 +83,7 @@ final class ZipTests: XCTestCase { try XCTAssertGreaterThan(Data(contentsOf: destinationPath.appendingPathComponent("3crBXeO.gif")).count, 0) try XCTAssertGreaterThan(Data(contentsOf: destinationPath.appendingPathComponent("kYkLkPf.gif")).count, 0) } - + func testImplicitProgressUnzip() throws { let progress = Progress(totalUnitCount: 1) @@ -100,7 +96,7 @@ final class ZipTests: XCTestCase { XCTAssertTrue(progress.totalUnitCount == progress.completedUnitCount) } - + func testImplicitProgressZip() throws { let progress = Progress(totalUnitCount: 1) @@ -115,7 +111,7 @@ final class ZipTests: XCTestCase { XCTAssertTrue(progress.totalUnitCount == progress.completedUnitCount) } - + func testQuickZip() throws { let imageURL1 = url(forResource: "3crBXeO", withExtension: "gif")! let imageURL2 = url(forResource: "kYkLkPf", withExtension: "gif")! @@ -133,13 +129,13 @@ final class ZipTests: XCTestCase { let destinationURL = try Zip.quickZipFiles([imageURL1, imageURL2], fileName: "archive") { progress in XCTAssertFalse(progress.isNaN) } - XCTAssertTrue(FileManager.default.fileExists(atPath:destinationURL.path)) + XCTAssertTrue(FileManager.default.fileExists(atPath: destinationURL.path)) try XCTAssertGreaterThan(Data(contentsOf: destinationURL).count, 0) addTeardownBlock { try? FileManager.default.removeItem(at: destinationURL) } } - + func testQuickZipFolder() throws { let fileManager = FileManager.default let imageURL1 = url(forResource: "3crBXeO", withExtension: "gif")! @@ -164,7 +160,7 @@ final class ZipTests: XCTestCase { XCTAssertNoThrow(try Zip.zipFiles(paths: [imageURL1, imageURL2], zipFilePath: zipFilePath, password: nil, progress: nil)) XCTAssertTrue(FileManager.default.fileExists(atPath: zipFilePath.path)) } - + func testZipUnzipPassword() throws { let imageURL1 = url(forResource: "3crBXeO", withExtension: "gif")! let imageURL2 = url(forResource: "kYkLkPf", withExtension: "gif")! @@ -183,7 +179,13 @@ final class ZipTests: XCTestCase { let unzipDestination = try Zip.quickUnzipFile(permissionsURL) let permission644 = unzipDestination.appendingPathComponent("unsupported_permission").appendingPathExtension("txt") let foundPermissions = try FileManager.default.attributesOfItem(atPath: permission644.path)[.posixPermissions] as? Int - let expectedPermissions = 0o644 + #if os(Windows) && compiler(<6.0) + let expectedPermissions = 0o700 + #elseif os(Windows) && compiler(>=6.0) + let expectedPermissions = 0o600 + #else + let expectedPermissions = 0o644 + #endif XCTAssertNotNil(foundPermissions) XCTAssertEqual( foundPermissions, @@ -206,9 +208,19 @@ final class ZipTests: XCTestCase { let attributes777 = try fileManager.attributesOfItem(atPath: permission777.path) let attributes600 = try fileManager.attributesOfItem(atPath: permission600.path) let attributes604 = try fileManager.attributesOfItem(atPath: permission604.path) - XCTAssertEqual(attributes777[.posixPermissions] as? Int, 0o777) - XCTAssertEqual(attributes600[.posixPermissions] as? Int, 0o600) - XCTAssertEqual(attributes604[.posixPermissions] as? Int, 0o604) + #if os(Windows) && compiler(<6.0) + XCTAssertEqual(attributes777[.posixPermissions] as? Int, 0o700) + XCTAssertEqual(attributes600[.posixPermissions] as? Int, 0o700) + XCTAssertEqual(attributes604[.posixPermissions] as? Int, 0o700) + #elseif os(Windows) && compiler(>=6.0) + XCTAssertEqual(attributes777[.posixPermissions] as? Int, 0o600) + XCTAssertEqual(attributes600[.posixPermissions] as? Int, 0o600) + XCTAssertEqual(attributes604[.posixPermissions] as? Int, 0o600) + #else + XCTAssertEqual(attributes777[.posixPermissions] as? Int, 0o777) + XCTAssertEqual(attributes600[.posixPermissions] as? Int, 0o600) + XCTAssertEqual(attributes604[.posixPermissions] as? Int, 0o604) + #endif } // Tests if https://github.com/marmelroy/Zip/issues/245 does not uccor anymore. @@ -220,9 +232,12 @@ final class ZipTests: XCTestCase { try Zip.unzipFile(filePath, destination: destinationPath, overwrite: true, password: "password", progress: nil) XCTFail("ZipError.unzipFail expected.") } catch {} - - let fileManager = FileManager.default - XCTAssertFalse(fileManager.fileExists(atPath: destinationPath.appendingPathComponent("../naughtyFile.txt").path)) + + XCTAssertFalse( + FileManager.default.fileExists( + atPath: destinationPath.appendingPathComponent("../naughtyFile.txt").path + ) + ) } func testQuickUnzipSubDir() throws { @@ -239,7 +254,7 @@ final class ZipTests: XCTestCase { XCTAssertTrue(fileManager.fileExists(atPath: subDir.path)) XCTAssertTrue(fileManager.fileExists(atPath: imageURL.path)) } - + func testAddedCustomFileExtensionIsValid() { let fileExtension = "cstm" Zip.addCustomFileExtension(fileExtension) @@ -247,7 +262,7 @@ final class ZipTests: XCTestCase { XCTAssertTrue(result) Zip.removeCustomFileExtension(fileExtension) } - + func testRemovedCustomFileExtensionIsInvalid() { let fileExtension = "cstm" Zip.addCustomFileExtension(fileExtension) @@ -255,12 +270,12 @@ final class ZipTests: XCTestCase { let result = Zip.isValidFileExtension(fileExtension) XCTAssertFalse(result) } - + func testDefaultFileExtensionsIsValid() { XCTAssertTrue(Zip.isValidFileExtension("zip")) XCTAssertTrue(Zip.isValidFileExtension("cbz")) } - + func testDefaultFileExtensionsIsNotRemoved() { Zip.removeCustomFileExtension("zip") Zip.removeCustomFileExtension("cbz") @@ -312,9 +327,10 @@ final class ZipTests: XCTestCase { } func testDosDate() { - XCTAssertEqual(0b10000011001100011000110000110001, Date(timeIntervalSince1970: 2389282415).dosDate) - XCTAssertEqual(0b00000001001100011000110000110001, Date(timeIntervalSince1970: 338060015).dosDate) - XCTAssertEqual(0b00000000001000010000000000000000, Date(timeIntervalSince1970: 315532800).dosDate) + NSTimeZone.default = NSTimeZone(forSecondsFromGMT: 0) as TimeZone + XCTAssertEqual(0b10000011_00110001_10001100_00110001, Date(timeIntervalSince1970: 2_389_282_415).dosDate) + XCTAssertEqual(0b00000001_00110001_10001100_00110001, Date(timeIntervalSince1970: 338_060_015).dosDate) + XCTAssertEqual(0b00000000_00100001_00000000_00000000, Date(timeIntervalSince1970: 315_532_800).dosDate) } func testInit() { @@ -324,6 +340,33 @@ final class ZipTests: XCTestCase { XCTAssertNil(zip) } + func testUnzipWithoutPassword() throws { + let imageURL1 = url(forResource: "3crBXeO", withExtension: "gif")! + let imageURL2 = url(forResource: "kYkLkPf", withExtension: "gif")! + let zipFilePath = try autoRemovingSandbox().appendingPathComponent("archive.zip") + try Zip.zipFiles(paths: [imageURL1, imageURL2], zipFilePath: zipFilePath, password: "password") + XCTAssertTrue(FileManager.default.fileExists(atPath: zipFilePath.path)) + let directoryName = zipFilePath.lastPathComponent.replacingOccurrences(of: ".\(zipFilePath.pathExtension)", with: "") + let destinationUrl = try autoRemovingSandbox().appendingPathComponent(directoryName, isDirectory: true) + XCTAssertThrowsError(try Zip.unzipFile(zipFilePath, destination: destinationUrl)) + } + + func testFileHandler() throws { + let filePath = url(forResource: "bb8", withExtension: "zip")! + let destinationPath = try autoRemovingSandbox() + XCTAssertNoThrow( + try Zip.unzipFile( + filePath, destination: destinationPath, password: "password", + fileOutputHandler: { fileURL in + XCTAssertTrue(FileManager.default.fileExists(atPath: fileURL.path)) + } + ) + ) + XCTAssertTrue(FileManager.default.fileExists(atPath: destinationPath.path)) + try XCTAssertGreaterThan(Data(contentsOf: destinationPath.appendingPathComponent("3crBXeO.gif")).count, 0) + try XCTAssertGreaterThan(Data(contentsOf: destinationPath.appendingPathComponent("kYkLkPf.gif")).count, 0) + } + // Tests if https://github.com/vapor-community/Zip/issues/4 does not occur anymore. func testRoundTripping() throws { // "prod-apple-swift-metrics-main-e6a00d36.zip" is the original zip file from the issue. @@ -365,4 +408,84 @@ final class ZipTests: XCTestCase { let newUnzippedFiles = try FileManager.default.contentsOfDirectory(atPath: newDestinationFolder.path) XCTAssertEqual(unzippedFiles, newUnzippedFiles) } + + #if os(Windows) + func testWindowsReservedChars() throws { + let txtFile = ArchiveFile(filename: "a_b.txt", data: "Hi Mom!".data(using: .utf8)!) + let txtFile1 = ArchiveFile(filename: "ab.txt", data: "Hello, Swift!".data(using: .utf8)!) + let txtFile3 = ArchiveFile(filename: "a:b.txt", data: "Hello, World!".data(using: .utf8)!) + let txtFile4 = ArchiveFile(filename: "a\"b.txt", data: "Hi Windows!".data(using: .utf8)!) + let txtFile5 = ArchiveFile(filename: "a|b.txt", data: "Hi Barbie!".data(using: .utf8)!) + let txtFile6 = ArchiveFile(filename: "a?b.txt", data: "Hi, Ken!".data(using: .utf8)!) + let txtFile7 = ArchiveFile(filename: "a*b.txt", data: "Hello Everyone!".data(using: .utf8)!) + + let file = ArchiveFile(filename: "a_b", data: "Hello, World!".data(using: .utf8)!) + let file1 = ArchiveFile(filename: "ab", data: "Hello, Swift!".data(using: .utf8)!) + let file3 = ArchiveFile(filename: "a:b", data: "Hello, World!".data(using: .utf8)!) + + let sandboxFolder = try autoRemovingSandbox() + let zipFilePath = sandboxFolder.appendingPathComponent("archive.zip") + try Zip.zipData( + archiveFiles: [ + txtFile, txtFile1, txtFile2, txtFile3, txtFile4, txtFile5, txtFile6, txtFile7, + file, file1, file2, file3, + ], + zipFilePath: zipFilePath + ) + + let destinationPath = try autoRemovingSandbox() + try Zip.unzipFile(zipFilePath, destination: destinationPath) + + let txtFileURL = destinationPath.appendingPathComponent("a_b.txt") + let txtFile1URL = destinationPath.appendingPathComponent("a_b (1).txt") + let txtFile2URL = destinationPath.appendingPathComponent("a_b (2).txt") + let txtFile3URL = destinationPath.appendingPathComponent("a_b (3).txt") + let txtFile4URL = destinationPath.appendingPathComponent("a_b (4).txt") + let txtFile5URL = destinationPath.appendingPathComponent("a_b (5).txt") + let txtFile6URL = destinationPath.appendingPathComponent("a_b (6).txt") + let txtFile7URL = destinationPath.appendingPathComponent("a_b (7).txt") + + let fileURL = destinationPath.appendingPathComponent("a_b") + let file1URL = destinationPath.appendingPathComponent("a_b (1)") + let file2URL = destinationPath.appendingPathComponent("a_b (2)") + let file3URL = destinationPath.appendingPathComponent("a_b (3)") + + XCTAssertTrue(FileManager.default.fileExists(atPath: txtFileURL.path)) + XCTAssertTrue(FileManager.default.fileExists(atPath: txtFile1URL.path)) + XCTAssertTrue(FileManager.default.fileExists(atPath: txtFile2URL.path)) + XCTAssertTrue(FileManager.default.fileExists(atPath: txtFile3URL.path)) + XCTAssertTrue(FileManager.default.fileExists(atPath: txtFile4URL.path)) + XCTAssertTrue(FileManager.default.fileExists(atPath: txtFile5URL.path)) + XCTAssertTrue(FileManager.default.fileExists(atPath: txtFile6URL.path)) + XCTAssertTrue(FileManager.default.fileExists(atPath: txtFile7URL.path)) + + XCTAssertTrue(FileManager.default.fileExists(atPath: fileURL.path)) + XCTAssertTrue(FileManager.default.fileExists(atPath: file1URL.path)) + XCTAssertTrue(FileManager.default.fileExists(atPath: file2URL.path)) + XCTAssertTrue(FileManager.default.fileExists(atPath: file3URL.path)) + } + #endif + + func testPassKitExtensions() throws { + let pkpassURL = url(forResource: "PassKitTest", withExtension: "pkpass")! + let pkpassDestination = try autoRemovingSandbox() + Zip.addCustomFileExtension("pkpass") + XCTAssertNoThrow(try Zip.unzipFile(pkpassURL, destination: pkpassDestination)) + XCTAssert(FileManager.default.fileExists(atPath: pkpassDestination.appendingPathComponent("pass.json").path)) + XCTAssert(FileManager.default.fileExists(atPath: pkpassDestination.appendingPathComponent("manifest.json").path)) + XCTAssert(FileManager.default.fileExists(atPath: pkpassDestination.appendingPathComponent("signature").path)) + XCTAssert(FileManager.default.fileExists(atPath: pkpassDestination.appendingPathComponent("icon.png").path)) + XCTAssert(FileManager.default.fileExists(atPath: pkpassDestination.appendingPathComponent("logo.png").path)) + + let orderURL = url(forResource: "PassKitTest", withExtension: "order")! + let orderDestination = try autoRemovingSandbox() + Zip.addCustomFileExtension("order") + XCTAssertNoThrow(try Zip.unzipFile(orderURL, destination: orderDestination)) + XCTAssert(FileManager.default.fileExists(atPath: orderDestination.appendingPathComponent("order.json").path)) + XCTAssert(FileManager.default.fileExists(atPath: orderDestination.appendingPathComponent("manifest.json").path)) + XCTAssert(FileManager.default.fileExists(atPath: orderDestination.appendingPathComponent("signature").path)) + XCTAssert(FileManager.default.fileExists(atPath: orderDestination.appendingPathComponent("icon.png").path)) + } }