Effortless ZIP Handling in Swift

Related tags

swift zip
Overview

Swift Package Manager compatible Carthage compatible CocoaPods Compatible Platform Twitter

ZIP Foundation is a library to create, read and modify ZIP archive files.
It is written in Swift and based on Apple's libcompression for high performance and energy efficiency.
To learn more about the performance characteristics of the framework, you can read this blog post.

Features

  • Modern Swift API
  • High Performance Compression and Decompression
  • Deterministic Memory Consumption
  • Linux compatibility
  • No 3rd party dependencies (on Apple platforms, zlib on Linux)
  • Comprehensive Unit and Performance Test Coverage
  • Complete Documentation

Requirements

  • iOS 12.0+ / macOS 10.11+ / tvOS 12.0+ / watchOS 2.0+
  • Or Linux with zlib development package
  • Xcode 11.0
  • Swift 4.0

Installation

Swift Package Manager

The Swift Package Manager is a dependency manager integrated with the Swift build system. To learn how to use the Swift Package Manager for your project, please read the official documentation.
To add ZIP Foundation as a dependency, you have to add it to the dependencies of your Package.swift file and refer to that dependency in your target.

// swift-tools-version:5.0
import PackageDescription
let package = Package(
    name: "<Your Product Name>",
    dependencies: [
		.package(url: "https://github.com/weichsel/ZIPFoundation.git", .upToNextMajor(from: "0.9.0"))
    ],
    targets: [
        .target(
		name: "<Your Target Name>",
		dependencies: ["ZIPFoundation"]),
    ]
)

After adding the dependency, you can fetch the library with:

$ swift package resolve

Carthage

Carthage is a decentralized dependency manager.
Installation instructions can be found in the project's README file.

To integrate ZIPFoundation into your Xcode project using Carthage, you have to add it to your Cartfile:

github "weichsel/ZIPFoundation" ~> 0.9

After adding ZIPFoundation to the Cartfile, you have to fetch the sources by running:

carthage update --no-build

The fetched project has to be integrated into your workspace by dragging ZIPFoundation.xcodeproj to Xcode's Project Navigator. (See official Carhage docs.)

CocoaPods

CocoaPods is a dependency manager for Objective-C and Swift.
To learn more about setting up your project for CocoaPods, please refer to the official documentation.
To integrate ZIP Foundation into your Xcode project using CocoaPods, you have to add it to your project's Podfile:

source 'https://github.com/CocoaPods/Specs.git'
platform :ios, '10.0'
use_frameworks!
target '<Your Target Name>' do
    pod 'ZIPFoundation', '~> 0.9'
end

Afterwards, run the following command:

$ pod install

Usage

ZIP Foundation provides two high level methods to zip and unzip items. Both are implemented as extension of FileManager.
The functionality of those methods is modeled after the behavior of the Archive Utility in macOS.

Note: There is a large performance discrepancy between Debug and Release builds of ZIP Foundation.
The main performance bottleneck is the code that calculates CRC32 checksums. This codepath executes slowly when Swift optimizations are turned off (-Onone). To avoid long wait times when debugging code that extracts archives, the skipCRC32 flag can be set. To learn more about the skipCRC32 parameter, please refer to the documentation strings of the Archive.extract and FileManager.unzipItem methods. Skippig CRC32 checks should only be enabled during debugging.

Zipping Files and Directories

To zip a single file you simply pass a file URL representing the item you want to zip and a destination URL to FileManager.zipItem(at sourceURL: URL, to destinationURL: URL):

let fileManager = FileManager()
let currentWorkingPath = fileManager.currentDirectoryPath
var sourceURL = URL(fileURLWithPath: currentWorkingPath)
sourceURL.appendPathComponent("file.txt")
var destinationURL = URL(fileURLWithPath: currentWorkingPath)
destinationURL.appendPathComponent("archive.zip")
do {
    try fileManager.zipItem(at: sourceURL, to: destinationURL)
} catch {
    print("Creation of ZIP archive failed with error:\(error)")
}

By default, archives are created without any compression. To create compressed ZIP archives, the optional compressionMethod parameter has to be set to .deflate.
The same method also accepts URLs that represent directory items. In that case, zipItem adds the directory content of sourceURL to the archive.
By default, a root directory entry named after the lastPathComponent of the sourceURL is added to the destination archive. If you don't want to preserve the parent directory of the source in your archive, you can pass shouldKeepParent: false.

Unzipping Archives

To unzip existing archives, you can use FileManager.unzipItem(at sourceURL: URL, to destinationURL: URL).
This recursively extracts all entries within the archive to the destination URL:

