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 @@
-
+
-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 00000000..184772b8
Binary files /dev/null and b/Tests/ZipTests/TestResources/PassKitTest.order differ
diff --git a/Tests/ZipTests/TestResources/PassKitTest.pkpass b/Tests/ZipTests/TestResources/PassKitTest.pkpass
new file mode 100644
index 00000000..91aadbfb
Binary files /dev/null and b/Tests/ZipTests/TestResources/PassKitTest.pkpass differ
diff --git a/Tests/ZipTests/Resources/bb8.zip b/Tests/ZipTests/TestResources/bb8.zip
similarity index 100%
rename from Tests/ZipTests/Resources/bb8.zip
rename to Tests/ZipTests/TestResources/bb8.zip
diff --git a/Tests/ZipTests/Resources/kYkLkPf.gif b/Tests/ZipTests/TestResources/kYkLkPf.gif
similarity index 100%
rename from Tests/ZipTests/Resources/kYkLkPf.gif
rename to Tests/ZipTests/TestResources/kYkLkPf.gif
diff --git a/Tests/ZipTests/Resources/pathTraversal.zip b/Tests/ZipTests/TestResources/pathTraversal.zip
similarity index 100%
rename from Tests/ZipTests/Resources/pathTraversal.zip
rename to Tests/ZipTests/TestResources/pathTraversal.zip
diff --git a/Tests/ZipTests/Resources/permissions.zip b/Tests/ZipTests/TestResources/permissions.zip
similarity index 100%
rename from Tests/ZipTests/Resources/permissions.zip
rename to Tests/ZipTests/TestResources/permissions.zip
diff --git a/Tests/ZipTests/Resources/prod-apple-swift-metrics-main-e6a00d36-finder.zip b/Tests/ZipTests/TestResources/prod-apple-swift-metrics-main-e6a00d36-finder.zip
similarity index 100%
rename from Tests/ZipTests/Resources/prod-apple-swift-metrics-main-e6a00d36-finder.zip
rename to Tests/ZipTests/TestResources/prod-apple-swift-metrics-main-e6a00d36-finder.zip
diff --git a/Tests/ZipTests/Resources/prod-apple-swift-metrics-main-e6a00d36-test.zip b/Tests/ZipTests/TestResources/prod-apple-swift-metrics-main-e6a00d36-test.zip
similarity index 100%
rename from Tests/ZipTests/Resources/prod-apple-swift-metrics-main-e6a00d36-test.zip
rename to Tests/ZipTests/TestResources/prod-apple-swift-metrics-main-e6a00d36-test.zip
diff --git a/Tests/ZipTests/Resources/prod-apple-swift-metrics-main-e6a00d36.zip b/Tests/ZipTests/TestResources/prod-apple-swift-metrics-main-e6a00d36.zip
similarity index 100%
rename from Tests/ZipTests/Resources/prod-apple-swift-metrics-main-e6a00d36.zip
rename to Tests/ZipTests/TestResources/prod-apple-swift-metrics-main-e6a00d36.zip
diff --git a/Tests/ZipTests/Resources/unsupported_permissions.zip b/Tests/ZipTests/TestResources/unsupported_permissions.zip
similarity index 100%
rename from Tests/ZipTests/Resources/unsupported_permissions.zip
rename to Tests/ZipTests/TestResources/unsupported_permissions.zip
diff --git a/Tests/ZipTests/ZipTests.swift b/Tests/ZipTests/ZipTests.swift
index 60b4a90a..1e735f27 100644
--- a/Tests/ZipTests/ZipTests.swift
+++ b/Tests/ZipTests/ZipTests.swift
@@ -7,18 +7,14 @@
//
import XCTest
+
@testable import Zip
final class ZipTests: XCTestCase {
private func url(forResource resource: String, withExtension ext: String? = nil) -> 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))
+ }
}