From cae987783b84aab254f2dab508920346c57222fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Menu?= Date: Wed, 11 Dec 2024 11:18:24 +0100 Subject: [PATCH 1/4] Add support for ZIPFoundation and streamline third-party dependencies --- Cartfile | 5 +- Package.swift | 42 +- README.md | 31 +- .../ZIPFoundation/ZIPFoundation.swift | 192 +++++++++ Sources/Internal/Extensions/Sequence.swift | 2 +- .../Container/ZIPLicenseContainer.swift | 28 +- Sources/LCP/License/License.swift | 2 +- Sources/OPDS/OPDS1Parser.swift | 16 +- .../HTMLResourceContentIterator.swift | 2 +- .../Toolkit/Format/MediaTypeSniffer.swift | 2 +- Sources/Shared/Toolkit/XML/Fuzi.swift | 18 +- Sources/Shared/Toolkit/ZIP/Minizip.swift | 373 ------------------ .../Shared/Toolkit/ZIP/ZIPArchiveOpener.swift | 8 +- .../Shared/Toolkit/ZIP/ZIPFoundation.swift | 138 ------- .../Parser/EPUB/EPUBContainerParser.swift | 4 +- .../Parser/EPUB/EPUBEncryptionParser.swift | 6 +- .../Parser/EPUB/EPUBMetadataParser.swift | 24 +- Sources/Streamer/Parser/EPUB/EPUBParser.swift | 2 +- Sources/Streamer/Parser/EPUB/NCXParser.swift | 8 +- .../EPUB/NavigationDocumentParser.swift | 10 +- Sources/Streamer/Parser/EPUB/OPFMeta.swift | 12 +- Sources/Streamer/Parser/EPUB/OPFParser.swift | 12 +- .../EPUB/Services/EPUBPositionsService.swift | 4 +- .../PDF/Services/LCPDFPositionsService.swift | 2 +- Support/Carthage/.xcodegen | 6 +- .../Readium.xcodeproj/project.pbxproj | 8 +- Support/Carthage/project.yml | 19 +- Support/CocoaPods/ReadiumLCP.podspec | 2 +- Support/CocoaPods/ReadiumNavigator.podspec | 1 + Support/CocoaPods/ReadiumOPDS.podspec | 2 +- Support/CocoaPods/ReadiumShared.podspec | 4 +- Support/CocoaPods/ReadiumStreamer.podspec | 2 +- TestApp/Integrations/Local/project+lcp.yml | 2 + TestApp/Integrations/Local/project.yml | 2 + TestApp/Sources/App/Readium.swift | 5 +- Tests/Adapters/ZIPFoundationTests/Fixtures | 1 + .../ZIPFoundationTests/Fixtures.swift | 29 ++ .../ZIPFoundationTests.swift | 158 ++++++++ .../Toolkit/Archive/Container+ZIPTests.swift | 2 +- .../EPUB/EPUBContainerParserTests.swift | 2 +- .../Parser/EPUB/EPUBMetadataParserTests.swift | 4 +- 41 files changed, 540 insertions(+), 652 deletions(-) create mode 100644 Sources/Adapters/ZIPFoundation/ZIPFoundation.swift delete mode 100644 Sources/Shared/Toolkit/ZIP/Minizip.swift delete mode 100644 Sources/Shared/Toolkit/ZIP/ZIPFoundation.swift create mode 120000 Tests/Adapters/ZIPFoundationTests/Fixtures create mode 100644 Tests/Adapters/ZIPFoundationTests/Fixtures.swift create mode 100644 Tests/Adapters/ZIPFoundationTests/ZIPFoundationTests.swift diff --git a/Cartfile b/Cartfile index bf2762daa..4597c8310 100644 --- a/Cartfile +++ b/Cartfile @@ -1,9 +1,8 @@ -github "dexman/Minizip" ~> 1.4.0 github "krzyzanowskim/CryptoSwift" ~> 1.8.0 github "ra1028/DifferenceKit" ~> 1.3.0 -github "readium/Fuzi" ~> 3.1.4 +github "readium/Fuzi" ~> 4.0.0 github "readium/GCDWebServer" ~> 4.0.0 +github "readium/ZIPFoundation" ~> 1.0.0 # There's a regression with 2.7.4 in SwiftSoup, because they used iOS 13 APIs without bumping the deployment target. github "scinfu/SwiftSoup" == 2.7.1 github "stephencelis/SQLite.swift" ~> 0.15.0 -github "weichsel/ZIPFoundation" ~> 0.9.0 diff --git a/Package.swift b/Package.swift index 1ce44f2a7..63d7c4676 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.3 +// swift-tools-version:5.10 // // Copyright 2021 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license @@ -21,26 +21,27 @@ let package = Package( // Adapters to third-party dependencies. .library(name: "ReadiumAdapterGCDWebServer", targets: ["ReadiumAdapterGCDWebServer"]), .library(name: "ReadiumAdapterLCPSQLite", targets: ["ReadiumAdapterLCPSQLite"]), + .library(name: "ReadiumAdapterZIPFoundation", targets: ["ReadiumAdapterZIPFoundation"]), ], dependencies: [ .package(url: "https://github.com/krzyzanowskim/CryptoSwift.git", from: "1.8.0"), - .package(url: "https://github.com/marmelroy/Zip.git", from: "2.1.0"), .package(url: "https://github.com/ra1028/DifferenceKit.git", from: "1.3.0"), - .package(url: "https://github.com/readium/Fuzi.git", from: "3.1.4"), + .package(url: "https://github.com/readium/Fuzi.git", from: "4.0.0"), .package(url: "https://github.com/readium/GCDWebServer.git", from: "4.0.0"), .package(url: "https://github.com/scinfu/SwiftSoup.git", from: "2.7.0"), .package(url: "https://github.com/stephencelis/SQLite.swift.git", from: "0.15.0"), - .package(url: "https://github.com/weichsel/ZIPFoundation.git", from: "0.9.0"), +// .package(url: "https://github.com/readium/ZIPFoundation.git", branch: "main"), + .package(path: "../ZIPFoundation"), ], targets: [ .target( name: "ReadiumShared", - dependencies: ["ReadiumInternal", "Fuzi", "SwiftSoup", "Zip"], - path: "Sources/Shared", - exclude: [ - // Support for ZIPFoundation is not yet achieved. - "Toolkit/ZIP/ZIPFoundation.swift", + dependencies: [ + "ReadiumInternal", + "SwiftSoup", + .product(name: "ReadiumFuzi", package: "Fuzi"), ], + path: "Sources/Shared", linkerSettings: [ .linkedFramework("CoreServices"), .linkedFramework("UIKit"), @@ -59,8 +60,8 @@ let package = Package( name: "ReadiumStreamer", dependencies: [ "CryptoSwift", - "Fuzi", "ReadiumShared", + .product(name: "ReadiumFuzi", package: "Fuzi"), ], path: "Sources/Streamer", resources: [ @@ -102,8 +103,8 @@ let package = Package( .target( name: "ReadiumOPDS", dependencies: [ - "Fuzi", "ReadiumShared", + .product(name: "ReadiumFuzi", package: "Fuzi"), ], path: "Sources/OPDS" ), @@ -120,8 +121,8 @@ let package = Package( name: "ReadiumLCP", dependencies: [ "CryptoSwift", - "ZIPFoundation", "ReadiumShared", + .product(name: "ReadiumZIPFoundation", package: "ZIPFoundation"), ], path: "Sources/LCP", resources: [ @@ -157,6 +158,23 @@ let package = Package( path: "Sources/Adapters/LCPSQLite" ), + .target( + name: "ReadiumAdapterZIPFoundation", + dependencies: [ + "ReadiumShared", + .product(name: "ReadiumZIPFoundation", package: "ZIPFoundation"), + ], + path: "Sources/Adapters/ZIPFoundation" + ), + .testTarget( + name: "ReadiumAdapterZIPFoundationTests", + dependencies: ["ReadiumAdapterZIPFoundation"], + path: "Tests/Adapters/ZIPFoundationTests", + resources: [ + .copy("Fixtures"), + ] + ), + .target( name: "ReadiumInternal", path: "Sources/Internal" diff --git a/README.md b/README.md index ecef380dc..1ede84b33 100644 --- a/README.md +++ b/README.md @@ -54,34 +54,31 @@ 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` | -|-----------------------|:------------------:|:------------------:|:------------------:|:------------------:|:------------------:|------------------------------|---------------------------| -| **`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: | | | | | -| `Fuzi` | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | | -| `Minizip` | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | | -| `ReadiumGCDWebServer` | | | | | | :heavy_check_mark: | | -| `SQLite.swift` | | | | | | | :heavy_check_mark: | -| `SwiftSoup` | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | | -| `ZIPFoundation` | | | | | :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: | | | +| `SQLite.swift` | | | | | | | :heavy_check_mark: | +| `SwiftSoup` | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | | ### CocoaPods Add the following `pod` statements to your `Podfile` for the Readium libraries you want to use: ``` -pod 'ReadiumShared', podspec: 'https://raw.githubusercontent.com/readium/swift-toolkit/3.0.0-beta.1/Support/CocoaPods/ReadiumShared.podspec' pod 'ReadiumStreamer', podspec: 'https://raw.githubusercontent.com/readium/swift-toolkit/3.0.0-beta.1/Support/CocoaPods/ReadiumStreamer.podspec' pod 'ReadiumNavigator', podspec: 'https://raw.githubusercontent.com/readium/swift-toolkit/3.0.0-beta.1/Support/CocoaPods/ReadiumNavigator.podspec' pod 'ReadiumOPDS', podspec: 'https://raw.githubusercontent.com/readium/swift-toolkit/3.0.0-beta.1/Support/CocoaPods/ReadiumOPDS.podspec' pod 'ReadiumLCP', podspec: 'https://raw.githubusercontent.com/readium/swift-toolkit/3.0.0-beta.1/Support/CocoaPods/ReadiumLCP.podspec' -pod 'ReadiumInternal', podspec: 'https://raw.githubusercontent.com/readium/swift-toolkit/3.0.0-beta.1/Support/CocoaPods/ReadiumInternal.podspec' -pod 'Fuzi', podspec: 'https://raw.githubusercontent.com/readium/Fuzi/refs/heads/master/Fuzi.podspec' -# Required if you use ReadiumAdapterGCDWebServer. -pod 'ReadiumGCDWebServer', podspec: 'https://raw.githubusercontent.com/readium/GCDWebServer/4.0.0/GCDWebServer.podspec' +# Required by all the other libraries +pod 'ReadiumShared', podspec: 'https://raw.githubusercontent.com/readium/swift-toolkit/3.0.0-beta.1/Support/CocoaPods/ReadiumShared.podspec' +pod 'ReadiumInternal', podspec: 'https://raw.githubusercontent.com/readium/swift-toolkit/3.0.0-beta.1/Support/CocoaPods/ReadiumInternal.podspec' ``` Take a look at [CocoaPods's documentation](https://guides.cocoapods.org/using/using-cocoapods.html) for more information. diff --git a/Sources/Adapters/ZIPFoundation/ZIPFoundation.swift b/Sources/Adapters/ZIPFoundation/ZIPFoundation.swift new file mode 100644 index 000000000..08cbf7dcf --- /dev/null +++ b/Sources/Adapters/ZIPFoundation/ZIPFoundation.swift @@ -0,0 +1,192 @@ +// +// Copyright 2024 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 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), + let file = resource.sourceURL?.fileURL + else { + return .failure(.formatNotSupported(format)) + } + + return await ZIPFoundationContainer.make(file: file) + .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 { + guard let file = resource.sourceURL?.fileURL else { + return .failure(.formatNotRecognized) + } + + return await ZIPFoundationContainer.make(file: file) + .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" + ) + ) + } + } +} + +/// A ZIP ``Container`` using the ZIPFoundation library. +final class ZIPFoundationContainer: Container, Loggable { + enum MakeError: Error { + case notAZIP + case reading(ReadError) + } + + static func make(file: FileURL) async -> Result { + guard await (try? file.exists()) ?? false else { + return .failure(.reading(.access(.fileSystem(.fileNotFound(nil))))) + } + + do { + let archive = try Archive(url: file.url, accessMode: .read, pathEncoding: nil) + var entries = [RelativeURL: ZIPFoundationEntryMetadata]() + + for entry in archive { + guard + entry.type == .file, + let url = RelativeURL(path: entry.path(using: .utf8)), + !url.path.isEmpty + else { + continue + } + entries[url] = ZIPFoundationEntryMetadata( + length: entry.uncompressedSize, + compressedLength: entry.isCompressed ? entry.compressedSize : nil + ) + } + + return .success(Self(file: file, entries: entries)) + + } catch { + return .failure(.reading(.decoding(error))) + } + } + + private let file: FileURL + private let entriesMetadata: [RelativeURL: ZIPFoundationEntryMetadata] + + public var sourceURL: AbsoluteURL? { file } + public let entries: Set + + private init(file: FileURL, entries: [RelativeURL: ZIPFoundationEntryMetadata]) { + self.file = file + entriesMetadata = entries + self.entries = Set(entries.keys.map(\.anyURL)) + } + + subscript(url: any URLConvertible) -> (any Resource)? { + guard + let url = url.relativeURL, + let metadata = entriesMetadata[url] + else { + return nil + } + return ZIPFoundationResource(file: file, entryPath: url.path, metadata: metadata) + } +} + +private struct ZIPFoundationEntryMetadata { + let length: UInt64 + let compressedLength: UInt64? +} + +private actor ZIPFoundationResource: Resource, Loggable { + private let file: FileURL + private let entryPath: String + private let metadata: ZIPFoundationEntryMetadata + + init(file: FileURL, entryPath: String, metadata: ZIPFoundationEntryMetadata) { + self.file = file + self.entryPath = entryPath + self.metadata = metadata + } + + public let sourceURL: AbsoluteURL? = nil + + func estimatedLength() async -> ReadResult { + .success(metadata.length) + } + + func properties() async -> ReadResult { + .success(ResourceProperties { + $0.filename = RelativeURL(path: entryPath)?.lastPathSegment + $0.archive = ArchiveProperties( + entryLength: metadata.compressedLength ?? metadata.length, + isEntryCompressed: metadata.compressedLength != nil + ) + }) + } + + func stream(range: Range?, consume: @escaping (Data) -> Void) async -> ReadResult { + if range != nil { + } + + return await archive().flatMap { archive in + guard let entry = archive[entryPath] else { + return .failure(.decoding("No entry found in the ZIP at \(entryPath)")) + } + + do { + if let range = range { + try archive.extractRange(range, of: entry) { data in + consume(data) + } + } else { + _ = try 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 = .success(try Archive(url: file.url, accessMode: .read, pathEncoding: nil)) + } catch { + _archive = .failure(.decoding(error)) + } + } + return _archive! + } +} diff --git a/Sources/Internal/Extensions/Sequence.swift b/Sources/Internal/Extensions/Sequence.swift index fe3f59d6d..645629562 100644 --- a/Sources/Internal/Extensions/Sequence.swift +++ b/Sources/Internal/Extensions/Sequence.swift @@ -8,7 +8,7 @@ import Foundation public extension Sequence { /// Asynchronous variant of `map`. - @inlinable func asyncmap( + @inlinable func asyncMap( _ transform: (Element) async throws -> NewElement ) async rethrows -> [NewElement] { var result: [NewElement] = [] diff --git a/Sources/LCP/License/Container/ZIPLicenseContainer.swift b/Sources/LCP/License/Container/ZIPLicenseContainer.swift index 3b8259247..4fb2e2e2d 100644 --- a/Sources/LCP/License/Container/ZIPLicenseContainer.swift +++ b/Sources/LCP/License/Container/ZIPLicenseContainer.swift @@ -6,7 +6,7 @@ import Foundation import ReadiumShared -import ZIPFoundation +import ReadiumZIPFoundation /// Access to a License Document stored in a ZIP archive. /// Meant to be subclassed to customize the pathInZIP property, eg. EPUBLicenseContainer. @@ -20,15 +20,20 @@ class ZIPLicenseContainer: LicenseContainer { } func containsLicense() async throws -> Bool { - guard let archive = Archive(url: zip.url, accessMode: .read) else { - return false + do { + let archive = try Archive(url: zip.url, accessMode: .read) + return archive[pathInZIP] != nil + } catch { + throw LCPError.licenseContainer(.openFailed(error)) } - return archive[pathInZIP] != nil } func read() async throws -> Data { - guard let archive = Archive(url: zip.url, accessMode: .read) else { - throw LCPError.licenseContainer(.openFailed(nil)) + let archive: Archive + do { + archive = try Archive(url: zip.url, accessMode: .read) + } catch { + throw LCPError.licenseContainer(.openFailed(error)) } guard let entry = archive[pathInZIP] else { throw LCPError.licenseContainer(.fileNotFound(pathInZIP)) @@ -47,8 +52,11 @@ class ZIPLicenseContainer: LicenseContainer { } func write(_ license: LicenseDocument) async throws { - guard let archive = Archive(url: zip.url, accessMode: .update) else { - throw LCPError.licenseContainer(.openFailed(nil)) + let archive: Archive + do { + archive = try Archive(url: zip.url, accessMode: .update) + } catch { + throw LCPError.licenseContainer(.openFailed(error)) } do { @@ -59,8 +67,8 @@ class ZIPLicenseContainer: LicenseContainer { // Stores the License into the ZIP file let data = license.jsonData - try archive.addEntry(with: pathInZIP, type: .file, uncompressedSize: UInt32(data.count), provider: { position, size -> Data in - data[position ..< size] + try archive.addEntry(with: pathInZIP, type: .file, uncompressedSize: Int64(data.count), provider: { position, size -> Data in + data[position ..< Int64(size)] }) } catch { throw LCPError.licenseContainer(.writeFailed(path: pathInZIP)) diff --git a/Sources/LCP/License/License.swift b/Sources/LCP/License/License.swift index 78ad271e2..9e652553e 100644 --- a/Sources/LCP/License/License.swift +++ b/Sources/LCP/License/License.swift @@ -6,7 +6,7 @@ import Foundation import ReadiumShared -import ZIPFoundation +import ReadiumZIPFoundation final class License: Loggable { // Last Documents which passed the integrity checks. diff --git a/Sources/OPDS/OPDS1Parser.swift b/Sources/OPDS/OPDS1Parser.swift index 35db096e1..5220cec0d 100644 --- a/Sources/OPDS/OPDS1Parser.swift +++ b/Sources/OPDS/OPDS1Parser.swift @@ -5,7 +5,7 @@ // import Foundation -import Fuzi +import ReadiumFuzi import ReadiumShared public enum OPDS1ParserError: Error { @@ -85,7 +85,7 @@ public class OPDS1Parser: Loggable { /// Feed can only be v1 (XML). /// - parameter document: The XMLDocument data /// - Returns: The resulting Feed - public static func parse(document: Fuzi.XMLDocument) throws -> Feed { + public static func parse(document: ReadiumFuzi.XMLDocument) throws -> Feed { document.definePrefix("thr", forNamespace: "http://purl.org/syndication/thread/1.0") document.definePrefix("dcterms", forNamespace: "http://purl.org/dc/terms/") document.definePrefix("opds", forNamespace: "http://opds-spec.org/2010/catalog") @@ -218,7 +218,7 @@ public class OPDS1Parser: Loggable { /// Publication can only be v1 (XML). /// - parameter document: The XMLDocument data /// - Returns: The resulting Publication - public static func parseEntry(document: Fuzi.XMLDocument) throws -> Publication? { + public static func parseEntry(document: ReadiumFuzi.XMLDocument) throws -> Publication? { guard let root = document.root else { throw OPDS1ParserError.rootNotFound } @@ -254,8 +254,8 @@ public class OPDS1Parser: Loggable { } // The OpenSearch document may contain multiple Urls, and we need to find the closest matching one. // We match by mimetype and profile; if that fails, by mimetype; and if that fails, the first url is returned - var typeAndProfileMatch: Fuzi.XMLElement? = nil - var typeMatch: Fuzi.XMLElement? = nil + var typeAndProfileMatch: ReadiumFuzi.XMLElement? = nil + var typeMatch: ReadiumFuzi.XMLElement? = nil if let selfMimeType = feed.links.firstWithRel(.self)?.mediaType { let selfMimeParams = parseMimeType(mimeTypeString: selfMimeType.string) for url in urls { @@ -297,7 +297,7 @@ public class OPDS1Parser: Loggable { return MimeTypeParameters(type: type, parameters: params) } - static func parseEntry(entry: Fuzi.XMLElement) -> Publication? { + static func parseEntry(entry: ReadiumFuzi.XMLElement) -> Publication? { // Shortcuts to get tag(s)' string value. func tag(_ name: String) -> String? { entry.firstChild(tag: name)?.stringValue @@ -456,7 +456,7 @@ public class OPDS1Parser: Loggable { } } - static func parseIndirectAcquisition(children: [Fuzi.XMLElement]) -> [OPDSAcquisition] { + static func parseIndirectAcquisition(children: [ReadiumFuzi.XMLElement]) -> [OPDSAcquisition] { children.compactMap { child in guard let type = child.attributes["type"] else { return nil @@ -470,7 +470,7 @@ public class OPDS1Parser: Loggable { } } - static func parsePrice(link: Fuzi.XMLElement) -> OPDSPrice? { + static func parsePrice(link: ReadiumFuzi.XMLElement) -> OPDSPrice? { guard let price = link.firstChild(tag: "price")?.stringValue, let value = Double(price), let currency = link.firstChild(tag: "price")?.attr("currencycode") diff --git a/Sources/Shared/Publication/Services/Content/Iterators/HTMLResourceContentIterator.swift b/Sources/Shared/Publication/Services/Content/Iterators/HTMLResourceContentIterator.swift index 64386f26f..a078da080 100644 --- a/Sources/Shared/Publication/Services/Content/Iterators/HTMLResourceContentIterator.swift +++ b/Sources/Shared/Publication/Services/Content/Iterators/HTMLResourceContentIterator.swift @@ -131,7 +131,7 @@ public class HTMLResourceContentIterator: ContentIterator { } var elements = elements - elements.elements = await elements.elements.enumerated().asyncmap { index, element in + elements.elements = await elements.elements.enumerated().asyncMap { index, element in let progression = Double(index) / count return await element.copy( progression: progression, diff --git a/Sources/Shared/Toolkit/Format/MediaTypeSniffer.swift b/Sources/Shared/Toolkit/Format/MediaTypeSniffer.swift index d0e84b6f5..2163d15d8 100644 --- a/Sources/Shared/Toolkit/Format/MediaTypeSniffer.swift +++ b/Sources/Shared/Toolkit/Format/MediaTypeSniffer.swift @@ -6,7 +6,7 @@ import CoreServices import Foundation -import Fuzi +import ReadiumFuzi public extension MediaType { @available(*, unavailable, message: "Use an `AssetRetriever` to sniff a `Format` instead") diff --git a/Sources/Shared/Toolkit/XML/Fuzi.swift b/Sources/Shared/Toolkit/XML/Fuzi.swift index c74284277..d1b04a316 100644 --- a/Sources/Shared/Toolkit/XML/Fuzi.swift +++ b/Sources/Shared/Toolkit/XML/Fuzi.swift @@ -5,24 +5,24 @@ // import Foundation -import Fuzi +import ReadiumFuzi final class FuziXMLDocument: XMLDocument, Loggable { enum ParseError: Error { case notAnXML } - fileprivate let document: Fuzi.XMLDocument + fileprivate let document: ReadiumFuzi.XMLDocument convenience init(data: Data, namespaces: [XMLNamespace]) throws { - try self.init(document: Fuzi.XMLDocument(data: data), namespaces: namespaces) + try self.init(document: ReadiumFuzi.XMLDocument(data: data), namespaces: namespaces) } convenience init(string: String, namespaces: [XMLNamespace]) throws { - try self.init(document: Fuzi.XMLDocument(string: string), namespaces: namespaces) + try self.init(document: ReadiumFuzi.XMLDocument(string: string), namespaces: namespaces) } - init(document: Fuzi.XMLDocument, namespaces: [XMLNamespace]) throws { + init(document: ReadiumFuzi.XMLDocument, namespaces: [XMLNamespace]) throws { guard document.root != nil else { throw ParseError.notAnXML } @@ -50,10 +50,10 @@ final class FuziXMLDocument: XMLDocument, Loggable { } final class FuziXMLElement: XMLElement, Loggable { - fileprivate let document: Fuzi.XMLDocument - fileprivate let element: Fuzi.XMLElement + fileprivate let document: ReadiumFuzi.XMLDocument + fileprivate let element: ReadiumFuzi.XMLElement - fileprivate init(document: Fuzi.XMLDocument, element: Fuzi.XMLElement) { + fileprivate init(document: ReadiumFuzi.XMLDocument, element: ReadiumFuzi.XMLElement) { self.document = document self.element = element } @@ -85,7 +85,7 @@ final class FuziXMLElement: XMLElement, Loggable { } } -private extension Fuzi.XMLDocument { +private extension ReadiumFuzi.XMLDocument { func definePrefixes(_ namespaces: [XMLNamespace]) { for namespace in namespaces { definePrefix(namespace.prefix, forNamespace: namespace.uri) diff --git a/Sources/Shared/Toolkit/ZIP/Minizip.swift b/Sources/Shared/Toolkit/ZIP/Minizip.swift deleted file mode 100644 index c3a7201b9..000000000 --- a/Sources/Shared/Toolkit/ZIP/Minizip.swift +++ /dev/null @@ -1,373 +0,0 @@ -// -// Copyright 2024 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 Minizip - -final class MinizipArchiveOpener: ArchiveOpener { - func open(resource: any Resource, format: Format) async -> Result { - guard - format.conformsTo(.zip), - let file = resource.sourceURL?.fileURL - else { - return .failure(.formatNotSupported(format)) - } - - return await MinizipContainer.make(file: file) - .mapError { - switch $0 { - case .notAZIP: - return .formatNotSupported(format) - case let .reading(error): - return .reading(error) - } - } - .map { ContainerAsset(container: $0, format: format) } - } - - func sniffOpen(resource: any Resource) async -> Result { - guard let file = resource.sourceURL?.fileURL else { - return .failure(.formatNotRecognized) - } - - return await MinizipContainer.make(file: file) - .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" - ) - ) - } - } -} - -/// A ZIP ``Container`` using the Minizip library. -final class MinizipContainer: Container, Loggable { - enum MakeError: Error { - case notAZIP - case reading(ReadError) - } - - static func make(file: FileURL) async -> Result { - guard await (try? file.exists()) ?? false else { - return .failure(.reading(.access(.fileSystem(.fileNotFound(nil))))) - } - guard let zipFile = MinizipFile(url: file.url) else { - return .failure(.notAZIP) - } - defer { try? zipFile.close() } - - do { - var entries = [RelativeURL: MinizipEntryMetadata]() - - try zipFile.goToFirstEntry() - repeat { - switch try zipFile.entryMetadataAtCurrentOffset() { - case let .file(path, length: length, compressedLength: compressedLength): - if let url = RelativeURL(path: path) { - entries[url] = MinizipEntryMetadata(length: length, compressedLength: compressedLength) - } - case .directory: - // Directories are ignored - break - } - } while try zipFile.goToNextEntry() - - return .success(Self(file: file, entries: entries)) - - } catch { - return .failure(.reading(.decoding(error))) - } - } - - private let file: FileURL - private let entriesMetadata: [RelativeURL: MinizipEntryMetadata] - - public var sourceURL: AbsoluteURL? { file } - public let entries: Set - - private init(file: FileURL, entries: [RelativeURL: MinizipEntryMetadata]) { - self.file = file - entriesMetadata = entries - self.entries = Set(entries.keys.map(\.anyURL)) - } - - subscript(url: any URLConvertible) -> (any Resource)? { - guard - let url = url.relativeURL, - let metadata = entriesMetadata[url] - else { - return nil - } - return MinizipResource(file: file, entryPath: url.path, metadata: metadata) - } -} - -private struct MinizipEntryMetadata { - let length: UInt64 - let compressedLength: UInt64? -} - -private actor MinizipResource: Resource, Loggable { - private let file: FileURL - private let entryPath: String - private let metadata: MinizipEntryMetadata - - init(file: FileURL, entryPath: String, metadata: MinizipEntryMetadata) { - self.file = file - self.entryPath = entryPath - self.metadata = metadata - } - - nonisolated func close() { - Task { await doClose() } - } - - func doClose() async { - do { - try _zipFile?.getOrNil()?.close() - _zipFile = .failure(.unsupportedOperation(DebugError("The Minizip resource is already closed"))) - } catch { - log(.error, error) - } - } - - public let sourceURL: AbsoluteURL? = nil - - func estimatedLength() async -> ReadResult { - .success(metadata.length) - } - - func properties() async -> ReadResult { - .success(ResourceProperties { - $0.filename = RelativeURL(path: entryPath)?.lastPathSegment - $0.archive = ArchiveProperties( - entryLength: metadata.compressedLength ?? metadata.length, - isEntryCompressed: metadata.compressedLength != nil - ) - }) - } - - func stream(range: Range?, consume: @escaping (Data) -> Void) async -> ReadResult { - let range = range ?? 0 ..< metadata.length - - return await zipFile().flatMap { zipFile in - do { - try zipFile.openEntry(at: entryPath, offset: range.lowerBound) - try consume(zipFile.readFromCurrentOffset(length: UInt64(range.count))) - return .success(()) - } catch { - return .failure(.decoding(error)) - } - } - } - - private var _zipFile: ReadResult? - private func zipFile() async -> ReadResult { - if _zipFile == nil { - if let zipFile = MinizipFile(url: file.url) { - _zipFile = .success(zipFile) - } else { - _zipFile = .failure(.decoding("Failed to open the ZIP file with Minizip")) - } - } - return _zipFile! - } -} - -/// Holds an opened Minizip file and provide a bridge to its C++ API. -private final class MinizipFile { - enum MinizipError: Error { - case status(Int32) - case noEntryOpened - case readFailed - } - - // Holds an entry's metadata. - enum Entry { - case file(String, length: UInt64, compressedLength: UInt64?) - case directory(String) - } - - private let file: unzFile - private var isClosed = false - /// Information about the currently opened entry. - private(set) var openedEntry: (path: String, offset: UInt64)? - /// Length of the buffer used when reading an entry's data. - private var bufferLength: Int { 1024 * 32 } - - init?(url: URL) { - guard let file = unzOpen64(url.path) else { - return nil - } - self.file = file - } - - deinit { - try? close() - } - - func close() throws { - guard !isClosed else { - return - } - try closeEntry() - try execute { unzClose(file) } - isClosed = true - } - - /// Moves offset to the first entry in the archive. - func goToFirstEntry() throws { - try closeEntry() - try execute { unzGoToFirstFile(file) } - } - - /// Moves offset to the next entry in the archive. - /// - /// - Returns: `false` when reaching the end of the archive. - func goToNextEntry() throws -> Bool { - try closeEntry() - - let status = unzGoToNextFile(file) - switch status { - case UNZ_END_OF_LIST_OF_FILE: - return false - case UNZ_OK: - return true - default: - throw MinizipError.status(status) - } - } - - /// Moves the offset to the entry at `path`. - func goToEntry(at path: String) throws { - try closeEntry() - try execute { unzLocateFile(file, path.removingPrefix("/"), nil) } - } - - /// Reads the metadata of the entry at the current offset in the archive. - func entryMetadataAtCurrentOffset() throws -> Entry { - let filenameMaxSize = 1024 - var fileInfo = unz_file_info64() - let filename = UnsafeMutablePointer.allocate(capacity: filenameMaxSize) - defer { - free(filename) - } - memset(&fileInfo, 0, MemoryLayout.size) - try execute { unzGetCurrentFileInfo64(file, &fileInfo, filename, UInt(filenameMaxSize), nil, 0, nil, 0) } - let path = String(cString: filename) - - if path.hasSuffix("/") { - return .directory(path) - } else { - let isCompressed = (fileInfo.compression_method != 0) - return .file(path, - length: UInt64(fileInfo.uncompressed_size), - compressedLength: isCompressed ? UInt64(fileInfo.compressed_size) : nil) - } - } - - /// Opens the entry at the given `path` and file `offset`, to read its content. - func openEntry(at path: String, offset: UInt64 = 0) throws { - if let entry = openedEntry, entry.path == path, entry.offset <= offset { - if entry.offset < offset { - try seek(by: offset - entry.offset) - } - - } else { - try goToEntry(at: path) - try execute { unzOpenCurrentFile(file) } - openedEntry = (path: path, offset: 0) - try seek(by: offset) - } - } - - /// Closes the current entry if it is opened. - func closeEntry() throws { - guard openedEntry != nil else { - return - } - openedEntry = nil - try execute { unzCloseCurrentFile(file) } - } - - /// Advances the current position in the archive by the given `offset`. - func seek(by offset: UInt64) throws { - // 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. - try readFromCurrentOffset(length: offset) { _, _ in } - - // For non-compressed entries, we can seek directly in the content. - // FIXME: https://github.com/readium/r2-shared-swift/issues/98 -// return execute { return unzseek64(archive, offset, SEEK_CUR) } - } - - /// Reads the given `length` of data at the current offset in the archive. - func readFromCurrentOffset(length: UInt64) throws -> Data { - guard length > 0 else { - return Data() - } - - var data = Data(capacity: Int(length)) - try readFromCurrentOffset(length: length) { bytes, length in - data.append(bytes, count: Int(length)) - } - return data - } - - typealias Consumer = (_ bytes: UnsafePointer, _ length: UInt64) -> Void - - /// Consumes the given `length` of data at the current offset in the archive. - func readFromCurrentOffset(length: UInt64, consumer: Consumer) throws { - guard var entry = openedEntry else { - throw MinizipError.noEntryOpened - } - guard length > 0 else { - return - } - - var totalBytesRead: UInt64 = 0 - defer { - entry.offset += totalBytesRead - openedEntry = entry - } - - while totalBytesRead < length { - let bytesToRead = min(UInt64(bufferLength), length - totalBytesRead) - var buffer = [CUnsignedChar](repeating: 0, count: Int(bytesToRead)) - let bytesRead = UInt64(unzReadCurrentFile(file, &buffer, UInt32(bytesToRead))) - if bytesRead == 0 { - break - } - if bytesRead > 0 { - totalBytesRead += bytesRead - consumer(buffer, bytesRead) - } else { - throw MinizipError.readFailed - } - } - } - - /// Executes a Minizip statement and checks its status. - private func execute(_ block: () -> Int32) throws { - let status = block() - guard status == UNZ_OK else { - throw MinizipError.status(status) - } - } -} diff --git a/Sources/Shared/Toolkit/ZIP/ZIPArchiveOpener.swift b/Sources/Shared/Toolkit/ZIP/ZIPArchiveOpener.swift index beb7638f6..a2e929773 100644 --- a/Sources/Shared/Toolkit/ZIP/ZIPArchiveOpener.swift +++ b/Sources/Shared/Toolkit/ZIP/ZIPArchiveOpener.swift @@ -8,15 +8,17 @@ import Foundation /// An ``ArchiveOpener`` for ZIP resources. public class ZIPArchiveOpener: ArchiveOpener { - private let opener = MinizipArchiveOpener() +// private let opener = MinizipArchiveOpener() public init() {} public func open(resource: any Resource, format: Format) async -> Result { - await opener.open(resource: resource, format: format) + return .failure(.formatNotSupported(format)) +// await opener.open(resource: resource, format: format) } public func sniffOpen(resource: any Resource) async -> Result { - await opener.sniffOpen(resource: resource) + return .failure(.formatNotRecognized) +// await opener.sniffOpen(resource: resource) } } diff --git a/Sources/Shared/Toolkit/ZIP/ZIPFoundation.swift b/Sources/Shared/Toolkit/ZIP/ZIPFoundation.swift deleted file mode 100644 index cfb68a6cb..000000000 --- a/Sources/Shared/Toolkit/ZIP/ZIPFoundation.swift +++ /dev/null @@ -1,138 +0,0 @@ -// -// Copyright 2024 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 ZIPFoundation - -/// A ZIP `Archive` using the ZIPFoundation library. -/// -/// Note: At the moment, the Minizip version is used. Keeping this in case we migrate to -/// ZIPFoundation. -final class ZIPFoundationArchive: Archive, Loggable { - fileprivate let archive: ZIPFoundation.Archive - - // Note: passwords are not supported with ZIPFoundation - required convenience init(file: URL, password: String?) throws { - try self.init(file: file, accessMode: .read) - } - - fileprivate init(file: URL, accessMode: Archive.AccessMode) throws { - guard let archive = Archive(url: file, accessMode: accessMode) else { - throw ArchiveError.openFailed - } - self.archive = archive - } - - lazy var entries: [ArchiveEntry] = archive.map(ArchiveEntry.init) - - func entry(at path: String) -> ArchiveEntry? { - archive[path] - .filter { $0.type != .directory } - .map(ArchiveEntry.init) - } - - func read(at path: String) -> Data? { - objc_sync_enter(archive) - defer { objc_sync_exit(archive) } - - guard let entry = archive[path] else { - return nil - } - - do { - var data = Data() - _ = try archive.extract(entry) { chunk in - data.append(chunk) - } - return data - } catch { - log(.error, error) - return nil - } - } - - func read(at path: String, range: Range) -> Data? { - objc_sync_enter(archive) - defer { objc_sync_exit(archive) } - - guard let entry = archive[path] else { - return nil - } - - let rangeLength = range.upperBound - range.lowerBound - var data = Data() - - do { - var offset = 0 - let progress = Progress() - - _ = try archive.extract(entry, progress: progress) { chunk in - let chunkLength = chunk.count - defer { - offset += chunkLength - if offset >= range.upperBound { - progress.cancel() - } - } - - guard offset < range.upperBound, offset + chunkLength >= range.lowerBound else { - return - } - - let startingIndex = (range.lowerBound > offset) - ? (range.lowerBound - offset) - : 0 - data.append(chunk[startingIndex...]) - } - } catch { - switch error { - case Archive.ArchiveError.cancelledOperation: - break - default: - log(.error, error) - return nil - } - } - - return data[0 ..< rangeLength] - } -} - -final class MutableZIPFoundationArchive: ZIPFoundationArchive, MutableArchive { - required convenience init(file: URL, password: String?) throws { - try self.init(file: file, accessMode: .update) - } - - func replace(at path: String, with data: Data, deflated: Bool) throws { - objc_sync_enter(archive) - defer { objc_sync_exit(archive) } - - do { - // Removes the old entry if it already exists in the archive, otherwise we get - // duplicated entries - if let entry = archive[path] { - try archive.remove(entry) - } - - try archive.addEntry(with: path, type: .file, uncompressedSize: UInt32(data.count), compressionMethod: deflated ? .deflate : .none, provider: { position, size in - data[position ..< size] - }) - } catch { - log(.error, error) - throw ArchiveError.updateFailed - } - } -} - -private extension ArchiveEntry { - init(entry: Entry) { - self.init( - path: entry.path, - length: entry.uncompressedSize, - compressedLength: entry.compressedSize - ) - } -} diff --git a/Sources/Streamer/Parser/EPUB/EPUBContainerParser.swift b/Sources/Streamer/Parser/EPUB/EPUBContainerParser.swift index cc4906fa8..00f8ee52c 100644 --- a/Sources/Streamer/Parser/EPUB/EPUBContainerParser.swift +++ b/Sources/Streamer/Parser/EPUB/EPUBContainerParser.swift @@ -5,12 +5,12 @@ // import Foundation -import Fuzi +import ReadiumFuzi import ReadiumShared /// A parser for the META-INF/container.xml file. final class EPUBContainerParser: Loggable { - private let document: Fuzi.XMLDocument + private let document: ReadiumFuzi.XMLDocument init(data: Data) throws { document = try XMLDocument(data: data) diff --git a/Sources/Streamer/Parser/EPUB/EPUBEncryptionParser.swift b/Sources/Streamer/Parser/EPUB/EPUBEncryptionParser.swift index c514394fe..0310ac897 100644 --- a/Sources/Streamer/Parser/EPUB/EPUBEncryptionParser.swift +++ b/Sources/Streamer/Parser/EPUB/EPUBEncryptionParser.swift @@ -5,7 +5,7 @@ // import Foundation -import Fuzi +import ReadiumFuzi import ReadiumShared /// A parser module which provide methods to parse encrypted XML elements. @@ -26,8 +26,8 @@ final class EPUBEncryptionParser: Loggable { self.init(container: container, data: data) } - private lazy var document: Fuzi.XMLDocument? = { - let document = try? Fuzi.XMLDocument(data: data) + private lazy var document: ReadiumFuzi.XMLDocument? = { + let document = try? ReadiumFuzi.XMLDocument(data: data) document?.definePrefix("enc", forNamespace: "http://www.w3.org/2001/04/xmlenc#") document?.definePrefix("ds", forNamespace: "http://www.w3.org/2000/09/xmldsig#") document?.definePrefix("comp", forNamespace: "http://www.idpf.org/2016/encryption#compression") diff --git a/Sources/Streamer/Parser/EPUB/EPUBMetadataParser.swift b/Sources/Streamer/Parser/EPUB/EPUBMetadataParser.swift index 255a21a43..cc43f2bd1 100644 --- a/Sources/Streamer/Parser/EPUB/EPUBMetadataParser.swift +++ b/Sources/Streamer/Parser/EPUB/EPUBMetadataParser.swift @@ -5,16 +5,16 @@ // import Foundation -import Fuzi +import ReadiumFuzi import ReadiumShared /// Reference: https://github.com/readium/architecture/blob/master/streamer/parser/metadata.md final class EPUBMetadataParser: Loggable { - private let document: Fuzi.XMLDocument - private let displayOptions: Fuzi.XMLDocument? + private let document: ReadiumFuzi.XMLDocument + private let displayOptions: ReadiumFuzi.XMLDocument? private let metas: OPFMetaList - init(document: Fuzi.XMLDocument, displayOptions: Fuzi.XMLDocument?, metas: OPFMetaList) { + init(document: ReadiumFuzi.XMLDocument, displayOptions: ReadiumFuzi.XMLDocument?, metas: OPFMetaList) { self.document = document self.displayOptions = displayOptions self.metas = metas @@ -25,7 +25,7 @@ final class EPUBMetadataParser: Loggable { document.definePrefix("rendition", forNamespace: "http://www.idpf.org/2013/rendition") } - private lazy var metadataElement: Fuzi.XMLElement? = document.firstChild(xpath: "/opf:package/opf:metadata") + private lazy var metadataElement: ReadiumFuzi.XMLElement? = document.firstChild(xpath: "/opf:package/opf:metadata") /// Parses the Metadata in the XML element. func parse() throws -> Metadata { @@ -73,7 +73,7 @@ final class EPUBMetadataParser: Loggable { /// 1. its xml:lang attribute /// 2. the package's xml:lang attribute /// 3. the primary language for the publication - private func language(for element: Fuzi.XMLElement) -> String? { + private func language(for element: ReadiumFuzi.XMLElement) -> String? { element.attr("lang") ?? packageLanguage ?? languages.first } @@ -142,7 +142,7 @@ final class EPUBMetadataParser: Loggable { /// Finds all the ` element matching the given `title-type`. /// The elements are then sorted by the `display-seq` refines, when available. - private func titleElements(ofType titleType: EPUBTitleType) -> [Fuzi.XMLElement] { + private func titleElements(ofType titleType: EPUBTitleType) -> [ReadiumFuzi.XMLElement] { // Finds the XML element corresponding to the specific title type // `titleType` metas["title-type"] @@ -173,7 +173,7 @@ final class EPUBMetadataParser: Loggable { } }() - private lazy var mainTitleElement: Fuzi.XMLElement? = titleElements(ofType: .main).first + private lazy var mainTitleElement: ReadiumFuzi.XMLElement? = titleElements(ofType: .main).first ?? metas["title", in: .dcterms].first?.element private lazy var mainTitle: LocalizedString? = localizedString(for: mainTitleElement) @@ -432,7 +432,7 @@ final class EPUBMetadataParser: Loggable { /// /// - Parameter metadata: The XML metadata element. /// - Returns: The array of XML element representing the contributors. - private func findContributorElements() -> [Fuzi.XMLElement] { + private func findContributorElements() -> [ReadiumFuzi.XMLElement] { let contributors = metas["creator", in: .dcterms] + metas["publisher", in: .dcterms] + metas["contributor", in: .dcterms] @@ -446,7 +446,7 @@ final class EPUBMetadataParser: Loggable { /// - Parameters: /// - element: The XML element reprensenting the contributor. /// - Returns: The newly created Contributor instance. - private func createContributor(from element: Fuzi.XMLElement, roles: [String] = []) -> Contributor? { + private func createContributor(from element: ReadiumFuzi.XMLElement, roles: [String] = []) -> Contributor? { guard let name = localizedString(for: element) else { return nil } @@ -531,7 +531,7 @@ final class EPUBMetadataParser: Loggable { /// /// - Parameters: /// - element: The element to parse (can be a title or a contributor). - private func localizedString(for element: Fuzi.XMLElement?) -> LocalizedString? { + private func localizedString(for element: ReadiumFuzi.XMLElement?) -> LocalizedString? { guard let element = element else { return nil } @@ -568,7 +568,7 @@ final class EPUBMetadataParser: Loggable { /// Returns the given `dc:` tag in the `metadata` element. /// /// This looks under `metadata/dc-metadata` as well, to be compatible with old EPUB 2 files. - private func dcElement(tag: String) -> Fuzi.XMLElement? { + private func dcElement(tag: String) -> ReadiumFuzi.XMLElement? { metadataElement? .firstChild(xpath: "(.|opf:dc-metadata)/dc:\(tag)") } diff --git a/Sources/Streamer/Parser/EPUB/EPUBParser.swift b/Sources/Streamer/Parser/EPUB/EPUBParser.swift index f38020d5e..aa893ba96 100644 --- a/Sources/Streamer/Parser/EPUB/EPUBParser.swift +++ b/Sources/Streamer/Parser/EPUB/EPUBParser.swift @@ -5,7 +5,7 @@ // import Foundation -import Fuzi +import ReadiumFuzi import ReadiumShared /// Epub related constants. diff --git a/Sources/Streamer/Parser/EPUB/NCXParser.swift b/Sources/Streamer/Parser/EPUB/NCXParser.swift index 20533ea24..465f3f805 100644 --- a/Sources/Streamer/Parser/EPUB/NCXParser.swift +++ b/Sources/Streamer/Parser/EPUB/NCXParser.swift @@ -5,7 +5,7 @@ // import Foundation -import Fuzi +import ReadiumFuzi import ReadiumShared /// From IDPF a11y-guidelines content/nav/toc.html : @@ -29,7 +29,7 @@ final class NCXParser { self.url = url } - private lazy var document: Fuzi.XMLDocument? = { + private lazy var document: ReadiumFuzi.XMLDocument? = { let document = try? XMLDocument(data: data) document?.definePrefix("ncx", forNamespace: "http://www.daisy.org/z3986/2005/ncx/") return document @@ -55,13 +55,13 @@ final class NCXParser { } /// Parses recursively a list of nodes as a list of `Link`. - private func links(in element: Fuzi.XMLElement, nodeTagName: String) -> [Link] { + private func links(in element: ReadiumFuzi.XMLElement, nodeTagName: String) -> [Link] { element.xpath("ncx:\(nodeTagName)") .compactMap { self.link(for: $0, nodeTagName: nodeTagName) } } /// Parses a node element as a `Link`. - private func link(for element: Fuzi.XMLElement, nodeTagName: String) -> Link? { + private func link(for element: ReadiumFuzi.XMLElement, nodeTagName: String) -> Link? { NavigationDocumentParser.makeLink( title: element.firstChild(xpath: "ncx:navLabel/ncx:text")?.stringValue, href: element.firstChild(xpath: "ncx:content")?.attr("src").flatMap(RelativeURL.init(epubHREF:)), diff --git a/Sources/Streamer/Parser/EPUB/NavigationDocumentParser.swift b/Sources/Streamer/Parser/EPUB/NavigationDocumentParser.swift index d2d2f2562..2521c841f 100644 --- a/Sources/Streamer/Parser/EPUB/NavigationDocumentParser.swift +++ b/Sources/Streamer/Parser/EPUB/NavigationDocumentParser.swift @@ -5,7 +5,7 @@ // import Foundation -import Fuzi +import ReadiumFuzi import ReadiumShared /// The navigation document if documented here at Navigation @@ -31,9 +31,9 @@ final class NavigationDocumentParser { self.url = url } - private lazy var document: Fuzi.XMLDocument? = { + private lazy var document: ReadiumFuzi.XMLDocument? = { // Warning: Somehow if we use HTMLDocument instead of XMLDocument, then the `epub` prefix doesn't work. - let document = try? Fuzi.XMLDocument(data: data) + let document = try? ReadiumFuzi.XMLDocument(data: data) document?.definePrefix("html", forNamespace: "http://www.w3.org/1999/xhtml") document?.definePrefix("epub", forNamespace: "http://www.idpf.org/2007/ops") return document @@ -52,13 +52,13 @@ final class NavigationDocumentParser { } /// Parses recursively an
    as a list of `Link`. - private func links(in element: Fuzi.XMLElement) -> [Link] { + private func links(in element: ReadiumFuzi.XMLElement) -> [Link] { element.xpath("html:ol[1]/html:li") .compactMap { self.link(for: $0) } } /// Parses a
  1. element as a `Link`. - private func link(for li: Fuzi.XMLElement) -> Link? { + private func link(for li: ReadiumFuzi.XMLElement) -> Link? { guard let label = li.firstChild(xpath: "html:a|html:span") else { return nil } diff --git a/Sources/Streamer/Parser/EPUB/OPFMeta.swift b/Sources/Streamer/Parser/EPUB/OPFMeta.swift index ca0394ca6..2de40bc4a 100644 --- a/Sources/Streamer/Parser/EPUB/OPFMeta.swift +++ b/Sources/Streamer/Parser/EPUB/OPFMeta.swift @@ -5,7 +5,7 @@ // import Foundation -import Fuzi +import ReadiumFuzi import ReadiumShared /// Package vocabularies used for `property`, `properties`, `scheme` and `rel`. @@ -96,7 +96,7 @@ enum OPFVocabulary: String { /// > Reserved prefixes should not be overridden in the prefix attribute, but Reading Systems /// > must use such local overrides when encountered. /// http://www.idpf.org/epub/301/spec/epub-publications.html#sec-metadata-reserved-vocabs - static func prefixes(in document: Fuzi.XMLDocument) -> [String: String] { + static func prefixes(in document: ReadiumFuzi.XMLDocument) -> [String: String] { document.definePrefix("opf", forNamespace: "http://www.idpf.org/2007/opf") guard let prefixAttribute = document.firstChild(xpath: "/opf:package")?.attr("prefix") else { return [:] @@ -128,7 +128,7 @@ struct OPFMeta { let id: String? /// ID of the metadata that is refined by this one, if any. let refines: String? - let element: Fuzi.XMLElement + let element: ReadiumFuzi.XMLElement } /// Represents a `link` tag in an OPF document. @@ -139,15 +139,15 @@ struct OPFLink { let href: String /// ID of the metadata that is refined by this one, if any. let refines: String? - let element: Fuzi.XMLElement + let element: ReadiumFuzi.XMLElement } struct OPFMetaList { - private let document: Fuzi.XMLDocument + private let document: ReadiumFuzi.XMLDocument private let metas: [OPFMeta] private let links: [OPFLink] - init(document: Fuzi.XMLDocument) { + init(document: ReadiumFuzi.XMLDocument) { self.document = document let prefixes = OPFVocabulary.prefixes(in: document) document.definePrefix("opf", forNamespace: "http://www.idpf.org/2007/opf") diff --git a/Sources/Streamer/Parser/EPUB/OPFParser.swift b/Sources/Streamer/Parser/EPUB/OPFParser.swift index a0f2055ab..1157b0e59 100644 --- a/Sources/Streamer/Parser/EPUB/OPFParser.swift +++ b/Sources/Streamer/Parser/EPUB/OPFParser.swift @@ -5,7 +5,7 @@ // import Foundation -import Fuzi +import ReadiumFuzi import ReadiumShared // http://www.idpf.org/epub/30/spec/epub30-publications.html#title-type @@ -33,11 +33,11 @@ final class OPFParser: Loggable { private let baseURL: RelativeURL /// DOM representation of the OPF file. - private let document: Fuzi.XMLDocument + private let document: ReadiumFuzi.XMLDocument /// iBooks Display Options XML file to use as a fallback for metadata. /// See https://github.com/readium/architecture/blob/master/streamer/parser/metadata.md#epub-2x-9 - private let displayOptions: Fuzi.XMLDocument? + private let displayOptions: ReadiumFuzi.XMLDocument? /// List of metadata declared in the package. private let metas: OPFMetaList @@ -47,9 +47,9 @@ final class OPFParser: Loggable { init(baseURL: RelativeURL, data: Data, displayOptionsData: Data? = nil, encryptions: [RelativeURL: Encryption]) throws { self.baseURL = baseURL - document = try Fuzi.XMLDocument(data: data) + document = try ReadiumFuzi.XMLDocument(data: data) document.definePrefix("opf", forNamespace: "http://www.idpf.org/2007/opf") - displayOptions = (displayOptionsData.map { try? Fuzi.XMLDocument(data: $0) }) ?? nil + displayOptions = (displayOptionsData.map { try? ReadiumFuzi.XMLDocument(data: $0) }) ?? nil metas = OPFMetaList(document: document) self.encryptions = encryptions } @@ -159,7 +159,7 @@ final class OPFParser: Loggable { return (resources, readingOrder) } - private func makeLink(manifestItem: Fuzi.XMLElement, spineItem: Fuzi.XMLElement?, isCover: Bool) -> Link? { + private func makeLink(manifestItem: ReadiumFuzi.XMLElement, spineItem: ReadiumFuzi.XMLElement?, isCover: Bool) -> Link? { guard let relativeHref = manifestItem.attr("href").flatMap(RelativeURL.init(epubHREF:)), let href = baseURL.resolve(relativeHref) diff --git a/Sources/Streamer/Parser/EPUB/Services/EPUBPositionsService.swift b/Sources/Streamer/Parser/EPUB/Services/EPUBPositionsService.swift index e4a216461..dc7c7c72d 100644 --- a/Sources/Streamer/Parser/EPUB/Services/EPUBPositionsService.swift +++ b/Sources/Streamer/Parser/EPUB/Services/EPUBPositionsService.swift @@ -86,7 +86,7 @@ public actor EPUBPositionsService: PositionsService { private func computePositionsByReadingOrder() async -> [[Locator]] { var lastPositionOfPreviousResource = 0 - var positions = await readingOrder.asyncmap { link -> [Locator] in + var positions = await readingOrder.asyncMap { link -> [Locator] in let (lastPosition, positions): (Int, [Locator]) = await { if presentation.layout(of: link) == .fixed { return makePositions(ofFixedResource: link, from: lastPositionOfPreviousResource) @@ -99,7 +99,7 @@ public actor EPUBPositionsService: PositionsService { } // Calculates totalProgression - let totalPageCount = await positions.asyncmap(\.count).reduce(0, +) + let totalPageCount = await positions.asyncMap(\.count).reduce(0, +) if totalPageCount > 0 { positions = positions.map { locators in locators.map { locator in diff --git a/Sources/Streamer/Parser/PDF/Services/LCPDFPositionsService.swift b/Sources/Streamer/Parser/PDF/Services/LCPDFPositionsService.swift index 64b3e9465..e39dff254 100644 --- a/Sources/Streamer/Parser/PDF/Services/LCPDFPositionsService.swift +++ b/Sources/Streamer/Parser/PDF/Services/LCPDFPositionsService.swift @@ -27,7 +27,7 @@ final class LCPDFPositionsService: PositionsService, PDFPublicationService, Logg private func computePositionsByReadingOrder() async -> ReadResult<[[Locator]]> { // Calculates the page count of each resource from the reading order. - let resources = await readingOrder.asyncmap { link -> (Int, Link) in + let resources = await readingOrder.asyncMap { link -> (Int, Link) in let href = link.url() guard let resource = container[href], diff --git a/Support/Carthage/.xcodegen b/Support/Carthage/.xcodegen index 7c32cfc16..81ad98c3c 100644 --- a/Support/Carthage/.xcodegen +++ b/Support/Carthage/.xcodegen @@ -145,7 +145,7 @@ "framework" : "..\/..\/Carthage\/Build\/Minizip.xcframework" }, { - "framework" : "..\/..\/Carthage\/Build\/ZIPFoundation.xcframework" + "framework" : "..\/..\/Carthage\/Build\/ReadiumZIPFoundation.xcframework" }, { "target" : "ReadiumShared" @@ -263,9 +263,6 @@ }, "sources" : [ { - "excludes" : [ - "Toolkit\/ZIP\/ZIPFoundation.swift" - ], "path" : "..\/..\/Sources\/Shared" } ], @@ -13979,7 +13976,6 @@ ../../Sources/Shared/Toolkit/ZIP ../../Sources/Shared/Toolkit/ZIP/Minizip.swift ../../Sources/Shared/Toolkit/ZIP/ZIPArchiveOpener.swift -../../Sources/Shared/Toolkit/ZIP/ZIPFoundation.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 0915c11fc..6fa0ef34e 100644 --- a/Support/Carthage/Readium.xcodeproj/project.pbxproj +++ b/Support/Carthage/Readium.xcodeproj/project.pbxproj @@ -135,7 +135,6 @@ 5730E84475195005D1291672 /* Publication.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DF03272C07D6951ADC1311E /* Publication.swift */; }; 57583D27AB12063C3D114A47 /* AudioParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9FFEB1FF4B5CD74EB35CD63 /* AudioParser.swift */; }; 58E8C178E0748B7CBEA9D7AC /* CSSLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = C51A36BFDC79EB5377D69582 /* CSSLayout.swift */; }; - 5903F431F5558958EF7CDDA0 /* ZIPFoundation.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8D187A577EBFCFF738D1CDC7 /* ZIPFoundation.xcframework */; }; 5912EC9BB073282862F325F2 /* DocumentTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A00FF0C84822A134A353BD4 /* DocumentTypes.swift */; }; 594CE84C2B11169AA0B86615 /* HTMLResourceContentIterator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34AB954525AC159166C96A36 /* HTMLResourceContentIterator.swift */; }; 5A9AEC4A9AE686ED74E9B8BF /* RWPMFormatSniffer.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFE4559CE100932572C843E5 /* RWPMFormatSniffer.swift */; }; @@ -293,6 +292,7 @@ BD520A603D8351461485877F /* ComicFormatSniffer.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC6271FA7F282364A4E68C4C /* ComicFormatSniffer.swift */; }; BE754F8C0536C8B7D28A294C /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 5507BD4012032A7567175B69 /* Localizable.strings */; }; BF0CC6F4EE0F18B52D2D8693 /* Properties+Encryption.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E175BF1A1F97687B4119BB1 /* Properties+Encryption.swift */; }; + C0A492BDDCB4A472F408B3B4 /* ReadiumZIPFoundation.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 69E17C4870C64264819EB227 /* ReadiumZIPFoundation.xcframework */; }; C122D3F719E77AEFA36E3034 /* RPFFormatSniffer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9ACA9031C70DAB05CE0DD463 /* RPFFormatSniffer.swift */; }; C1A94B2A9C446CB03650DC47 /* NCXParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4363E8A92B1EA9AF2561DCE9 /* NCXParser.swift */; }; C1CD3DA1A9EF154667E5B50B /* WebServerResourceResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = D008F7BB187AE82CBB115D0F /* WebServerResourceResponse.swift */; }; @@ -638,6 +638,7 @@ 68FF131876FA3A63025F2662 /* Language.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Language.swift; sourceTree = ""; }; 691C96D23D42A0C6AC03B1AE /* FileAsset.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileAsset.swift; sourceTree = ""; }; 69212974E62EF509BC1F0C7A /* UnknownAbsoluteURL.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnknownAbsoluteURL.swift; sourceTree = ""; }; + 69E17C4870C64264819EB227 /* ReadiumZIPFoundation.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = ReadiumZIPFoundation.xcframework; path = ../../Carthage/Build/ReadiumZIPFoundation.xcframework; sourceTree = ""; }; 6BC71BAFF7A20D7903E6EE4D /* Properties+EPUB.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Properties+EPUB.swift"; sourceTree = ""; }; 6D80848AADD20D4384D9AF59 /* HTMLFontFamilyDeclaration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTMLFontFamilyDeclaration.swift; sourceTree = ""; }; 6FA3AA272772FCF7D6268A74 /* FileURL.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileURL.swift; sourceTree = ""; }; @@ -681,7 +682,6 @@ 8B6A5B12925813FB40C41034 /* Presentation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Presentation.swift; sourceTree = ""; }; 8B72B76AB39E09E4A2E465AF /* PDFFormatSniffer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PDFFormatSniffer.swift; sourceTree = ""; }; 8BF64D7C05A790D9CA5DD442 /* Types.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Types.swift; sourceTree = ""; }; - 8D187A577EBFCFF738D1CDC7 /* ZIPFoundation.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = ZIPFoundation.xcframework; path = ../../Carthage/Build/ZIPFoundation.xcframework; sourceTree = ""; }; 8DA31089FCAD8DFB9AC46E4E /* Tokenizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tokenizer.swift; sourceTree = ""; }; 8EA0008AF1B9B97962824D85 /* FallbackContentProtection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FallbackContentProtection.swift; sourceTree = ""; }; 91F34B9B08BC6FB84CE54A26 /* LCPProgress.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LCPProgress.swift; sourceTree = ""; }; @@ -878,7 +878,7 @@ 26CC9CB6CA4D81EB60AE860C /* CryptoSwift.xcframework in Frameworks */, DAA4EA6A44E288F015A638D0 /* Fuzi.xcframework in Frameworks */, 90769CA2ABCBD1F7203697DC /* Minizip.xcframework in Frameworks */, - 5903F431F5558958EF7CDDA0 /* ZIPFoundation.xcframework in Frameworks */, + C0A492BDDCB4A472F408B3B4 /* ReadiumZIPFoundation.xcframework in Frameworks */, 13534618D5095F49627C4409 /* ReadiumShared.framework in Frameworks */, C3F4CBE80D741D4158CA8407 /* ReadiumInternal.framework in Frameworks */, ); @@ -1752,9 +1752,9 @@ B421601FB56132514CCD9699 /* Fuzi.xcframework */, CFFEBDFE931745C07DACD4A3 /* Minizip.xcframework */, 6536C07F5A50F7F25FDBF69C /* ReadiumGCDWebServer.xcframework */, + 69E17C4870C64264819EB227 /* ReadiumZIPFoundation.xcframework */, F07214E263C6589987A561F9 /* SQLite.xcframework */, BE09289EB0FEA5FEC8506B1F /* SwiftSoup.xcframework */, - 8D187A577EBFCFF738D1CDC7 /* ZIPFoundation.xcframework */, ); name = Frameworks; sourceTree = ""; diff --git a/Support/Carthage/project.yml b/Support/Carthage/project.yml index 5482b4011..52337b616 100644 --- a/Support/Carthage/project.yml +++ b/Support/Carthage/project.yml @@ -12,11 +12,8 @@ targets: deploymentTarget: "13.0" sources: - path: ../../Sources/Shared - excludes: - - Toolkit/ZIP/ZIPFoundation.swift dependencies: - - framework: ../../Carthage/Build/Fuzi.xcframework - - framework: ../../Carthage/Build/Minizip.xcframework + - framework: ../../Carthage/Build/ReadiumFuzi.xcframework - framework: ../../Carthage/Build/SwiftSoup.xcframework - target: ReadiumInternal - sdk: CoreServices.framework @@ -36,8 +33,7 @@ targets: type: folder dependencies: - framework: ../../Carthage/Build/CryptoSwift.xcframework - - framework: ../../Carthage/Build/Fuzi.xcframework - - framework: ../../Carthage/Build/Minizip.xcframework + - framework: ../../Carthage/Build/ReadiumFuzi.xcframework - target: ReadiumShared - target: ReadiumInternal settings: @@ -57,8 +53,7 @@ targets: type: folder dependencies: - framework: ../../Carthage/Build/DifferenceKit.xcframework - - framework: ../../Carthage/Build/Fuzi.xcframework - - framework: ../../Carthage/Build/Minizip.xcframework + - framework: ../../Carthage/Build/ReadiumFuzi.xcframework - framework: ../../Carthage/Build/SwiftSoup.xcframework - target: ReadiumShared - target: ReadiumInternal @@ -73,8 +68,7 @@ targets: sources: - path: ../../Sources/OPDS dependencies: - - framework: ../../Carthage/Build/Fuzi.xcframework - - framework: ../../Carthage/Build/Minizip.xcframework + - framework: ../../Carthage/Build/ReadiumFuzi.xcframework - target: ReadiumShared - target: ReadiumInternal settings: @@ -89,9 +83,8 @@ targets: - path: ../../Sources/LCP dependencies: - framework: ../../Carthage/Build/CryptoSwift.xcframework - - framework: ../../Carthage/Build/Fuzi.xcframework - - framework: ../../Carthage/Build/Minizip.xcframework - - framework: ../../Carthage/Build/ZIPFoundation.xcframework + - framework: ../../Carthage/Build/ReadiumFuzi.xcframework + - framework: ../../Carthage/Build/ReadiumZIPFoundation.xcframework - target: ReadiumShared - target: ReadiumInternal settings: diff --git a/Support/CocoaPods/ReadiumLCP.podspec b/Support/CocoaPods/ReadiumLCP.podspec index 41f188111..08e649dde 100644 --- a/Support/CocoaPods/ReadiumLCP.podspec +++ b/Support/CocoaPods/ReadiumLCP.podspec @@ -21,6 +21,6 @@ Pod::Spec.new do |s| s.dependency 'ReadiumShared' s.dependency 'ReadiumInternal' - s.dependency 'ZIPFoundation', '~> 0.9.0' + s.dependency 'ReadiumZIPFoundation', '~> 0.9.0' s.dependency 'CryptoSwift', '~> 1.8.0' end diff --git a/Support/CocoaPods/ReadiumNavigator.podspec b/Support/CocoaPods/ReadiumNavigator.podspec index a9b5c21c8..754c111ab 100644 --- a/Support/CocoaPods/ReadiumNavigator.podspec +++ b/Support/CocoaPods/ReadiumNavigator.podspec @@ -17,6 +17,7 @@ Pod::Spec.new do |s| s.source_files = "Sources/Navigator/**/*.{m,h,swift}" s.platform = :ios s.ios.deployment_target = "13.0" + s.dependency 'ReadiumShared' s.dependency 'ReadiumInternal' s.dependency 'DifferenceKit', '~> 1.0' diff --git a/Support/CocoaPods/ReadiumOPDS.podspec b/Support/CocoaPods/ReadiumOPDS.podspec index c57d3f3c3..3483c2cc6 100644 --- a/Support/CocoaPods/ReadiumOPDS.podspec +++ b/Support/CocoaPods/ReadiumOPDS.podspec @@ -18,6 +18,6 @@ Pod::Spec.new do |s| s.dependency 'ReadiumShared' s.dependency 'ReadiumInternal' - s.dependency 'Fuzi', '~> 3.1.0' + s.dependency 'ReadiumFuzi', '~> 4.0.0' end diff --git a/Support/CocoaPods/ReadiumShared.podspec b/Support/CocoaPods/ReadiumShared.podspec index 20065249d..484fe1672 100644 --- a/Support/CocoaPods/ReadiumShared.podspec +++ b/Support/CocoaPods/ReadiumShared.podspec @@ -7,7 +7,6 @@ Pod::Spec.new do |s| s.homepage = "http://readium.github.io" s.author = { "Readium" => "contact@readium.org" } s.source = { :git => 'https://github.com/readium/swift-toolkit.git', :branch => "develop" } - s.exclude_files = ["Sources/Shared/Toolkit/ZIP/ZIPFoundation.swift"] s.requires_arc = true s.source_files = "Sources/Shared/**/*.{m,h,swift}" s.platform = :ios @@ -16,9 +15,8 @@ Pod::Spec.new do |s| s.libraries = "xml2" s.xcconfig = { 'HEADER_SEARCH_PATHS' => '$(SDKROOT)/usr/include/libxml2' } - s.dependency 'Fuzi', '~> 3.1.0' - s.dependency 'Minizip', '~> 1.0.0' s.dependency 'SwiftSoup', '~> 2.7.0' + s.dependency 'ReadiumFuzi', '~> 4.0.0' s.dependency 'ReadiumInternal' end diff --git a/Support/CocoaPods/ReadiumStreamer.podspec b/Support/CocoaPods/ReadiumStreamer.podspec index e20c669f3..515f801d7 100644 --- a/Support/CocoaPods/ReadiumStreamer.podspec +++ b/Support/CocoaPods/ReadiumStreamer.podspec @@ -20,9 +20,9 @@ Pod::Spec.new do |s| s.libraries = 'z', 'xml2' s.xcconfig = { 'HEADER_SEARCH_PATHS' => '$(SDKROOT)/usr/include/libxml2' } + s.dependency 'ReadiumFuzi', '~> 4.0.0' s.dependency 'ReadiumShared' s.dependency 'ReadiumInternal' s.dependency 'CryptoSwift', '~> 1.8.0' - s.dependency 'Fuzi', '~> 3.1.0' end diff --git a/TestApp/Integrations/Local/project+lcp.yml b/TestApp/Integrations/Local/project+lcp.yml index 4cc5bbbba..b168510c3 100644 --- a/TestApp/Integrations/Local/project+lcp.yml +++ b/TestApp/Integrations/Local/project+lcp.yml @@ -51,6 +51,8 @@ targets: product: ReadiumAdapterGCDWebServer - package: Readium product: ReadiumAdapterLCPSQLite + - package: Readium + product: ReadiumAdapterZIPFoundation - package: Readium product: ReadiumOPDS - package: Readium diff --git a/TestApp/Integrations/Local/project.yml b/TestApp/Integrations/Local/project.yml index 0c26b5d0d..8a692b51d 100644 --- a/TestApp/Integrations/Local/project.yml +++ b/TestApp/Integrations/Local/project.yml @@ -45,6 +45,8 @@ targets: product: ReadiumNavigator - package: Readium product: ReadiumAdapterGCDWebServer + - package: Readium + product: ReadiumAdapterZIPFoundation - package: Readium product: ReadiumOPDS - package: GRDB diff --git a/TestApp/Sources/App/Readium.swift b/TestApp/Sources/App/Readium.swift index 3ec960557..9ac6a8fb7 100644 --- a/TestApp/Sources/App/Readium.swift +++ b/TestApp/Sources/App/Readium.swift @@ -6,6 +6,7 @@ import Foundation import ReadiumAdapterGCDWebServer +import ReadiumAdapterZIPFoundation import ReadiumNavigator import ReadiumShared import ReadiumStreamer @@ -23,7 +24,9 @@ final class Readium { lazy var formatSniffer: FormatSniffer = DefaultFormatSniffer() lazy var assetRetriever = AssetRetriever( - httpClient: httpClient + formatSniffer: DefaultFormatSniffer(), + resourceFactory: DefaultResourceFactory(httpClient: httpClient), + archiveOpener: ZIPFoundationArchiveOpener() ) lazy var publicationOpener = PublicationOpener( diff --git a/Tests/Adapters/ZIPFoundationTests/Fixtures b/Tests/Adapters/ZIPFoundationTests/Fixtures new file mode 120000 index 000000000..29c2bca42 --- /dev/null +++ b/Tests/Adapters/ZIPFoundationTests/Fixtures @@ -0,0 +1 @@ +../../SharedTests/Fixtures/Archive \ No newline at end of file diff --git a/Tests/Adapters/ZIPFoundationTests/Fixtures.swift b/Tests/Adapters/ZIPFoundationTests/Fixtures.swift new file mode 100644 index 000000000..b47f90943 --- /dev/null +++ b/Tests/Adapters/ZIPFoundationTests/Fixtures.swift @@ -0,0 +1,29 @@ +// +// Copyright 2024 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 { + func url(for filepath: String) -> FileURL { + FileURL(url: Bundle.module.resourceURL!.appendingPathComponent("Fixtures/\(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/ZIPFoundationTests/ZIPFoundationTests.swift b/Tests/Adapters/ZIPFoundationTests/ZIPFoundationTests.swift new file mode 100644 index 000000000..d64081163 --- /dev/null +++ b/Tests/Adapters/ZIPFoundationTests/ZIPFoundationTests.swift @@ -0,0 +1,158 @@ +// +// Copyright 2024 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 ReadiumShared +@testable import ReadiumAdapterZIPFoundation +import XCTest + +private let fixtures = Fixtures() + +class ZIPFoundationTests: XCTestCase { + + private func container(for filename: String) async throws -> Container { + try await ZIPFoundationContainer.make(file: fixtures.url(for: filename)).get() + } + + func testOpenSuccess() async throws { + _ = try await container(for: "test.zip") + } + + func testOpenNotFound() async { + do { + _ = try await container(for: "unknown.zip") + XCTFail("Expected an error") + } catch {} + } + + func testOpenNotAZIP() async { + do { + _ = try await container(for: "not-a.zip") + XCTFail("Expected an error") + } catch {} + } + + func testGetNonExistingEntry() async throws { + let container = try await container(for: "test.zip") + XCTAssertNil(container[AnyURL(path: "unknown")!]) + } + + func testEntries() async throws { + let container = try await container(for: "test.zip") + + XCTAssertEqual( + container.entries, + Set([ + AnyURL(path: ".hidden")!, + AnyURL(path: "A folder/Sub.folder%/file.txt")!, + AnyURL(path: "A folder/wasteland-cover.jpg")!, + AnyURL(path: "root.txt")!, + AnyURL(path: "uncompressed.jpg")!, + AnyURL(path: "uncompressed.txt")!, + AnyURL(path: "A folder/Sub.folder%/file-compressed.txt")!, + ]) + ) + } + + func testResources() async throws { + let container = try await container(for: "test.zip") + + try await AssertEntry(path: ".hidden", in: container, isCompressed: false, length: 0, originalLength: 0) + try await AssertEntry(path: "A folder/Sub.folder%/file.txt", in: container, isCompressed: false, length: 20, originalLength: 20) + try await AssertEntry(path: "A folder/wasteland-cover.jpg", in: container, isCompressed: true, length: 82374, originalLength: 103_477) + try await AssertEntry(path: "root.txt", in: container, isCompressed: false, length: 0, originalLength: 0) + try await AssertEntry(path: "uncompressed.jpg", in: container, isCompressed: false, length: 279_551, originalLength: 279_551) + try await AssertEntry(path: "uncompressed.txt", in: container, isCompressed: false, length: 30, originalLength: 30) + try await AssertEntry(path: "A folder/Sub.folder%/file-compressed.txt", in: container, isCompressed: true, length: 8659, originalLength: 29609) + } + + func testReadCompressedEntry() async throws { + let container = try await container(for: "test.zip") + let entry = try XCTUnwrap(container[AnyURL(path: "A folder/Sub.folder%/file-compressed.txt")!]) + let data = try await entry.read().get() + let string = String(data: data, encoding: .utf8)! + XCTAssertEqual(string.count, 29609) + XCTAssertTrue(string.hasPrefix("I'm inside\nthe ZIP.")) + } + + func testReadUncompressedEntry() async throws { + let container = try await container(for: "test.zip") + let entry = try XCTUnwrap(container[AnyURL(path: "A folder/Sub.folder%/file.txt")!]) + let data = try await entry.read().get() + XCTAssertNotNil(data) + XCTAssertEqual( + String(data: data, encoding: .utf8), + "I'm inside\nthe ZIP.\n" + ) + } + + func testReadUncompressedRange() async throws { + // 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 container = try await container(for: "test.zip") + let entry = try XCTUnwrap(container[AnyURL(path: "A folder/Sub.folder%/file.txt")!]) + let data = try await entry.read(range: 14 ..< 20).get() + XCTAssertEqual( + String(data: data, encoding: .utf8), + " ZIP.\n" + ) + } + + func testReadCompressedRange() async throws { + let container = try await container(for: "test.zip") + let entry = try XCTUnwrap(container[AnyURL(path: "A folder/Sub.folder%/file-compressed.txt")!]) + let data = try await entry.read(range: 14 ..< 20).get() + XCTAssertEqual( + String(data: data, encoding: .utf8), + " ZIP.\n" + ) + } + + func testRandomCompressedRead() async throws { + for _ in 0..<100 { + let container = try await container(for: "test.zip") + let entry = try XCTUnwrap(container[AnyURL(path: "A folder/wasteland-cover.jpg")!]) + let length: UInt64 = 103_477 + let lower = UInt64.random(in: 0 ..< length - 100) + let upper = UInt64.random(in: lower ..< length) + let range = lower ..< upper + _ = try await entry.read(range: range).get() + } + } + + func testRandomStoredRead() async throws { + for _ in 0..<100 { + let container = try await container(for: "test.zip") + let entry = try XCTUnwrap(container[AnyURL(path: "uncompressed.jpg")!]) + let length: UInt64 = 279_551 + let lower = UInt64.random(in: 0 ..< length - 100) + let upper = UInt64.random(in: lower ..< length) + let range = lower ..< upper + _ = try await entry.read(range: range).get() + } + } + + private func AssertEntry( + path: String, + in container: Container, + isCompressed: Bool, + length: UInt64, + originalLength: UInt64 + ) async throws { + let resource = try XCTUnwrap(container[AnyURL(path: path)!]) + + let estimatedLength = try await resource.estimatedLength().get() + XCTAssertEqual(estimatedLength, originalLength) + + let properties = try await resource.properties().get() + XCTAssertEqual( + properties.archive, + ArchiveProperties( + entryLength: length, + isEntryCompressed: isCompressed + ) + ) + XCTAssertEqual(properties.filename, RelativeURL(path: path)!.lastPathSegment) + } +} diff --git a/Tests/SharedTests/Toolkit/Archive/Container+ZIPTests.swift b/Tests/SharedTests/Toolkit/Archive/Container+ZIPTests.swift index 1959b6845..b3f4d0df4 100644 --- a/Tests/SharedTests/Toolkit/Archive/Container+ZIPTests.swift +++ b/Tests/SharedTests/Toolkit/Archive/Container+ZIPTests.swift @@ -183,7 +183,7 @@ class ZIPBenchmarkingTests: XCTestCase { let lower = UInt64.random(in: 0 ..< length - 100) let upper = UInt64.random(in: lower ..< length) let range = lower ..< upper - let datas = await entries.asyncmap { await $0.read(range: range).getOrNil() } + let datas = await entries.asyncMap { await $0.read(range: range).getOrNil() } let data = datas[0] XCTAssertTrue(datas.allSatisfy { $0 == data }) exp.fulfill() diff --git a/Tests/StreamerTests/Parser/EPUB/EPUBContainerParserTests.swift b/Tests/StreamerTests/Parser/EPUB/EPUBContainerParserTests.swift index aa4f78115..c878f9453 100644 --- a/Tests/StreamerTests/Parser/EPUB/EPUBContainerParserTests.swift +++ b/Tests/StreamerTests/Parser/EPUB/EPUBContainerParserTests.swift @@ -4,7 +4,7 @@ // available in the top-level LICENSE file of the project. // -import Fuzi +import ReadiumFuzi import ReadiumShared @testable import ReadiumStreamer import XCTest diff --git a/Tests/StreamerTests/Parser/EPUB/EPUBMetadataParserTests.swift b/Tests/StreamerTests/Parser/EPUB/EPUBMetadataParserTests.swift index 80da508bc..2caeefea7 100644 --- a/Tests/StreamerTests/Parser/EPUB/EPUBMetadataParserTests.swift +++ b/Tests/StreamerTests/Parser/EPUB/EPUBMetadataParserTests.swift @@ -4,7 +4,7 @@ // available in the top-level LICENSE file of the project. // -import Fuzi +import ReadiumFuzi import ReadiumShared @testable import ReadiumStreamer import XCTest @@ -356,7 +356,7 @@ class EPUBMetadataParserTests: XCTestCase { // MARK: - Toolkit func parseMetadata(_ name: String, displayOptions: String? = nil) throws -> Metadata { - func parseDocument(named name: String, type: String) throws -> Fuzi.XMLDocument { + func parseDocument(named name: String, type: String) throws -> ReadiumFuzi.XMLDocument { try XMLDocument(data: fixtures.data(at: "\(name).\(type)")) } From 20fc7bc2d36b12846b5d18faf2768d0f215d13d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Menu?= Date: Wed, 11 Dec 2024 11:51:41 +0100 Subject: [PATCH 2/4] Remove the ZIPFoundation adapter --- CHANGELOG.md | 9 +- Package.swift | 22 +- .../Shared/Toolkit/ZIP/ZIPArchiveOpener.swift | 8 +- .../Toolkit/ZIP}/ZIPFoundation.swift | 9 +- Support/Carthage/.xcodegen | 25 +-- .../Readium.xcodeproj/project.pbxproj | 42 ++-- Support/Carthage/project.yml | 1 + Support/CocoaPods/ReadiumLCP.podspec | 2 +- Support/CocoaPods/ReadiumShared.podspec | 1 + TestApp/Integrations/Local/project+lcp.yml | 2 - TestApp/Integrations/Local/project.yml | 2 - TestApp/Sources/App/Readium.swift | 4 +- .../Toolkit/Archive/Container+ZIPTests.swift | 194 ------------------ .../Toolkit/Archive}/ZIPFoundationTests.swift | 5 +- docs/Migration Guide.md | 12 ++ 15 files changed, 55 insertions(+), 283 deletions(-) rename Sources/{Adapters/ZIPFoundation => Shared/Toolkit/ZIP}/ZIPFoundation.swift (94%) delete mode 100644 Tests/SharedTests/Toolkit/Archive/Container+ZIPTests.swift rename Tests/{Adapters/ZIPFoundationTests => SharedTests/Toolkit/Archive}/ZIPFoundationTests.swift (98%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3677fbd3b..cf7d8c03e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,14 @@ All notable changes to this project will be documented in this file. Take a look **Warning:** Features marked as *alpha* may change or be removed in a future release without notice. Use with caution. - +## [Unreleased] + +### Changed + +#### Shared + +* The default `ZIPArchiveOpener` is now using ZIPFoundation instead of Minizip, with improved performances when reading ranges of `stored` ZIP entries. + ## [3.0.0-beta.1] diff --git a/Package.swift b/Package.swift index 63d7c4676..cc883c4ec 100644 --- a/Package.swift +++ b/Package.swift @@ -21,17 +21,15 @@ let package = Package( // Adapters to third-party dependencies. .library(name: "ReadiumAdapterGCDWebServer", targets: ["ReadiumAdapterGCDWebServer"]), .library(name: "ReadiumAdapterLCPSQLite", targets: ["ReadiumAdapterLCPSQLite"]), - .library(name: "ReadiumAdapterZIPFoundation", targets: ["ReadiumAdapterZIPFoundation"]), ], dependencies: [ .package(url: "https://github.com/krzyzanowskim/CryptoSwift.git", from: "1.8.0"), .package(url: "https://github.com/ra1028/DifferenceKit.git", from: "1.3.0"), .package(url: "https://github.com/readium/Fuzi.git", from: "4.0.0"), .package(url: "https://github.com/readium/GCDWebServer.git", from: "4.0.0"), + .package(url: "https://github.com/readium/ZIPFoundation.git", from: "1.0.0"), .package(url: "https://github.com/scinfu/SwiftSoup.git", from: "2.7.0"), .package(url: "https://github.com/stephencelis/SQLite.swift.git", from: "0.15.0"), -// .package(url: "https://github.com/readium/ZIPFoundation.git", branch: "main"), - .package(path: "../ZIPFoundation"), ], targets: [ .target( @@ -40,6 +38,7 @@ let package = Package( "ReadiumInternal", "SwiftSoup", .product(name: "ReadiumFuzi", package: "Fuzi"), + .product(name: "ReadiumZIPFoundation", package: "ZIPFoundation"), ], path: "Sources/Shared", linkerSettings: [ @@ -158,23 +157,6 @@ let package = Package( path: "Sources/Adapters/LCPSQLite" ), - .target( - name: "ReadiumAdapterZIPFoundation", - dependencies: [ - "ReadiumShared", - .product(name: "ReadiumZIPFoundation", package: "ZIPFoundation"), - ], - path: "Sources/Adapters/ZIPFoundation" - ), - .testTarget( - name: "ReadiumAdapterZIPFoundationTests", - dependencies: ["ReadiumAdapterZIPFoundation"], - path: "Tests/Adapters/ZIPFoundationTests", - resources: [ - .copy("Fixtures"), - ] - ), - .target( name: "ReadiumInternal", path: "Sources/Internal" diff --git a/Sources/Shared/Toolkit/ZIP/ZIPArchiveOpener.swift b/Sources/Shared/Toolkit/ZIP/ZIPArchiveOpener.swift index a2e929773..d0b5a6cd7 100644 --- a/Sources/Shared/Toolkit/ZIP/ZIPArchiveOpener.swift +++ b/Sources/Shared/Toolkit/ZIP/ZIPArchiveOpener.swift @@ -8,17 +8,15 @@ import Foundation /// An ``ArchiveOpener`` for ZIP resources. public class ZIPArchiveOpener: ArchiveOpener { -// private let opener = MinizipArchiveOpener() + private let opener = ZIPFoundationArchiveOpener() public init() {} public func open(resource: any Resource, format: Format) async -> Result { - return .failure(.formatNotSupported(format)) -// await opener.open(resource: resource, format: format) + await opener.open(resource: resource, format: format) } public func sniffOpen(resource: any Resource) async -> Result { - return .failure(.formatNotRecognized) -// await opener.sniffOpen(resource: resource) + await opener.sniffOpen(resource: resource) } } diff --git a/Sources/Adapters/ZIPFoundation/ZIPFoundation.swift b/Sources/Shared/Toolkit/ZIP/ZIPFoundation.swift similarity index 94% rename from Sources/Adapters/ZIPFoundation/ZIPFoundation.swift rename to Sources/Shared/Toolkit/ZIP/ZIPFoundation.swift index 08cbf7dcf..ad448cc82 100644 --- a/Sources/Adapters/ZIPFoundation/ZIPFoundation.swift +++ b/Sources/Shared/Toolkit/ZIP/ZIPFoundation.swift @@ -5,7 +5,6 @@ // import Foundation -import ReadiumShared import ReadiumZIPFoundation /// An ``ArchiveOpener`` able to open ZIP archives using ZIPFoundation. @@ -73,7 +72,7 @@ final class ZIPFoundationContainer: Container, Loggable { } do { - let archive = try Archive(url: file.url, accessMode: .read, pathEncoding: nil) + let archive = try ReadiumZIPFoundation.Archive(url: file.url, accessMode: .read) var entries = [RelativeURL: ZIPFoundationEntryMetadata]() for entry in archive { @@ -178,11 +177,11 @@ private actor ZIPFoundationResource: Resource, Loggable { } } - private var _archive: ReadResult? - private func archive() async -> ReadResult { + private var _archive: ReadResult? + private func archive() async -> ReadResult { if _archive == nil { do { - _archive = .success(try Archive(url: file.url, accessMode: .read, pathEncoding: nil)) + _archive = .success(try ReadiumZIPFoundation.Archive(url: file.url, accessMode: .read, pathEncoding: nil)) } catch { _archive = .failure(.decoding(error)) } diff --git a/Support/Carthage/.xcodegen b/Support/Carthage/.xcodegen index 81ad98c3c..fea07ec7c 100644 --- a/Support/Carthage/.xcodegen +++ b/Support/Carthage/.xcodegen @@ -139,10 +139,7 @@ "framework" : "..\/..\/Carthage\/Build\/CryptoSwift.xcframework" }, { - "framework" : "..\/..\/Carthage\/Build\/Fuzi.xcframework" - }, - { - "framework" : "..\/..\/Carthage\/Build\/Minizip.xcframework" + "framework" : "..\/..\/Carthage\/Build\/ReadiumFuzi.xcframework" }, { "framework" : "..\/..\/Carthage\/Build\/ReadiumZIPFoundation.xcframework" @@ -173,10 +170,7 @@ "framework" : "..\/..\/Carthage\/Build\/DifferenceKit.xcframework" }, { - "framework" : "..\/..\/Carthage\/Build\/Fuzi.xcframework" - }, - { - "framework" : "..\/..\/Carthage\/Build\/Minizip.xcframework" + "framework" : "..\/..\/Carthage\/Build\/ReadiumFuzi.xcframework" }, { "framework" : "..\/..\/Carthage\/Build\/SwiftSoup.xcframework" @@ -212,10 +206,7 @@ "ReadiumOPDS" : { "dependencies" : [ { - "framework" : "..\/..\/Carthage\/Build\/Fuzi.xcframework" - }, - { - "framework" : "..\/..\/Carthage\/Build\/Minizip.xcframework" + "framework" : "..\/..\/Carthage\/Build\/ReadiumFuzi.xcframework" }, { "target" : "ReadiumShared" @@ -240,10 +231,10 @@ "ReadiumShared" : { "dependencies" : [ { - "framework" : "..\/..\/Carthage\/Build\/Fuzi.xcframework" + "framework" : "..\/..\/Carthage\/Build\/ReadiumFuzi.xcframework" }, { - "framework" : "..\/..\/Carthage\/Build\/Minizip.xcframework" + "framework" : "..\/..\/Carthage\/Build\/ReadiumZIPFoundation.xcframework" }, { "framework" : "..\/..\/Carthage\/Build\/SwiftSoup.xcframework" @@ -274,10 +265,7 @@ "framework" : "..\/..\/Carthage\/Build\/CryptoSwift.xcframework" }, { - "framework" : "..\/..\/Carthage\/Build\/Fuzi.xcframework" - }, - { - "framework" : "..\/..\/Carthage\/Build\/Minizip.xcframework" + "framework" : "..\/..\/Carthage\/Build\/ReadiumFuzi.xcframework" }, { "target" : "ReadiumShared" @@ -13974,7 +13962,6 @@ ../../Sources/Shared/Toolkit/XML/Fuzi.swift ../../Sources/Shared/Toolkit/XML/XML.swift ../../Sources/Shared/Toolkit/ZIP -../../Sources/Shared/Toolkit/ZIP/Minizip.swift ../../Sources/Shared/Toolkit/ZIP/ZIPArchiveOpener.swift ../../Sources/Streamer ../../Sources/Streamer/Assets diff --git a/Support/Carthage/Readium.xcodeproj/project.pbxproj b/Support/Carthage/Readium.xcodeproj/project.pbxproj index 6fa0ef34e..7baf1f194 100644 --- a/Support/Carthage/Readium.xcodeproj/project.pbxproj +++ b/Support/Carthage/Readium.xcodeproj/project.pbxproj @@ -8,23 +8,23 @@ /* Begin PBXBuildFile section */ 0025BE4D568B560277323B95 /* FileResource.swift in Sources */ = {isa = PBXBuildFile; fileRef = C05502AD8940D6616D6C386F /* FileResource.swift */; }; - 01AC52ADE389D14F5274CEB2 /* Minizip.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = CFFEBDFE931745C07DACD4A3 /* Minizip.xcframework */; }; 01AD628D6DE82E1C1C4C281D /* NavigationDocumentParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29AD63CD2A41586290547212 /* NavigationDocumentParser.swift */; }; 01E785BEA7F30AD1C8A5F3DE /* SearchService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B5B029CA09EE1F86A19612A /* SearchService.swift */; }; 01F29BAD25D4597903AF253F /* CompositeFormatSniffer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41E54A18E2983098A6A4DAE0 /* CompositeFormatSniffer.swift */; }; 035807359AFA2EE23E00F8AB /* TransformingContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0918DA360AAB646144E435D5 /* TransformingContainer.swift */; }; 03B5862162A21448DC7813C6 /* HTTPServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BCDF341872EEFB88B6674DE /* HTTPServer.swift */; }; 0411D7B6ACD75F2D03547787 /* RARFormatSniffer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FEF14E6F136136D43A39897 /* RARFormatSniffer.swift */; }; + 048A8C8447FBF687484CD248 /* ReadiumFuzi.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2828D89EBB52CCA782ED1146 /* ReadiumFuzi.xcframework */; }; 0569A7E5E5B31AF3B4C8C234 /* EPUBContainerParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78033AFDF98C92351785D17F /* EPUBContainerParser.swift */; }; 061C798F8A5DACFACB414B15 /* CGRect.swift in Sources */ = {isa = PBXBuildFile; fileRef = 48856E9AB402E2907B5230F3 /* CGRect.swift */; }; 06CF9F75A9DB1B6241CA7719 /* LCPService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67DEBFCD9D71243C4ACC3A49 /* LCPService.swift */; }; 077AD829863BD952DEBFB5A0 /* StatusDocument.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45319A96D79565A2CD31B650 /* StatusDocument.swift */; }; - 0828EA20B47F13D1FC4C364F /* Fuzi.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = B421601FB56132514CCD9699 /* Fuzi.xcframework */; }; 0A262D1DF87FBBAA402A1194 /* ContentProtection.swift in Sources */ = {isa = PBXBuildFile; fileRef = C57EC6B0ADED2B0D395F2AEA /* ContentProtection.swift */; }; 0A6BF62D6FE0C04DA8B8D3CA /* AnyURL.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE350D88BC82408491D8B516 /* AnyURL.swift */; }; 0AF2BBF12939AFBF6173E333 /* Observable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BC6AE42A31D77B548CB0BB4 /* Observable.swift */; }; 0B9AC6EF44DA518E9F37FB49 /* ContentService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18E809378D79D09192A0AAE1 /* ContentService.swift */; }; 0BFCDAEC82CFF09AFC53A5D0 /* LCPDFTableOfContentsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94414130EC3731CD9920F27D /* LCPDFTableOfContentsService.swift */; }; + 0C038E3525BB600EF6815EB9 /* ReadiumFuzi.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2828D89EBB52CCA782ED1146 /* ReadiumFuzi.xcframework */; }; 0ECE94F27E005FC454EA9D12 /* DecorableNavigator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 626CFFF131E0E840B76428F1 /* DecorableNavigator.swift */; }; 0F1AAB56A6ADEDDE2AD7E41E /* Content.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1039900AC78465AD989D7464 /* Content.swift */; }; 1004CE1C72C85CC3702C09C0 /* Asset.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC811653B33761089E270C4A /* Asset.swift */; }; @@ -40,7 +40,6 @@ 1600DB04CEACF97EE8AD9CEE /* URLProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 258351CE21165EDED7F87878 /* URLProtocol.swift */; }; 17108D46A0353A254DA193B0 /* Container.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7D002FDDAD1A21AC5BB84CE /* Container.swift */; }; 1752D756BED37325D6D4ED29 /* ReadiumInternal.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 42FD63C2720614E558522675 /* ReadiumInternal.framework */; }; - 180E78AF6C7507F06AEEB806 /* Minizip.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = CFFEBDFE931745C07DACD4A3 /* Minizip.xcframework */; }; 18217BC157557A5DDA4BA119 /* User.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADDB8B9906FC78C038203BDD /* User.swift */; }; 188D742F80B70DE8A625AD21 /* Facet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 387B19B66C4D91A295B5EFA6 /* Facet.swift */; }; 1AEF63A8471C7676092842D2 /* FileExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = D555435E2BADB2B877FD50C7 /* FileExtension.swift */; }; @@ -72,7 +71,6 @@ 2AE6A09759A80A668D08CDEC /* Archive.swift in Sources */ = {isa = PBXBuildFile; fileRef = A90EA81ECD9488CB3CBDAB41 /* Archive.swift */; }; 2B4D0BD8E7A2E53319CD7782 /* TransformingResource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88ACF6B7BA6B9BDE7DD076BE /* TransformingResource.swift */; }; 2B8BC06B6B366E67C716DDA1 /* OPFMeta.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FC49AFB32B525AAC5BF7612 /* OPFMeta.swift */; }; - 2C1934F6A462DA5F5EE9B13C /* Fuzi.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = B421601FB56132514CCD9699 /* Fuzi.xcframework */; }; 2C5436091DD72FDBF6FF136D /* OPFParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61575203A3BEB8E218CAFE38 /* OPFParser.swift */; }; 2CD7B92212F4757C0DF02BD7 /* HTTPResource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42BAA4F11B7355F1962FEAD9 /* HTTPResource.swift */; }; 2E518C960D386F13E0A5E9B7 /* EPUBFixedSpreadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EF99DAF66659A218CEC25EAE /* EPUBFixedSpreadView.swift */; }; @@ -108,11 +106,13 @@ 3E9F244ACDA938D330B9EAEA /* Subject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98CD4C99103DC795E44F56AE /* Subject.swift */; }; 3ECB525CEB712CEC5EFCD26D /* WarningLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3510E7E84A5361BCECC90569 /* WarningLogger.swift */; }; 40A44414CC911BF49BB5EE60 /* Tokenizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DA31089FCAD8DFB9AC46E4E /* Tokenizer.swift */; }; + 422FF6643B32F9BCE8043757 /* ReadiumFuzi.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2828D89EBB52CCA782ED1146 /* ReadiumFuzi.xcframework */; }; 44152DBECE34F063AD0E93BC /* Link.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE3E6442F0C7FE2098D71F27 /* Link.swift */; }; 448374F2605586249A6CB4C8 /* FailureResource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78FFDF8CF77437EDB41E4547 /* FailureResource.swift */; }; 46D29739FB017C62767CBE63 /* Presentation+EPUB.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42EFF9139B59D763CE254F92 /* Presentation+EPUB.swift */; }; 47125BFFEC67DEB2C3D1B48C /* AudioNavigator.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCE34D74E282834684E1C999 /* AudioNavigator.swift */; }; 47FE7B25B5DB153D406CF0CE /* UserSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4937644CB65AE6801CE3295 /* UserSettings.swift */; }; + 4977279900B4BA602D92B5C4 /* ReadiumFuzi.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2828D89EBB52CCA782ED1146 /* ReadiumFuzi.xcframework */; }; 4A5F53CCC083D3E348379963 /* Types.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BF64D7C05A790D9CA5DD442 /* Types.swift */; }; 4AD286114A634A74BE78B1A0 /* LicenseContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 15980B67505AAF10642B56C8 /* LicenseContainer.swift */; }; 4AE70F783C07D9938B40E792 /* StringExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 125BAF5FDFA097BA5CC63539 /* StringExtension.swift */; }; @@ -183,7 +183,6 @@ 75E5BBD405026A68BF6741F9 /* AbsoluteURL.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5DF154DCC73CFBDB0F919DE /* AbsoluteURL.swift */; }; 762CF84C6FB1FCCF03EA91B6 /* Loggable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 067E58BE65BCB4F8D1E8B911 /* Loggable.swift */; }; 76F6EE39F504B6A80837C90D /* MediaOverlayNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1DAAE19E8372F6ECF772E0A /* MediaOverlayNode.swift */; }; - 77006A5BF6FCBFFD60B15EBA /* Minizip.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD824370ACE916C9BB9CAF84 /* Minizip.swift */; }; 77C828CCC90DF784B2D75774 /* OpdsMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DDB25FC1693613B72DFDB6E /* OpdsMetadata.swift */; }; 78C52EED635B5F8C38A02298 /* Metadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01B24895126F2A744A8E9E61 /* Metadata.swift */; }; 79763D2B25EC6585792C958D /* FileResourceFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCD0F310FEAC86A2DD40B8AE /* FileResourceFactory.swift */; }; @@ -204,10 +203,8 @@ 844135596BE2BBA9C570F6C4 /* ProxyPreference.swift in Sources */ = {isa = PBXBuildFile; fileRef = A77C8BE0AC489E5FF2DA1FE3 /* ProxyPreference.swift */; }; 84BB48C576F7F1DBE32D9745 /* FormatSniffer.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1FB533E84CE563807BDB012 /* FormatSniffer.swift */; }; 86D8ED9188418FCFA290CD1C /* Assets in Resources */ = {isa = PBXBuildFile; fileRef = 251275D0DF87F85158A5FEA9 /* Assets */; }; - 8709012E5CE29CC28E7470C4 /* Fuzi.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = B421601FB56132514CCD9699 /* Fuzi.xcframework */; }; 8889176F35267918E2B3577E /* PublicationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 667B76C4766DFF58D066D40B /* PublicationService.swift */; }; 89A7CA4173B39673A237EAED /* Weak.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE7B762C97CFC214997EC677 /* Weak.swift */; }; - 8B9BCCB1D724E18BF2893629 /* Fuzi.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = B421601FB56132514CCD9699 /* Fuzi.xcframework */; }; 8BA982B992E0370C7BF94DF6 /* UIView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55D0EAD1ABB7B829A3891D3A /* UIView.swift */; }; 8BD3DB373A8785BE8E71845D /* EPUBFormatSniffer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0495085D1A7758A1197F666E /* EPUBFormatSniffer.swift */; }; 8CCDF77696A0F2C7BF3171CC /* LocalizedString.swift in Sources */ = {isa = PBXBuildFile; fileRef = 600D8714B762FE37DE405C2E /* LocalizedString.swift */; }; @@ -215,7 +212,6 @@ 8E25FF2EEFA72D9B0F3025C5 /* PDFFormatSniffer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B72B76AB39E09E4A2E465AF /* PDFFormatSniffer.swift */; }; 8F5B0B5B83BF7F1145556FF8 /* Properties+OPDS.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAD79372361D085CA0500CF4 /* Properties+OPDS.swift */; }; 9065C4C0F40B6A5601541EF7 /* Streamable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 622CB8B75A568846FECA44D6 /* Streamable.swift */; }; - 90769CA2ABCBD1F7203697DC /* Minizip.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = CFFEBDFE931745C07DACD4A3 /* Minizip.xcframework */; }; 90CFD62B993F6759716C0AF0 /* LicensesService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56286133DD0AE093F2C5E9FD /* LicensesService.swift */; }; 914DEDFE5594761D3F180491 /* EPUBPositionsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C93236E313B55D8B835D9F /* EPUBPositionsService.swift */; }; 917AD2492BFA4FD8B2FC85B8 /* ResourceResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6104651E0A538A99B44B328F /* ResourceResponse.swift */; }; @@ -263,6 +259,7 @@ AD572C6A7AD031FEC40A0BD7 /* LanguageFormatSniffer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78E5D0B9449607B2906901C2 /* LanguageFormatSniffer.swift */; }; AD87094AA40926939955E9F2 /* LCPRenewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 230985A228FA74F24735D6BB /* LCPRenewDelegate.swift */; }; B01E5FC64FD1411DD9899ADC /* HTMLInjection.swift in Sources */ = {isa = PBXBuildFile; fileRef = A75B535BBE6EE5898327DB52 /* HTMLInjection.swift */; }; + B02109488289D631DF959B8D /* ReadiumFuzi.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2828D89EBB52CCA782ED1146 /* ReadiumFuzi.xcframework */; }; B066F9DDCD00A8917478CB6C /* LCPDialogViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BB64178365BFA9ED75C7078 /* LCPDialogViewController.swift */; }; B0B2C38D1A7B36E73C3E3779 /* DifferenceKit.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3F95F3F20D758BE0E7005EA3 /* DifferenceKit.xcframework */; }; B0F62AC136EF3587E147468E /* AssetRetriever.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C054DDC6D1BCF4A420C980C /* AssetRetriever.swift */; }; @@ -317,16 +314,15 @@ CC85122A71D3145940827338 /* Comparable.swift in Sources */ = {isa = PBXBuildFile; fileRef = F90C4D94134D9F741D38D8AA /* Comparable.swift */; }; CCAF8FB4DBD81448C99D589A /* Language.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BB152578CBA091A41A51B25 /* Language.swift */; }; CD0243B5EB8B408E34786214 /* ReadiumInternal.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 42FD63C2720614E558522675 /* ReadiumInternal.framework */; }; + CDE5EE79D3F0D00F9CAED8B8 /* ReadiumZIPFoundation.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 69E17C4870C64264819EB227 /* ReadiumZIPFoundation.xcframework */; }; CE0720B9C9C3D9C4353F6ED2 /* OPDSFormatSniffer.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAA7CEF568A02BA2CB4AAD7F /* OPDSFormatSniffer.swift */; }; CEC42C02BB2B02562785B259 /* Range.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB3E08C8187DCC3099CF9D22 /* Range.swift */; }; D1248D9E9EE269C3245927F7 /* Cancellable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BBD54FD376456C1925316BC /* Cancellable.swift */; }; D25058A427D47ABEA88A3F4B /* PublicationSpeechSynthesizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99DE4955327D8C2DE6F866D3 /* PublicationSpeechSynthesizer.swift */; }; D29E1DBC5BD1B82C996427C4 /* Closeable.swift in Sources */ = {isa = PBXBuildFile; fileRef = C02A9225D636D845BF24F6AC /* Closeable.swift */; }; - D2FA324CCED60BCF338C46F7 /* Minizip.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = CFFEBDFE931745C07DACD4A3 /* Minizip.xcframework */; }; D4BBC0AD7652265497B5CD1C /* URLExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 10894CC9684584098A22D8FA /* URLExtensions.swift */; }; D50FE2B82BB34E2881723BE9 /* ZIPLicenseContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6453ABD6DF237362C6EECD2 /* ZIPLicenseContainer.swift */; }; D525E9E2AFA0ED63151A4FBC /* Assets in Resources */ = {isa = PBXBuildFile; fileRef = DBCE9786DD346E6BDB2E50FF /* Assets */; }; - D5543164860E3B7B8FD10012 /* Minizip.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = CFFEBDFE931745C07DACD4A3 /* Minizip.xcframework */; }; D589F337B244F5B58E85AEFB /* AudioSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC9D0ACCBA7B6E4473A95D5E /* AudioSettings.swift */; }; D5AF26F18E98CEF06AEC0329 /* SQLiteLCPLicenseRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17ACAC3E8F61DA108DCC9F51 /* SQLiteLCPLicenseRepository.swift */; }; D65BEF9DAF1FEB1A5BEED700 /* EPUBParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03C234075C7F7573BA54B77D /* EPUBParser.swift */; }; @@ -339,7 +335,6 @@ D8C9C54CC84D1C0810343D9E /* UIColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E788FD34BE635B4B80C18A6 /* UIColor.swift */; }; D9499DACC5329F3774CBEDAC /* SwiftSoup.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = BE09289EB0FEA5FEC8506B1F /* SwiftSoup.xcframework */; }; D94A07E9627214888D37C6DF /* Bundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = F64FBE3CA5C1B0C73A22E86D /* Bundle.swift */; }; - DAA4EA6A44E288F015A638D0 /* Fuzi.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = B421601FB56132514CCD9699 /* Fuzi.xcframework */; }; DB423F5860A1C47EF2E18113 /* PDFTapGestureController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DF324AD5E3E30687AC5262D /* PDFTapGestureController.swift */; }; DC0487666F03A3FAFE49D0B9 /* EPUBLicenseContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 500E55D9CA753D6D6AA76D10 /* EPUBLicenseContainer.swift */; }; DD04CA793E06BBAD6A75329F /* EPUBPreferences+Legacy.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF31AEFB5FF0E7892C6D903E /* EPUBPreferences+Legacy.swift */; }; @@ -539,6 +534,7 @@ 251275D0DF87F85158A5FEA9 /* Assets */ = {isa = PBXFileReference; lastKnownFileType = folder; name = Assets; path = ../../Sources/Navigator/EPUB/Assets; sourceTree = SOURCE_ROOT; }; 258351CE21165EDED7F87878 /* URLProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLProtocol.swift; sourceTree = ""; }; 2732AFC91AB15FA09C60207A /* Locator+Audio.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Locator+Audio.swift"; sourceTree = ""; }; + 2828D89EBB52CCA782ED1146 /* ReadiumFuzi.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = ReadiumFuzi.xcframework; path = ../../Carthage/Build/ReadiumFuzi.xcframework; sourceTree = ""; }; 294E01A2E6FF25539EBC1082 /* Properties+Archive.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Properties+Archive.swift"; sourceTree = ""; }; 29AD63CD2A41586290547212 /* NavigationDocumentParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationDocumentParser.swift; sourceTree = ""; }; 2AF56CF04F94B7BE45631897 /* LCPContentProtection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LCPContentProtection.swift; sourceTree = ""; }; @@ -738,7 +734,6 @@ B15C9123EA383ED81DE0393A /* AVTTSEngine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AVTTSEngine.swift; sourceTree = ""; }; B22A0E76866F626D79F0A64C /* SQLiteLCPPassphraseRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SQLiteLCPPassphraseRepository.swift; sourceTree = ""; }; B39BF68DBB28D655023ADB62 /* Fetcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Fetcher.swift; sourceTree = ""; }; - B421601FB56132514CCD9699 /* Fuzi.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = Fuzi.xcframework; path = ../../Carthage/Build/Fuzi.xcframework; sourceTree = ""; }; B53B841C2F5A59BA3B161258 /* Resource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Resource.swift; sourceTree = ""; }; B5CE464C519852D38F873ADB /* PotentialRights.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PotentialRights.swift; sourceTree = ""; }; B7C9D54352714641A87F64A0 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; @@ -775,7 +770,6 @@ CF31AEFB5FF0E7892C6D903E /* EPUBPreferences+Legacy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EPUBPreferences+Legacy.swift"; sourceTree = ""; }; CFE1142A6C038A35C527CE84 /* URITemplate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URITemplate.swift; sourceTree = ""; }; CFE34EA8AF2D815F7169CA45 /* Fuzi.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Fuzi.swift; sourceTree = ""; }; - CFFEBDFE931745C07DACD4A3 /* Minizip.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = Minizip.xcframework; path = ../../Carthage/Build/Minizip.xcframework; 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 = ""; }; D13272E03B63E96D4246F79D /* PDFParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PDFParser.swift; sourceTree = ""; }; @@ -798,7 +792,6 @@ DBCE9786DD346E6BDB2E50FF /* Assets */ = {isa = PBXFileReference; lastKnownFileType = folder; name = Assets; path = ../../Sources/Streamer/Assets; sourceTree = SOURCE_ROOT; }; DCE34D74E282834684E1C999 /* AudioNavigator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioNavigator.swift; sourceTree = ""; }; DCF20C1D3C33365D25704663 /* XMLFormatSniffer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XMLFormatSniffer.swift; sourceTree = ""; }; - DD824370ACE916C9BB9CAF84 /* Minizip.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Minizip.swift; sourceTree = ""; }; DE5CE54D209AEA4AE5B07618 /* AudioFormatSniffer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioFormatSniffer.swift; sourceTree = ""; }; DF89316F77F23DACA2E04696 /* PDFDocument.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PDFDocument.swift; sourceTree = ""; }; DF92954C8C8C3EC50C835CBA /* Link.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Link.swift; sourceTree = ""; }; @@ -863,8 +856,7 @@ buildActionMask = 2147483647; files = ( B0B2C38D1A7B36E73C3E3779 /* DifferenceKit.xcframework in Frameworks */, - 8709012E5CE29CC28E7470C4 /* Fuzi.xcframework in Frameworks */, - 180E78AF6C7507F06AEEB806 /* Minizip.xcframework in Frameworks */, + 4977279900B4BA602D92B5C4 /* ReadiumFuzi.xcframework in Frameworks */, A040DE6F2D9A6B8D16B063B9 /* SwiftSoup.xcframework in Frameworks */, 33E3A544AFA1F9CF71771558 /* ReadiumShared.framework in Frameworks */, 70853F23094C2BAFAD422AFD /* ReadiumInternal.framework in Frameworks */, @@ -876,8 +868,7 @@ buildActionMask = 2147483647; files = ( 26CC9CB6CA4D81EB60AE860C /* CryptoSwift.xcframework in Frameworks */, - DAA4EA6A44E288F015A638D0 /* Fuzi.xcframework in Frameworks */, - 90769CA2ABCBD1F7203697DC /* Minizip.xcframework in Frameworks */, + 0C038E3525BB600EF6815EB9 /* ReadiumFuzi.xcframework in Frameworks */, C0A492BDDCB4A472F408B3B4 /* ReadiumZIPFoundation.xcframework in Frameworks */, 13534618D5095F49627C4409 /* ReadiumShared.framework in Frameworks */, C3F4CBE80D741D4158CA8407 /* ReadiumInternal.framework in Frameworks */, @@ -898,8 +889,8 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 8B9BCCB1D724E18BF2893629 /* Fuzi.xcframework in Frameworks */, - 01AC52ADE389D14F5274CEB2 /* Minizip.xcframework in Frameworks */, + B02109488289D631DF959B8D /* ReadiumFuzi.xcframework in Frameworks */, + CDE5EE79D3F0D00F9CAED8B8 /* ReadiumZIPFoundation.xcframework in Frameworks */, D9499DACC5329F3774CBEDAC /* SwiftSoup.xcframework in Frameworks */, EE16BE486539BC9B3C3C6896 /* ReadiumInternal.framework in Frameworks */, 38E81FCDABFBB514685402B8 /* CoreServices.framework in Frameworks */, @@ -911,8 +902,7 @@ buildActionMask = 2147483647; files = ( 682DFC1AF2BD7CAE0862B331 /* CryptoSwift.xcframework in Frameworks */, - 2C1934F6A462DA5F5EE9B13C /* Fuzi.xcframework in Frameworks */, - D2FA324CCED60BCF338C46F7 /* Minizip.xcframework in Frameworks */, + 048A8C8447FBF687484CD248 /* ReadiumFuzi.xcframework in Frameworks */, ACD1914D2D9BB7141148740F /* ReadiumShared.framework in Frameworks */, 1752D756BED37325D6D4ED29 /* ReadiumInternal.framework in Frameworks */, ); @@ -932,8 +922,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 0828EA20B47F13D1FC4C364F /* Fuzi.xcframework in Frameworks */, - D5543164860E3B7B8FD10012 /* Minizip.xcframework in Frameworks */, + 422FF6643B32F9BCE8043757 /* ReadiumFuzi.xcframework in Frameworks */, 39326587EF76BFD5AD68AED2 /* ReadiumShared.framework in Frameworks */, CD0243B5EB8B408E34786214 /* ReadiumInternal.framework in Frameworks */, ); @@ -1115,7 +1104,6 @@ 248500BDC6637BD9EDEEB8A0 /* ZIP */ = { isa = PBXGroup; children = ( - DD824370ACE916C9BB9CAF84 /* Minizip.swift */, C51C74A5990A3BA93B3DC587 /* ZIPArchiveOpener.swift */, ); path = ZIP; @@ -1749,8 +1737,7 @@ 342D5C0FEE79A2ABEE24A43E /* CoreServices.framework */, E37F94C388A86CB8A34812A5 /* CryptoSwift.xcframework */, 3F95F3F20D758BE0E7005EA3 /* DifferenceKit.xcframework */, - B421601FB56132514CCD9699 /* Fuzi.xcframework */, - CFFEBDFE931745C07DACD4A3 /* Minizip.xcframework */, + 2828D89EBB52CCA782ED1146 /* ReadiumFuzi.xcframework */, 6536C07F5A50F7F25FDBF69C /* ReadiumGCDWebServer.xcframework */, 69E17C4870C64264819EB227 /* ReadiumZIPFoundation.xcframework */, F07214E263C6589987A561F9 /* SQLite.xcframework */, @@ -2601,7 +2588,6 @@ 26CA8F00CA85378CA0F8B3F2 /* MediaTypeSniffer.swift in Sources */, 7E45E10720EA6B4F18196316 /* Metadata+Presentation.swift in Sources */, 78C52EED635B5F8C38A02298 /* Metadata.swift in Sources */, - 77006A5BF6FCBFFD60B15EBA /* Minizip.swift in Sources */, 74E94DF537F0DE19706003DA /* NowPlayingInfo.swift in Sources */, 3079CCA7A341CB0A43A8DA74 /* OPDSAcquisition.swift in Sources */, C35C6AB637D2048D9B0A3C62 /* OPDSAvailability.swift in Sources */, diff --git a/Support/Carthage/project.yml b/Support/Carthage/project.yml index 52337b616..032d25857 100644 --- a/Support/Carthage/project.yml +++ b/Support/Carthage/project.yml @@ -14,6 +14,7 @@ targets: - path: ../../Sources/Shared dependencies: - framework: ../../Carthage/Build/ReadiumFuzi.xcframework + - framework: ../../Carthage/Build/ReadiumZIPFoundation.xcframework - framework: ../../Carthage/Build/SwiftSoup.xcframework - target: ReadiumInternal - sdk: CoreServices.framework diff --git a/Support/CocoaPods/ReadiumLCP.podspec b/Support/CocoaPods/ReadiumLCP.podspec index 08e649dde..24d218fed 100644 --- a/Support/CocoaPods/ReadiumLCP.podspec +++ b/Support/CocoaPods/ReadiumLCP.podspec @@ -21,6 +21,6 @@ Pod::Spec.new do |s| s.dependency 'ReadiumShared' s.dependency 'ReadiumInternal' - s.dependency 'ReadiumZIPFoundation', '~> 0.9.0' + s.dependency 'ReadiumZIPFoundation', '~> 1.0.0' s.dependency 'CryptoSwift', '~> 1.8.0' end diff --git a/Support/CocoaPods/ReadiumShared.podspec b/Support/CocoaPods/ReadiumShared.podspec index 484fe1672..d8cc070f9 100644 --- a/Support/CocoaPods/ReadiumShared.podspec +++ b/Support/CocoaPods/ReadiumShared.podspec @@ -17,6 +17,7 @@ Pod::Spec.new do |s| s.dependency 'SwiftSoup', '~> 2.7.0' s.dependency 'ReadiumFuzi', '~> 4.0.0' + s.dependency 'ReadiumZIPFoundation', '~> 1.0.0' s.dependency 'ReadiumInternal' end diff --git a/TestApp/Integrations/Local/project+lcp.yml b/TestApp/Integrations/Local/project+lcp.yml index b168510c3..4cc5bbbba 100644 --- a/TestApp/Integrations/Local/project+lcp.yml +++ b/TestApp/Integrations/Local/project+lcp.yml @@ -51,8 +51,6 @@ targets: product: ReadiumAdapterGCDWebServer - package: Readium product: ReadiumAdapterLCPSQLite - - package: Readium - product: ReadiumAdapterZIPFoundation - package: Readium product: ReadiumOPDS - package: Readium diff --git a/TestApp/Integrations/Local/project.yml b/TestApp/Integrations/Local/project.yml index 8a692b51d..0c26b5d0d 100644 --- a/TestApp/Integrations/Local/project.yml +++ b/TestApp/Integrations/Local/project.yml @@ -45,8 +45,6 @@ targets: product: ReadiumNavigator - package: Readium product: ReadiumAdapterGCDWebServer - - package: Readium - product: ReadiumAdapterZIPFoundation - package: Readium product: ReadiumOPDS - package: GRDB diff --git a/TestApp/Sources/App/Readium.swift b/TestApp/Sources/App/Readium.swift index 9ac6a8fb7..598b1a0d4 100644 --- a/TestApp/Sources/App/Readium.swift +++ b/TestApp/Sources/App/Readium.swift @@ -24,9 +24,7 @@ final class Readium { lazy var formatSniffer: FormatSniffer = DefaultFormatSniffer() lazy var assetRetriever = AssetRetriever( - formatSniffer: DefaultFormatSniffer(), - resourceFactory: DefaultResourceFactory(httpClient: httpClient), - archiveOpener: ZIPFoundationArchiveOpener() + httpClient: httpClient ) lazy var publicationOpener = PublicationOpener( diff --git a/Tests/SharedTests/Toolkit/Archive/Container+ZIPTests.swift b/Tests/SharedTests/Toolkit/Archive/Container+ZIPTests.swift deleted file mode 100644 index b3f4d0df4..000000000 --- a/Tests/SharedTests/Toolkit/Archive/Container+ZIPTests.swift +++ /dev/null @@ -1,194 +0,0 @@ -// -// Copyright 2024 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. -// - -@testable import ReadiumShared -import XCTest - -private let fixtures = Fixtures(path: "Archive") - -struct ZIPTester { - let make: (FileURL) async throws -> Container - - func testOpenSuccess() async throws { - _ = try await make(fixtures.url(for: "test.zip")) - } - - func testOpenNotFound() async { - do { - _ = try await make(fixtures.url(for: "unknown.zip")) - XCTFail("Expected an error") - } catch {} - } - - func testOpenNotAZIP() async { - do { - _ = try await make(fixtures.url(for: "not-a.zip")) - XCTFail("Expected an error") - } catch {} - } - - func testGetNonExistingEntry() async throws { - let container = try await make(fixtures.url(for: "test.zip")) - XCTAssertNil(container[AnyURL(path: "unknown")!]) - } - - func testEntries() async throws { - let container = try await make(fixtures.url(for: "test.zip")) - - XCTAssertEqual( - container.entries, - Set([ - AnyURL(path: ".hidden")!, - AnyURL(path: "A folder/Sub.folder%/file.txt")!, - AnyURL(path: "A folder/wasteland-cover.jpg")!, - AnyURL(path: "root.txt")!, - AnyURL(path: "uncompressed.jpg")!, - AnyURL(path: "uncompressed.txt")!, - AnyURL(path: "A folder/Sub.folder%/file-compressed.txt")!, - ]) - ) - } - - func testResources() async throws { - let container = try await make(fixtures.url(for: "test.zip")) - - try await AssertEntry(path: ".hidden", in: container, isCompressed: false, length: 0, originalLength: 0) - try await AssertEntry(path: "A folder/Sub.folder%/file.txt", in: container, isCompressed: false, length: 20, originalLength: 20) - try await AssertEntry(path: "A folder/wasteland-cover.jpg", in: container, isCompressed: true, length: 82374, originalLength: 103_477) - try await AssertEntry(path: "root.txt", in: container, isCompressed: false, length: 0, originalLength: 0) - try await AssertEntry(path: "uncompressed.jpg", in: container, isCompressed: false, length: 279_551, originalLength: 279_551) - try await AssertEntry(path: "uncompressed.txt", in: container, isCompressed: false, length: 30, originalLength: 30) - try await AssertEntry(path: "A folder/Sub.folder%/file-compressed.txt", in: container, isCompressed: true, length: 8659, originalLength: 29609) - } - - func testReadCompressedEntry() async throws { - let container = try await make(fixtures.url(for: "test.zip")) - let entry = try XCTUnwrap(container[AnyURL(path: "A folder/Sub.folder%/file-compressed.txt")!]) - let data = try await entry.read().get() - let string = String(data: data, encoding: .utf8)! - XCTAssertEqual(string.count, 29609) - XCTAssertTrue(string.hasPrefix("I'm inside\nthe ZIP.")) - } - - func testReadUncompressedEntry() async throws { - let container = try await make(fixtures.url(for: "test.zip")) - let entry = try XCTUnwrap(container[AnyURL(path: "A folder/Sub.folder%/file.txt")!]) - let data = try await entry.read().get() - XCTAssertNotNil(data) - XCTAssertEqual( - String(data: data, encoding: .utf8), - "I'm inside\nthe ZIP.\n" - ) - } - - func testReadUncompressedRange() async throws { - // 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 container = try await make(fixtures.url(for: "test.zip")) - let entry = try XCTUnwrap(container[AnyURL(path: "A folder/Sub.folder%/file.txt")!]) - let data = try await entry.read(range: 14 ..< 20).get() - XCTAssertEqual( - String(data: data, encoding: .utf8), - " ZIP.\n" - ) - } - - func testReadCompressedRange() async throws { - let container = try await make(fixtures.url(for: "test.zip")) - let entry = try XCTUnwrap(container[AnyURL(path: "A folder/Sub.folder%/file-compressed.txt")!]) - let data = try await entry.read(range: 14 ..< 20).get() - XCTAssertEqual( - String(data: data, encoding: .utf8), - " ZIP.\n" - ) - } - - private func AssertEntry( - path: String, - in container: Container, - isCompressed: Bool, - length: UInt64, - originalLength: UInt64 - ) async throws { - let resource = try XCTUnwrap(container[AnyURL(path: path)!]) - - let estimatedLength = try await resource.estimatedLength().get() - XCTAssertEqual(estimatedLength, originalLength) - - let properties = try await resource.properties().get() - XCTAssertEqual( - properties.archive, - ArchiveProperties( - entryLength: length, - isEntryCompressed: isCompressed - ) - ) - XCTAssertEqual(properties.filename, RelativeURL(path: path)!.lastPathSegment) - } -} - -class MinizipTests: XCTestCase { - lazy var tester = ZIPTester { - try await MinizipContainer.make(file: $0).get() - } - - func testOpenSuccess() async throws { try await tester.testOpenSuccess() } - func testOpenNotFound() async { await tester.testOpenNotFound() } - func testOpenNotAZIP() async { await tester.testOpenNotAZIP() } - func testGetNonExistingEntry() async throws { try await tester.testGetNonExistingEntry() } - func testEntries() async throws { try await tester.testEntries() } - func testResources() async throws { try await tester.testResources() } - func testReadCompressedEntry() async throws { try await tester.testReadCompressedEntry() } - func testReadUncompressedEntry() async throws { try await tester.testReadUncompressedEntry() } - func testReadCompressedRange() async throws { try await tester.testReadCompressedRange() } - func testReadUncompressedRange() async throws { try await tester.testReadUncompressedRange() } -} - -// class ZIPFoundationTests: XCTestCase { -// -// lazy var tester = ZIPTester { -// url in try ZIPFoundation.make(url: url).get() -// } -// -// func testOpenSuccess() async throws { try await tester.testOpenSuccess() } -// func testOpenNotFound() async { await tester.testOpenNotFound() } -// func testOpenNotAZIP() async { await tester.testOpenNotAZIP() } -// func testGetNonExistingEntry() async throws { try await tester.testGetNonExistingEntry() } -// func testEntries() async throws { try await tester.testEntries() } -// func testResources() async throws { try await tester.testResources() } -// func testReadCompressedEntry() async throws { try await tester.testReadCompressedEntry() } -// func testReadUncompressedEntry() async throws { try await tester.testReadUncompressedEntry() } -// func testReadCompressedRange() async throws { try await tester.testReadCompressedRange() } -// func testReadUncompressedRange() async throws { try await tester.testReadUncompressedRange() } -// -// } - -class ZIPBenchmarkingTests: XCTestCase { - func testCompareRange() async throws { - let containers: [Container] = [ - try! await MinizipContainer.make(file: fixtures.url(for: "test.zip")).get(), -// try! ZIPFoundationArchive(url: fixtures.url(for: "test.zip")) - ] - let path = AnyURL(path: "A folder/wasteland-cover.jpg")! - let length: UInt64 = 103_477 - - let entries = try containers - .map { try XCTUnwrap($0[path]) } - - measure { - let exp = expectation(description: "Finished") - Task { - let lower = UInt64.random(in: 0 ..< length - 100) - let upper = UInt64.random(in: lower ..< length) - let range = lower ..< upper - let datas = await entries.asyncMap { await $0.read(range: range).getOrNil() } - let data = datas[0] - XCTAssertTrue(datas.allSatisfy { $0 == data }) - exp.fulfill() - } - wait(for: [exp], timeout: 200.0) - } - } -} diff --git a/Tests/Adapters/ZIPFoundationTests/ZIPFoundationTests.swift b/Tests/SharedTests/Toolkit/Archive/ZIPFoundationTests.swift similarity index 98% rename from Tests/Adapters/ZIPFoundationTests/ZIPFoundationTests.swift rename to Tests/SharedTests/Toolkit/Archive/ZIPFoundationTests.swift index d64081163..4fc800cf0 100644 --- a/Tests/Adapters/ZIPFoundationTests/ZIPFoundationTests.swift +++ b/Tests/SharedTests/Toolkit/Archive/ZIPFoundationTests.swift @@ -4,11 +4,10 @@ // available in the top-level LICENSE file of the project. // -import ReadiumShared -@testable import ReadiumAdapterZIPFoundation +@testable import ReadiumShared import XCTest -private let fixtures = Fixtures() +private let fixtures = Fixtures(path: "Archive") class ZIPFoundationTests: XCTestCase { diff --git a/docs/Migration Guide.md b/docs/Migration Guide.md index 78293ba7d..607336185 100644 --- a/docs/Migration Guide.md +++ b/docs/Migration Guide.md @@ -2,6 +2,18 @@ All migration steps necessary in reading apps to upgrade to major versions of the Swift Readium toolkit will be documented in this file. +## Unreleased + +### CocoaPods dependencies + +Some CocoaPods dependencies are now part of the official trunk and handled automatically. You must remove the custom declarations from your `Podfile`: + +```diff +-pod 'Fuzi', podspec: 'https://raw.githubusercontent.com/readium/Fuzi/refs/heads/master/Fuzi.podspec' +-pod 'ReadiumGCDWebServer', podspec: 'https://raw.githubusercontent.com/readium/GCDWebServer/4.0.0/GCDWebServer.podspec' +``` + + ## 3.0.0-alpha.2 ### Error management From 6d44cd5f72521b9729336db5d4a0512d24f2f05f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Menu?= Date: Wed, 11 Dec 2024 11:58:31 +0100 Subject: [PATCH 3/4] Clean up test app integrations --- TestApp/Integrations/Carthage/project+lcp.yml | 10 ++++------ TestApp/Integrations/Carthage/project.yml | 10 +++++----- TestApp/Integrations/CocoaPods/Podfile | 4 ---- TestApp/Integrations/CocoaPods/Podfile+lcp | 4 ---- docs/Migration Guide.md | 6 ++++++ 5 files changed, 15 insertions(+), 19 deletions(-) diff --git a/TestApp/Integrations/Carthage/project+lcp.yml b/TestApp/Integrations/Carthage/project+lcp.yml index 71d2ba30f..a51ff0bdd 100644 --- a/TestApp/Integrations/Carthage/project+lcp.yml +++ b/TestApp/Integrations/Carthage/project+lcp.yml @@ -25,21 +25,19 @@ targets: dependencies: - framework: Carthage/Build/CryptoSwift.xcframework - framework: Carthage/Build/DifferenceKit.xcframework - - framework: Carthage/Build/Fuzi.xcframework - - framework: Carthage/Build/Minizip.xcframework - framework: Carthage/Build/R2LCPClient.xcframework - - framework: Carthage/Build/ReadiumNavigator.xcframework - - framework: Carthage/Build/ReadiumShared.xcframework - - framework: Carthage/Build/ReadiumStreamer.xcframework - framework: Carthage/Build/ReadiumAdapterGCDWebServer.xcframework - framework: Carthage/Build/ReadiumAdapterLCPSQLite.xcframework - framework: Carthage/Build/ReadiumGCDWebServer.xcframework - framework: Carthage/Build/ReadiumInternal.xcframework - framework: Carthage/Build/ReadiumLCP.xcframework + - framework: Carthage/Build/ReadiumNavigator.xcframework - framework: Carthage/Build/ReadiumOPDS.xcframework + - framework: Carthage/Build/ReadiumShared.xcframework + - framework: Carthage/Build/ReadiumStreamer.xcframework + - framework: Carthage/Build/ReadiumZIPFoundation.xcframework - framework: Carthage/Build/SQLite.xcframework - framework: Carthage/Build/SwiftSoup.xcframework - - framework: Carthage/Build/ZIPFoundation.xcframework - package: GRDB - package: Kingfisher - package: MBProgressHUD diff --git a/TestApp/Integrations/Carthage/project.yml b/TestApp/Integrations/Carthage/project.yml index a3a264099..6e035222c 100644 --- a/TestApp/Integrations/Carthage/project.yml +++ b/TestApp/Integrations/Carthage/project.yml @@ -25,15 +25,15 @@ targets: dependencies: - framework: Carthage/Build/CryptoSwift.xcframework - framework: Carthage/Build/DifferenceKit.xcframework - - framework: Carthage/Build/Fuzi.xcframework - - framework: Carthage/Build/Minizip.xcframework - - framework: Carthage/Build/ReadiumNavigator.xcframework - - framework: Carthage/Build/ReadiumShared.xcframework - - framework: Carthage/Build/ReadiumStreamer.xcframework - framework: Carthage/Build/ReadiumAdapterGCDWebServer.xcframework + - framework: Carthage/Build/ReadiumFuzi.xcframework - framework: Carthage/Build/ReadiumGCDWebServer.xcframework - framework: Carthage/Build/ReadiumInternal.xcframework + - framework: Carthage/Build/ReadiumNavigator.xcframework - framework: Carthage/Build/ReadiumOPDS.xcframework + - framework: Carthage/Build/ReadiumShared.xcframework + - framework: Carthage/Build/ReadiumStreamer.xcframework + - framework: Carthage/Build/ReadiumZIPFoundation.xcframework - framework: Carthage/Build/SwiftSoup.xcframework - package: GRDB - package: Kingfisher diff --git a/TestApp/Integrations/CocoaPods/Podfile b/TestApp/Integrations/CocoaPods/Podfile index 155e4b7d6..b66f6aaf8 100644 --- a/TestApp/Integrations/CocoaPods/Podfile +++ b/TestApp/Integrations/CocoaPods/Podfile @@ -10,10 +10,6 @@ target 'TestApp' do pod 'ReadiumAdapterGCDWebServer', podspec: 'https://raw.githubusercontent.com/readium/swift-toolkit/VERSION/Support/CocoaPods/ReadiumAdapterGCDWebServer.podspec' pod 'ReadiumOPDS', podspec: 'https://raw.githubusercontent.com/readium/swift-toolkit/VERSION/Support/CocoaPods/ReadiumOPDS.podspec' pod 'ReadiumInternal', podspec: 'https://raw.githubusercontent.com/readium/swift-toolkit/VERSION/Support/CocoaPods/ReadiumInternal.podspec' - pod 'Fuzi', podspec: 'https://raw.githubusercontent.com/readium/Fuzi/refs/heads/master/Fuzi.podspec' - - # Required for R2Streamer and ReadiumAdapterGCDWebServer. - pod 'ReadiumGCDWebServer', podspec: 'https://raw.githubusercontent.com/readium/GCDWebServer/master/GCDWebServer.podspec' pod 'GRDB.swift', '~> 6.0' pod 'Kingfisher', '~> 5.0' diff --git a/TestApp/Integrations/CocoaPods/Podfile+lcp b/TestApp/Integrations/CocoaPods/Podfile+lcp index bd97896cc..394acff97 100644 --- a/TestApp/Integrations/CocoaPods/Podfile+lcp +++ b/TestApp/Integrations/CocoaPods/Podfile+lcp @@ -12,12 +12,8 @@ target 'TestApp' do pod 'ReadiumOPDS', podspec: 'https://raw.githubusercontent.com/readium/swift-toolkit/VERSION/Support/CocoaPods/ReadiumOPDS.podspec' pod 'ReadiumLCP', podspec: 'https://raw.githubusercontent.com/readium/swift-toolkit/VERSION/Support/CocoaPods/ReadiumLCP.podspec' pod 'ReadiumInternal', podspec: 'https://raw.githubusercontent.com/readium/swift-toolkit/VERSION/Support/CocoaPods/ReadiumInternal.podspec' - pod 'Fuzi', podspec: 'https://raw.githubusercontent.com/readium/Fuzi/refs/heads/master/Fuzi.podspec' pod 'R2LCPClient', podspec: 'LCP_URL' - # Required for ReadiumStreamer and ReadiumAdapterGCDWebServer. - pod 'ReadiumGCDWebServer', podspec: 'https://raw.githubusercontent.com/readium/GCDWebServer/master/GCDWebServer.podspec' - pod 'GRDB.swift', '~> 6.0' pod 'Kingfisher', '~> 5.0' pod 'MBProgressHUD', '~> 1.0' diff --git a/docs/Migration Guide.md b/docs/Migration Guide.md index 607336185..5d744639e 100644 --- a/docs/Migration Guide.md +++ b/docs/Migration Guide.md @@ -4,6 +4,12 @@ All migration steps necessary in reading apps to upgrade to major versions of th ## Unreleased +### ZIPFoundation replaces Minizip + +The default `ZIPArchiveOpener` is now using ZIPFoundation instead of Minizip, with improved performances when reading ranges of `stored` ZIP entries. + +If you use Carthage, remove `Minizip.xcframework` from your dependencies and add `ReadiumZIPFoundation.xcframework` instead. No changes are needed when using Swift Package Manager or CocoaPods. + ### CocoaPods dependencies Some CocoaPods dependencies are now part of the official trunk and handled automatically. You must remove the custom declarations from your `Podfile`: From 68d0a037930af1ce4f7f0a32a0308a83b11506d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Menu?= Date: Wed, 11 Dec 2024 12:01:27 +0100 Subject: [PATCH 4/4] Lint --- .../Shared/Toolkit/ZIP/ZIPFoundation.swift | 12 ++++---- Support/Carthage/.xcodegen | 1 + .../Readium.xcodeproj/project.pbxproj | 4 +++ TestApp/Sources/App/Readium.swift | 1 - Tests/Adapters/ZIPFoundationTests/Fixtures | 1 - .../ZIPFoundationTests/Fixtures.swift | 29 ------------------- .../Toolkit/Archive/ZIPFoundationTests.swift | 11 ++++--- 7 files changed, 15 insertions(+), 44 deletions(-) delete mode 120000 Tests/Adapters/ZIPFoundationTests/Fixtures delete mode 100644 Tests/Adapters/ZIPFoundationTests/Fixtures.swift diff --git a/Sources/Shared/Toolkit/ZIP/ZIPFoundation.swift b/Sources/Shared/Toolkit/ZIP/ZIPFoundation.swift index ad448cc82..cc2c0d778 100644 --- a/Sources/Shared/Toolkit/ZIP/ZIPFoundation.swift +++ b/Sources/Shared/Toolkit/ZIP/ZIPFoundation.swift @@ -9,9 +9,8 @@ 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), @@ -152,14 +151,13 @@ private actor ZIPFoundationResource: Resource, Loggable { } func stream(range: Range?, consume: @escaping (Data) -> Void) async -> ReadResult { - if range != nil { - } + if range != nil {} return await archive().flatMap { archive in guard let entry = archive[entryPath] else { return .failure(.decoding("No entry found in the ZIP at \(entryPath)")) } - + do { if let range = range { try archive.extractRange(range, of: entry) { data in @@ -176,12 +174,12 @@ private actor ZIPFoundationResource: Resource, Loggable { } } } - + private var _archive: ReadResult? private func archive() async -> ReadResult { if _archive == nil { do { - _archive = .success(try ReadiumZIPFoundation.Archive(url: file.url, accessMode: .read, pathEncoding: nil)) + _archive = try .success(ReadiumZIPFoundation.Archive(url: file.url, accessMode: .read)) } catch { _archive = .failure(.decoding(error)) } diff --git a/Support/Carthage/.xcodegen b/Support/Carthage/.xcodegen index fea07ec7c..893a38592 100644 --- a/Support/Carthage/.xcodegen +++ b/Support/Carthage/.xcodegen @@ -13963,6 +13963,7 @@ ../../Sources/Shared/Toolkit/XML/XML.swift ../../Sources/Shared/Toolkit/ZIP ../../Sources/Shared/Toolkit/ZIP/ZIPArchiveOpener.swift +../../Sources/Shared/Toolkit/ZIP/ZIPFoundation.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 7baf1f194..0b159fa3e 100644 --- a/Support/Carthage/Readium.xcodeproj/project.pbxproj +++ b/Support/Carthage/Readium.xcodeproj/project.pbxproj @@ -155,6 +155,7 @@ 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 */; }; @@ -611,6 +612,7 @@ 567C115FF0939F69AD83AE82 /* UserRights.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserRights.swift; sourceTree = ""; }; 56C489452239BF85F4D14E95 /* PublicationMediaLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublicationMediaLoader.swift; sourceTree = ""; }; 57338C29681D4872D425AB81 /* UInt64.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UInt64.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 = ""; }; @@ -1105,6 +1107,7 @@ isa = PBXGroup; children = ( C51C74A5990A3BA93B3DC587 /* ZIPArchiveOpener.swift */, + 59624A8738EB3A791CEF8E4C /* ZIPFoundation.swift */, ); path = ZIP; sourceTree = ""; @@ -2664,6 +2667,7 @@ 5591563FD08A956B80C37716 /* XMLFormatSniffer.swift in Sources */, BBF5AAEEE90BD88D58191DA3 /* ZIPArchiveOpener.swift in Sources */, B49522888052E9F41D0DD013 /* ZIPFormatSniffer.swift in Sources */, + 6724EAA0D931CF252E5FAEDF /* ZIPFoundation.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/TestApp/Sources/App/Readium.swift b/TestApp/Sources/App/Readium.swift index 598b1a0d4..3ec960557 100644 --- a/TestApp/Sources/App/Readium.swift +++ b/TestApp/Sources/App/Readium.swift @@ -6,7 +6,6 @@ import Foundation import ReadiumAdapterGCDWebServer -import ReadiumAdapterZIPFoundation import ReadiumNavigator import ReadiumShared import ReadiumStreamer diff --git a/Tests/Adapters/ZIPFoundationTests/Fixtures b/Tests/Adapters/ZIPFoundationTests/Fixtures deleted file mode 120000 index 29c2bca42..000000000 --- a/Tests/Adapters/ZIPFoundationTests/Fixtures +++ /dev/null @@ -1 +0,0 @@ -../../SharedTests/Fixtures/Archive \ No newline at end of file diff --git a/Tests/Adapters/ZIPFoundationTests/Fixtures.swift b/Tests/Adapters/ZIPFoundationTests/Fixtures.swift deleted file mode 100644 index b47f90943..000000000 --- a/Tests/Adapters/ZIPFoundationTests/Fixtures.swift +++ /dev/null @@ -1,29 +0,0 @@ -// -// Copyright 2024 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 { - func url(for filepath: String) -> FileURL { - FileURL(url: Bundle.module.resourceURL!.appendingPathComponent("Fixtures/\(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/SharedTests/Toolkit/Archive/ZIPFoundationTests.swift b/Tests/SharedTests/Toolkit/Archive/ZIPFoundationTests.swift index 4fc800cf0..057df9a9a 100644 --- a/Tests/SharedTests/Toolkit/Archive/ZIPFoundationTests.swift +++ b/Tests/SharedTests/Toolkit/Archive/ZIPFoundationTests.swift @@ -10,11 +10,10 @@ import XCTest private let fixtures = Fixtures(path: "Archive") class ZIPFoundationTests: XCTestCase { - private func container(for filename: String) async throws -> Container { try await ZIPFoundationContainer.make(file: fixtures.url(for: filename)).get() } - + func testOpenSuccess() async throws { _ = try await container(for: "test.zip") } @@ -107,9 +106,9 @@ class ZIPFoundationTests: XCTestCase { " ZIP.\n" ) } - + func testRandomCompressedRead() async throws { - for _ in 0..<100 { + for _ in 0 ..< 100 { let container = try await container(for: "test.zip") let entry = try XCTUnwrap(container[AnyURL(path: "A folder/wasteland-cover.jpg")!]) let length: UInt64 = 103_477 @@ -119,9 +118,9 @@ class ZIPFoundationTests: XCTestCase { _ = try await entry.read(range: range).get() } } - + func testRandomStoredRead() async throws { - for _ in 0..<100 { + for _ in 0 ..< 100 { let container = try await container(for: "test.zip") let entry = try XCTUnwrap(container[AnyURL(path: "uncompressed.jpg")!]) let length: UInt64 = 279_551