From 175ac08c25a0043de86025e531c4597fd214a6c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mickae=CC=88l=20Menu?= Date: Mon, 8 Jun 2020 15:41:34 +0200 Subject: [PATCH 1/2] Seek in non-deflated ZIP entries --- r2-shared-swift/Toolkit/ZIP/Minizip.swift | 26 +++++++-- r2-shared-swift/Toolkit/ZIP/ZIP.swift | 3 ++ r2-shared-swiftTests/Fixtures/ZIP/test.zip | Bin 363289 -> 372156 bytes .../Toolkit/ZIP/ZIPTests.swift | 50 ++++++++++++------ 4 files changed, 59 insertions(+), 20 deletions(-) diff --git a/r2-shared-swift/Toolkit/ZIP/Minizip.swift b/r2-shared-swift/Toolkit/ZIP/Minizip.swift index 338c70f5..e31117ac 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,28 @@ 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 { + // There's a bug with `unzseek64`, which seems to start from the ZIP entry local + // header by default. Reading a first byte seems to reset the seeking position. + if unztell(archive) == 0 { + _ = readFromCurrentOffset(length: 1) { _, _ in } + return unzseek64(archive, offset, SEEK_SET) + } else { + 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 db63c523232d2153f17d34357e58ae328d1d83b6..2d1e49a1b9cc4ed10cf73399712786ff37db235d 100644 GIT binary patch delta 8940 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 delta 46 ycmdmUTWsbuv4$4L7N!>F7M3ln{vF#}R6p { 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,21 @@ struct ZIPTester { ) } - func testReadUncompressedEntry() { + func testReadUncompressedRange() { 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 +174,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() } } From 0e21a5c4b98a7ea65ccbe3c73c522d0f01dcc686 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mickae=CC=88l=20Menu?= Date: Mon, 8 Jun 2020 16:00:46 +0200 Subject: [PATCH 2/2] Fix seeking in ZIP archive --- r2-shared-swift/Toolkit/ZIP/Minizip.swift | 11 +---------- r2-shared-swiftTests/Toolkit/ZIP/ZIPTests.swift | 1 + 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/r2-shared-swift/Toolkit/ZIP/Minizip.swift b/r2-shared-swift/Toolkit/ZIP/Minizip.swift index e31117ac..12669226 100644 --- a/r2-shared-swift/Toolkit/ZIP/Minizip.swift +++ b/r2-shared-swift/Toolkit/ZIP/Minizip.swift @@ -183,16 +183,7 @@ private extension MinizipArchive { } else { // For non-compressed entries, we can seek directly in the content. - return execute { - // There's a bug with `unzseek64`, which seems to start from the ZIP entry local - // header by default. Reading a first byte seems to reset the seeking position. - if unztell(archive) == 0 { - _ = readFromCurrentOffset(length: 1) { _, _ in } - return unzseek64(archive, offset, SEEK_SET) - } else { - return unzseek64(archive, offset, SEEK_CUR) - } - } + return execute { return unzseek64(archive, offset, SEEK_CUR) } } } diff --git a/r2-shared-swiftTests/Toolkit/ZIP/ZIPTests.swift b/r2-shared-swiftTests/Toolkit/ZIP/ZIPTests.swift index 0ba7eec8..1d8ea72e 100644 --- a/r2-shared-swiftTests/Toolkit/ZIP/ZIPTests.swift +++ b/r2-shared-swiftTests/Toolkit/ZIP/ZIPTests.swift @@ -118,6 +118,7 @@ struct ZIPTester { } 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: "A folder/Sub.folder%/file.txt")! let data = archive.read(at: entry.path, range: 14..<20)