From 9c6ae98322d010b37cbbef032119debd0a321010 Mon Sep 17 00:00:00 2001 From: Francesco Paolo Severino Date: Wed, 6 Nov 2024 21:40:32 +0100 Subject: [PATCH 1/7] Isolate Windows specific changes --- .github/workflows/test.yml | 40 ++++ Package.swift | 51 ++++-- Sources/Zip/ArchiveFile.swift | 4 +- Sources/Zip/Zip+ProcessedFilePath.swift | 64 +++++++ Sources/Zip/Zip.swift | 231 +++++++++++++----------- Tests/ZipTests/ZipTests.swift | 136 ++++++++++---- 6 files changed, 377 insertions(+), 149 deletions(-) create mode 100644 Sources/Zip/Zip+ProcessedFilePath.swift diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3dbf46d0..f9fbe336 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,3 +11,43 @@ jobs: uses: vapor/ci/.github/workflows/run-unit-tests.yml@main 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/Package.swift b/Package.swift index db8f5915..de4756e1 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: [ @@ -10,21 +16,20 @@ let package = Package( .target( name: "Minizip", exclude: ["module"], - swiftSettings: [ - .enableUpcomingFeature("ConciseMagicFile"), + cSettings: [ + .define("_CRT_SECURE_NO_WARNINGS", .when(platforms: [.windows])) ], - linkerSettings: [ - .linkedLibrary("z") - ] + swiftSettings: swiftSettings ), .target( name: "Zip", dependencies: [ .target(name: "Minizip"), ], - swiftSettings: [ - .enableUpcomingFeature("ConciseMagicFile"), - ] + cSettings: [ + .define("_CRT_SECURE_NO_WARNINGS", .when(platforms: [.windows])) + ], + swiftSettings: swiftSettings ), .testTarget( name: "ZipTests", @@ -34,9 +39,31 @@ let package = Package( resources: [ .copy("Resources"), ], - 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/Sources/Zip/ArchiveFile.swift b/Sources/Zip/ArchiveFile.swift index 9b3335b3..43143f6c 100644 --- a/Sources/Zip/ArchiveFile.swift +++ b/Sources/Zip/ArchiveFile.swift @@ -55,8 +55,6 @@ extension Zip { compression: ZipCompression = .DefaultCompression, progress: ((_ progress: Double) -> ())? = nil ) throws { - let destinationPath = zipFilePath.path - // Progress handler set up var currentPosition: Int = 0 var totalSize: Int = 0 @@ -71,7 +69,7 @@ extension Zip { progressTracker.kind = ProgressKind.file // Begin Zipping - let zip = zipOpen(destinationPath, APPEND_STATUS_CREATE) + let zip = zipOpen(zipFilePath.withUnsafeFileSystemRepresentation { String(cString: $0!) }, APPEND_STATUS_CREATE) for archiveFile in archiveFiles { // Skip empty data diff --git a/Sources/Zip/Zip+ProcessedFilePath.swift b/Sources/Zip/Zip+ProcessedFilePath.swift new file mode 100644 index 00000000..ccfa43bb --- /dev/null +++ b/Sources/Zip/Zip+ProcessedFilePath.swift @@ -0,0 +1,64 @@ +import Foundation + +extension Zip { + struct ProcessedFilePath { + let filePathURL: URL + let fileName: String? + + var filePath: String { + filePathURL.withUnsafeFileSystemRepresentation { String(cString: $0!) } + } + } + + /// Process zip paths. + /// + /// - Parameter paths: Paths as `URL`. + /// + /// - Returns: Array of ``ProcessedFilePath`` structs. + static func processZipPaths(_ paths: [URL]) -> [ProcessedFilePath] { + var processedFilePaths = [ProcessedFilePath]() + for pathURL in paths { + var isDirectory: ObjCBool = false + _ = FileManager.default.fileExists( + atPath: pathURL.withUnsafeFileSystemRepresentation { String(cString: $0!) }, + 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.withUnsafeFileSystemRepresentation { String(cString: $0!) }) { + while let filePathComponent = enumerator.nextObject() as? String { + let pathURL = directory.appendingPathComponent(filePathComponent) + + var isDirectory: ObjCBool = false + _ = FileManager.default.fileExists( + atPath: pathURL.withUnsafeFileSystemRepresentation { String(cString: $0!) }, + 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/Zip.swift b/Sources/Zip/Zip.swift index 16b90807..56c6af14 100644 --- a/Sources/Zip/Zip.swift +++ b/Sources/Zip/Zip.swift @@ -40,115 +40,144 @@ public class Zip { progress: ((_ progress: Double) -> ())? = 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.withUnsafeFileSystemRepresentation { String(cString: $0!) } + 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.withUnsafeFileSystemRepresentation { + String(cString: $0!) + } + + // `.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.withUnsafeFileSystemRepresentation { String(cString: $0!) }) 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 { if isDirectory { - try fileManager.createDirectory(atPath: fullPath, withIntermediateDirectories: true, attributes: directoryAttributes) + try FileManager.default.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 as NSString).deletingLastPathComponent, + withIntermediateDirectories: true, + 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 +187,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,33 +198,30 @@ 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) @@ -223,54 +249,48 @@ public class Zip { 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 = Self.processZipPaths(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.withUnsafeFileSystemRepresentation { String(cString: $0!) }, 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,33 +298,38 @@ 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) diff --git a/Tests/ZipTests/ZipTests.swift b/Tests/ZipTests/ZipTests.swift index 60b4a90a..631aa3ef 100644 --- a/Tests/ZipTests/ZipTests.swift +++ b/Tests/ZipTests/ZipTests.swift @@ -11,12 +11,7 @@ import XCTest 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(resource) @@ -48,17 +43,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 +66,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 +82,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 +95,7 @@ final class ZipTests: XCTestCase { XCTAssertTrue(progress.totalUnitCount == progress.completedUnitCount) } - + func testImplicitProgressZip() throws { let progress = Progress(totalUnitCount: 1) @@ -115,7 +110,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 +128,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 +159,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 +178,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 +207,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 +231,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 +253,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 +261,7 @@ final class ZipTests: XCTestCase { XCTAssertTrue(result) Zip.removeCustomFileExtension(fileExtension) } - + func testRemovedCustomFileExtensionIsInvalid() { let fileExtension = "cstm" Zip.addCustomFileExtension(fileExtension) @@ -255,12 +269,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 +326,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() { @@ -365,4 +380,63 @@ 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 } From 217cfc0b5707137ca60c51b8b637000b0ad15216 Mon Sep 17 00:00:00 2001 From: Francesco Paolo Severino Date: Wed, 6 Nov 2024 21:46:14 +0100 Subject: [PATCH 2/7] Remove "z" link --- Sources/Minizip/module/module.modulemap | 1 - 1 file changed, 1 deletion(-) diff --git a/Sources/Minizip/module/module.modulemap b/Sources/Minizip/module/module.modulemap index 59eaacd7..100b7290 100644 --- a/Sources/Minizip/module/module.modulemap +++ b/Sources/Minizip/module/module.modulemap @@ -1,5 +1,4 @@ module Minizip [system][extern_c] { header "../include/Minizip.h" - link "z" export * } From e9d677b91889d7484e82a9af8ae31f8519e53fd8 Mon Sep 17 00:00:00 2001 From: Francesco Paolo Severino Date: Wed, 6 Nov 2024 21:48:07 +0100 Subject: [PATCH 3/7] Fix target --- Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index de4756e1..fc5e9e54 100644 --- a/Package.swift +++ b/Package.swift @@ -55,7 +55,7 @@ var swiftSettings: [SwiftSetting] { ] } -if let target = package.targets.filter({ $0.name == "CMinizip" }).first { +if let target = package.targets.filter({ $0.name == "Minizip" }).first { #if os(Windows) if ProcessInfo.processInfo.environment["ZIP_USE_DYNAMIC_ZLIB"] == nil { target.cSettings?.append(contentsOf: [.define("ZLIB_STATIC")]) From 833bcedcbfb7ddfe43d4e7ea3aa77b37be7fb2c7 Mon Sep 17 00:00:00 2001 From: Francesco Paolo Severino Date: Wed, 6 Nov 2024 21:51:41 +0100 Subject: [PATCH 4/7] Fix include --- Sources/Minizip/include/Minizip.h | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/Minizip/include/Minizip.h b/Sources/Minizip/include/Minizip.h index ef753eb9..ce1c99e2 100644 --- a/Sources/Minizip/include/Minizip.h +++ b/Sources/Minizip/include/Minizip.h @@ -9,9 +9,9 @@ #ifndef Minizip_h #define Minizip_h -#import "ioapi.h" -#import "crypt.h" -#import "unzip.h" -#import "zip.h" +#include "ioapi.h" +#include "crypt.h" +#include "unzip.h" +#include "zip.h" #endif /* Minizip_h */ From 0a8b5bdf48b3f3924d47879958ec256196ba7a26 Mon Sep 17 00:00:00 2001 From: Francesco Paolo Severino Date: Wed, 6 Nov 2024 23:08:53 +0100 Subject: [PATCH 5/7] Add URL extension --- Sources/Zip/ArchiveFile.swift | 2 +- Sources/Zip/URL+nativePath.swift | 11 +++++++++++ Sources/Zip/Zip+ProcessedFilePath.swift | 8 ++++---- Sources/Zip/Zip.swift | 10 ++++------ 4 files changed, 20 insertions(+), 11 deletions(-) create mode 100644 Sources/Zip/URL+nativePath.swift diff --git a/Sources/Zip/ArchiveFile.swift b/Sources/Zip/ArchiveFile.swift index 43143f6c..d72b6bbe 100644 --- a/Sources/Zip/ArchiveFile.swift +++ b/Sources/Zip/ArchiveFile.swift @@ -69,7 +69,7 @@ extension Zip { progressTracker.kind = ProgressKind.file // Begin Zipping - let zip = zipOpen(zipFilePath.withUnsafeFileSystemRepresentation { String(cString: $0!) }, APPEND_STATUS_CREATE) + let zip = zipOpen(zipFilePath.nativePath, APPEND_STATUS_CREATE) for archiveFile in archiveFiles { // Skip empty data diff --git a/Sources/Zip/URL+nativePath.swift b/Sources/Zip/URL+nativePath.swift new file mode 100644 index 00000000..5cfc0211 --- /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!) } + } +} \ No newline at end of file diff --git a/Sources/Zip/Zip+ProcessedFilePath.swift b/Sources/Zip/Zip+ProcessedFilePath.swift index ccfa43bb..2e836d16 100644 --- a/Sources/Zip/Zip+ProcessedFilePath.swift +++ b/Sources/Zip/Zip+ProcessedFilePath.swift @@ -6,7 +6,7 @@ extension Zip { let fileName: String? var filePath: String { - filePathURL.withUnsafeFileSystemRepresentation { String(cString: $0!) } + filePathURL.nativePath } } @@ -20,7 +20,7 @@ extension Zip { for pathURL in paths { var isDirectory: ObjCBool = false _ = FileManager.default.fileExists( - atPath: pathURL.withUnsafeFileSystemRepresentation { String(cString: $0!) }, + atPath: pathURL.nativePath, isDirectory: &isDirectory ) @@ -42,13 +42,13 @@ extension Zip { /// - Returns: Array of ``ProcessedFilePath`` structs. private static func expandDirectoryFilePath(_ directory: URL) -> [ProcessedFilePath] { var processedFilePaths = [ProcessedFilePath]() - if let enumerator = FileManager.default.enumerator(atPath: directory.withUnsafeFileSystemRepresentation { String(cString: $0!) }) { + 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.withUnsafeFileSystemRepresentation { String(cString: $0!) }, + atPath: pathURL.nativePath, isDirectory: &isDirectory ) diff --git a/Sources/Zip/Zip.swift b/Sources/Zip/Zip.swift index 56c6af14..20af5bfd 100644 --- a/Sources/Zip/Zip.swift +++ b/Sources/Zip/Zip.swift @@ -41,7 +41,7 @@ public class Zip { fileOutputHandler: ((_ unzippedFile: URL) -> Void)? = nil ) throws { // Check whether a zip file exists at path. - let path = zipFilePath.withUnsafeFileSystemRepresentation { String(cString: $0!) } + let path = zipFilePath.nativePath if !FileManager.default.fileExists(atPath: path) || !isValidFileExtension(zipFilePath.pathExtension) { throw ZipError.fileNotFound } @@ -129,13 +129,11 @@ public class Zip { pathString = pathString.replacingOccurrences(of: "\\", with: "/") } - let fullPath = destination.appendingPathComponent(pathString).standardizedFileURL.withUnsafeFileSystemRepresentation { - String(cString: $0!) - } + 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.standardizedFileURL.withUnsafeFileSystemRepresentation { String(cString: $0!) }) else { + guard fullPath.starts(with: destination.standardizedFileURL.nativePath) else { throw ZipError.unzipFail } @@ -274,7 +272,7 @@ public class Zip { progressTracker.kind = ProgressKind.file // Begin Zipping - let zip = zipOpen(zipFilePath.withUnsafeFileSystemRepresentation { String(cString: $0!) }, APPEND_STATUS_CREATE) + let zip = zipOpen(zipFilePath.nativePath, APPEND_STATUS_CREATE) for path in processedPaths { let filePath = path.filePath From 018c44ab06dafd289f0d5a9dc099f34fff896d32 Mon Sep 17 00:00:00 2001 From: Francesco Paolo Severino Date: Wed, 6 Nov 2024 23:12:07 +0100 Subject: [PATCH 6/7] Simplify `isDirectory` logic --- Sources/Zip/Zip.swift | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/Sources/Zip/Zip.swift b/Sources/Zip/Zip.swift index 20af5bfd..516dba4c 100644 --- a/Sources/Zip/Zip.swift +++ b/Sources/Zip/Zip.swift @@ -153,15 +153,16 @@ public class Zip { || 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.default.createDirectory( atPath: fullPath, - withIntermediateDirectories: true, - attributes: directoryAttributes) - } else { - try FileManager.default.createDirectory( - atPath: (fullPath as NSString).deletingLastPathComponent, - withIntermediateDirectories: true, + withIntermediateDirectories: false, attributes: directoryAttributes ) } From 668ab715589bd39f9c8db9f2e8b4af4bf472cf7e Mon Sep 17 00:00:00 2001 From: Francesco Paolo Severino Date: Wed, 6 Nov 2024 23:18:54 +0100 Subject: [PATCH 7/7] Move `ProcessedFilePath` to `FileManager` --- Sources/Zip/Zip+ProcessedFilePath.swift | 8 ++++---- Sources/Zip/Zip.swift | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Sources/Zip/Zip+ProcessedFilePath.swift b/Sources/Zip/Zip+ProcessedFilePath.swift index 2e836d16..0b399ec6 100644 --- a/Sources/Zip/Zip+ProcessedFilePath.swift +++ b/Sources/Zip/Zip+ProcessedFilePath.swift @@ -1,6 +1,6 @@ import Foundation -extension Zip { +extension FileManager { struct ProcessedFilePath { let filePathURL: URL let fileName: String? @@ -12,12 +12,12 @@ extension Zip { /// Process zip paths. /// - /// - Parameter paths: Paths as `URL`. + /// - Parameter roots: Paths as `URL`. /// /// - Returns: Array of ``ProcessedFilePath`` structs. - static func processZipPaths(_ paths: [URL]) -> [ProcessedFilePath] { + static func fileSubPaths(from roots: [URL]) -> [ProcessedFilePath] { var processedFilePaths = [ProcessedFilePath]() - for pathURL in paths { + for pathURL in roots { var isDirectory: ObjCBool = false _ = FileManager.default.fileExists( atPath: pathURL.nativePath, diff --git a/Sources/Zip/Zip.swift b/Sources/Zip/Zip.swift index 516dba4c..2940b896 100644 --- a/Sources/Zip/Zip.swift +++ b/Sources/Zip/Zip.swift @@ -250,7 +250,7 @@ public class Zip { compression: ZipCompression = .DefaultCompression, progress: ((_ progress: Double) -> Void)? = nil ) throws { - let processedPaths = Self.processZipPaths(paths) + let processedPaths = FileManager.fileSubPaths(from: paths) let chunkSize = 16384