diff --git a/r2-shared-swift/Toolkit/ZIP/Minizip.swift b/r2-shared-swift/Toolkit/ZIP/Minizip.swift index 338c70f5..12669226 100644 --- a/r2-shared-swift/Toolkit/ZIP/Minizip.swift +++ b/r2-shared-swift/Toolkit/ZIP/Minizip.swift @@ -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) ) } @@ -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) } } } diff --git a/r2-shared-swift/Toolkit/ZIP/ZIP.swift b/r2-shared-swift/Toolkit/ZIP/ZIP.swift index a33dea83..e4cb353f 100644 --- a/r2-shared-swift/Toolkit/ZIP/ZIP.swift +++ b/r2-shared-swift/Toolkit/ZIP/ZIP.swift @@ -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 diff --git a/r2-shared-swiftTests/Fixtures/ZIP/test.zip b/r2-shared-swiftTests/Fixtures/ZIP/test.zip index db63c523..2d1e49a1 100644 Binary files a/r2-shared-swiftTests/Fixtures/ZIP/test.zip and b/r2-shared-swiftTests/Fixtures/ZIP/test.zip differ diff --git a/r2-shared-swiftTests/Toolkit/ZIP/ZIPTests.swift b/r2-shared-swiftTests/Toolkit/ZIP/ZIPTests.swift index aa024fe8..1d8ea72e 100644 --- a/r2-shared-swiftTests/Toolkit/ZIP/ZIPTests.swift +++ b/r2-shared-swiftTests/Toolkit/ZIP/ZIPTests.swift @@ -46,6 +46,7 @@ struct ZIPTester { path: "A folder/wasteland-cover.jpg", isDirectory: false, length: 103477, + isCompressed: true, compressedLength: 82374 ) ) @@ -59,6 +60,7 @@ struct ZIPTester { path: "uncompressed.jpg", isDirectory: false, length: 279551, + isCompressed: false, compressedLength: 279551 ) ) @@ -73,6 +75,7 @@ struct ZIPTester { path: "A folder/", isDirectory: true, length: 0, + isCompressed: false, compressedLength: 0 ) ) @@ -81,18 +84,29 @@ struct ZIPTester { 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) @@ -103,21 +117,22 @@ struct ZIPTester { ) } - 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).. { // func testGetEntries() { tester.testGetEntries() } // func testReadCompressedEntry() { tester.testReadCompressedEntry() } // func testReadUncompressedEntry() { tester.testReadUncompressedEntry() } -// func testReadRange() { tester.testReadRange() } +// func testReadCompressedRange() { tester.testReadCompressedRange() } +// func testReadUncompressedRange() { tester.testReadUncompressedRange() } // //} @@ -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() } }