Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions Sources/ZIPFoundation/Archive+Helpers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,27 @@ extension Archive {
})
}

func readSymbolicLink(entry: Entry, bufferSize: Int, skipCRC32: Bool,
progress: Progress? = nil, with consumer: Consumer) throws -> CRC32 {
var checksum = CRC32(0)
let localFileHeader = entry.localFileHeader
guard let compressionMethod = CompressionMethod(rawValue: localFileHeader.compressionMethod) else {
throw ArchiveError.invalidCompressionMethod
}
switch compressionMethod {
case .none:
let localFileHeader = entry.localFileHeader
let size = Int(localFileHeader.compressedSize)
let data = try Data.readChunk(of: size, from: self.archiveFile)
checksum = data.crc32(checksum: 0)
try consumer(data)
progress?.completedUnitCount = self.totalUnitCountForReading(entry)
case .deflate: checksum = try self.readCompressed(entry: entry, bufferSize: bufferSize,
skipCRC32: skipCRC32, progress: progress, with: consumer)
}
return checksum
}

// MARK: - Writing

func writeEntry(uncompressedSize: Int64, type: Entry.EntryType,
Expand Down
16 changes: 7 additions & 9 deletions Sources/ZIPFoundation/Archive+Reading.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,13 @@ extension Archive {
/// - url: The destination file URL.
/// - bufferSize: The maximum size of the read buffer and the decompression buffer (if needed).
/// - skipCRC32: Optional flag to skip calculation of the CRC32 checksum to improve performance.
/// - allowUncontainedSymlinks: Optional flag to allow symlinks that point to paths outside the destination.
/// - symlinksValidWithin: Symbolic links are valid within the url
/// - progress: A progress object that can be used to track or cancel the extract operation.
/// - Returns: The checksum of the processed content or 0 if the `skipCRC32` flag was set to `true`.
/// - Throws: An error if the destination file cannot be written or the entry contains malformed content.
public func extract(_ entry: Entry, to url: URL, bufferSize: Int = defaultReadChunkSize,
skipCRC32: Bool = false, allowUncontainedSymlinks: Bool = false,
skipCRC32: Bool = false,
symlinksValidWithin: URL? = nil,
progress: Progress? = nil) throws -> CRC32 {
guard bufferSize > 0 else {
throw ArchiveError.invalidBufferSize
Expand Down Expand Up @@ -60,7 +61,7 @@ extension Archive {
let parentURL = url.deletingLastPathComponent()
let isAbsolutePath = (linkPath as NSString).isAbsolutePath
let linkURL = URL(fileURLWithPath: linkPath, relativeTo: isAbsolutePath ? nil : parentURL)
let isContained = allowUncontainedSymlinks || linkURL.isContained(in: parentURL)
let isContained = linkURL.isContained(in: symlinksValidWithin ?? parentURL)
guard isContained else { throw ArchiveError.uncontainedSymlink }

try fileManager.createParentDirectoryStructure(for: url)
Expand Down Expand Up @@ -108,12 +109,9 @@ extension Archive {
try consumer(Data())
progress?.completedUnitCount = self.totalUnitCountForReading(entry)
case .symlink:
let localFileHeader = entry.localFileHeader
let size = Int(localFileHeader.compressedSize)
let data = try Data.readChunk(of: size, from: self.archiveFile)
checksum = data.crc32(checksum: 0)
try consumer(data)
progress?.completedUnitCount = self.totalUnitCountForReading(entry)
checksum = try self.readSymbolicLink(entry: entry, bufferSize: bufferSize,
skipCRC32: skipCRC32, progress: progress, with: consumer)

}
return checksum
}
Expand Down
14 changes: 14 additions & 0 deletions Sources/ZIPFoundation/Archive+ReadingDeprecated.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,18 @@ public extension Archive {
try self.extract(entry, bufferSize: Int(bufferSize), skipCRC32: skipCRC32,
progress: progress, consumer: consumer)
}

@available(*, deprecated,
message: "Please use `symlinksValidWithin` for validate symlinks")
func extract(_ entry: Entry, to url: URL, bufferSize: Int = defaultReadChunkSize,
skipCRC32: Bool = false,
allowUncontainedSymlinks: Bool,
progress: Progress? = nil) throws -> CRC32 {
var symlinksValidWithin: URL?
if allowUncontainedSymlinks {
symlinksValidWithin = URL.rootFS
}
return try self.extract(entry, to: url, skipCRC32: skipCRC32,
symlinksValidWithin: symlinksValidWithin, progress: progress)
}
}
11 changes: 7 additions & 4 deletions Sources/ZIPFoundation/FileManager+ZIP.swift
Original file line number Diff line number Diff line change
Expand Up @@ -87,19 +87,20 @@ extension FileManager {
/// - sourceURL: The file URL pointing to an existing ZIP file.
/// - destinationURL: The file URL that identifies the destination directory of the unzip operation.
/// - skipCRC32: Optional flag to skip calculation of the CRC32 checksum to improve performance.
/// - allowUncontainedSymlinks: Optional flag to allow symlinks that point to paths outside the destination.
/// - symlinksValidWithin: Symbolic links are valid within the url. Defalut: destinationURL.
/// - progress: A progress object that can be used to track or cancel the unzip operation.
/// - pathEncoding: Encoding for entry paths. Overrides the encoding specified in the archive.
/// - Throws: Throws an error if the source item does not exist or the destination URL is not writable.
public func unzipItem(at sourceURL: URL, to destinationURL: URL,
skipCRC32: Bool = false, allowUncontainedSymlinks: Bool = false,
skipCRC32: Bool = false, symlinksValidWithin: URL? = nil,
progress: Progress? = nil, pathEncoding: String.Encoding? = nil) throws {
let fileManager = FileManager()
guard fileManager.itemExists(at: sourceURL) else {
throw CocoaError(.fileReadNoSuchFile, userInfo: [NSFilePathErrorKey: sourceURL.path])
}
let archive = try Archive(url: sourceURL, accessMode: .read, pathEncoding: pathEncoding)
var totalUnitCount = Int64(0)
let symlinksValidWithin = symlinksValidWithin ?? destinationURL
if let progress = progress {
totalUnitCount = archive.reduce(0, { $0 + archive.totalUnitCountForReading($1) })
progress.totalUnitCount = totalUnitCount
Expand All @@ -117,11 +118,13 @@ extension FileManager {
let entryProgress = archive.makeProgressForReading(entry)
progress.addChild(entryProgress, withPendingUnitCount: entryProgress.totalUnitCount)
crc32 = try archive.extract(entry, to: entryURL,
skipCRC32: skipCRC32, allowUncontainedSymlinks: allowUncontainedSymlinks,
skipCRC32: skipCRC32,
symlinksValidWithin: symlinksValidWithin,
progress: entryProgress)
} else {
crc32 = try archive.extract(entry, to: entryURL,
skipCRC32: skipCRC32, allowUncontainedSymlinks: allowUncontainedSymlinks)
skipCRC32: skipCRC32,
symlinksValidWithin: symlinksValidWithin)
}

func verifyChecksumIfNecessary() throws {
Expand Down
14 changes: 14 additions & 0 deletions Sources/ZIPFoundation/FileManager+ZIPDeprecated.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,18 @@ public extension FileManager {
try self.unzipItem(at: sourceURL, to: destinationURL, skipCRC32: skipCRC32,
progress: progress, pathEncoding: preferredEncoding)
}

@available(*, deprecated,
message: "Please use `symlinksValidWithin` for validate symlinks")
func unzipItem(at sourceURL: URL, to destinationURL: URL,
skipCRC32: Bool = false, allowUncontainedSymlinks: Bool,
progress: Progress? = nil, pathEncoding: String.Encoding? = nil) throws {
var symlinksValidWithin: URL?
if allowUncontainedSymlinks {
symlinksValidWithin = URL.rootFS
}
try self.unzipItem(at: sourceURL, to: destinationURL, skipCRC32: skipCRC32,
symlinksValidWithin: symlinksValidWithin,
progress: progress, pathEncoding: pathEncoding)
}
}
4 changes: 4 additions & 0 deletions Sources/ZIPFoundation/URL+ZIP.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,8 @@ extension URL {
return URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(
ProcessInfo.processInfo.globallyUniqueString)
}

static var rootFS: URL {
URL(fileURLWithPath: "/")
}
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
28 changes: 28 additions & 0 deletions Tests/ZIPFoundationTests/ZIPFoundationFileManagerTests+ZIP64.swift
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,34 @@ extension ZIPFoundationTests {
}
}

func testUnzipSymlink() {
// stored by zip 3.0 via command line: zip -ry
//
// testUnzipSymlink.zip/
// ├─ directory1
// ├─ testUnzipSymlink
// ├─ directory2
// ├─ testUnzipSymlink.png
do {
try unarchiveZIP64Item(for: #function)
} catch {
XCTFail("\(error)")
}
}

func testUnzipCompressedSymlink() {
// testUnzipCompressedSymlink.zip/
// ├─ directory1
// ├─ testUnzipSymlink (compressed)
// ├─ directory2
// ├─ testUnzipSymlink.png
do {
try unarchiveZIP64Item(for: #function)
} catch {
XCTFail("\(error)")
}
}

// MARK: - Helpers

private func archiveZIP64Item(for testFunction: String, compressionMethod: CompressionMethod) throws {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ extension ZIPFoundationTests {
provider: { (_, _) -> Data in
return Data(linkTarget.utf8)
})
try? fileManager.unzipItem(at: linkArchiveURL, to: destinationURL, allowUncontainedSymlinks: true)
try? fileManager.unzipItem(at: linkArchiveURL, to: destinationURL, symlinksValidWithin: URL.rootFS)
XCTAssert(fileManager.itemExists(at: destinationURL.appendingPathComponent("link")))
}

Expand Down
11 changes: 11 additions & 0 deletions Tests/ZIPFoundationTests/ZIPFoundationReadingTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -290,4 +290,15 @@ extension ZIPFoundationTests {
XCTAssertCocoaError(try fileManager.unzipItem(at: archive.url, to: destinationURL),
throwsErrorWithCode: .fileReadInvalidFileName)
}

func testInvalidSymlinkCompressionMethodErrorConditions() {
let archive = self.archive(for: #function, mode: .read)
guard let entry = archive["symlink"] else {
XCTFail("Missing entry in test archive")
return
}

XCTAssertSwiftError(try archive.extract(entry, consumer: { (_) in }),
throws: Archive.ArchiveError.invalidCompressionMethod)
}
}
7 changes: 5 additions & 2 deletions Tests/ZIPFoundationTests/ZIPFoundationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,8 @@ extension ZIPFoundationTests {
("testZipItem", testZipItem),
("testLinuxTestSuiteIncludesAllTests", testLinuxTestSuiteIncludesAllTests),
("testFileModificationDate", testFileModificationDate),
("testFileModificationDateHelperMethods", testFileModificationDateHelperMethods)
("testFileModificationDateHelperMethods", testFileModificationDateHelperMethods),
("testInvalidSymlinkCompressionMethodErrorConditions", testInvalidSymlinkCompressionMethodErrorConditions)
] + zip64Tests + darwinOnlyTests + swift5OnlyTests
}

Expand Down Expand Up @@ -289,7 +290,9 @@ extension ZIPFoundationTests {
("testWriteLargeChunk", testWriteLargeChunk),
("testExtractUncompressedZIP64Entries", testExtractUncompressedZIP64Entries),
("testExtractCompressedZIP64Entries", testExtractCompressedZIP64Entries),
("testExtractEntryWithZIP64DataDescriptor", testExtractEntryWithZIP64DataDescriptor)
("testExtractEntryWithZIP64DataDescriptor", testExtractEntryWithZIP64DataDescriptor),
("testUnzipSymlink", testUnzipSymlink),
("testUnzipCompressedSymlink", testUnzipCompressedSymlink)
]
}

Expand Down