Skip to content

Commit

Permalink
Add range extraction
Browse files Browse the repository at this point in the history
  • Loading branch information
mickael-menu committed Dec 10, 2024
1 parent fac78df commit 2665d81
Show file tree
Hide file tree
Showing 2 changed files with 111 additions and 0 deletions.
107 changes: 107 additions & 0 deletions Sources/ZIPFoundation/Archive+Reading.swift
Original file line number Diff line number Diff line change
Expand Up @@ -118,4 +118,111 @@ extension Archive {
}
return checksum
}

/// Read a portion of a ZIP `Entry` from the receiver and forward its contents to a `Consumer` closure.
///
/// - Parameters:
/// - range: The portion range in the (decompressed) entry.
/// - entry: The ZIP `Entry` to read.
/// - bufferSize: The maximum size of the read buffer and the decompression buffer (if needed).
/// - consumer: A closure that consumes contents of `Entry` as `Data` chunks.
/// - Throws: An error if the destination file cannot be written or the entry contains malformed content.
public func extractRange(
_ range: Range<UInt64>,
of entry: Entry,
bufferSize: Int = defaultReadChunkSize,
consumer: Consumer
) throws {
guard entry.type == .file else {
throw ArchiveError.entryIsNotAFile
}
guard bufferSize > 0 else {
throw ArchiveError.invalidBufferSize
}
guard range.lowerBound >= 0, range.upperBound <= entry.uncompressedSize else {
throw ArchiveError.rangeOutOfBounds
}
let localFileHeader = entry.localFileHeader
guard entry.dataOffset <= .max else {
throw ArchiveError.invalidLocalHeaderDataOffset
}

guard let compressionMethod = CompressionMethod(rawValue: localFileHeader.compressionMethod) else {
throw ArchiveError.invalidCompressionMethod
}

switch compressionMethod {
case .none:
try extractStoredRange(range, of: entry, bufferSize: bufferSize, consumer: consumer)

case .deflate:
try extractCompressedRange(range, of: entry, bufferSize: bufferSize, consumer: consumer)
}
}

/// Ranges of stored entries can be accessed directly, as the requested
/// indices match the ones in the archive file.
private func extractStoredRange(
_ range: Range<UInt64>,
of entry: Entry,
bufferSize: Int,
consumer: Consumer
) throws {
fseeko(archiveFile, off_t(entry.dataOffset + range.lowerBound), SEEK_SET)

_ = try Data.consumePart(
of: Int64(range.count),
chunkSize: bufferSize,
skipCRC32: true,
provider: { pos, chunkSize -> Data in
try Data.readChunk(of: chunkSize, from: self.archiveFile)
},
consumer: consumer
)
}

/// Ranges of deflated entries cannot be accessed randomly. We must read
/// and inflate the entry from the start until we reach the requested range.
private func extractCompressedRange(
_ range: Range<UInt64>,
of entry: Entry,
bufferSize: Int,
consumer: Consumer
) throws {
var bytesRead: UInt64 = 0

do {
fseeko(archiveFile, off_t(entry.dataOffset), SEEK_SET)

_ = try readCompressed(
entry: entry,
bufferSize: bufferSize,
skipCRC32: true
) { chunk in
let chunkSize = UInt64(chunk.count)

if bytesRead >= range.lowerBound {
if bytesRead + chunkSize > range.upperBound {
let remainingBytes = range.upperBound - bytesRead
try consumer(chunk[..<remainingBytes])
} else {
try consumer(chunk)
}
} else if bytesRead + chunkSize > range.lowerBound {
// Calculate the overlap and pass the relevant portion of the chunk
let start = range.lowerBound - bytesRead
let end = Swift.min(chunkSize, range.upperBound - bytesRead)
try consumer(chunk[start..<end])
}

bytesRead += chunkSize

guard bytesRead < range.upperBound else {
throw EndOfRange()
}
}
} catch is EndOfRange { }
}

private struct EndOfRange: Error {}
}
4 changes: 4 additions & 0 deletions Sources/ZIPFoundation/Archive.swift
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,10 @@ public final class Archive: Sequence {
case missingEndOfCentralDirectoryRecord
/// Thrown when an entry contains a symlink pointing to a path outside the destination directory.
case uncontainedSymlink
/// Thrown when the requested range is out of bounds for the entry.
case rangeOutOfBounds
/// The requested entry is not a file but a directory or symlink.
case entryIsNotAFile
}

/// The access mode for an `Archive`.
Expand Down

0 comments on commit 2665d81

Please sign in to comment.