let fileManager = FileManager()
let currentWorkingPath = fileManager.currentDirectoryPath
var sourceURL = URL(fileURLWithPath: currentWorkingPath)
sourceURL.appendPathComponent("archive.zip")
var destinationURL = URL(fileURLWithPath: currentWorkingPath)
destinationURL.appendPathComponent("directory")
do {
    try fileManager.createDirectory(at: destinationURL, withIntermediateDirectories: true, attributes: nil)
    try fileManager.unzipItem(at: sourceURL, to: destinationURL)
} catch {
    print("Extraction of ZIP archive failed with error:\(error)")
}

Advanced Usage

ZIP Foundation also allows you to individually access specific entries without the need to extract the whole archive. Additionally it comes with the ability to incrementally update archive contents.

Accessing individual Entries

To gain access to specific ZIP entries, you have to initialize an Archive object with a file URL that represents an existing archive. After doing that, entries can be retrieved via their relative path. Archive conforms to Sequence and therefore supports subscripting:

let fileManager = FileManager()
let currentWorkingPath = fileManager.currentDirectoryPath
var archiveURL = URL(fileURLWithPath: currentWorkingPath)
archiveURL.appendPathComponent("archive.zip")
guard let archive = Archive(url: archiveURL, accessMode: .read) else  {
    return
}
guard let entry = archive["file.txt"] else {
    return
}
var destinationURL = URL(fileURLWithPath: currentWorkingPath)
destinationURL.appendPathComponent("out.txt")
do {
    try archive.extract(entry, to: destinationURL)
} catch {
    print("Extracting entry from archive failed with error:\(error)")
}

The extract method accepts optional parameters that allow you to control compression and memory consumption.
You can find detailed information about that parameters in the method's documentation.

Creating Archives

To create a new Archive, pass in a non-existing file URL and AccessMode.create.

let currentWorkingPath = fileManager.currentDirectoryPath
var archiveURL = URL(fileURLWithPath: currentWorkingPath)
archiveURL.appendPathComponent("newArchive.zip")
guard let archive = Archive(url: archiveURL, accessMode: .create) else  {
    return
}

Adding and Removing Entries

You can add or remove entries to/from archives that have been opened with .create or .update AccessMode. To add an entry from an existing file, you can pass a relative path and a base URL to addEntry. The relative path identifies the entry within the ZIP archive. The relative path and the base URL must form an absolute file URL that points to the file you want to add to the archive:

let fileManager = FileManager()
let currentWorkingPath = fileManager.currentDirectoryPath
var archiveURL = URL(fileURLWithPath: currentWorkingPath)
archiveURL.appendPathComponent("archive.zip")
guard let archive = Archive(url: archiveURL, accessMode: .update) else  {
    return
}
var fileURL = URL(fileURLWithPath: currentWorkingPath)
fileURL.appendPathComponent("file.txt")
do {
    try archive.addEntry(with: fileURL.lastPathComponent, relativeTo: fileURL.deletingLastPathComponent())
} catch {
    print("Adding entry to ZIP archive failed with error:\(error)")
}

Alternatively, the addEntry(with path: String, fileURL: URL) method can be used to add files that are not sharing a common base directory. The fileURL parameter must contain an absolute file URL that points to a file, symlink or directory on an arbitrary file system location.

The addEntry method accepts several optional parameters that allow you to control compression, memory consumption and file attributes.
You can find detailed information about that parameters in the method's documentation.

To remove an entry, you need a reference to an entry within an archive that you can pass to removeEntry:

guard let entry = archive["file.txt"] else {
    return
}
do {
    try archive.remove(entry)
} catch {
    print("Removing entry from ZIP archive failed with error:\(error)")
}

Closure based Reading and Writing

ZIP Foundation also allows you to consume ZIP entry contents without writing them to the file system. The extract method accepts a closure of type Consumer. This closure is called during extraction until the contents of an entry are exhausted:

try archive.extract(entry, consumer: { (data) in
    print(data.count)
})

The data passed into the closure contains chunks of the current entry. You can control the chunk size of the entry by providing the optional bufferSize parameter.

You can also add entries from an in-memory data source. To do this you have to provide a closure of type Provider to the addEntry method:

let string = "abcdefghijkl"
guard let data = string.data(using: .utf8) else { return }
try? archive.addEntry(with: "fromMemory.txt", type: .file, uncompressedSize: UInt32(string.count), bufferSize: 4, provider: { (position, size) -> Data in
    // This will be called until `data` is exhausted (3x in this case).
    return data.subdata(in: position..<position+size)
})

The closure is called until enough data has been provided to create an entry of uncompressedSize. The closure receives position and size arguments so that you can manage the state of your data source.

In-Memory Archives

Besides closure based reading and writing of file based archives, ZIP Foundation also provides capabilities to process in-memory archives. This allows creation or extraction of archives that only reside in RAM. One use case for this functionality is dynamic creation of ZIP archives that are later sent to a client - without performing any disk IO.

