diff --git a/Cartfile b/Cartfile index df01464a4..5c4361965 100644 --- a/Cartfile +++ b/Cartfile @@ -1,3 +1,4 @@ +github "dexman/Minizip" ~> 1.4.0 github "krzyzanowskim/CryptoSwift" ~> 1.8.0 github "ra1028/DifferenceKit" ~> 1.3.0 github "readium/Fuzi" ~> 4.0.0 diff --git a/Package.swift b/Package.swift index 2d99a36cb..ba80fe3a8 100644 --- a/Package.swift +++ b/Package.swift @@ -21,7 +21,6 @@ let package = Package( // Adapters to third-party dependencies. .library(name: "ReadiumAdapterGCDWebServer", targets: ["ReadiumAdapterGCDWebServer"]), .library(name: "ReadiumAdapterLCPSQLite", targets: ["ReadiumAdapterLCPSQLite"]), - .library(name: "ReadiumAdapterMinizip", targets: ["ReadiumAdapterMinizip"]), ], dependencies: [ .package(url: "https://github.com/krzyzanowskim/CryptoSwift.git", from: "1.8.0"), @@ -39,6 +38,7 @@ let package = Package( dependencies: [ "ReadiumInternal", "SwiftSoup", + "Zip", .product(name: "ReadiumFuzi", package: "Fuzi"), .product(name: "ReadiumZIPFoundation", package: "ZIPFoundation"), ], @@ -159,23 +159,6 @@ let package = Package( path: "Sources/Adapters/LCPSQLite" ), - .target( - name: "ReadiumAdapterMinizip", - dependencies: [ - "ReadiumShared", - "Zip", - ], - path: "Sources/Adapters/Minizip" - ), - .testTarget( - name: "ReadiumAdapterMinizipTests", - dependencies: ["ReadiumAdapterMinizip"], - path: "Tests/Adapters/MinizipTests", - resources: [ - .copy("Fixtures"), - ] - ), - .target( name: "ReadiumInternal", path: "Sources/Internal" diff --git a/README.md b/README.md index 24907f687..19a2d6ee1 100644 --- a/README.md +++ b/README.md @@ -54,18 +54,18 @@ Note that Carthage will build all Readium modules and their dependencies, but yo Refer to the following table to know which dependencies are required for each Readium library. -| | `ReadiumShared` | `ReadiumStreamer` | `ReadiumNavigator` | `ReadiumOPDS` | `ReadiumLCP` | `ReadiumAdapterGCDWebServer` | `ReadiumAdapterLCPSQLite` | `ReadiumAdapterMinizip` | -|------------------------|:------------------:|:------------------:|:------------------:|:------------------:|:------------------:|------------------------------|---------------------------|-------------------------| -| **`ReadiumShared`** | | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | -| **`ReadiumInternal`** | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | | | -| `CryptoSwift` | | :heavy_check_mark: | | | :heavy_check_mark: | | | | -| `DifferenceKit` | | | :heavy_check_mark: | | | | | | -| `ReadiumFuzi` | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | | | -| `ReadiumGCDWebServer` | | | | | | :heavy_check_mark: | | | -| `ReadiumZIPFoundation` | :heavy_check_mark: | | | | :heavy_check_mark: | | | | -| `Minizip` | | | | | | | | :heavy_check_mark: | -| `SQLite.swift` | | | | | | | :heavy_check_mark: | | -| `SwiftSoup` | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | | | +| | `ReadiumShared` | `ReadiumStreamer` | `ReadiumNavigator` | `ReadiumOPDS` | `ReadiumLCP` | `ReadiumAdapterGCDWebServer` | `ReadiumAdapterLCPSQLite` | +|------------------------|:------------------:|:------------------:|:------------------:|:------------------:|:------------------:|------------------------------|---------------------------| +| **`ReadiumShared`** | | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | +| **`ReadiumInternal`** | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | | +| `CryptoSwift` | | :heavy_check_mark: | | | :heavy_check_mark: | | | +| `DifferenceKit` | | | :heavy_check_mark: | | | | | +| `ReadiumFuzi` | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | | +| `ReadiumGCDWebServer` | | | | | | :heavy_check_mark: | | +| `ReadiumZIPFoundation` | :heavy_check_mark: | | | | :heavy_check_mark: | | | +| `Minizip` | :heavy_check_mark: | | | | | | | +| `SQLite.swift` | | | | | | | :heavy_check_mark: | +| `SwiftSoup` | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | | ### CocoaPods diff --git a/Sources/Adapters/Minizip/MinizipArchiveOpener.swift b/Sources/Shared/Toolkit/ZIP/Minizip/MinizipArchiveOpener.swift similarity index 93% rename from Sources/Adapters/Minizip/MinizipArchiveOpener.swift rename to Sources/Shared/Toolkit/ZIP/Minizip/MinizipArchiveOpener.swift index 33d1116b8..809fdd6a3 100644 --- a/Sources/Adapters/Minizip/MinizipArchiveOpener.swift +++ b/Sources/Shared/Toolkit/ZIP/Minizip/MinizipArchiveOpener.swift @@ -5,13 +5,10 @@ // import Foundation -import ReadiumShared /// An ``ArchiveOpener`` able to open ZIP archives using Minizip. /// -/// Compared to the default ZIPFoundation implementation shipped with -/// ReadiumShared, the ``MinizipArchiveOpener``: -/// +/// Compared to the ``ZIPFoundationArchiveOpener`` it: /// - Does not support HTTP streaming of ZIP archives. /// - Has better performance when reading an LCP-protected package containing /// large deflated ZIP entries (instead of stored). diff --git a/Sources/Adapters/Minizip/MinizipContainer.swift b/Sources/Shared/Toolkit/ZIP/Minizip/MinizipContainer.swift similarity index 99% rename from Sources/Adapters/Minizip/MinizipContainer.swift rename to Sources/Shared/Toolkit/ZIP/Minizip/MinizipContainer.swift index fa2d0fd86..f284a052b 100644 --- a/Sources/Adapters/Minizip/MinizipContainer.swift +++ b/Sources/Shared/Toolkit/ZIP/Minizip/MinizipContainer.swift @@ -6,7 +6,6 @@ import Foundation import Minizip -import ReadiumShared /// A ZIP ``Container`` using the Minizip library. final class MinizipContainer: Container, Loggable { diff --git a/Sources/Shared/Toolkit/ZIP/ZIPArchiveOpener.swift b/Sources/Shared/Toolkit/ZIP/ZIPArchiveOpener.swift index 970e675f8..9b66cd4ec 100644 --- a/Sources/Shared/Toolkit/ZIP/ZIPArchiveOpener.swift +++ b/Sources/Shared/Toolkit/ZIP/ZIPArchiveOpener.swift @@ -7,16 +7,11 @@ import Foundation /// An ``ArchiveOpener`` for ZIP resources. -public class ZIPArchiveOpener: ArchiveOpener { - private let opener = ZIPFoundationArchiveOpener() - - public init() {} - - public func open(resource: any Resource, format: Format) async -> Result { - await opener.open(resource: resource, format: format) - } - - public func sniffOpen(resource: any Resource) async -> Result { - await opener.sniffOpen(resource: resource) +public class ZIPArchiveOpener: CompositeArchiveOpener { + public init() { + super.init([ + MinizipArchiveOpener(), + ZIPFoundationArchiveOpener(), + ]) } } diff --git a/Sources/Shared/Toolkit/ZIP/ZIPFoundation.swift b/Sources/Shared/Toolkit/ZIP/ZIPFoundation.swift deleted file mode 100644 index 87cf3143f..000000000 --- a/Sources/Shared/Toolkit/ZIP/ZIPFoundation.swift +++ /dev/null @@ -1,307 +0,0 @@ -// -// Copyright 2025 Readium Foundation. All rights reserved. -// Use of this source code is governed by the BSD-style license -// available in the top-level LICENSE file of the project. -// - -import Foundation -import ReadiumInternal -import ReadiumZIPFoundation - -/// An ``ArchiveOpener`` able to open ZIP archives using ZIPFoundation. -public final class ZIPFoundationArchiveOpener: ArchiveOpener { - public init() {} - - public func open(resource: any Resource, format: Format) async -> Result { - guard format.conformsTo(.zip) else { - return .failure(.formatNotSupported(format)) - } - - return await ZIPFoundationContainer.make(resource: resource) - .mapError { - switch $0 { - case .notAZIP: - return .formatNotSupported(format) - case let .reading(error): - return .reading(error) - } - } - .map { ContainerAsset(container: $0, format: format) } - } - - public func sniffOpen(resource: any Resource) async -> Result { - await ZIPFoundationContainer.make(resource: resource) - .mapError { - switch $0 { - case .notAZIP: - return .formatNotRecognized - case let .reading(error): - return .reading(error) - } - } - .map { - ContainerAsset( - container: $0, - format: Format( - specifications: .zip, - mediaType: .zip, - fileExtension: "zip" - ) - ) - } - } -} - -/// The ZIP End of Central Directory Record should be at most 65557 bytes, -/// according to the ZIP specification. The ZIP 64 EOCD should be -/// an extra 76 bytes according to ZIPFoundation implementation. -private let zipEOCDMaximumLength: UInt64 = 65557 + 76 - -/// The maximum length of a non-local ZIP package to be cached entirely in -/// memory instead of streamed. -private let maximumZIPLengthToFullyCache = 5.MB - -/// Creates new ZIPFoundation ``Archive`` objects from a shared ``Resource``. -private final class ZIPFoundationArchiveFactory { - enum Source { - case file(FileURL) - case resource(Resource) - } - - private let source: Source - private let bufferSize: Int - - var sourceURL: AbsoluteURL? { - switch source { - case let .file(file): - return file - case let .resource(resource): - return resource.sourceURL - } - } - - init(resource: Resource) async { - if let file = resource.sourceURL?.fileURL { - source = .file(file) - bufferSize = 16.kB - - } else { - // We use a large buffer to avoid making hundreds of small HTTP - // range requests. - let bufferSize = 512.kB - var resource: Resource = resource.buffered(size: bufferSize) - - if let optionalLength = await resource.estimatedLength().getOrNil(), let length = optionalLength { - // The End of Central Directory Record, located at the end of - // the ZIP file, will be read each time we create a new - // `Archive` object. To optimize requests, we cache the end of - // the resource. - // - // Additionally, if the ZIP file is small enough, we will cache - // it completely in memory. - resource = TailCachingResource( - resource: resource, - cacheFromOffset: (!canAllocate(maximumZIPLengthToFullyCache * 2) || length > maximumZIPLengthToFullyCache) - ? Swift.max(0, length - zipEOCDMaximumLength) - : 0 - ) - } - - source = .resource(resource) - self.bufferSize = bufferSize - } - } - - func make() async throws -> ReadiumZIPFoundation.Archive { - switch source { - case let .file(url): - return try await .init( - url: url.url, - accessMode: .read, - defaultReadChunkSize: bufferSize - ) - - case let .resource(resource): - return try await .init( - url: resource.sourceURL?.url, - dataSource: ResourceDataSource(resource: resource), - defaultReadChunkSize: bufferSize - ) - } - } -} - -/// Indicates whether there is enough available free memory to allocate `length` -/// bytes. -private func canAllocate(_ length: Int) -> Bool { - os_proc_available_memory() > length -} - -/// A ZIP ``Container`` using the ZIPFoundation library. -final class ZIPFoundationContainer: Container, Loggable { - enum MakeError: Error { - case notAZIP - case reading(ReadError) - } - - static func make(resource: Resource) async -> Result { - do { - let archiveFactory = await ZIPFoundationArchiveFactory(resource: resource) - let archive = try await archiveFactory.make() - - var entries = [RelativeURL: Entry]() - - for try await entry in archive { - guard - entry.type == .file, - let url = RelativeURL(path: entry.path)?.normalized, - !url.path.isEmpty - else { - continue - } - entries[url] = entry - } - - return .success(Self(archiveFactory: archiveFactory, entries: entries)) - - } catch { - return .failure(.reading(.decoding(error))) - } - } - - private let archiveFactory: ZIPFoundationArchiveFactory - private let entriesByPath: [RelativeURL: Entry] - - public var sourceURL: AbsoluteURL? { archiveFactory.sourceURL } - public let entries: Set - - private init( - archiveFactory: ZIPFoundationArchiveFactory, - entries: [RelativeURL: Entry] - ) { - let entries = entries.reduce(into: [:]) { result, item in - result[item.key.normalized] = item.value - } - - self.archiveFactory = archiveFactory - entriesByPath = entries - self.entries = Set(entries.keys.map(\.anyURL)) - } - - subscript(url: any URLConvertible) -> (any Resource)? { - guard - let url = url.relativeURL?.normalized, - let entry = entriesByPath[url] - else { - return nil - } - return ZIPFoundationResource( - archiveFactory: archiveFactory, - entry: entry - ) - } -} - -/// A ``Resource`` providing access to a single entry in a ZIPFoundation archive. -private actor ZIPFoundationResource: Resource, Loggable { - private let archiveFactory: ZIPFoundationArchiveFactory - private let entry: Entry - - init( - archiveFactory: ZIPFoundationArchiveFactory, - entry: Entry - ) { - self.archiveFactory = archiveFactory - self.entry = entry - } - - public let sourceURL: AbsoluteURL? = nil - - func estimatedLength() async -> ReadResult { - .success(entry.uncompressedSize) - } - - func properties() async -> ReadResult { - .success(ResourceProperties { - $0.filename = RelativeURL(path: entry.path)?.lastPathSegment - $0.archive = ArchiveProperties( - entryLength: entry.isCompressed ? entry.compressedSize : entry.uncompressedSize, - isEntryCompressed: entry.isCompressed - ) - }) - } - - func stream(range: Range?, consume: @escaping (Data) -> Void) async -> ReadResult { - if range != nil {} - - return await archive().asyncFlatMap { archive in - do { - if let range = range { - try await archive.extractRange(range, of: entry) { data in - consume(data) - } - } else { - _ = try await archive.extract(entry, skipCRC32: true) { data in - consume(data) - } - } - return .success(()) - } catch { - return .failure(.decoding(error)) - } - } - } - - private var _archive: ReadResult? - private func archive() async -> ReadResult { - if _archive == nil { - do { - _archive = try await .success(archiveFactory.make()) - } catch { - _archive = .failure(.decoding(error)) - } - } - return _archive! - } -} - -enum ResourceDataSourceError: Error { - case unknownContentLength -} - -/// Bridges the ZIPFoundation's ``DataSource`` with our ``Resource``. -private final class ResourceDataSource: ReadiumZIPFoundation.DataSource { - private let resource: Resource - private var _position: UInt64 = 0 - - init(resource: Resource) { - self.resource = resource - } - - func close() throws {} - - func length() async throws -> UInt64 { - guard let length = try await resource.estimatedLength().get() else { - throw ResourceDataSourceError.unknownContentLength - } - return length - } - - func position() async throws -> UInt64 { - _position - } - - func seek(to position: UInt64) async throws { - _position = position - } - - func read(length: Int) async throws -> Data { - guard length > 0 else { - return Data() - } - let range = _position ..< (_position + UInt64(length)) - let data = try await resource.read(range: range).get() - _position += UInt64(data.count) - return data - } -} diff --git a/Sources/Shared/Toolkit/ZIP/ZIPFoundation/ZIPFoundationArchiveFactory.swift b/Sources/Shared/Toolkit/ZIP/ZIPFoundation/ZIPFoundationArchiveFactory.swift new file mode 100644 index 000000000..798415009 --- /dev/null +++ b/Sources/Shared/Toolkit/ZIP/ZIPFoundation/ZIPFoundationArchiveFactory.swift @@ -0,0 +1,134 @@ +// +// Copyright 2025 Readium Foundation. All rights reserved. +// Use of this source code is governed by the BSD-style license +// available in the top-level LICENSE file of the project. +// + +import Foundation +import ReadiumZIPFoundation + +/// The ZIP End of Central Directory Record should be at most 65557 bytes, +/// according to the ZIP specification. The ZIP 64 EOCD should be +/// an extra 76 bytes according to ZIPFoundation implementation. +private let zipEOCDMaximumLength: UInt64 = 65557 + 76 + +/// The maximum length of a non-local ZIP package to be cached entirely in +/// memory instead of streamed. +private let maximumZIPLengthToFullyCache = 5.MB + +/// Creates new ZIPFoundation ``Archive`` objects from a shared ``Resource``. +final class ZIPFoundationArchiveFactory { + enum Source { + case file(FileURL) + case resource(Resource) + } + + private let source: Source + private let bufferSize: Int + + var sourceURL: AbsoluteURL? { + switch source { + case let .file(file): + return file + case let .resource(resource): + return resource.sourceURL + } + } + + init(resource: Resource) async { + if let file = resource.sourceURL?.fileURL { + source = .file(file) + bufferSize = 16.kB + + } else { + // We use a large buffer to avoid making hundreds of small HTTP + // range requests. + let bufferSize = 512.kB + var resource: Resource = resource.buffered(size: bufferSize) + + if let optionalLength = await resource.estimatedLength().getOrNil(), let length = optionalLength { + // The End of Central Directory Record, located at the end of + // the ZIP file, will be read each time we create a new + // `Archive` object. To optimize requests, we cache the end of + // the resource. + // + // Additionally, if the ZIP file is small enough, we will cache + // it completely in memory. + resource = TailCachingResource( + resource: resource, + cacheFromOffset: (!canAllocate(maximumZIPLengthToFullyCache * 2) || length > maximumZIPLengthToFullyCache) + ? Swift.max(0, length - zipEOCDMaximumLength) + : 0 + ) + } + + source = .resource(resource) + self.bufferSize = bufferSize + } + } + + func make() async throws -> ReadiumZIPFoundation.Archive { + switch source { + case let .file(url): + return try await .init( + url: url.url, + accessMode: .read, + defaultReadChunkSize: bufferSize + ) + + case let .resource(resource): + return try await .init( + url: resource.sourceURL?.url, + dataSource: ResourceDataSource(resource: resource), + defaultReadChunkSize: bufferSize + ) + } + } +} + +/// Indicates whether there is enough available free memory to allocate `length` +/// bytes. +private func canAllocate(_ length: Int) -> Bool { + os_proc_available_memory() > length +} + +enum ResourceDataSourceError: Error { + case unknownContentLength +} + +/// Bridges the ZIPFoundation's ``DataSource`` with our ``Resource``. +private final class ResourceDataSource: ReadiumZIPFoundation.DataSource { + private let resource: Resource + private var _position: UInt64 = 0 + + init(resource: Resource) { + self.resource = resource + } + + func close() throws {} + + func length() async throws -> UInt64 { + guard let length = try await resource.estimatedLength().get() else { + throw ResourceDataSourceError.unknownContentLength + } + return length + } + + func position() async throws -> UInt64 { + _position + } + + func seek(to position: UInt64) async throws { + _position = position + } + + func read(length: Int) async throws -> Data { + guard length > 0 else { + return Data() + } + let range = _position ..< (_position + UInt64(length)) + let data = try await resource.read(range: range).get() + _position += UInt64(data.count) + return data + } +} diff --git a/Sources/Shared/Toolkit/ZIP/ZIPFoundation/ZIPFoundationArchiveOpener.swift b/Sources/Shared/Toolkit/ZIP/ZIPFoundation/ZIPFoundationArchiveOpener.swift new file mode 100644 index 000000000..fff693202 --- /dev/null +++ b/Sources/Shared/Toolkit/ZIP/ZIPFoundation/ZIPFoundationArchiveOpener.swift @@ -0,0 +1,51 @@ +// +// Copyright 2025 Readium Foundation. All rights reserved. +// Use of this source code is governed by the BSD-style license +// available in the top-level LICENSE file of the project. +// + +import Foundation + +/// An ``ArchiveOpener`` able to open ZIP archives using ZIPFoundation. +public final class ZIPFoundationArchiveOpener: ArchiveOpener { + public init() {} + + public func open(resource: any Resource, format: Format) async -> Result { + guard format.conformsTo(.zip) else { + return .failure(.formatNotSupported(format)) + } + + return await ZIPFoundationContainer.make(resource: resource) + .mapError { + switch $0 { + case .notAZIP: + return .formatNotSupported(format) + case let .reading(error): + return .reading(error) + } + } + .map { ContainerAsset(container: $0, format: format) } + } + + public func sniffOpen(resource: any Resource) async -> Result { + await ZIPFoundationContainer.make(resource: resource) + .mapError { + switch $0 { + case .notAZIP: + return .formatNotRecognized + case let .reading(error): + return .reading(error) + } + } + .map { + ContainerAsset( + container: $0, + format: Format( + specifications: .zip, + mediaType: .zip, + fileExtension: "zip" + ) + ) + } + } +} diff --git a/Sources/Shared/Toolkit/ZIP/ZIPFoundation/ZIPFoundationContainer.swift b/Sources/Shared/Toolkit/ZIP/ZIPFoundation/ZIPFoundationContainer.swift new file mode 100644 index 000000000..83e4bc1d4 --- /dev/null +++ b/Sources/Shared/Toolkit/ZIP/ZIPFoundation/ZIPFoundationContainer.swift @@ -0,0 +1,136 @@ +// +// Copyright 2025 Readium Foundation. All rights reserved. +// Use of this source code is governed by the BSD-style license +// available in the top-level LICENSE file of the project. +// + +import Foundation +import ReadiumZIPFoundation + +/// A ZIP ``Container`` using the ZIPFoundation library. +final class ZIPFoundationContainer: Container, Loggable { + enum MakeError: Error { + case notAZIP + case reading(ReadError) + } + + static func make(resource: Resource) async -> Result { + do { + let archiveFactory = await ZIPFoundationArchiveFactory(resource: resource) + let archive = try await archiveFactory.make() + + var entries = [RelativeURL: Entry]() + + for try await entry in archive { + guard + entry.type == .file, + let url = RelativeURL(path: entry.path)?.normalized, + !url.path.isEmpty + else { + continue + } + entries[url] = entry + } + + return .success(Self(archiveFactory: archiveFactory, entries: entries)) + + } catch { + return .failure(.reading(.decoding(error))) + } + } + + private let archiveFactory: ZIPFoundationArchiveFactory + private let entriesByPath: [RelativeURL: Entry] + + public var sourceURL: AbsoluteURL? { archiveFactory.sourceURL } + public let entries: Set + + private init( + archiveFactory: ZIPFoundationArchiveFactory, + entries: [RelativeURL: Entry] + ) { + let entries = entries.reduce(into: [:]) { result, item in + result[item.key.normalized] = item.value + } + + self.archiveFactory = archiveFactory + entriesByPath = entries + self.entries = Set(entries.keys.map(\.anyURL)) + } + + subscript(url: any URLConvertible) -> (any Resource)? { + guard + let url = url.relativeURL?.normalized, + let entry = entriesByPath[url] + else { + return nil + } + return ZIPFoundationResource( + archiveFactory: archiveFactory, + entry: entry + ) + } +} + +/// A ``Resource`` providing access to a single entry in a ZIPFoundation archive. +private actor ZIPFoundationResource: Resource, Loggable { + private let archiveFactory: ZIPFoundationArchiveFactory + private let entry: Entry + + init( + archiveFactory: ZIPFoundationArchiveFactory, + entry: Entry + ) { + self.archiveFactory = archiveFactory + self.entry = entry + } + + public let sourceURL: AbsoluteURL? = nil + + func estimatedLength() async -> ReadResult { + .success(entry.uncompressedSize) + } + + func properties() async -> ReadResult { + .success(ResourceProperties { + $0.filename = RelativeURL(path: entry.path)?.lastPathSegment + $0.archive = ArchiveProperties( + entryLength: entry.isCompressed ? entry.compressedSize : entry.uncompressedSize, + isEntryCompressed: entry.isCompressed + ) + }) + } + + func stream(range: Range?, consume: @escaping (Data) -> Void) async -> ReadResult { + if range != nil {} + + return await archive().asyncFlatMap { archive in + do { + if let range = range { + try await archive.extractRange(range, of: entry) { data in + consume(data) + } + } else { + _ = try await archive.extract(entry, skipCRC32: true) { data in + consume(data) + } + } + return .success(()) + } catch { + return .failure(.decoding(error)) + } + } + } + + private var _archive: ReadResult? + private func archive() async -> ReadResult { + if _archive == nil { + do { + _archive = try await .success(archiveFactory.make()) + } catch { + _archive = .failure(.decoding(error)) + } + } + return _archive! + } +} diff --git a/Support/Carthage/.xcodegen b/Support/Carthage/.xcodegen index 0f798f1ef..1bc323e12 100644 --- a/Support/Carthage/.xcodegen +++ b/Support/Carthage/.xcodegen @@ -439,9 +439,13 @@ ../../Sources/Navigator/EPUB/Assets/Static/scripts ../../Sources/Navigator/EPUB/Assets/Static/scripts/.gitignore ../../Sources/Navigator/EPUB/Assets/Static/scripts/readium-fixed-wrapper-one.js +../../Sources/Navigator/EPUB/Assets/Static/scripts/readium-fixed-wrapper-one.js.map ../../Sources/Navigator/EPUB/Assets/Static/scripts/readium-fixed-wrapper-two.js +../../Sources/Navigator/EPUB/Assets/Static/scripts/readium-fixed-wrapper-two.js.map ../../Sources/Navigator/EPUB/Assets/Static/scripts/readium-fixed.js +../../Sources/Navigator/EPUB/Assets/Static/scripts/readium-fixed.js.map ../../Sources/Navigator/EPUB/Assets/Static/scripts/readium-reflowable.js +../../Sources/Navigator/EPUB/Assets/Static/scripts/readium-reflowable.js.map ../../Sources/Navigator/EPUB/CSS ../../Sources/Navigator/EPUB/CSS/CSSLayout.swift ../../Sources/Navigator/EPUB/CSS/CSSProperties.swift @@ -785,8 +789,14 @@ ../../Sources/Shared/Toolkit/XML/Fuzi.swift ../../Sources/Shared/Toolkit/XML/XML.swift ../../Sources/Shared/Toolkit/ZIP +../../Sources/Shared/Toolkit/ZIP/Minizip +../../Sources/Shared/Toolkit/ZIP/Minizip/MinizipArchiveOpener.swift +../../Sources/Shared/Toolkit/ZIP/Minizip/MinizipContainer.swift ../../Sources/Shared/Toolkit/ZIP/ZIPArchiveOpener.swift -../../Sources/Shared/Toolkit/ZIP/ZIPFoundation.swift +../../Sources/Shared/Toolkit/ZIP/ZIPFoundation +../../Sources/Shared/Toolkit/ZIP/ZIPFoundation/ZIPFoundationArchiveFactory.swift +../../Sources/Shared/Toolkit/ZIP/ZIPFoundation/ZIPFoundationArchiveOpener.swift +../../Sources/Shared/Toolkit/ZIP/ZIPFoundation/ZIPFoundationContainer.swift ../../Sources/Streamer ../../Sources/Streamer/Assets ../../Sources/Streamer/Assets/fonts diff --git a/Support/Carthage/Readium.xcodeproj/project.pbxproj b/Support/Carthage/Readium.xcodeproj/project.pbxproj index dcae8f16d..0fa83f056 100644 --- a/Support/Carthage/Readium.xcodeproj/project.pbxproj +++ b/Support/Carthage/Readium.xcodeproj/project.pbxproj @@ -44,6 +44,7 @@ 1752D756BED37325D6D4ED29 /* ReadiumInternal.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 42FD63C2720614E558522675 /* ReadiumInternal.framework */; }; 18217BC157557A5DDA4BA119 /* User.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADDB8B9906FC78C038203BDD /* User.swift */; }; 188D742F80B70DE8A625AD21 /* Facet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 387B19B66C4D91A295B5EFA6 /* Facet.swift */; }; + 1A4F41D5A7E48472DB9A181E /* ZIPFoundationArchiveOpener.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50064FE9BBCEA4C00BA6BBEF /* ZIPFoundationArchiveOpener.swift */; }; 1AEF63A8471C7676092842D2 /* FileExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = D555435E2BADB2B877FD50C7 /* FileExtension.swift */; }; 1B15166C79C7C05CE491AD2C /* CSSProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = E0E6147EF790DE532CE1699D /* CSSProperties.swift */; }; 1BF9469B4574D30E5C9BB75E /* Event.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCF859D4933121BDC376CC8A /* Event.swift */; }; @@ -99,6 +100,7 @@ 39326587EF76BFD5AD68AED2 /* ReadiumShared.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 97BC822B36D72EF548162129 /* ReadiumShared.framework */; }; 39B1DDE3571AB3F3CC6824F4 /* JSON.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDA827FC94F5CB3F9032028F /* JSON.swift */; }; 3AD9E86BB1621CF836919E33 /* ReadiumLocalizedString.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38984FD65CFF1D54FF7F794F /* ReadiumLocalizedString.swift */; }; + 3B138483530FDCC545A12D6F /* ZIPFoundationArchiveFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31C1E0FDB5373E672D5FF80F /* ZIPFoundationArchiveFactory.swift */; }; 3B5A8A76665391D2D32CB012 /* LCPAcquiredPublication.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C61B620DE6C012805269111 /* LCPAcquiredPublication.swift */; }; 3BB313823F043BA2C7D7D2F7 /* Locator.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE7D07E66B7E820D1A509A27 /* Locator.swift */; }; 3C4847FD7D5C5ABCF71A3E7B /* URL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 388D85FB7475709CB6CEA59E /* URL.swift */; }; @@ -158,7 +160,6 @@ 66018235ED40B89D27EE9F33 /* Group.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FAAD26EE52713DB9F103610 /* Group.swift */; }; 66A251DA78C53384E94F169F /* PublicationServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = F6D87AB6FB1B213E6269736B /* PublicationServer.swift */; }; 6719F981514309A65D206A85 /* LCPAcquisition.swift in Sources */ = {isa = PBXBuildFile; fileRef = F622773881411FB8BE686B9F /* LCPAcquisition.swift */; }; - 6724EAA0D931CF252E5FAEDF /* ZIPFoundation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59624A8738EB3A791CEF8E4C /* ZIPFoundation.swift */; }; 674BEEF110667C3051296E9B /* Double.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F3481F848A616A9A825A4BD /* Double.swift */; }; 67F1C7C3D434D2AA542376E3 /* PublicationParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = F609C27F073E40D662CFE093 /* PublicationParser.swift */; }; 682DFC1AF2BD7CAE0862B331 /* CryptoSwift.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = E37F94C388A86CB8A34812A5 /* CryptoSwift.xcframework */; }; @@ -239,6 +240,7 @@ 9BC4D1F2958D2F7D7BDB88DA /* CursorList.swift in Sources */ = {isa = PBXBuildFile; fileRef = C361F965E7A7962CA3E4C0BA /* CursorList.swift */; }; 9DB9674C11DF356966CBFA79 /* EPUBNavigatorViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC9ACC1EB3903149EBF21BC0 /* EPUBNavigatorViewModel.swift */; }; 9E064BC9E99D4F7D8AC3109B /* MediaOverlays.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1F5FEE0323287B9CAA09F03 /* MediaOverlays.swift */; }; + 9E6522796719FF1F16C243E7 /* MinizipContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E8D322A523DA324E3E2E59 /* MinizipContainer.swift */; }; 9E76790BAFFF08F0BFEA1BB0 /* DefaultPublicationParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5BBF2FA3188DFFCF7B88A75 /* DefaultPublicationParser.swift */; }; A036CCF310EB7408408FFF00 /* ImageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87629BF68F1EDBF06FC0AD54 /* ImageViewController.swift */; }; A040DE6F2D9A6B8D16B063B9 /* SwiftSoup.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = BE09289EB0FEA5FEC8506B1F /* SwiftSoup.xcframework */; }; @@ -350,6 +352,7 @@ E08CABFE57EC96D9F7062AF1 /* PDFKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABAF1D0444B94E2CDD80087D /* PDFKit.swift */; }; E12A731DD41BD2BC3C8076F8 /* TargetAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E564AE6D5137499C81FEBE2 /* TargetAction.swift */; }; E16AA8A927AAE141702F2D3B /* HTMLFontFamilyDeclaration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D80848AADD20D4384D9AF59 /* HTMLFontFamilyDeclaration.swift */; }; + E39B7BCA5ACB6D33C47FCB38 /* MinizipArchiveOpener.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79CE7AFF3ECCD705A80685BB /* MinizipArchiveOpener.swift */; }; E408BBB74A13AFB83C953C67 /* Properties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76638D3D1220E4C2620B9A80 /* Properties.swift */; }; E42CCC4CB1D491564664B5B6 /* Configurable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06AD6A912937694B20AD54C9 /* Configurable.swift */; }; E58910A3992CC88DE5BC0AA0 /* AudioLocatorService.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEB6D68278E0A593C810E2C0 /* AudioLocatorService.swift */; }; @@ -359,6 +362,7 @@ E75D342B54BD25242A29B105 /* Publication+EPUB.swift in Sources */ = {isa = PBXBuildFile; fileRef = 508E0CD4F9F02CC851E6D1E1 /* Publication+EPUB.swift */; }; E782980A49279BCB0C70C24C /* OPDSCopies.swift in Sources */ = {isa = PBXBuildFile; fileRef = 819D931708B3EE95CF9ADFED /* OPDSCopies.swift */; }; E8293787CB5E5CECE38A63B2 /* Encryption.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54699BC0E00F327E67908F6A /* Encryption.swift */; }; + E90B8CDCDEE4B20B3A6AB2FC /* ZIPFoundationContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8A3CDE3EA555A97BBFE1EEF /* ZIPFoundationContainer.swift */; }; EBD75415CB917F57D1ACF10B /* UTI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 218BE3110D2886B252A769A2 /* UTI.swift */; }; ECCE64CDEDA32509382A8520 /* CachingResource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 789B56D6D8A6AA79CD3643F4 /* CachingResource.swift */; }; ED67A0EFAE830F72846BF9C0 /* Range.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3231F989F7D7E560DD5364B9 /* Range.swift */; }; @@ -549,6 +553,7 @@ 2F3481F848A616A9A825A4BD /* Double.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Double.swift; sourceTree = ""; }; 2FEF14E6F136136D43A39897 /* RARFormatSniffer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RARFormatSniffer.swift; sourceTree = ""; }; 305833C6F16FAB2E23F40382 /* PDFSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PDFSettings.swift; sourceTree = ""; }; + 31C1E0FDB5373E672D5FF80F /* ZIPFoundationArchiveFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZIPFoundationArchiveFactory.swift; sourceTree = ""; }; 3230FB63D7ADDD514D74F7E6 /* LCPLicenseRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LCPLicenseRepository.swift; sourceTree = ""; }; 3231F989F7D7E560DD5364B9 /* Range.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Range.swift; sourceTree = ""; }; 339637CCF01E665F4CB78B01 /* EPUBLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EPUBLayout.swift; sourceTree = ""; }; @@ -600,6 +605,7 @@ 4BF38F71FDEC1920325B62D3 /* PublicationContentIterator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublicationContentIterator.swift; sourceTree = ""; }; 4BF3D04FB9D6A90EE77F1F02 /* ResourceProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResourceProperties.swift; sourceTree = ""; }; 4E564AE6D5137499C81FEBE2 /* TargetAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TargetAction.swift; sourceTree = ""; }; + 50064FE9BBCEA4C00BA6BBEF /* ZIPFoundationArchiveOpener.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZIPFoundationArchiveOpener.swift; sourceTree = ""; }; 500E55D9CA753D6D6AA76D10 /* EPUBLicenseContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EPUBLicenseContainer.swift; sourceTree = ""; }; 505BF8A630F7C7B96754E333 /* InMemoryPositionsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InMemoryPositionsService.swift; sourceTree = ""; }; 508E0CD4F9F02CC851E6D1E1 /* Publication+EPUB.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Publication+EPUB.swift"; sourceTree = ""; }; @@ -615,7 +621,6 @@ 567C115FF0939F69AD83AE82 /* UserRights.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserRights.swift; sourceTree = ""; }; 56C489452239BF85F4D14E95 /* PublicationMediaLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublicationMediaLoader.swift; sourceTree = ""; }; 571BBA35C6F496B007C5158C /* TailCachingResource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TailCachingResource.swift; sourceTree = ""; }; - 59624A8738EB3A791CEF8E4C /* ZIPFoundation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZIPFoundation.swift; sourceTree = ""; }; 5A85DB4931BA5D965042CC6F /* CompositePublicationParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompositePublicationParser.swift; sourceTree = ""; }; 5BC6AE42A31D77B548CB0BB4 /* Observable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Observable.swift; sourceTree = ""; }; 5E788FD34BE635B4B80C18A6 /* UIColor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIColor.swift; sourceTree = ""; }; @@ -658,6 +663,7 @@ 789B56D6D8A6AA79CD3643F4 /* CachingResource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CachingResource.swift; sourceTree = ""; }; 78E5D0B9449607B2906901C2 /* LanguageFormatSniffer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LanguageFormatSniffer.swift; sourceTree = ""; }; 78FFDF8CF77437EDB41E4547 /* FailureResource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FailureResource.swift; sourceTree = ""; }; + 79CE7AFF3ECCD705A80685BB /* MinizipArchiveOpener.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MinizipArchiveOpener.swift; sourceTree = ""; }; 7BB152578CBA091A41A51B25 /* Language.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Language.swift; sourceTree = ""; }; 7BBD54FD376456C1925316BC /* Cancellable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Cancellable.swift; sourceTree = ""; }; 7C2787EBE9D5565DA8593711 /* Properties+Presentation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Properties+Presentation.swift"; sourceTree = ""; }; @@ -777,6 +783,7 @@ CFE34EA8AF2D815F7169CA45 /* Fuzi.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Fuzi.swift; sourceTree = ""; }; D008F7BB187AE82CBB115D0F /* WebServerResourceResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebServerResourceResponse.swift; sourceTree = ""; }; D0C2A38D366CE8560BCBAC8B /* PDFPositionsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PDFPositionsService.swift; sourceTree = ""; }; + D0E8D322A523DA324E3E2E59 /* MinizipContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MinizipContainer.swift; sourceTree = ""; }; D13272E03B63E96D4246F79D /* PDFParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PDFParser.swift; sourceTree = ""; }; D388387CD2A23CD5DB30F74A /* FormatSnifferBlob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FormatSnifferBlob.swift; sourceTree = ""; }; D3D785FEFDA202A61E620890 /* EPUBDeobfuscator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EPUBDeobfuscator.swift; sourceTree = ""; }; @@ -844,6 +851,7 @@ F6E45005E776078B46DB8E14 /* Memoize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Memoize.swift; sourceTree = ""; }; F6EB7CAF6D058380A2AB711A /* CGPDF.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CGPDF.swift; sourceTree = ""; }; F820DAECA1FC23C42719CD68 /* EncryptionParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptionParser.swift; sourceTree = ""; }; + F8A3CDE3EA555A97BBFE1EEF /* ZIPFoundationContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZIPFoundationContainer.swift; sourceTree = ""; }; F8C32FDF5E2D35BE71E45ED0 /* AudioPreferencesEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPreferencesEditor.swift; sourceTree = ""; }; F90C4D94134D9F741D38D8AA /* Comparable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Comparable.swift; sourceTree = ""; }; FC3996B9F88089C7F752C531 /* DefaultFormatSniffer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultFormatSniffer.swift; sourceTree = ""; }; @@ -1113,7 +1121,8 @@ isa = PBXGroup; children = ( C51C74A5990A3BA93B3DC587 /* ZIPArchiveOpener.swift */, - 59624A8738EB3A791CEF8E4C /* ZIPFoundation.swift */, + 626EAD4D69FF21C4D34F4A76 /* Minizip */, + 54C7B5BB86C1728419585D6C /* ZIPFoundation */, ); path = ZIP; sourceTree = ""; @@ -1342,6 +1351,16 @@ path = ../../Sources/Internal; sourceTree = ""; }; + 54C7B5BB86C1728419585D6C /* ZIPFoundation */ = { + isa = PBXGroup; + children = ( + 31C1E0FDB5373E672D5FF80F /* ZIPFoundationArchiveFactory.swift */, + 50064FE9BBCEA4C00BA6BBEF /* ZIPFoundationArchiveOpener.swift */, + F8A3CDE3EA555A97BBFE1EEF /* ZIPFoundationContainer.swift */, + ); + path = ZIPFoundation; + sourceTree = ""; + }; 55198591F887417680938950 /* Tokenizer */ = { isa = PBXGroup; children = ( @@ -1417,6 +1436,15 @@ path = EPUB; sourceTree = ""; }; + 626EAD4D69FF21C4D34F4A76 /* Minizip */ = { + isa = PBXGroup; + children = ( + 79CE7AFF3ECCD705A80685BB /* MinizipArchiveOpener.swift */, + D0E8D322A523DA324E3E2E59 /* MinizipContainer.swift */, + ); + path = Minizip; + sourceTree = ""; + }; 62874E8B5F560A749FA8A70B /* Absolute URL */ = { isa = PBXGroup; children = ( @@ -2604,6 +2632,8 @@ 26CA8F00CA85378CA0F8B3F2 /* MediaTypeSniffer.swift in Sources */, 7E45E10720EA6B4F18196316 /* Metadata+Presentation.swift in Sources */, 78C52EED635B5F8C38A02298 /* Metadata.swift in Sources */, + E39B7BCA5ACB6D33C47FCB38 /* MinizipArchiveOpener.swift in Sources */, + 9E6522796719FF1F16C243E7 /* MinizipContainer.swift in Sources */, 74E94DF537F0DE19706003DA /* NowPlayingInfo.swift in Sources */, 3079CCA7A341CB0A43A8DA74 /* OPDSAcquisition.swift in Sources */, C35C6AB637D2048D9B0A3C62 /* OPDSAvailability.swift in Sources */, @@ -2679,7 +2709,9 @@ 5591563FD08A956B80C37716 /* XMLFormatSniffer.swift in Sources */, BBF5AAEEE90BD88D58191DA3 /* ZIPArchiveOpener.swift in Sources */, B49522888052E9F41D0DD013 /* ZIPFormatSniffer.swift in Sources */, - 6724EAA0D931CF252E5FAEDF /* ZIPFoundation.swift in Sources */, + 3B138483530FDCC545A12D6F /* ZIPFoundationArchiveFactory.swift in Sources */, + 1A4F41D5A7E48472DB9A181E /* ZIPFoundationArchiveOpener.swift in Sources */, + E90B8CDCDEE4B20B3A6AB2FC /* ZIPFoundationContainer.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Support/CocoaPods/ReadiumShared.podspec b/Support/CocoaPods/ReadiumShared.podspec index e528e9af1..4b9c6ae02 100644 --- a/Support/CocoaPods/ReadiumShared.podspec +++ b/Support/CocoaPods/ReadiumShared.podspec @@ -16,6 +16,7 @@ Pod::Spec.new do |s| s.libraries = "xml2" s.xcconfig = { 'HEADER_SEARCH_PATHS' => '$(SDKROOT)/usr/include/libxml2' } + s.dependency 'Minizip', '~> 1.0.0' s.dependency 'SwiftSoup', '~> 2.7.0' s.dependency 'ReadiumFuzi', '~> 4.0.0' s.dependency 'ReadiumZIPFoundation', '~> 2.0.0' diff --git a/Tests/Adapters/MinizipTests/Fixtures.swift b/Tests/Adapters/MinizipTests/Fixtures.swift deleted file mode 100644 index d232a7e59..000000000 --- a/Tests/Adapters/MinizipTests/Fixtures.swift +++ /dev/null @@ -1,35 +0,0 @@ -// -// Copyright 2025 Readium Foundation. All rights reserved. -// Use of this source code is governed by the BSD-style license -// available in the top-level LICENSE file of the project. -// - -import Foundation -import ReadiumShared -import XCTest - -#if !SWIFT_PACKAGE - extension Bundle { - static let module = Bundle(for: Fixtures.self) - } -#endif - -class Fixtures { - let path: String? - - init(path: String? = nil) { - self.path = path - } - - func url(for filepath: String) -> FileURL { - FileURL(url: Bundle.module.resourceURL!.appendingPathComponent("Fixtures/\(path ?? "")/\(filepath)"))! - } - - func data(at filepath: String) -> Data { - try! XCTUnwrap(try? Data(contentsOf: url(for: filepath).url)) - } - - func json(at filepath: String) -> T { - try! XCTUnwrap(JSONSerialization.jsonObject(with: data(at: filepath)) as? T) - } -} diff --git a/Tests/Adapters/MinizipTests/Fixtures/not-a.zip b/Tests/Adapters/MinizipTests/Fixtures/not-a.zip deleted file mode 100644 index 8308ed4f5..000000000 Binary files a/Tests/Adapters/MinizipTests/Fixtures/not-a.zip and /dev/null differ diff --git a/Tests/Adapters/MinizipTests/Fixtures/test.zip b/Tests/Adapters/MinizipTests/Fixtures/test.zip deleted file mode 100644 index be06efea0..000000000 Binary files a/Tests/Adapters/MinizipTests/Fixtures/test.zip and /dev/null differ diff --git a/Tests/Adapters/MinizipTests/MinizipContainerTests.swift b/Tests/SharedTests/Toolkit/ZIP/MinizipContainerTests.swift similarity index 98% rename from Tests/Adapters/MinizipTests/MinizipContainerTests.swift rename to Tests/SharedTests/Toolkit/ZIP/MinizipContainerTests.swift index 2183de12c..1891c8320 100644 --- a/Tests/Adapters/MinizipTests/MinizipContainerTests.swift +++ b/Tests/SharedTests/Toolkit/ZIP/MinizipContainerTests.swift @@ -4,11 +4,10 @@ // available in the top-level LICENSE file of the project. // -@testable import ReadiumAdapterMinizip -import ReadiumShared +@testable import ReadiumShared import XCTest -private let fixtures = Fixtures() +private let fixtures = Fixtures(path: "Archive") class MinizipContainerTests: XCTestCase { private func container(for filename: String) async throws -> Container { diff --git a/Tests/SharedTests/Toolkit/Archive/ZIPFoundationTests.swift b/Tests/SharedTests/Toolkit/ZIP/ZIPFoundationContainerTests.swift similarity index 99% rename from Tests/SharedTests/Toolkit/Archive/ZIPFoundationTests.swift rename to Tests/SharedTests/Toolkit/ZIP/ZIPFoundationContainerTests.swift index c1dc6bb57..db1b72e8d 100644 --- a/Tests/SharedTests/Toolkit/Archive/ZIPFoundationTests.swift +++ b/Tests/SharedTests/Toolkit/ZIP/ZIPFoundationContainerTests.swift @@ -9,7 +9,7 @@ import XCTest private let fixtures = Fixtures(path: "Archive") -class ZIPFoundationTests: XCTestCase { +class ZIPFoundationContainerTests: XCTestCase { private func container(for filename: String) async throws -> Container { try await ZIPFoundationContainer.make(resource: FileResource(file: fixtures.url(for: filename))).get() } diff --git a/docs/Guides/Getting Started.md b/docs/Guides/Getting Started.md index 3e0412ca5..e6a3aaa69 100644 --- a/docs/Guides/Getting Started.md +++ b/docs/Guides/Getting Started.md @@ -29,9 +29,6 @@ The toolkit has been designed following these core tenets: * `ReadiumAdapterGCDWebServer` provides an HTTP server built with [GCDWebServer](https://github.com/swisspol/GCDWebServer). * `ReadiumAdapterLCPSQLite` provides implementations of the `ReadiumLCP` license and passphrase repositories using [SQLite.swift](https://github.com/stephencelis/SQLite.swift). -* `ReadiumAdapterMinizip` provides an implementation of `ArchiveOpener` using [Minizip](https://github.com/marmelroy/Zip). Compared to the default `ZIPArchiveOpener`, it has the following differences: - * It does not support HTTP streaming of ZIP packages. - * It offers better performance for LCP-protected publications containing large resources that are `deflated` instead of `stored` in the archive, which is not recommended. ## Overview of the shared models (`ReadiumShared`) diff --git a/docs/Migration Guide.md b/docs/Migration Guide.md index 2dd3654b7..0859db19d 100644 --- a/docs/Migration Guide.md +++ b/docs/Migration Guide.md @@ -2,7 +2,14 @@ All migration steps necessary in reading apps to upgrade to major versions of the Swift Readium toolkit will be documented in this file. - +## Unreleased + +### Bringing back Minizip + +Minizip was removed in version 3.0.0 and replaced by ZIPFoundation. Until the ZIPFoundation implementation is thoroughly tested, we have reintroduced Minizip as a dependency in version 3.1.0. + +If you use Carthage, add `Minizip.xcframework` back to your dependencies. No changes are required when using Swift Package Manager or CocoaPods. + ## 3.0.0