Skip to content

Commit 72c50e0

Browse files
p.volkhinF0xWh1sp3r
authored andcommitted
Improved symlinks support
1 parent 22787ff commit 72c50e0

7 files changed

Lines changed: 65 additions & 11 deletions

File tree

Sources/ZIPFoundation/Archive+Helpers.swift

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,27 @@ extension Archive {
4242
})
4343
}
4444

45+
func readSymbolicLink(entry: Entry, bufferSize: Int, skipCRC32: Bool,
46+
progress: Progress? = nil, with consumer: Consumer) throws -> CRC32 {
47+
var checksum = CRC32(0)
48+
let localFileHeader = entry.localFileHeader
49+
guard let compressionMethod = CompressionMethod(rawValue: entry.localFileHeader.compressionMethod) else {
50+
throw ArchiveError.invalidCompressionMethod
51+
}
52+
switch compressionMethod {
53+
case .none:
54+
let localFileHeader = entry.localFileHeader
55+
let size = Int(localFileHeader.compressedSize)
56+
let data = try Data.readChunk(of: size, from: self.archiveFile)
57+
checksum = data.crc32(checksum: 0)
58+
try consumer(data)
59+
progress?.completedUnitCount = self.totalUnitCountForReading(entry)
60+
case .deflate: checksum = try self.readCompressed(entry: entry, bufferSize: bufferSize,
61+
skipCRC32: skipCRC32, progress: progress, with: consumer)
62+
}
63+
return checksum
64+
}
65+
4566
// MARK: - Writing
4667

4768
func writeEntry(uncompressedSize: Int64, type: Entry.EntryType,

Sources/ZIPFoundation/Archive+Reading.swift

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,14 @@ extension Archive {
1818
/// - url: The destination file URL.
1919
/// - bufferSize: The maximum size of the read buffer and the decompression buffer (if needed).
2020
/// - skipCRC32: Optional flag to skip calculation of the CRC32 checksum to improve performance.
21+
/// - symlinksValidWithin: Symbolic links are valid within the url
2122
/// - allowUncontainedSymlinks: Optional flag to allow symlinks that point to paths outside the destination.
2223
/// - progress: A progress object that can be used to track or cancel the extract operation.
2324
/// - Returns: The checksum of the processed content or 0 if the `skipCRC32` flag was set to `true`.
2425
/// - Throws: An error if the destination file cannot be written or the entry contains malformed content.
2526
public func extract(_ entry: Entry, to url: URL, bufferSize: Int = defaultReadChunkSize,
26-
skipCRC32: Bool = false, allowUncontainedSymlinks: Bool = false,
27+
skipCRC32: Bool = false,
28+
symlinksValidWithin: URL? = nil, allowUncontainedSymlinks: Bool = false,
2729
progress: Progress? = nil) throws -> CRC32 {
2830
guard bufferSize > 0 else {
2931
throw ArchiveError.invalidBufferSize
@@ -60,7 +62,7 @@ extension Archive {
6062
let parentURL = url.deletingLastPathComponent()
6163
let isAbsolutePath = (linkPath as NSString).isAbsolutePath
6264
let linkURL = URL(fileURLWithPath: linkPath, relativeTo: isAbsolutePath ? nil : parentURL)
63-
let isContained = allowUncontainedSymlinks || linkURL.isContained(in: parentURL)
65+
let isContained = allowUncontainedSymlinks || linkURL.isContained(in: symlinksValidWithin ?? parentURL)
6466
guard isContained else { throw ArchiveError.uncontainedSymlink }
6567

6668
try fileManager.createParentDirectoryStructure(for: url)
@@ -108,12 +110,9 @@ extension Archive {
108110
try consumer(Data())
109111
progress?.completedUnitCount = self.totalUnitCountForReading(entry)
110112
case .symlink:
111-
let localFileHeader = entry.localFileHeader
112-
let size = Int(localFileHeader.compressedSize)
113-
let data = try Data.readChunk(of: size, from: self.archiveFile)
114-
checksum = data.crc32(checksum: 0)
115-
try consumer(data)
116-
progress?.completedUnitCount = self.totalUnitCountForReading(entry)
113+
checksum = try self.readSymbolicLink(entry: entry, bufferSize: bufferSize,
114+
skipCRC32: skipCRC32, progress: progress, with: consumer)
115+
117116
}
118117
return checksum
119118
}

Sources/ZIPFoundation/FileManager+ZIP.swift

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,11 +117,15 @@ extension FileManager {
117117
let entryProgress = archive.makeProgressForReading(entry)
118118
progress.addChild(entryProgress, withPendingUnitCount: entryProgress.totalUnitCount)
119119
crc32 = try archive.extract(entry, to: entryURL,
120-
skipCRC32: skipCRC32, allowUncontainedSymlinks: allowUncontainedSymlinks,
120+
skipCRC32: skipCRC32,
121+
symlinksValidWithin: destinationURL,
122+
allowUncontainedSymlinks: allowUncontainedSymlinks,
121123
progress: entryProgress)
122124
} else {
123125
crc32 = try archive.extract(entry, to: entryURL,
124-
skipCRC32: skipCRC32, allowUncontainedSymlinks: allowUncontainedSymlinks)
126+
skipCRC32: skipCRC32,
127+
symlinksValidWithin: destinationURL,
128+
allowUncontainedSymlinks: allowUncontainedSymlinks)
125129
}
126130

127131
func verifyChecksumIfNecessary() throws {
1008 Bytes
Binary file not shown.
1.24 KB
Binary file not shown.

Tests/ZIPFoundationTests/ZIPFoundationFileManagerTests+ZIP64.swift

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,34 @@ extension ZIPFoundationTests {
100100
}
101101
}
102102

103+
func testUnzipSymlink() {
104+
// stored by zip 3.0 via command line: zip -ry
105+
//
106+
// testUnzipSymlink.zip/
107+
// ├─ directory1
108+
// ├─ testUnzipSymlink
109+
// ├─ directory2
110+
// ├─ testUnzipSymlink.png
111+
do {
112+
try unarchiveZIP64Item(for: #function)
113+
} catch {
114+
XCTFail("\(error)")
115+
}
116+
}
117+
118+
func testUnzipCompressedSymlink() {
119+
// testUnzipCompressedSymlink.zip/
120+
// ├─ directory1
121+
// ├─ testUnzipSymlink (compressed)
122+
// ├─ directory2
123+
// ├─ testUnzipSymlink.png
124+
do {
125+
try unarchiveZIP64Item(for: #function)
126+
} catch {
127+
XCTFail("\(error)")
128+
}
129+
}
130+
103131
// MARK: - Helpers
104132

105133
private func archiveZIP64Item(for testFunction: String, compressionMethod: CompressionMethod) throws {

Tests/ZIPFoundationTests/ZIPFoundationTests.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -289,7 +289,9 @@ extension ZIPFoundationTests {
289289
("testWriteLargeChunk", testWriteLargeChunk),
290290
("testExtractUncompressedZIP64Entries", testExtractUncompressedZIP64Entries),
291291
("testExtractCompressedZIP64Entries", testExtractCompressedZIP64Entries),
292-
("testExtractEntryWithZIP64DataDescriptor", testExtractEntryWithZIP64DataDescriptor)
292+
("testExtractEntryWithZIP64DataDescriptor", testExtractEntryWithZIP64DataDescriptor),
293+
("testUnzipSymlink", testUnzipSymlink),
294+
("testUnzipCompressedSymlink", testUnzipCompressedSymlink)
293295
]
294296
}
295297

0 commit comments

Comments
 (0)