To work with in-memory archives the init(data: Data, accessMode: AccessMode) initializer must be used.
To read or update an in-memory archive, the passed-in data must contain a representation of a valid ZIP archive.
To create an in-memory archive, the data parameter can be omitted:

let string = "Some string!"
guard let archive = Archive(accessMode: .create),
        let data = string.data(using: .utf8) else { return }
    try? archive.addEntry(with: "inMemory.txt", type: .file, uncompressedSize: UInt32(string.count), bufferSize: 4, provider: { (position, size) -> Data in
        return data.subdata(in: position..<position+size)
    })
let archiveData = archive.data

Progress Tracking and Cancellation

All Archive operations take an optional progress parameter. By passing in an instance of Progress, you indicate that you want to track the progress of the current ZIP operation. ZIP Foundation automatically configures the totalUnitCount of the progress object and continuously updates its completedUnitCount.
To get notifications about the completed work of the current operation, you can attach a Key-Value Observer to the fractionCompleted property of your progress object.
The ZIP Foundation FileManager extension methods also accept optional progress parameters. zipItem and unzipItem both automatically create a hierarchy of progress objects that reflect the progress of all items contained in a directory or an archive that contains multiple items.

The cancel() method of Progress can be used to terminate an unfinished ZIP operation. In case of cancelation, the current operation throws an ArchiveError.cancelledOperation exception.

Credits

ZIP Foundation is written and maintained by Thomas Zoechling.
Twitter: @weichsel.

License

ZIP Foundation is released under the MIT License.
See LICENSE for details.

