Skip to content
This repository was archived by the owner on Aug 12, 2022. It is now read-only.

Seek in non-deflated ZIP entries #92

Merged
merged 2 commits into from
Jun 22, 2020
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
17 changes: 14 additions & 3 deletions r2-shared-swift/Toolkit/ZIP/Minizip.swift
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ private extension MinizipArchive {
path: path,
isDirectory: path.hasSuffix("/"),
length: UInt64(fileInfo.uncompressed_size),
isCompressed: fileInfo.compression_method != 0,
compressedLength: UInt64(fileInfo.compressed_size)
)
}
Expand All @@ -170,9 +171,19 @@ private extension MinizipArchive {
///
/// - Returns: Whether the seeking operation was successful.
func seek(by offset: UInt64) -> Bool {
return readFromCurrentOffset(length: offset) { _, _ in
// Unfortunately, deflate doesn't support random access, so we need to discard the content
// until we reach the offset.
guard let entry = makeEntryAtCurrentOffset() else {
return false
}

if entry.isCompressed {
// Deflate is stream-based, and can't be used for random access. Therefore, if the file
// is compressed we need to read and discard the content from the start until we reach
// the desired offset.
return readFromCurrentOffset(length: offset) { _, _ in }

} else {
// For non-compressed entries, we can seek directly in the content.
return execute { return unzseek64(archive, offset, SEEK_CUR) }
}
}

Expand Down
3 changes: 3 additions & 0 deletions r2-shared-swift/Toolkit/ZIP/ZIP.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ struct ZIPEntry: Equatable {
/// Returns 0 if the entry is a directory.
let length: UInt64

/// Whether the entry is compressed.
let isCompressed: Bool

/// Compressed data length.
/// Returns 0 if the entry is a directory.
let compressedLength: UInt64
Expand Down
Binary file modified r2-shared-swiftTests/Fixtures/ZIP/test.zip
Binary file not shown.
51 changes: 34 additions & 17 deletions r2-shared-swiftTests/Toolkit/ZIP/ZIPTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ struct ZIPTester<Archive: ZIPArchive> {
path: "A folder/wasteland-cover.jpg",
isDirectory: false,
length: 103477,
isCompressed: true,
compressedLength: 82374
)
)
Expand All @@ -59,6 +60,7 @@ struct ZIPTester<Archive: ZIPArchive> {
path: "uncompressed.jpg",
isDirectory: false,
length: 279551,
isCompressed: false,
compressedLength: 279551
)
)
Expand All @@ -73,6 +75,7 @@ struct ZIPTester<Archive: ZIPArchive> {
path: "A folder/",
isDirectory: true,
length: 0,
isCompressed: false,
compressedLength: 0
)
)
Expand All @@ -81,18 +84,29 @@ struct ZIPTester<Archive: ZIPArchive> {
func testGetEntries() {
let archive = try! Archive(file: fixtures.url(for: "test.zip"))
XCTAssertEqual(archive.entries, [
ZIPEntry(path: ".hidden", isDirectory: false, length: 0, compressedLength: 0),
ZIPEntry(path: "A folder/", isDirectory: true, length: 0, compressedLength: 0),
ZIPEntry(path: "A folder/Sub.folder%/", isDirectory: true, length: 0, compressedLength: 0),
ZIPEntry(path: "A folder/Sub.folder%/file.txt", isDirectory: false, length: 20, compressedLength: 20),
ZIPEntry(path: "A folder/wasteland-cover.jpg", isDirectory: false, length: 103477, compressedLength: 82374),
ZIPEntry(path: "root.txt", isDirectory: false, length: 0, compressedLength: 0),
ZIPEntry(path: "uncompressed.jpg", isDirectory: false, length: 279551, compressedLength: 279551),
ZIPEntry(path: "uncompressed.txt", isDirectory: false, length: 30, compressedLength: 30)
ZIPEntry(path: ".hidden", isDirectory: false, length: 0, isCompressed: false, compressedLength: 0),
ZIPEntry(path: "A folder/", isDirectory: true, length: 0, isCompressed: false, compressedLength: 0),
ZIPEntry(path: "A folder/Sub.folder%/", isDirectory: true, length: 0, isCompressed: false, compressedLength: 0),
ZIPEntry(path: "A folder/Sub.folder%/file.txt", isDirectory: false, length: 20, isCompressed: false, compressedLength: 20),
ZIPEntry(path: "A folder/wasteland-cover.jpg", isDirectory: false, length: 103477, isCompressed: true, compressedLength: 82374),
ZIPEntry(path: "root.txt", isDirectory: false, length: 0, isCompressed: false, compressedLength: 0),
ZIPEntry(path: "uncompressed.jpg", isDirectory: false, length: 279551, isCompressed: false, compressedLength: 279551),
ZIPEntry(path: "uncompressed.txt", isDirectory: false, length: 30, isCompressed: false, compressedLength: 30),
ZIPEntry(path: "A folder/Sub.folder%/file-compressed.txt", isDirectory: false, length: 29609, isCompressed: true, compressedLength: 8659),
])
}

func testReadCompressedEntry() {
let archive = try! Archive(file: fixtures.url(for: "test.zip"))
let entry = archive.entry(at: "A folder/Sub.folder%/file-compressed.txt")!
let data = archive.read(at: entry.path)
XCTAssertNotNil(data)
let string = String(data: data!, encoding: .utf8)!
XCTAssertEqual(string.count, 29609)
XCTAssertTrue(string.hasPrefix("I'm inside\nthe ZIP."))
}

func testReadUncompressedEntry() {
let archive = try! Archive(file: fixtures.url(for: "test.zip"))
let entry = archive.entry(at: "A folder/Sub.folder%/file.txt")!
let data = archive.read(at: entry.path)
Expand All @@ -103,21 +117,22 @@ struct ZIPTester<Archive: ZIPArchive> {
)
}

func testReadUncompressedEntry() {
func testReadUncompressedRange() {
// FIXME: It looks like unzseek64 starts from the beginning of the file header, instead of the content. Reading a first byte solves this but then Minizip crashes randomly... Note that this only fails in the test case. I didn't see actual issues in LCPDF or videos embedded in EPUBs.
let archive = try! Archive(file: fixtures.url(for: "test.zip"))
let entry = archive.entry(at: "uncompressed.txt")!
let data = archive.read(at: entry.path)
let entry = archive.entry(at: "A folder/Sub.folder%/file.txt")!
let data = archive.read(at: entry.path, range: 14..<20)
XCTAssertNotNil(data)
XCTAssertEqual(
String(data: data!, encoding: .utf8),
"This content is uncompressed.\n"
" ZIP.\n"
)
}

func testReadRange() {
func testReadCompressedRange() {
let archive = try! Archive(file: fixtures.url(for: "test.zip"))
let entry = archive.entry(at: "A folder/Sub.folder%/file.txt")!
let data = archive.read(at: entry.path, range: (entry.length - 6)..<entry.length)
let entry = archive.entry(at: "A folder/Sub.folder%/file-compressed.txt")!
let data = archive.read(at: entry.path, range: 14..<20)
XCTAssertNotNil(data)
XCTAssertEqual(
String(data: data!, encoding: .utf8),
Expand All @@ -141,7 +156,8 @@ struct ZIPTester<Archive: ZIPArchive> {
// func testGetEntries() { tester.testGetEntries() }
// func testReadCompressedEntry() { tester.testReadCompressedEntry() }
// func testReadUncompressedEntry() { tester.testReadUncompressedEntry() }
// func testReadRange() { tester.testReadRange() }
// func testReadCompressedRange() { tester.testReadCompressedRange() }
// func testReadUncompressedRange() { tester.testReadUncompressedRange() }
//
//}

Expand All @@ -159,7 +175,8 @@ class MinizipTests: XCTestCase {
func testGetEntries() { tester.testGetEntries() }
func testReadCompressedEntry() { tester.testReadCompressedEntry() }
func testReadUncompressedEntry() { tester.testReadUncompressedEntry() }
func testReadRange() { tester.testReadRange() }
func testReadCompressedRange() { tester.testReadCompressedRange() }
func testReadUncompressedRange() { tester.testReadUncompressedRange() }

}

Expand Down