Issues
  • cannot link ZIPFoundation 0.9.x in debug with xcode 12.5

    cannot link ZIPFoundation 0.9.x in debug with xcode 12.5

    Summary

    My app using ZIPFoundation cannot link anymore in debug with xcode 12.5 due to the following error: Undefined symbols for architecture arm64: "Swift._ArrayBuffer._copyContents(initializing: Swift.UnsafeMutableBufferPointer) -> (Swift.IndexingIterator<Swift._ArrayBuffer>, Swift.Int)", referenced from: generic specialization <serialized, Swift._ArrayBuffer<Swift.Int8>> of Swift._copyCollectionToContiguousArray(A) -> Swift.ContiguousArray<A.Element> in AppDelegate.o

    Steps to Reproduce

    i was happily using ZIPFoundation 0.9.9 within my project with xcode12.4. After upgrading to xcode 12.5 i had to recompile ZIPFoundation due to swift compiler changes. After that i recompiled ZIPFoundation i tried to compile my project but could not due to the error above. A few notes:

    1. i tried upgrading to ZF 0.9.12 or even latest git: same error
    2. compiling in release works as expected
    3. removing ZF from my project i managed to compile it in debug, so the problem resides within the linking in debug with ZF

    Expected Results

    successful linking

    Actual Results

    linking error: Undefined symbols for architecture arm64: "Swift._ArrayBuffer._copyContents(initializing: Swift.UnsafeMutableBufferPointer) -> (Swift.IndexingIterator<Swift._ArrayBuffer>, Swift.Int)", referenced from: generic specialization <serialized, Swift._ArrayBuffer<Swift.Int8>> of Swift._copyCollectionToContiguousArray(A) -> Swift.ContiguousArray<A.Element> in AppDelegate.o

    Regression & Version

    xcode 12.5 and ZF 0.9.9 / 0.9.12 / git 2021-05-12

    Related Link

    opened by alienpenguin 17
  • Memory archive v2

    Memory archive v2

    Sorry about the long radio silence! I finally got around to working on https://github.com/weichsel/ZIPFoundation/pull/78 again, and ended up with a complete redesign that massively simplifies the semantics of the user API.

    Changes proposed in this PR

    • Replace the previous use of fmemopen and open_memstream with a new abstraction MemoryFile which is implemented with funopen on Apple platforms and fopencookie on Linux.
    • Add Archive.init?(data: Data = Data(), accessMode mode: AccessMode, preferredEncoding: String.Encoding? = nil) for in-memory processing of either an existing Data or a new Data to be created.
    • Add Archive.data() -> Data? which for in-memory archives returns the resulting file.
    • So for .read processing, you'd do:
    let archive = Archive(data: myData, accessMode: .read)
    // Usual extraction, as with a file based archive
    
    • For .write and .update processing, there is one additional call:
    let archive = Archive(accessMode: .write)
    // Add entries to archive, as with a file based archive
    if let myData = archive.data {
       //
    }
    
    • On an unrelated matter, I've made Data.compress and Data.uncompress public, because I needed this for a different project (Gzip compression in a web server).

    Tests performed

    • Added unit tests for both the low level MemoryFile functionality and the in-memory Archive abstraction on top of it.
    • Passes on both macOS and Linux

    Further info for the reviewer

    Open Issues

    • The one flaw is that on Linux, this requires passing an extra flag to the Swift compilation. In Swift 5.0 and later, this is possible through Package.swift, and I've added the flag there. On Swift < 5, I believe this functionality is not yet available in the Swift package manager, so you have to manually add -Xcc -D_GNU_SOURCE=1 to the swift invocation. I don't know whether it's possible to add the flag to the Cocoapods build.
    opened by microtherion 14
  • Support in-memory processing of archives

    Support in-memory processing of archives

    Hi! I'm implementing a HTTP server that sometimes needs to extract data from uploaded dictionaries. Since these are ZIP encoded, I was looking for a library, and came across your project. However, I wanted to avoid writing out the uploaded data to a temp file, so I added a new method to your Archive class.

    Changes proposed in this PR

    • Added initializer Archive.init?(data: UnsafeMutableRawPointer, length: Int) that creates a read-only archive from an in-memory pointer instead of an URL.

    Further info for the reviewer

    This is almost certainly not yet in a state where you'd consider merging it; I wanted to send you this to start a discussion on whether you'd be potentially interested in the change. See open issues below.

    Open Issues

    • Code relies on fmemopen which was only introduced in macOS 10.13 / iOS 11.0
    • Code only implements .read mode, because that's my only use case, and buffer handling for the other modes could get tricky.
    • No unit tests yet.
    opened by microtherion 12
  • App crash Thread 21: signal SIGABRT

    App crash Thread 21: signal SIGABRT

    Hello I faced app crash here

    try fileManager.unzipItem(at: archiveURL, to: packDirURL, progress: progress)
    

    when I updated from from 0.9.8 to 0.9.9 version

    opened by alexanderkhitev 11
  • Filename decoding issue for zip files archived by macOS

    Filename decoding issue for zip files archived by macOS

    Summary

    Zip entries filenames are not decoded correctly when reading the content of a zip file produced by macOS.

    Steps to Reproduce

    1. on macOS 10.13.4, make a zip file archive.zip with 2 files "Benoît.txt" and "pic👨‍👩‍👧‍👦🎂.jpg"
    2. Read the path entries of this zip archive on iOS 11.3.1:
    let archive = Archive(url: /urlto:archive.zip/, accessMode: .read)!
    for entry in archive {
      print("entry: \(entry.path)")
    }
    

    Expected Results

    It should log "Benoît.txt" and "pic👨‍👩‍👧‍👦🎂.jpg"

    Actual Results

    It logs "Benoît.txt" and "pic👨‍👩‍👧‍👦🎂.jpg"

    Regression & Version

    • I'm using ZIPFoundation on the master branch (e44ff6b)

    • The workaround to fix this issue is to force to use the UTF-8 encoder in the path getter of the Entry struct in Entry.Swift

    public var path: String {
      ...
      let encoding = String.Encoding.utf8 //isUTF8 ? String.Encoding.utf8 : codepage437
      return String(data: self.centralDirectoryStructure.fileNameData, encoding: encoding) ?? ""
    }
    

    Related Link

    • archive.zip to reproduce this issue. https://www.dropbox.com/s/5tuf7ll4hapwgp1/utfissue.zip?dl=0
    help wanted 
    opened by benoitsan 10
  • Fatal Error: NSFileModificationDate not implemented when unzip item on Linux.

    Fatal Error: NSFileModificationDate not implemented when unzip item on Linux.

    Summary

    Fatal error when tried to unzip on linux. Same code, everything works well on macOS, but failed on linux.

    Steps to Reproduce

    try FileManager.default.unzipItem(at: urlToZip, to: urlToDest)
    

    I make a small demo to reproduce this:

    git clone https://gitlab.com/isaacxen/zipFoundationErrorDemo.git
    cd zipFoundationErrorDemo/
    cp example.epub /tmp
    swift package update
    swift run
    

    Expected Results

    successfully unzip.

    Actual Results

    Fatal error: Attribute type not implemented: FileAttributeKey(rawValue: "NSFileModificationDate"): file Foundation/FileManager.swift, line 139
    

    Regression & Version

    ZipFoundation: 0.9.6 Ubuntu 16.04 Swift 4.1

    Related Link

    opened by IsaacXen 10
  • Levi/108

    Levi/108

    Fixes #108

    Changes proposed in this PR

    • Using modern temporary directory mechanism, with fallback if needed.
    • Addresses archive replacement issue when archives reside on different volumes.
    • Removed fileExists check when creating directory as this is contrary to recommended practice.

    Tests performed

    • Added test for temporary directory creation mechanism.
    • Added test for cross-link (separate volume) archive replacement changes.

    Further info for the reviewer

    • Opened up private methods to internal (implicit) for testing purposes.
    • Maintained 100% code coverage and no swiftlint issues as per contribution guidelines.
    opened by levigroker 10
  • Set `DYLIB_COMPATIBILITY_VERSION` and `DYLIB_CURRENT_VERSION`.

    Set `DYLIB_COMPATIBILITY_VERSION` and `DYLIB_CURRENT_VERSION`.

    IOS-Pods-DFU-Library recently updated its dependency to use ZIPFoundation. In our project we're using Carthage to include that framework, so I also included ZIPFouncation using Carthage. However this results into an error when running the application.

    dyld: Library not loaded: @rpath/ZIPFoundation.framework/ZIPFoundation
      Referenced from: /private/var/containers/Bundle/Application/6768A1C7-5C6E-471B-9969-F84AEFE42EA3/App.app/Frameworks/iOSDFULibrary.framework/iOSDFULibrary
      Reason: Incompatible library version: iOSDFULibrary requires version 1.0.0 or later, but ZIPFoundation provides version 0.0.0
    

    After doing some search I found a similar issue in a different project which was solved by setting DYLIB_COMPATIBILITY_VERSION and DYLIB_CURRENT_VERSION.

    Changes proposed in this PR

    • This PR sets the mentioned project settings to 1 to resolve the mentioned incompatibility when building the project using Carthage.

    Tests performed

    • I don't know what I can or should test, please advise. Our project compiles with this change or fails with the mentioned error without it.

    Further info for the reviewer

    • The mentioned solution comes from here: https://github.com/jpsim/Yams/issues/131
    opened by DevAndArtist 10
  • Suggestion: Replace CRC32 with call to zlib for better performance?

    Suggestion: Replace CRC32 with call to zlib for better performance?

    Is your feature request related to a problem? Please describe. Despite #40, calculating the CRC32 is still slow, especially in debug builds. Given that zlib exposes a CRC32 method and zlib ships with all Macs, would you be open to replacing the built-in CRC32 computation with a call to zlib's crc32 method?

    Describe the solution you'd like Data.crc32 calls into zlib instead of implementing the CRC32 computation itself.

    Describe alternatives you've considered Keep things as is, accepting slow performance in debug builds.

    opened by MrMage 8
  • Some files are missing after extracting ZIP

    Some files are missing after extracting ZIP

    Summary

    Some files are missing after extracting ZIP archive on iOS. I tried to create an archive without compression or from different OS versions but it did not help. Everything works fine when the archive is slightly smaller.

    Steps to Reproduce

    Download the archive (~3GiB) and try to extract it on your iOS and laptop, a lot of files will be missing on iOS

    Expected Results

    Around 192 309 files after unpacking on a computer

    Actual Results

    Around 34 309 files after unpacking on IOS

    Regression & Version

    ZIPFoundation (0.9.12)

    opened by shlima 8
  • **Is your feature request related to a problem? Please describe.**

    **Is your feature request related to a problem? Please describe.**

    Is your feature request related to a problem? Please describe. A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]

    Describe the solution you'd like A clear and concise description of what you want to happen.

    Describe alternatives you've considered A clear and concise description of any alternative solutions or features you've considered.

    Additional context Add any other context or screenshots about the feature request here.

    opened by festival-up 0
  • Why doesn't ios11 support

    Why doesn't ios11 support

    opened by nandock 0
  • add replace option if file is existed when unzip

    add replace option if file is existed when unzip

    Fixes #

    Changes proposed in this PR

    Tests performed

    Further info for the reviewer

    Open Issues

    opened by vit1812 0
  • ZIP64 Support

    ZIP64 Support

    Fixes #118

    Changes proposed in this PR

    • Add zip64 structures
      • Archive.Zip64EndOfCentralDirectoryRecord
      • Archive.Zip64EndOfCentralDirectoryLocator
      • Entry.Zip64ExtendedInformation
        • Using in both Local Header File and Central Directory, if uncompressed size or compressed size exceeds the limit, extended information should include both uncompressed and compressed file size fields. (at least in the Local header File)
    • Support writing zip64 file

    Tests performed

    • All unit tests pass and coverage rate is 100%.
    • Going to resolve SwiftLint errors after reviewer finished.
    • In unit tests, the limit size of each field is actually a mock size.
      • Also tested archive files over 4GB on macOS, zip64 files were written out, and they can be opened successfully by double-click.

    Further info for the reviewer

    Note

    1. Since this PR is large enough, the remaining tasks will be handled in the #Next step(See below). Some methods don't work very well enough currently, for this reason some code was commented out in the test cases.
    2. Modified the github action settings temporarily, will revert this change when everything is done.
    3. Code review workflow: https://github.com/weichsel/ZIPFoundation/issues/118#issuecomment-893362662

    Document

    Zip specification: https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT

    Overall .ZIP file format:

    [local file header 1]
    [file data 1]
     . 
    .
     .
    [local file header n]
    [file data n]
    [archive extra data record] 
    [central directory header 1]
    .
    .
    .
    [central directory header n]
    [zip64 end of central directory record]
    [zip64 end of central directory locator] 
    [end of central directory record]
    

    Others

    @weichsel Please take a look when you have time, there's no need to hurry~ @billparousis I remember you commented on the zip64 issue, please take look if you still interested in it.

    Next step

    • Unzip zip64 file and reading part.
      • Reading zip64 file
      • Fix entry extraction
      • Fix entry removal
      • DataDescriptor
      • ...
    opened by Ckitakishi 4
  • CRC Calculations Are Ocassionally Incorrect

    CRC Calculations Are Ocassionally Incorrect

    The CRC calculation in the Data.process(...) is incorrect. In the case where the loop continues without stream.src_size getting set to zero, another pass with the same datablock is done on the CRC, causing it to be incorrect. compression_stream_process does not necessarily consume all the bytes.

    unzip utilities on macOS are forgiving on incorrect CRCs (and ZIPFoundation itself lets you ignore them), but many other libraries will error out if the stored CRCs in the ZIP file don't match the CRC of the decoded file/bytes.

    The correct action for COMPRESSION_STREAM_ENCODE is to run the CRC pass only when a new block of data is read from the provider.

    opened by barnstar 1
  • Use zlib.crc32 when available to improve performance of Data.crc32()

    Use zlib.crc32 when available to improve performance of Data.crc32()

    Use zlib.crc32 when available to improve performance of Data.crc32() by 3x in Release mode, 3000x in Debug mode

    Tests performed

    Unit tests pass Performance test comparison (on-device in Release mode: 3x speedup. Simulator in Debug mode: 3000x speedup) Performed bitwise comparison of pure-swift path and zlib.crc32 path for random buffers; results identical.

    Further info for the reviewer

    Our project was previously able to utilize ZIPFoundation only by forcing the optimizer on via settings overrides applied in the Podfile; otherwise, in Debug mode, reading/writing ZIP archives in our use case took >10s rather than ~1.3s. As we switch to SwiftPM, this tweak is no longer possible. However, with this modification, both Debug and Release builds can execute the operations in ~1s.

    Open Issues

    opened by scier 2
  • FileManager.unzipItem doesn't work with some URLs created from relative paths

    FileManager.unzipItem doesn't work with some URLs created from relative paths

    Summary

    If I try to unzip a file (using the FileManager extension) to a URL which was created with a relative path, the extraction fails with an error. Using the relative URL's absoluteURL does work.

    Steps to Reproduce

    1. Create a "relative" URL, such as: URL(fileURLWithPath: "../../Resources", isDirectory: true).
    2. Pass that URL as the to parameter of FileManager.unzipItem (along with a URL to a valid zip file).

    Expected Results

    The contents of the zip file is successfully extracted to the target location.

    Actual Results

    An error is thrown. For example, if the zip file contains a compressed directory called "media", the error is:

    The item couldn’t be opened because the file name “media” is invalid.

    If I get the absoluteURL of the target URL first, it does work.

    Regression & Version

    Tested with 0.9.12.

    Findings

    I think this is caused because the implementation of URL.isContained(in:) (defined in FileManager+ZIP.swift) calls standardized on the URLs first, which outputs the wrong URL when it is created with a relative path. For example:

    fileManager.changeCurrentDirectoryPath("/Users/Robert/Documents")
    
    let relativePath = "../../David/Documents"
    let relativeURL = URL(fileURLWithPath: relativePath)
    
    relativeURL.absoluteString
    // file:///Users/David/Documents
    
    relativeURL.standardized.absoluteString
    // file:///Users/Robert/Documents/David/Documents
    
    opened by Aquilosion 0
  • uncompressedSize is always zero

    uncompressedSize is always zero

    I have a problem: I can't monitor the progress, so I debug and find that the uncompressedSize of entry in the zip always zero.

    public var uncompressedSize: Int {
            return Int(dataDescriptor?.uncompressedSize ?? localFileHeader.uncompressedSize)
        }
    

    The code uses dataDescriptor.uncompressedSize and localFileHeader.uncompressedSize, but both of these values in the entity are zero :

    ▿ Entry
      ▿ centralDirectoryStructure : CentralDirectoryStructure
        ......
        - compressedSize : 4524191
        - uncompressedSize : 4646944
        ......
      ▿ localFileHeader : LocalFileHeader
       ......
        - compressedSize : 0
        - uncompressedSize : 0
         ......
      ▿ dataDescriptor : Optional<DataDescriptor>
        ▿ some : DataDescriptor
         ......
          - compressedSize : 4524191
          - uncompressedSize : 0
    
    opened by zyvv 0
  • Caller of `FileManager`'s `unzipItem(at:to:skipCRC32:progress:preferredEncoding:)` need to know what was unzipped

    Caller of `FileManager`'s `unzipItem(at:to:skipCRC32:progress:preferredEncoding:)` need to know what was unzipped

    Is your feature request related to a problem? Please describe. In most use cases of FileManager's unzipItem(at:to:skipCRC32:progress:preferredEncoding:) callers need to know what was unzipped. Now this can be achieved by reading contents of destination. That's extra file operation.

    Describe the solution you'd like This method should return an array of URL's of unzipped items.

    Describe alternatives you've considered Alternatively method might have optional delegate or callback parameter.

    Additional context n/a

    opened by valeriyvan 1
  • Consumer callback is called infinitely while extracting compressed 0byte entry

    Consumer callback is called infinitely while extracting compressed 0byte entry

    Summary

    Extract a compressed 0byte file, Consumer callback is called infinitely.

    I write my code using entry.uncompressedSize for bufferSize to reduce count of consumer closure is called. (like below test)

    My app has infinitely loop.

    Steps to Reproduce

    This test never stop.

    Put testExtractCopressedEmptyData.zip to Tests/ZIPFoundationTests/Resources

    func testExtractCopressedEmptyData() {
        let expectation = XCTestExpectation(description: "Called extract consumer once")
        let archive = self.archive(for: #function, mode: .read)
        // empty.txt is compressed emtpy file.
        guard let entry = archive["empty.txt"] else { XCTFail("Failed to extract entry."); return }
        do {
            // Use uncompressedSize to reduce count of closure called
            let checksum = try archive.extract(entry, bufferSize: UInt32(entry.uncompressedSize)) { (data) in
                XCTAssertEqual(data.count, 0)
                expectation.fulfill()
            }
            XCTAssert(entry.checksum == checksum)
        } catch {
            XCTFail("Failed to unzip data descriptor archive")
        }
        wait(for: [expectation], timeout: 3.0)
    }
    

    To stop infinity loop, I can rewrite the extract code:

    let checksum = try archive.extract(entry, bufferSize: max(UInt32(entry.uncompressedSize), 1)) { /* consumer code */ }
    

    However, consumer calls twice. (It should be once)

    With Xcode debugger, I notice that the test calls Data.process method with bufferSize = 0. Data.process creates 0-byte Data inifinity.

    NOTE: macOS can creates Zip file have 0byte compressed entry. I create sample zip file and attach in this issue. It contains 0 byte "empty.txt" and 86336byte "faust.txt" (zip command creates stored no-compressed entry for 0 byte file)

    $ unzip -v Tests/ZIPFoundationTests/Resources/testExtractCopressedEmptyData.zip
    Archive:  Tests/ZIPFoundationTests/Resources/testExtractCopressedEmptyData.zip
     Length   Method    Size  Cmpr    Date    Time   CRC-32   Name
    --------  ------  ------- ---- ---------- ----- --------  ----
           0  Defl:N        2   0% 10-23-2020 16:13 00000000  empty.txt
      222214  Defl:N    86336  61% 06-19-2014 17:09 af9c0a0d  faust.txt
    --------          -------  ---                            -------
      222214            86338  61%                            2 files
    

    Method Defl:N means deflated entry.

    Expected Results

    consumer closure called only once with empty data (Data())

    Actual Results

    consumer closure called infinitely.

    Regression & Version

    I am using v0.9.11 Xcode 12.2 & macOS 10.15.7

    Related Link

    opened by mtgto 2
Releases(0.9.12)
  • 0.9.12(Feb 22, 2021)

    Added

    • Added check to disallow removal of entries from readonly archives
    • Added guard against API misuse by providing zero byte buffer sizes

    Updated

    • Fixed an UInt16 overflow when calculating the end of the central directory record
    • Fixed detection of ZIP version required to extract
    • Fixed missing consumer closure call for zero byte entries
    • Fixed erroneous application of .deflate compression on .symlink and .directory entries
    • Improved detection of .directory entries
    • Improved performance when looking up entries via subscripting
    • Improved consistency of URL format used in the Swift package description
    Source code(tar.gz)
    Source code(zip)
  • 0.9.11(Mar 26, 2020)

    Added

    • Read/Write support for in-memory archives

    Updated

    • Fixed a memory safety issue during (de)compression
    • Fixed dangling pointer warnings when serializing ZIP internal structs to Data
    • Fixed missing Swift 5 language version when integrating via CocoaPods
    • Fixed inconsistent usage of the optional preferredEncoding parameter during entry addition
    • Improved documentation for compression settings
    Source code(tar.gz)
    Source code(zip)
  • 0.9.10(Dec 2, 2019)

    Added

    • Optional skipCRC32 parameter to speed up entry reading

    Updated

    • Fixed a race condition during archive creation or extraction
    • Fixed an error when trying to add broken symlinks to an archive
    • Fixed an App Store submission issue by updating the product identifier to use reverse DNS notation
    • Improved CRC32 calculation performance
    • Improved entry replacement performance on separate volumes
    • Improved documentation for closure-based writing
    Source code(tar.gz)
    Source code(zip)
  • 0.9.9(Apr 10, 2019)

    Added

    • Swift 5.0 support
    • Optional preferredEncoding parameter to explicitly configure an encoding for filepaths

    Updated

    • Fixed a library load error related to dylib versioning
    • Fixed a hang during read when decoding small, .deflate compressed entries
    • Improved Linux support
    • Improved test suite on non-Darwin platforms
    Source code(tar.gz)
    Source code(zip)
  • 0.9.8(Jan 21, 2019)

  • 0.9.7(Jan 21, 2019)

    Added

    • App extension support
    • Optional compressionMethod paramter for zipItem:

    Updated

    • Fixed a path traversal attack vulnerability
    • Fixed a crash due to wrong error handling after failed fopen calls

    Removed

    • Temporarily removed the currently unsupported .modificationDate attribute on non-Darwin platforms
    Source code(tar.gz)
    Source code(zip)
  • 0.9.6(Apr 24, 2018)

  • 0.9.5(Feb 9, 2018)

    Added

    • Progress tracking support
    • Operation cancellation support

    Updated

    • Improved performance of CRC32 calculations
    • Improved Linux support
    • Fixed wrong behaviour when using the shouldKeepParent flag
    • Fixed a linker error during archive builds when integrating via Carthage
    Source code(tar.gz)
    Source code(zip)
  • 0.9.4(Dec 8, 2017)

    Updated

    • Fixed a wrong setting for FRAMEWORK_SEARCH_PATHS that interfered with code signing
    • Added a proper value for CURRENT_PROJECT_VERSION to make the framework App Store compliant when using Carthage
    Source code(tar.gz)
    Source code(zip)
  • 0.9.3(Nov 7, 2017)

  • 0.9.2(Aug 5, 2017)

    Updated

    • Changed default POSIX permissions when file attributes are missing
    • Improved docs
    • Fixed a compiler warning when compiling with the latest Xcode 9 beta
    Source code(tar.gz)
    Source code(zip)
  • 0.9.1(Jul 10, 2017)

    Added

    • Optional parameter to skip CRC32 checksum calculation

    Updated

    • Tweaked POSIX buffer sizes to improve IO and compression performance
    • Improved source readability
    • Refined documentation

    Removed

    • Optional parameter to skip decompression during entry retrieval
    Source code(tar.gz)
    Source code(zip)
  • 0.9.0(Jun 26, 2017)

Owner
Thomas Zoechling
I write software for the Mac. Currently helping to build MindNode (macOS, iOS) @IdeasOnCanvas. Also working on Claquette (macOS) https://peakstep.com/claquette
Thomas Zoechling
FileManager replacement for Local, iCloud and Remote (WebDAV/FTP/Dropbox/OneDrive) files -- Swift

This Swift library provide a swifty way to deal with local and remote files and directories in a unified way. This library provides implementaion of W

Amir Abbas Mousavian 770 Sep 15, 2021
Simple and expressive file management in Swift

Installation • Usage • License • Documentation FileKit is a Swift framework that allows for simple and expressive file management. Development happens

Nikolai Vazquez 2.2k Sep 17, 2021
zip file I/O library for iOS, macOS and tvOS

ZipZap is a zip file I/O library for iOS, macOS and tvOS. The zip file is an ideal container for compound Objective-C documents. Zip files are widely

Glen Low 1.2k Aug 18, 2021
ZipArchive is a simple utility class for zipping and unzipping files on iOS, macOS and tvOS.

SSZipArchive ZipArchive is a simple utility class for zipping and unzipping files on iOS, macOS and tvOS. Unzip zip files; Unzip password protected zi

ZipArchive 4.9k Sep 17, 2021
A micro-framework for observing file changes, both local and remote. Helpful in building developer tools.

KZFileWatchers Wouldn't it be great if we could adjust feeds and configurations of our native apps without having to sit back to Xcode, change code, r

Krzysztof Zabłocki 1k Sep 14, 2021