From 018bac49bff9b4f6e7d2da0b53309d175ef0fdbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mickae=CC=88l=20Menu?= Date: Tue, 10 Oct 2023 14:58:11 +0200 Subject: [PATCH 01/11] Remove deprecated APIs --- Sources/LCP/Deprecated.swift | 50 --- Sources/OPDS/Deprecated.swift | 28 -- Sources/Shared/DRM/DRM+Deprecated.swift | 144 ------- .../Shared/Publication/Asset/FileAsset.swift | 6 - .../Shared/Publication/ContentLayout.swift | 25 -- .../Presentation/Presentation.swift | 3 - Sources/Shared/Publication/Locator.swift | 23 -- .../Publication/Publication+Deprecated.swift | 353 +----------------- Sources/Shared/Publication/Publication.swift | 144 +++---- .../Services/Positions/PositionsService.swift | 6 - Sources/Shared/Toolkit/DocumentTypes.swift | 39 -- .../Media Type/MediaType+Deprecated.swift | 42 --- Sources/Streamer/Model/Container.swift | 80 ---- Sources/Streamer/Model/HTTPContainer.swift | 34 -- .../Streamer/Model/PublicationContainer.swift | 36 -- .../Streamer/Parser/Audio/AudioParser.swift | 6 - Sources/Streamer/Parser/EPUB/EPUBParser.swift | 12 - Sources/Streamer/Parser/Image/CBZParser.swift | 23 -- .../Streamer/Parser/Image/ImageParser.swift | 6 - .../Streamer/Parser/PDF/PDFFileParser.swift | 113 ------ Sources/Streamer/Parser/PDF/PDFParser.swift | 11 - .../Streamer/Parser/Parser+Deprecated.swift | 29 -- .../Parser/Readium/ReadiumWebPubParser.swift | 12 - .../Streamer/Server/PublicationServer.swift | 5 - Sources/Streamer/Streamer.swift | 3 - .../Streamer/Toolkit/Extensions/Fetcher.swift | 18 - Sources/Streamer/Toolkit/Logger.swift | 13 - 27 files changed, 73 insertions(+), 1191 deletions(-) delete mode 100644 Sources/LCP/Deprecated.swift delete mode 100644 Sources/OPDS/Deprecated.swift delete mode 100644 Sources/Shared/DRM/DRM+Deprecated.swift delete mode 100644 Sources/Shared/Publication/ContentLayout.swift delete mode 100644 Sources/Shared/Toolkit/Media Type/MediaType+Deprecated.swift delete mode 100644 Sources/Streamer/Model/Container.swift delete mode 100644 Sources/Streamer/Model/HTTPContainer.swift delete mode 100644 Sources/Streamer/Model/PublicationContainer.swift delete mode 100644 Sources/Streamer/Parser/Image/CBZParser.swift delete mode 100644 Sources/Streamer/Parser/PDF/PDFFileParser.swift delete mode 100644 Sources/Streamer/Parser/Parser+Deprecated.swift delete mode 100644 Sources/Streamer/Toolkit/Logger.swift diff --git a/Sources/LCP/Deprecated.swift b/Sources/LCP/Deprecated.swift deleted file mode 100644 index 6692d514a..000000000 --- a/Sources/LCP/Deprecated.swift +++ /dev/null @@ -1,50 +0,0 @@ -// -// Copyright 2023 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 R2Shared - -public extension LCPService { - /// Imports a protected publication from a standalone LCPL file. - @available(*, unavailable, message: "Use `acquirePublication()` instead", renamed: "acquirePublication") - func importPublication(from lcpl: URL, authentication: LCPAuthenticating?, sender: Any?, completion: @escaping (CancellableResult) -> Void) -> Observable { - fatalError("Not available anymore") - } - - @available(*, unavailable, message: "Use `acquirePublication()` instead", renamed: "acquirePublication") - func importPublication(from lcpl: URL, authentication: LCPAuthenticating?, completion: @escaping (CancellableResult) -> Void) -> Observable { - fatalError("Not available anymore") - } -} - -/// LCP service factory. -@available(*, unavailable, message: "Use `LCPService()` instead", renamed: "LCPService") -public func R2MakeLCPService() -> LCPService { - fatalError("Not implemented") -} - -@available(*, unavailable, message: "Remove all the code in `handleLcpPublication` and use `LCPLibraryService.loadPublication` instead, in the latest version of r2-testapp-swift") -public final class LcpSession {} - -public final class LcpLicense { - @available(*, unavailable, message: "Replace all the LCP code in `publication(at:)` by `LCPService.importPublication` (see `LCPLibraryService.fulfill` in the latest version)") - public init(withLicenseDocumentAt url: URL) throws {} - - @available(*, unavailable) - public init(withLicenseDocumentIn url: URL) throws {} - - @available(*, unavailable, message: "Removing the LCP license is not needed anymore, delete the LCP-related code in `remove(publication:)`") - public func removeDataBaseItem() throws {} - - @available(*, unavailable, message: "Removing the LCP license is not needed anymore, delete the LCP-related code in `remove(publication:)`") - public static func removeDataBaseItem(licenseID: String) throws {} -} - -@available(*, unavailable, message: "Remove `promptPassphrase` and implement the protocol `LCPAuthenticating` instead (see LCPLibraryService in the latest version)") -public enum LcpError: Error {} - -@available(*, unavailable, renamed: "LCPAcquisition.Publication") -public typealias LCPImportedPublication = LCPAcquisition.Publication diff --git a/Sources/OPDS/Deprecated.swift b/Sources/OPDS/Deprecated.swift deleted file mode 100644 index b6e36c9e2..000000000 --- a/Sources/OPDS/Deprecated.swift +++ /dev/null @@ -1,28 +0,0 @@ -// -// Copyright 2023 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 R2Shared - -public typealias Promise = Void - -public extension OPDSParser { - @available(*, unavailable, message: "Use `parseURL(url:completion:)` instead") - static func parseURL(url: URL) -> Promise {} -} - -public extension OPDS1Parser { - @available(*, unavailable, message: "Use `parseURL(url:completion:)` instead") - static func parseURL(url: URL) -> Promise {} - - @available(*, unavailable, message: "Use `fetchOpenSearchTemplate(feed:completion:)` instead") - static func fetchOpenSearchTemplate(feed: Feed) -> Promise {} -} - -public extension OPDS2Parser { - @available(*, unavailable, message: "Use `parseURL(url:completion:)` instead") - static func parseURL(url: URL) -> Promise {} -} diff --git a/Sources/Shared/DRM/DRM+Deprecated.swift b/Sources/Shared/DRM/DRM+Deprecated.swift deleted file mode 100644 index 3ecb307c1..000000000 --- a/Sources/Shared/DRM/DRM+Deprecated.swift +++ /dev/null @@ -1,144 +0,0 @@ -// -// Copyright 2023 Readium Foundation. All rights reserved. -// Use of this source code is governed by the BSD-style license -// available in the top-level LICENSE file of the project. -// - -import Foundation - -/// An object giving info about the DRM encrypting a publication. -/// This object come back from the streamer, and can be filled by a DRM module, then sent back to the streamer (with the decypher func filled) in order to allow the fetcher to be able to decypher content later on. -@available(*, unavailable, message: "The new `Streamer` is handling DRM through a `ContentProtectionService`") -public struct DRM { - public let brand: Brand - public let scheme: Scheme - - /// The license will be filled when passed back to the DRM module. - public var license: DRMLicense? - - public enum Brand: String { - case lcp - } - - public enum Scheme: String { - case lcp = "http://readium.org/2014/01/lcp" - } - - public init(brand: Brand) { - self.brand = brand - switch brand { - case .lcp: - scheme = .lcp - } - } -} - -/// Shared DRM behavior for a particular license/publication. -/// DRMs can be very different beasts, so DRMLicense is not meant to be a generic interface for all DRM behaviors (eg. loan return). The goal of DRMLicense is to provide generic features that are used inside Readium's projects directly. For example, data decryption or copy of text selection in the navigator. -/// If there's a need for other generic DRM features, it can be implemented as a set of adapters in the client app, to cater to the interface's needs and capabilities. -@available(*, unavailable, message: "The new `Streamer` is handling DRM through a `ContentProtectionService`") -public protocol DRMLicense { - /// Encryption profile, if available. - var encryptionProfile: String? { get } - - /// Depichers the given encrypted data to be displayed in the reader. - func decipher(_ data: Data) throws -> Data? - - /// Returns whether the user can copy extracts from the publication. - var canCopy: Bool { get } - - /// Processes the given text to be copied by the user. - /// For example, you can save how much characters was copied to limit the overall quantity. - /// - Parameter consumes: If true, then the user's copy right is consumed accordingly to the `text` input. Sets to false if you want to peek at the processed text without debiting the rights straight away. - /// - Returns: The (potentially modified) text to put in the user clipboard, or nil if the user is not allowed to copy it. - func copy(_ text: String, consumes: Bool) -> String? -} - -@available(*, unavailable) -public extension DRMLicense { - var encryptionProfile: String? { nil } - - var canCopy: Bool { true } - - func copy(_ text: String, consumes: Bool) -> String? { - canCopy ? text : nil - } -} - -@available(*, unavailable, renamed: "DRM") -public typealias Drm = DRM - -@available(*, unavailable, renamed: "DRMLicense") -public typealias DrmLicense = DRMLicense - -@available(*, unavailable) -public extension DRM { - @available(*, unavailable, message: "Use `license?.encryptionProfile` instead") - var profile: String? { - license?.encryptionProfile - } -} - -@available(*, unavailable) -public extension DRMLicense { - @available(*, unavailable, message: "Use `LCPLicense.renewLoan` instead") - func renew(endDate: Date?, completion: @escaping (Error?) -> Void) { - completion(nil) - } - - @available(*, unavailable, message: "Use `LCPLicense.returnPublication` instead") - func `return`(completion: @escaping (Error?) -> Void) { - completion(nil) - } - - @available(*, unavailable, message: "Checking for the rights is handled by r2-lcp-swift now") - func areRightsValid() throws {} - - @available(*, unavailable, message: "Registering the device is handled by r2-lcp-swift now") - func register() {} - - @available(*, unavailable, message: "Update DrmManagementTableViewController from r2-testapp-swift") - func currentStatus() -> String { - "" - } - - @available(*, unavailable, message: "Update DrmManagementTableViewController from r2-testapp-swift") - func lastUpdate() -> Date { - Date() - } - - @available(*, unavailable, message: "Update DrmManagementTableViewController from r2-testapp-swift") - func issued() -> Date { - Date() - } - - @available(*, unavailable, message: "Update DrmManagementTableViewController from r2-testapp-swift") - func provider() -> URL { - URL(fileURLWithPath: "/") - } - - @available(*, unavailable, message: "Update DrmManagementTableViewController from r2-testapp-swift") - func rightsEnd() -> Date? { - nil - } - - @available(*, unavailable, message: "Update DrmManagementTableViewController from r2-testapp-swift") - func potentialRightsEnd() -> Date? { - nil - } - - @available(*, unavailable, message: "Update DrmManagementTableViewController from r2-testapp-swift") - func rightsStart() -> Date? { - nil - } - - @available(*, unavailable, message: "Update DrmManagementTableViewController from r2-testapp-swift") - func rightsPrints() -> Int? { - nil - } - - @available(*, unavailable, message: "Update DrmManagementTableViewController from r2-testapp-swift") - func rightsCopies() -> Int? { - nil - } -} diff --git a/Sources/Shared/Publication/Asset/FileAsset.swift b/Sources/Shared/Publication/Asset/FileAsset.swift index e846943c9..f0e027772 100644 --- a/Sources/Shared/Publication/Asset/FileAsset.swift +++ b/Sources/Shared/Publication/Asset/FileAsset.swift @@ -69,9 +69,3 @@ extension FileAsset: CustomStringConvertible { "FileAsset(\(url.path))" } } - -/// Represents a path on the file system. -/// -/// Used to cache the `MediaType` to avoid computing it at different locations. -@available(*, unavailable, renamed: "FileAsset") -public typealias File = FileAsset diff --git a/Sources/Shared/Publication/ContentLayout.swift b/Sources/Shared/Publication/ContentLayout.swift deleted file mode 100644 index 02bd2e436..000000000 --- a/Sources/Shared/Publication/ContentLayout.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// Copyright 2023 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 - -@available(*, unavailable, message: "Use `publication.metadata.effectiveReadingProgression` instead") -public enum ContentLayout: String { - case rtl - case ltr - case cjkVertical = "cjk-vertical" - case cjkHorizontal = "cjk-horizontal" - - @available(*, unavailable, message: "Use `publication.metadata.effectiveReadingProgression` instead", renamed: "metadata.effectiveReadingProgression") - public var readingProgression: ReadingProgression { - switch self { - case .rtl, .cjkVertical: - return .rtl - case .ltr, .cjkHorizontal: - return .ltr - } - } -} diff --git a/Sources/Shared/Publication/Extensions/Presentation/Presentation.swift b/Sources/Shared/Publication/Extensions/Presentation/Presentation.swift index caf0c5185..2d0bd5b87 100644 --- a/Sources/Shared/Publication/Extensions/Presentation/Presentation.swift +++ b/Sources/Shared/Publication/Extensions/Presentation/Presentation.swift @@ -119,9 +119,6 @@ public struct Presentation: Equatable { case scrolled /// The User Agent can decide how overflow should be handled. case auto - - @available(*, unavailable, message: "Use `Presentation.continuous` instead") - static let scrolledContinuous: Overflow = .scrolled } /// Indicates how the linked resource should be displayed in a reading environment that diff --git a/Sources/Shared/Publication/Locator.swift b/Sources/Shared/Publication/Locator.swift index cf3c7931f..adb84a310 100644 --- a/Sources/Shared/Publication/Locator.swift +++ b/Sources/Shared/Publication/Locator.swift @@ -191,19 +191,6 @@ public struct Locator: Hashable, CustomStringConvertible, Loggable { /// Syntactic sugar to access the `otherLocations` values by subscripting `Locations` directly. /// locations["cssSelector"] == locations.otherLocations["cssSelector"] public subscript(key: String) -> Any? { otherLocations[key] } - - @available(*, unavailable, renamed: "init(jsonString:)") - public init(fromString: String) { - fatalError() - } - - @available(*, unavailable, renamed: "jsonString") - public func toString() -> String? { - fatalError() - } - - @available(*, unavailable, message: "Use `fragments.first` instead") - public var fragment: String? { fragments.first } } public struct Text: Hashable, Loggable { @@ -283,16 +270,6 @@ public struct Locator: Hashable, CustomStringConvertible, Loggable { highlight: Optional(String(newHighlight)).takeIf { !$0.isEmpty } ) } - - @available(*, unavailable, renamed: "init(jsonString:)") - public init(fromString: String) { - fatalError() - } - - @available(*, unavailable, renamed: "jsonString") - public func toString() -> String? { - fatalError() - } } } diff --git a/Sources/Shared/Publication/Publication+Deprecated.swift b/Sources/Shared/Publication/Publication+Deprecated.swift index fdeff8f01..c2ab27528 100644 --- a/Sources/Shared/Publication/Publication+Deprecated.swift +++ b/Sources/Shared/Publication/Publication+Deprecated.swift @@ -6,360 +6,9 @@ import Foundation -@available(*, unavailable, renamed: "Publication") -public typealias WebPublication = Publication - public extension Publication { - @available(*, deprecated, message: "format and formatVersion are deprecated", renamed: "init(manifest:fetcher:servicesBuilder:)") + @available(*, unavailable, message: "format and formatVersion are deprecated", renamed: "init(manifest:fetcher:servicesBuilder:)") convenience init(manifest: Manifest, fetcher: Fetcher = EmptyFetcher(), servicesBuilder: PublicationServicesBuilder = .init(), format: Format = .unknown, formatVersion: String? = nil) { - self.init(manifest: manifest, fetcher: fetcher, servicesBuilder: servicesBuilder) - self.format = format - self.formatVersion = formatVersion - } - - @available(*, unavailable, renamed: "init(manifest:)") - convenience init() { - self.init(manifest: Manifest(metadata: Metadata(title: ""))) - } - - @available(*, unavailable, renamed: "init(format:formatVersion:manifest:)") - convenience init(format: Format = .unknown, formatVersion: String? = nil, positionListFactory: @escaping (Publication) -> [Locator] = { _ in [] }, context: [String] = [], metadata: Metadata, links: [Link] = [], readingOrder: [Link] = [], resources: [Link] = [], tableOfContents: [Link] = [], otherCollections: [String: [PublicationCollection]] = [:]) { - self.init( - manifest: Manifest(context: context, metadata: metadata, links: links, readingOrder: readingOrder, resources: resources, tableOfContents: tableOfContents, subcollections: otherCollections) - ) - } - - @available(*, unavailable, message: "Custom HREF normalization is not supported anymore", renamed: "init(json:)") - convenience init(json: Any, warnings: WarningLogger? = nil, normalizeHref: (String) -> String = { $0 }) throws { - fatalError("Not available.") - } - - @available(*, unavailable, renamed: "formatVersion") - var version: Double { 0 } - - /// Factory used to build lazily the `positionList`. - /// By default, a parser will set this to parse the `positionList` from the publication. But the host app might want to overwrite this with a custom closure to implement for example a cache mechanism. - @available(*, unavailable, message: "Implement `PositionsService` instead") - var positionListFactory: (Publication) -> [Locator] { { _ in [] } } - - @available(*, unavailable, renamed: "baseURL") - var baseUrl: URL? { baseURL } - - @available(*, unavailable, message: "This is not used anymore, don't set it") - var updatedDate: Date { Date() } - - @available(*, unavailable, message: "Check the publication's type using `conforms(to:)` instead") - var internalData: [String: String] { [:] } - - @available(*, unavailable, renamed: "json") - var manifestCanonical: String { jsonManifest ?? "" } - - @available(*, unavailable, renamed: "init(json:)") - static func parse(pubDict: [String: Any]) throws -> Publication { - fatalError("Not available") - } - - @available(*, unavailable, renamed: "positions") - var positionList: [Locator] { positions } - - @available(*, unavailable, renamed: "positionsByResource") - var positionListByResource: [String: [Locator]] { [:] } - - @available(*, unavailable, renamed: "subcollections") - var otherCollections: [String: [PublicationCollection]] { subcollections } - - @available(*, unavailable, renamed: "link(withHREF:)") - func resource(withRelativePath path: String) -> Link? { - link(withHREF: path) - } - - @available(*, unavailable, renamed: "link(withHREF:)") - func resource(withHref href: String) -> Link? { - link(withHREF: href) - } - - @available(*, unavailable, message: "Use `setSelfLink` instead") - func addSelfLink(endpoint: String, for baseURL: URL) { - let manifestURL = baseURL.appendingPathComponent("\(endpoint)/manifest.json") - setSelfLink(href: manifestURL.absoluteString) - } - - @available(*, unavailable, message: "`Publication` is now immutable") - internal func setCollectionLinks(_ links: [Link], forRole role: String) {} - - @available(*, unavailable, renamed: "link(withHREF:)") - func link(withHref href: String) -> Link? { - link(withHREF: href) - } - - @available(*, unavailable, message: "This will be removed in a future version") - func link(where predicate: (Link) -> Bool) -> Link? { - (resources + readingOrder + links).first(where: predicate) - } - - @available(*, unavailable, message: "Use `link.url(relativeTo: publication.baseURL)` instead") - func uriTo(link: Link?) -> URL? { - link?.url(relativeTo: baseURL) - } - - @available(*, unavailable, message: "Use `link.url(relativeTo: publication.baseURL)` instead") - func url(to link: Link?) -> URL? { - link?.url(relativeTo: baseURL) - } - - @available(*, unavailable, message: "Use `link.url(relativeTo: publication.baseURL)` instead") - func url(to href: String?) -> URL? { - href.flatMap { link(withHREF: $0)?.url(relativeTo: baseURL) } - } - - @available(*, unavailable, message: "Use `cover` to get the `UIImage` directly, or `link(withRel: \"cover\")` if you really want the cover link", renamed: "cover") - var coverLink: Link? { link(withRel: .cover) } - - @available(*, unavailable, message: "Use `metadata.effectiveReadingProgression` instead", renamed: "metadata.effectiveReadingProgression") - var contentLayout: ReadingProgression { metadata.effectiveReadingProgression } - - @available(*, unavailable, message: "Use `metadata.effectiveReadingProgression` instead", renamed: "metadata.effectiveReadingProgression") - func contentLayout(forLanguage language: String?) -> ReadingProgression { metadata.effectiveReadingProgression } -} - -public extension Publication { - @available(*, unavailable, renamed: "listOfAudioClips") - var listOfAudioFiles: [Link] { listOfAudioClips } - - @available(*, unavailable, renamed: "listOfVideoClips") - var listOfVideos: [Link] { listOfVideoClips } -} - -@available(*, unavailable, renamed: "LocalizedString") -public typealias MultilangString = LocalizedString - -public extension LocalizedString { - @available(*, unavailable, message: "Get with the property `string`") - var singleString: String? { - string.isEmpty ? nil : string - } - - @available(*, unavailable, message: "Get with `string(forLanguageCode:)`") - var multiString: [String: String] { - guard case let .localized(strings) = self else { - return [:] - } - return strings - } - - @available(*, unavailable, renamed: "LocalizedString.localized") - init() { - self = .localized([:]) - } -} - -public extension Metadata { - @available(*, unavailable, renamed: "type") - var rdfType: String? { type } - - @available(*, unavailable, renamed: "localizedTitle") - var multilangTitle: LocalizedString { localizedTitle } - - @available(*, unavailable, renamed: "localizedSubtitle") - var multilangSubtitle: LocalizedString? { localizedSubtitle } - - @available(*, unavailable, message: "Not used anymore, you can store the rights in `otherMetadata[\"rights\"]` if necessary") - var rights: String? { nil } - - @available(*, unavailable, message: "Not used anymore, you can store the source in `otherMetadata[\"source\"]` if necessary") - var source: String? { nil } - - @available(*, unavailable, renamed: "init(title:)") - init() { - self.init(title: "") - } - - @available(*, unavailable, message: "Use `localizedTitle.string(forLanguageCode:)` instead") - func titleForLang(_ lang: String) -> String? { - localizedTitle.string(forLanguageCode: lang) - } - - @available(*, unavailable, message: "Use `localizedSubtitle.string(forLanguageCode:)` instead") - func subtitleForLang(_ lang: String) -> String? { - localizedSubtitle?.string(forLanguageCode: lang) - } - - @available(*, unavailable, renamed: "init(json:)") - static func parse(metadataDict: [String: Any]) throws -> Metadata { - try Metadata(json: metadataDict, normalizeHREF: { $0 }) - } - - @available(*, unavailable, renamed: "presentation") - var rendition: EPUBRendition { presentation } - - @available(*, unavailable, message: "Use `effectiveReadingProgression` instead", renamed: "effectiveReadingProgression") - var contentLayout: ReadingProgression { effectiveReadingProgression } - - @available(*, unavailable, message: "Use `effectiveReadingProgression` instead", renamed: "effectiveReadingProgression") - func contentLayout(forLanguage language: String?) -> ReadingProgression { effectiveReadingProgression } -} - -public extension PublicationCollection { - @available(*, unavailable, renamed: "subcollections") - var otherCollections: [String: [PublicationCollection]] { subcollections } -} - -public extension Contributor { - @available(*, unavailable, renamed: "localizedName") - var multilangName: LocalizedString { localizedName } - - @available(*, unavailable, renamed: "init(name:)") - init() { - self.init(name: "") - } - - @available(*, unavailable, renamed: "init(json:)") - static func parse(_ cDict: [String: Any]) throws -> Contributor { - fatalError() - } - - @available(*, unavailable, message: "Use `[Contributor](json:)` instead") - static func parse(contributors: Any) throws -> [Contributor] { fatalError() } } - -public extension Subject { - @available(*, unavailable, renamed: "init(name:)") - init() { - self.init(name: "") - } -} - -public extension Link { - @available(*, unavailable, renamed: "type") - var typeLink: String? { type } - - @available(*, unavailable, renamed: "rels") - var rel: [String] { rels.map(\.string) } - - @available(*, unavailable, renamed: "href") - var absoluteHref: String? { href } - - @available(*, unavailable, renamed: "init(href:)") - init() { - self.init(href: "") - } - - @available(*, unavailable, renamed: "init(json:warnings:normalizeHREF:)") - init(json: Any, warnings: WarningLogger? = nil, normalizeHref: (String) -> String) throws { - try self.init(json: json, warnings: warnings, normalizeHREF: normalizeHref) - } - - @available(*, unavailable, renamed: "init(json:)") - static func parse(linkDict: [String: Any]) throws -> Link { - fatalError() - } - - @available(*, unavailable, message: "The media overlay API was only half implemented and will be refactored later") - var mediaOverlays: MediaOverlays { MediaOverlays() } -} - -public extension Array where Element == Link { - @available(*, unavailable, renamed: "init(json:warnings:normalizeHREF:)") - init(json: Any?, warnings: WarningLogger? = nil, normalizeHref: (String) -> String) { - self.init(json: json, warnings: warnings, normalizeHREF: normalizeHref) - } -} - -public extension Properties { - @available(*, unavailable, renamed: "Presentation.Orientation") - typealias Orientation = Presentation.Orientation - - @available(*, unavailable, renamed: "Presentation.Page") - typealias Page = Presentation.Page - - @available(*, unavailable, renamed: "indirectAcquisitions") - var indirectAcquisition: [OPDSAcquisition] { - indirectAcquisitions - } - - @available(*, unavailable, message: "The media overlay API was only half implemented and will be refactored later") - var mediaOverlay: String? { nil } - - @available(*, unavailable, message: "`Properties` is now immutable") - internal mutating func setProperty(_ value: T?, forKey key: String) {} - - @available(*, unavailable, message: "`Properties` is now immutable") - internal mutating func setProperty(_ value: T?, forKey key: String) {} -} - -public extension Presentation { - @available(*, unavailable, renamed: "EPUBLayout") - typealias Layout = EPUBLayout -} - -@available(*, unavailable, renamed: "OPDSPrice") -public typealias Price = OPDSPrice - -@available(*, unavailable, renamed: "OPDSAcquisition") -public typealias IndirectAcquisition = OPDSAcquisition - -public extension OPDSAcquisition { - @available(*, unavailable, renamed: "type") - var typeAcquisition: String { type } - - @available(*, unavailable, renamed: "children") - var child: [OPDSAcquisition] { children } -} - -@available(*, unavailable, renamed: "ContentLayout") -public typealias ContentLayoutStyle = ContentLayout - -@available(*, unavailable, renamed: "Presentation") -public typealias EPUBRendition = Presentation - -@available(*, unavailable, renamed: "Encryption") -public typealias EPUBEncryption = Encryption - -@available(*, unavailable, renamed: "Locator.Locations") -public typealias Locations = Locator.Locations - -@available(*, unavailable, renamed: "Locator.Text") -public typealias LocatorText = Locator.Text - -@available(*, unavailable, message: "Use your own Bookmark model in your app, this one is not used by Readium 2 anymore") -public class Bookmark { - public var id: Int64? - public var bookID: Int = 0 - public var publicationID: String - public var resourceIndex: Int - public var locator: Locator - public var creationDate: Date - - public init(id: Int64? = nil, publicationID: String, resourceIndex: Int, locator: Locator, creationDate: Date = Date()) { - self.id = id - self.publicationID = publicationID - self.resourceIndex = resourceIndex - self.locator = locator - self.creationDate = creationDate - } - - public convenience init(bookID: Int, publicationID: String, resourceIndex: Int, resourceHref: String, resourceType: String, resourceTitle: String, location: Locations, locatorText: LocatorText, creationDate: Date = Date(), id: Int64? = nil) { - self.init( - id: id, - publicationID: publicationID, - resourceIndex: resourceIndex, - locator: Locator( - href: resourceHref, - type: resourceType, - title: resourceTitle, - locations: location, - text: locatorText - ), - creationDate: creationDate - ) - } - - public var resourceHref: String { locator.href } - public var resourceType: String { locator.type } - public var resourceTitle: String { locator.title ?? "" } - public var location: Locations { locator.locations } - public var locations: Locations? { locator.locations } - public var locatorText: LocatorText { locator.text } -} diff --git a/Sources/Shared/Publication/Publication.swift b/Sources/Shared/Publication/Publication.swift index 3a3481674..c146e5b40 100644 --- a/Sources/Shared/Publication/Publication.swift +++ b/Sources/Shared/Publication/Publication.swift @@ -9,12 +9,6 @@ import Foundation /// Shared model for a Readium Publication. public class Publication: Loggable { - /// Format of the publication, if specified. - @available(*, deprecated, message: "Use publication.conforms(to:) to check the profile of a Publication") - public var format: Format = .unknown - /// Version of the publication's format, eg. 3 for EPUB 3 - @available(*, deprecated, message: "This API will be removed in a future version. If you still need it, please explain your use case at https://github.com/readium/swift-toolkit/issues/new") - public var formatVersion: String? private var manifest: Manifest private let fetcher: Fetcher @@ -172,67 +166,6 @@ public class Publication: Loggable { public static let pdf = Profile("https://readium.org/webpub-manifest/profiles/pdf") } - public enum Format: Equatable, Hashable { - /// Formats natively supported by Readium. - case cbz, epub, pdf, webpub - /// Default value when the format is not specified. - case unknown - - /// Finds the format for the given mimetype. - public init(mimetype: String?) { - guard let mimetype = mimetype else { - self = .unknown - return - } - self.init(mimetypes: [mimetype]) - } - - /// Finds the format from a list of possible mimetypes or fallback on a file extension. - public init(mimetypes: [String] = [], fileExtension: String? = nil) { - self.init(mediaType: .of(mediaTypes: mimetypes, fileExtensions: Array(ofNotNil: fileExtension))) - } - - /// Finds the format of the publication at the given url. - /// Uses the format declared as exported UTIs in the app's Info.plist, or fallbacks on the file extension. - /// - /// - Parameter mimetype: Fallback mimetype if the UTI can't be determined. - public init(file: URL, mimetype: String) { - self.init(file: file, mimetypes: [mimetype]) - } - - /// Finds the format of the publication at the given url. - /// Uses the format declared as exported UTIs in the app's Info.plist, or fallbacks on the file extension. - /// - /// - Parameter mimetypes: Fallback mimetypes if the UTI can't be determined. - public init(file: URL, mimetypes: [String] = []) { - self.init(mediaType: .of(file, mediaTypes: mimetypes, fileExtensions: [])) - } - - private init(mediaType: MediaType?) { - guard let mediaType = mediaType else { - self = .unknown - return - } - switch mediaType { - case .epub: - self = .epub - case .cbz: - self = .cbz - case .pdf, .lcpProtectedPDF: - self = .pdf - case .readiumWebPubManifest, .readiumAudiobookManifest: - self = .webpub - default: - self = .unknown - } - } - - @available(*, unavailable, renamed: "init(file:)") - public init(url: URL) { - self.init(file: url) - } - } - /// Errors occurring while opening a Publication. public enum OpeningError: LocalizedError { /// The file format could not be recognized by any parser. @@ -279,7 +212,6 @@ public class Publication: Loggable { public typealias Transform = (_ mediaType: MediaType, _ manifest: inout Manifest, _ fetcher: inout Fetcher, _ services: inout PublicationServicesBuilder) -> Void private let mediaType: MediaType - private let format: Format private var manifest: Manifest private var fetcher: Fetcher private var servicesBuilder: PublicationServicesBuilder @@ -288,9 +220,14 @@ public class Publication: Loggable { /// This is used for backwrad compatibility, until `Publication` is purely immutable. private let setupPublication: ((Publication) -> Void)? - public init(mediaType: MediaType, format: Format, manifest: Manifest, fetcher: Fetcher, servicesBuilder: PublicationServicesBuilder = .init(), setupPublication: ((Publication) -> Void)? = nil) { + public init( + mediaType: MediaType, + manifest: Manifest, + fetcher: Fetcher, + servicesBuilder: PublicationServicesBuilder = .init(), + setupPublication: ((Publication) -> Void)? = nil + ) { self.mediaType = mediaType - self.format = format self.manifest = manifest self.fetcher = fetcher self.servicesBuilder = servicesBuilder @@ -310,11 +247,74 @@ public class Publication: Loggable { let publication = Publication( manifest: manifest, fetcher: fetcher, - servicesBuilder: servicesBuilder, - format: format + servicesBuilder: servicesBuilder ) setupPublication?(publication) return publication } } + + /// Format of the publication, if specified. + @available(*, unavailable, message: "Use publication.conforms(to:) to check the profile of a Publication") + public var format: Format { fatalError() } + /// Version of the publication's format, eg. 3 for EPUB 3 + @available(*, unavailable, message: "This API will be removed in a future version. If you still need it, please explain your use case at https://github.com/readium/swift-toolkit/issues/new") + public var formatVersion: String? { fatalError() } + + @available(*, unavailable, message: "Use publication.conforms(to:) to check the profile of a Publication") + public enum Format: Equatable, Hashable { + /// Formats natively supported by Readium. + case cbz, epub, pdf, webpub + /// Default value when the format is not specified. + case unknown + + /// Finds the format for the given mimetype. + public init(mimetype: String?) { + guard let mimetype = mimetype else { + self = .unknown + return + } + self.init(mimetypes: [mimetype]) + } + + /// Finds the format from a list of possible mimetypes or fallback on a file extension. + public init(mimetypes: [String] = [], fileExtension: String? = nil) { + self.init(mediaType: .of(mediaTypes: mimetypes, fileExtensions: Array(ofNotNil: fileExtension))) + } + + /// Finds the format of the publication at the given url. + /// Uses the format declared as exported UTIs in the app's Info.plist, or fallbacks on the file extension. + /// + /// - Parameter mimetype: Fallback mimetype if the UTI can't be determined. + public init(file: URL, mimetype: String) { + self.init(file: file, mimetypes: [mimetype]) + } + + /// Finds the format of the publication at the given url. + /// Uses the format declared as exported UTIs in the app's Info.plist, or fallbacks on the file extension. + /// + /// - Parameter mimetypes: Fallback mimetypes if the UTI can't be determined. + public init(file: URL, mimetypes: [String] = []) { + self.init(mediaType: .of(file, mediaTypes: mimetypes, fileExtensions: [])) + } + + private init(mediaType: MediaType?) { + guard let mediaType = mediaType else { + self = .unknown + return + } + switch mediaType { + case .epub: + self = .epub + case .cbz: + self = .cbz + case .pdf, .lcpProtectedPDF: + self = .pdf + case .readiumWebPubManifest, .readiumAudiobookManifest: + self = .webpub + default: + self = .unknown + } + } + } } diff --git a/Sources/Shared/Publication/Services/Positions/PositionsService.swift b/Sources/Shared/Publication/Services/Positions/PositionsService.swift index 6ded2290b..847c0c2c1 100644 --- a/Sources/Shared/Publication/Services/Positions/PositionsService.swift +++ b/Sources/Shared/Publication/Services/Positions/PositionsService.swift @@ -72,12 +72,6 @@ public extension Publication { ?? positionsFromManifest() } - /// List of all the positions in each resource, indexed by their `href`. - @available(*, unavailable, message: "Use `positionsByReadingOrder` instead", renamed: "positionsByReadingOrder") - var positionsByResource: [String: [Locator]] { - Dictionary(grouping: positions, by: { $0.href }) - } - /// Fetches the positions from a web service declared in the manifest, if there's one. private func positionsFromManifest() -> [Locator] { links.first(withMediaType: .readiumPositions) diff --git a/Sources/Shared/Toolkit/DocumentTypes.swift b/Sources/Shared/Toolkit/DocumentTypes.swift index 9a593456b..9380a4e52 100644 --- a/Sources/Shared/Toolkit/DocumentTypes.swift +++ b/Sources/Shared/Toolkit/DocumentTypes.swift @@ -152,43 +152,4 @@ public struct DocumentType: Equatable, Loggable { .flatMap { MediaType($0, name: name, fileExtension: preferredFileExtension?.lowercased()) } ?? mediaTypes.first } - - @available(*, unavailable, renamed: "preferredMediaType") - public var format: MediaType? { preferredMediaType } -} - -// MARK: Deprecated - -public extension DocumentTypes { - // See this commit for an example of the changes to do in your reading app: - // https://github.com/readium/r2-testapp-swift/commit/7e98784c01f781c962aab87cd79af09dde900b00 - - @available(*, unavailable, message: "Use `main.utis` instead", renamed: "main.supportedUTIs") - static let utis: [String] = main.supportedUTIs - @available(*, unavailable, message: "Use `main.supportsMediaType()` instead", renamed: "main.supportsMediaType()") - static let contentTypes: [String] = main.supportedMediaTypes.map(\.string) - @available(*, unavailable, message: "Use `main.supportsFileExtension()` instead", renamed: "main.supportsFileExtension()") - static let extensions: [String] = main.supportedFileExtensions - - /// Returns the content type for the given URL. - @available(*, unavailable, message: "Use `Format.of` to determine the format of a file from its media type or file extension") - static func contentType(for url: URL?) -> String? { nil } - - /// Returns the content type for the given document extension. - @available(*, unavailable, message: "Use `Format.of` to determine the format of a file from its media type or file extension") - static func contentType(forExtension ext: String?) -> String? { - guard let fileExtension = ext else { - return nil - } - return MediaType.of(fileExtension: fileExtension)?.string - } - - /// Returns the document extension for given content type. - @available(*, unavailable, message: "Use `Format.of` to determine the format of a file from its media type or file extension") - static func `extension`(forContentType contentType: String?) -> String? { - guard let mediaType = contentType else { - return nil - } - return MediaType.of(mediaType: mediaType)?.fileExtension - } } diff --git a/Sources/Shared/Toolkit/Media Type/MediaType+Deprecated.swift b/Sources/Shared/Toolkit/Media Type/MediaType+Deprecated.swift deleted file mode 100644 index c1c0b520b..000000000 --- a/Sources/Shared/Toolkit/Media Type/MediaType+Deprecated.swift +++ /dev/null @@ -1,42 +0,0 @@ -// -// Copyright 2023 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 - -@available(*, unavailable, message: "Format and MediaType got merged together", renamed: "MediaType") -public typealias Format = MediaType -@available(*, unavailable, renamed: "MediaTypeSnifferContext") -public typealias FormatSnifferContext = MediaTypeSnifferContext - -public extension MediaType { - @available(*, unavailable, message: "Format and MediaType got merged together") - var mediaType: MediaType { self } - - @available(*, unavailable, renamed: "readiumAudiobook") - static var audiobook: MediaType { readiumAudiobook } - @available(*, unavailable, renamed: "readiumAudiobookManifest") - static var audiobookManifest: MediaType { readiumAudiobookManifest } - @available(*, unavailable, renamed: "readiumWebPub") - static var webpub: MediaType { readiumWebPub } - @available(*, unavailable, renamed: "readiumWebPubManifest") - static var webpubManifest: MediaType { readiumWebPubManifest } - @available(*, unavailable, renamed: "lcpLicenseDocument") - static var lcpLicense: MediaType { lcpLicenseDocument } - @available(*, unavailable, renamed: "opds1") - static var opds1Feed: MediaType { opds1 } - @available(*, unavailable, renamed: "opds2") - static var opds2Feed: MediaType { opds2 } -} - -public extension URLResponse { - @available(*, unavailable, renamed: "mediaType") - var format: MediaType? { mediaType } - - @available(*, unavailable, renamed: "sniffMediaType") - func sniffFormat(data: (() -> Data)? = nil, mediaTypes: [String] = [], fileExtensions: [String] = [], sniffers: [MediaType.Sniffer] = MediaType.sniffers) -> MediaType? { - sniffMediaType(data: data, mediaTypes: mediaTypes, fileExtensions: fileExtensions, sniffers: sniffers) - } -} diff --git a/Sources/Streamer/Model/Container.swift b/Sources/Streamer/Model/Container.swift deleted file mode 100644 index 72c33a7fc..000000000 --- a/Sources/Streamer/Model/Container.swift +++ /dev/null @@ -1,80 +0,0 @@ -// -// Copyright 2023 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 R2Shared - -/// Container protocol associated errors. -/// -/// - streamInitFailed: The inputStream initialisation failed. -/// - fileNotFound: The file could not be found. -/// - fileError: An error occured while accessing the file attributes. -/// - missingFile: The file at the given path couldn't not be found. -/// - xmlParse: An error occured while parsing XML (See underlyingError for more infos). -/// - missingLink: The given `Link` ressource couldn't be found in the container. -public enum ContainerError: Error { - // Stream initialization failed. - case streamInitFailed - // The file couldn't be found. - case fileNotFound - // An error occured while accessing the file attributes. - case fileError - // The file is missing from the publication. - case missingFile(path: String) - // Error while parsing XML - case xmlParse(underlyingError: Error) - // The link with given title couldn't be found in the container - case missingLink(title: String?) -} - -/// Provide methods for accessing raw data from container's files. -@available(*, unavailable, message: "Use `Publication.get()` to access a publication's resources") -public protocol Container: AnyObject { - /// See `RootFile`. - var rootFile: RootFile { get set } - - /// Last modification date of the container. - var modificationDate: Date { get } - - /// The DRM protecting resources (some) in the container. - var drm: DRM? { get set } - - /// Get the raw (possibly encrypted) data of an asset in the container - /// - /// - Parameter relativePath: The relative path to the asset. - /// - Returns: The data of the asset. - /// - Throws: An error from EpubDataContainerError enum depending of the - /// overriding method's implementation. - func data(relativePath: String) throws -> Data - - /// Get the size of an asset in the container. - /// - /// - Parameter relativePath: The relative path to the asset. - /// - Returns: The size of the asset. - /// - Throws: An error from EpubDataContainerError enum depending of the - /// overrding method's implementation. - func dataLength(relativePath: String) throws -> UInt64 - - /// Get an seekable input stream with the bytes of the asset in the container. - /// - /// - Parameter relativePath: The relative path to the asset. - /// - Returns: A seekable input stream. - /// - Throws: An error from EpubDataContainerError enum depending of the - /// overrding method's implementation. - func dataInputStream(relativePath: String) throws -> SeekableInputStream -} - -@available(*, unavailable, message: "Use `Publication.get()` to access a publication's resources") -public extension Container { - /// The default implementation reads the modification date from the root file. - /// FIXME: This is needed because the PublicationServer is returning the Publications sorted by date, so that the most recent are visible at the top in the library. But this is UX behavior and should be refactored in the test app's LibraryViewController, instead of exposing it here. - var modificationDate: Date { - let url = NSURL(fileURLWithPath: rootFile.rootPath) - var modificationDate: AnyObject? - try? url.getResourceValue(&modificationDate, forKey: .contentModificationDateKey) - return (modificationDate as? Date) ?? Date() - } -} diff --git a/Sources/Streamer/Model/HTTPContainer.swift b/Sources/Streamer/Model/HTTPContainer.swift deleted file mode 100644 index f135addba..000000000 --- a/Sources/Streamer/Model/HTTPContainer.swift +++ /dev/null @@ -1,34 +0,0 @@ -// -// Copyright 2023 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 R2Shared - -/// Container to access remote files through HTTP requests. -@available(*, unavailable, message: "Use `Publication.get()` to access a publication's resources") -class HTTPContainer: Container, Loggable { - var rootFile: RootFile - var drm: DRM? - - let baseURL: URL - - init(baseURL: URL, mimetype: String) { - self.baseURL = baseURL - rootFile = RootFile(rootPath: baseURL.absoluteString, mimetype: mimetype) - } - - func data(relativePath: String) throws -> Data { - try Data(contentsOf: baseURL.appendingPathComponent(relativePath)) - } - - func dataLength(relativePath: String) throws -> UInt64 { - try UInt64(data(relativePath: relativePath).count) - } - - func dataInputStream(relativePath: String) throws -> SeekableInputStream { - try DataInputStream(data: data(relativePath: relativePath)) - } -} diff --git a/Sources/Streamer/Model/PublicationContainer.swift b/Sources/Streamer/Model/PublicationContainer.swift deleted file mode 100644 index 8c2259ba5..000000000 --- a/Sources/Streamer/Model/PublicationContainer.swift +++ /dev/null @@ -1,36 +0,0 @@ -// -// Copyright 2023 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 R2Shared - -/// Temporary solution to migrate `Publication.get()` while ensuring backward compatibility with -/// `Container`. -@available(*, unavailable, message: "Use `Publication.get()` to access a publication's resources") -final class PublicationContainer: Container { - var rootFile: RootFile - var drm: DRM? - - private let publication: Publication - - init(publication: Publication, path: String, mimetype: String, drm: DRM? = nil) { - self.publication = publication - rootFile = RootFile(rootPath: path, mimetype: mimetype) - self.drm = drm - } - - func data(relativePath: String) throws -> Data { - try publication.get(relativePath).read().get() - } - - func dataLength(relativePath: String) throws -> UInt64 { - try publication.get(relativePath).length.get() - } - - func dataInputStream(relativePath: String) throws -> SeekableInputStream { - try publication.get(relativePath).stream().get() - } -} diff --git a/Sources/Streamer/Parser/Audio/AudioParser.swift b/Sources/Streamer/Parser/Audio/AudioParser.swift index bcecc7518..ed91ed729 100644 --- a/Sources/Streamer/Parser/Audio/AudioParser.swift +++ b/Sources/Streamer/Parser/Audio/AudioParser.swift @@ -29,7 +29,6 @@ public final class AudioParser: PublicationParser { return Publication.Builder( mediaType: .zab, - format: .cbz, manifest: Manifest( metadata: Metadata( conformsTo: [.audiobook], @@ -63,9 +62,4 @@ public final class AudioParser: PublicationParser { || filename.hasPrefix(".") || filename == "Thumbs.db" } - - @available(*, unavailable, message: "Not supported for `AudioParser`") - public static func parse(at url: URL) throws -> (PubBox, PubParsingCallback) { - fatalError("Not supported for `AudioParser`") - } } diff --git a/Sources/Streamer/Parser/EPUB/EPUBParser.swift b/Sources/Streamer/Parser/EPUB/EPUBParser.swift index 9b2bd3ce1..0cca914eb 100644 --- a/Sources/Streamer/Parser/EPUB/EPUBParser.swift +++ b/Sources/Streamer/Parser/EPUB/EPUBParser.swift @@ -30,12 +30,6 @@ public enum EPUBParserError: Error { case missingRootfile } -@available(*, unavailable, renamed: "EPUBParserError") -public typealias EpubParserError = EPUBParserError - -@available(*, unavailable, renamed: "EPUBParser") -public typealias EpubParser = EPUBParser - extension EPUBParser: Loggable {} /// An EPUB container parser that extracts the information from the relevant @@ -67,7 +61,6 @@ public final class EPUBParser: PublicationParser { return Publication.Builder( mediaType: .epub, - format: .epub, manifest: Manifest( metadata: metadata, readingOrder: components.readingOrder, @@ -94,11 +87,6 @@ public final class EPUBParser: PublicationParser { ) } - @available(*, unavailable, message: "Use an instance of `Streamer` to open a `Publication`") - public static func parse(at url: URL) throws -> (PubBox, PubParsingCallback) { - fatalError("Not available") - } - private func parseCollections(in fetcher: Fetcher, links: [Link]) -> [String: [PublicationCollection]] { var collections = parseNavigationDocument(in: fetcher, links: links) if collections["toc"]?.first?.links.isEmpty != false { diff --git a/Sources/Streamer/Parser/Image/CBZParser.swift b/Sources/Streamer/Parser/Image/CBZParser.swift deleted file mode 100644 index db5de9088..000000000 --- a/Sources/Streamer/Parser/Image/CBZParser.swift +++ /dev/null @@ -1,23 +0,0 @@ -// -// Copyright 2023 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 R2Shared - -public enum CBZParserError: Error { - case invalidCBZ(path: String) -} - -@available(*, unavailable, renamed: "CBZParserError") -public typealias CbzParserError = CBZParserError - -/// CBZ publication parsing class. -@available(*, unavailable, message: "Use `ImageParser` instead") -public class CbzParser: PublicationParser { - public func parse(asset: PublicationAsset, fetcher: Fetcher, warnings: WarningLogger?) throws -> Publication.Builder? { - try ImageParser().parse(asset: asset, fetcher: fetcher, warnings: warnings) - } -} diff --git a/Sources/Streamer/Parser/Image/ImageParser.swift b/Sources/Streamer/Parser/Image/ImageParser.swift index 228d18ded..334bd85a0 100644 --- a/Sources/Streamer/Parser/Image/ImageParser.swift +++ b/Sources/Streamer/Parser/Image/ImageParser.swift @@ -32,7 +32,6 @@ public final class ImageParser: PublicationParser { return Publication.Builder( mediaType: .cbz, - format: .cbz, manifest: Manifest( metadata: Metadata( conformsTo: [.divina], @@ -66,9 +65,4 @@ public final class ImageParser: PublicationParser { || filename.hasPrefix(".") || filename == "Thumbs.db" } - - @available(*, unavailable, message: "Not supported for `ImageParser`") - public static func parse(at url: URL) throws -> (PubBox, PubParsingCallback) { - fatalError("Not supported for `ImageParser`") - } } diff --git a/Sources/Streamer/Parser/PDF/PDFFileParser.swift b/Sources/Streamer/Parser/PDF/PDFFileParser.swift deleted file mode 100644 index 968bd2fa5..000000000 --- a/Sources/Streamer/Parser/PDF/PDFFileParser.swift +++ /dev/null @@ -1,113 +0,0 @@ -// -// Copyright 2023 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 R2Shared -import UIKit - -/// Structure holding the metadata from a standalone PDF file. -@available(*, unavailable, message: "Use `PDFDocument` from r2-shared instead") -public struct PDFFileMetadata { - // Permanent identifier based on the contents of the file at the time it was originally created. - let identifier: String? - - // The version of the PDF specification to which the document conforms (for example, 1.4). - let version: String? - - /// Values extracted from the document information dictionary, defined in PDF specification. - - // The document's title. - let title: String? - // The name of the person who created the document. - let author: String? - // The subject of the document. - let subject: String? - // Keywords associated with the document. - let keywords: [String] - // Outline to build the table of contents. - let outline: [PDFOutlineNode] -} - -@available(*, unavailable, message: "Use `PDFDocument` from r2-shared instead") -public struct PDFOutlineNode { - let title: String? - let pageNumber: Int - let children: [PDFOutlineNode] -} - -@available(*, unavailable) -extension Array where Element == PDFOutlineNode { - @available(*, unavailable) - func links(withHref href: String) -> [Link] { [] } -} - -/// Protocol to implement if you want to use a different PDF engine than the one provided with Readium 2 to parse the PDF's metadata. -/// Note: this is not used in the case of .lcpdf files, since the metadata are parsed from the manifest.json file. -@available(*, unavailable, message: "Use `PDFDocumentFactory` from r2-shared instead") -public protocol PDFFileParser: PDFDocument { - /// Initializes the parser with the given PDF data stream. - /// You must `open` and `close` the stream when needed. - init(stream: SeekableInputStream) throws - - /// Renders the PDF's first page. - func renderCover() throws -> UIImage? - - /// Parses the number of pages in the PDF. - func parseNumberOfPages() throws -> Int - - /// Parses the PDF file metadata. - func parseMetadata() throws -> PDFFileMetadata -} - -@available(*, unavailable) -public extension PDFFileParser { - @available(*, unavailable) - var identifier: String? { try? parseMetadata().identifier } - @available(*, unavailable) - var pageCount: Int { (try? parseNumberOfPages()) ?? 0 } - @available(*, unavailable) - var cover: UIImage? { try? renderCover() } - @available(*, unavailable) - var title: String? { try? parseMetadata().title } - @available(*, unavailable) - var author: String? { try? parseMetadata().author } - @available(*, unavailable) - var subject: String? { try? parseMetadata().subject } - @available(*, unavailable) - var keywords: [String] { (try? parseMetadata().keywords) ?? [] } - @available(*, unavailable) - var outline: [R2Shared.PDFOutlineNode] { [] } -} - -@available(*, unavailable) -extension PDFOutlineNode { - @available(*, unavailable) - func asShared() -> R2Shared.PDFOutlineNode { fatalError("Unavailable") } -} - -@available(*, unavailable, message: "Use `PDFDocumentFactory` from r2-shared instead") -class PDFFileParserFactory: PDFDocumentFactory { - enum Error: Swift.Error { - case invalidFile(URL) - } - - private let parserType: PDFFileParser.Type - - init(parserType: PDFFileParser.Type) { - self.parserType = parserType - } - - func open(resource: Resource, password: String?) throws -> PDFDocument { - try parserType.init(stream: ResourceInputStream(resource: resource, length: resource.length.get())) - } - - func open(url: URL, password: String?) throws -> PDFDocument { - guard let stream = FileInputStream(fileAtPath: url.path) else { - throw Error.invalidFile(url) - } - return try parserType.init(stream: stream) - } -} diff --git a/Sources/Streamer/Parser/PDF/PDFParser.swift b/Sources/Streamer/Parser/PDF/PDFParser.swift index 7cd73c8d5..adfbd5f35 100644 --- a/Sources/Streamer/Parser/PDF/PDFParser.swift +++ b/Sources/Streamer/Parser/PDF/PDFParser.swift @@ -47,7 +47,6 @@ public final class PDFParser: PublicationParser, Loggable { return Publication.Builder( mediaType: .pdf, - format: .pdf, manifest: Manifest( metadata: Metadata( identifier: document.identifier, @@ -66,14 +65,4 @@ public final class PDFParser: PublicationParser, Loggable { ) ) } - - @available(*, unavailable, message: "Use `init(pdfFactory:)` instead") - public convenience init(parserType: PDFFileParser.Type) { - self.init(pdfFactory: PDFFileParserFactory(parserType: parserType)) - } - - @available(*, unavailable, message: "Use an instance of `Streamer` to open a `Publication`") - public static func parse(at url: URL) throws -> (PubBox, PubParsingCallback) { - fatalError("Not available") - } } diff --git a/Sources/Streamer/Parser/Parser+Deprecated.swift b/Sources/Streamer/Parser/Parser+Deprecated.swift deleted file mode 100644 index 00d31d2c1..000000000 --- a/Sources/Streamer/Parser/Parser+Deprecated.swift +++ /dev/null @@ -1,29 +0,0 @@ -// -// Copyright 2023 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 R2Shared - -/// `Publication` and the associated `Container`. -@available(*, unavailable, message: "Use an instance of `Streamer` to open a `Publication`") -public typealias PubBox = (publication: Publication, associatedContainer: Container) -/// A callback called when the publication license is loaded in the given DRM object. -@available(*, unavailable, message: "Use an instance of `Streamer` to open a `Publication`") -public typealias PubParsingCallback = (DRM?) throws -> Void - -public extension PublicationParser { - @available(*, unavailable, message: "Use an instance of `Streamer` to open a `Publication`") - static func parse(fileAtPath path: String) throws -> (PubBox, PubParsingCallback) { - fatalError("Not available") - } -} - -public extension Publication { - @available(*, unavailable, message: "Use an instance of `Streamer` to parse a publication") - static func parse(at url: URL) throws -> (PubBox, PubParsingCallback)? { - fatalError("Not available") - } -} diff --git a/Sources/Streamer/Parser/Readium/ReadiumWebPubParser.swift b/Sources/Streamer/Parser/Readium/ReadiumWebPubParser.swift index 374750e81..d08c7f01f 100644 --- a/Sources/Streamer/Parser/Readium/ReadiumWebPubParser.swift +++ b/Sources/Streamer/Parser/Readium/ReadiumWebPubParser.swift @@ -78,7 +78,6 @@ public class ReadiumWebPubParser: PublicationParser, Loggable { return Publication.Builder( mediaType: mediaType, - format: mediaType.matches(.lcpProtectedPDF) ? .pdf : .webpub, manifest: manifest, fetcher: fetcher, servicesBuilder: PublicationServicesBuilder(setup: { @@ -112,11 +111,6 @@ public class ReadiumWebPubParser: PublicationParser, Loggable { } ) } - - @available(*, unavailable, message: "Use an instance of `Streamer` to open a `Publication`") - public static func parse(at url: URL) throws -> (PubBox, PubParsingCallback) { - fatalError("Not available") - } } private extension MediaType { @@ -128,9 +122,3 @@ private extension MediaType { ) } } - -@available(*, unavailable, renamed: "ReadiumWebPubParserError") -public typealias WEBPUBParserError = ReadiumWebPubParserError - -@available(*, unavailable, renamed: "ReadiumWebPubParser") -public typealias WEBPUBParser = ReadiumWebPubParser diff --git a/Sources/Streamer/Server/PublicationServer.swift b/Sources/Streamer/Server/PublicationServer.swift index 6f8a1e204..78bcdefc0 100644 --- a/Sources/Streamer/Server/PublicationServer.swift +++ b/Sources/Streamer/Server/PublicationServer.swift @@ -350,9 +350,4 @@ public class PublicationServer: ResourcesServer, Loggable { assert(file.pathExtension.lowercased() != "css" || contentType == "text/css") return GCDWebServerDataResponse(data: data, contentType: contentType) } - - @available(*, unavailable, message: "Passing a `Container` is not needed anymore") - public func add(_ publication: Publication, with container: Container, at endpoint: String = UUID().uuidString) throws { - try add(publication, at: endpoint) - } } diff --git a/Sources/Streamer/Streamer.swift b/Sources/Streamer/Streamer.swift index 363bb86cc..05c9c8fb9 100644 --- a/Sources/Streamer/Streamer.swift +++ b/Sources/Streamer/Streamer.swift @@ -173,9 +173,6 @@ public final class Streamer: Loggable { return .success(builder.build()) } } - - @available(*, unavailable, message: "Provide a `FileAsset` instead", renamed: "open(asset:credentials:allowUserInteraction:sender:warnings:onCreatePublication:completion:)") - public func open(file: File, credentials: String? = nil, allowUserInteraction: Bool, sender: Any? = nil, warnings: WarningLogger? = nil, onCreatePublication: Publication.Builder.Transform? = nil, completion: @escaping (CancellableResult) -> Void) {} } private typealias OpenedAsset = (asset: PublicationAsset, fetcher: Fetcher, onCreatePublication: Publication.Builder.Transform?) diff --git a/Sources/Streamer/Toolkit/Extensions/Fetcher.swift b/Sources/Streamer/Toolkit/Extensions/Fetcher.swift index d34ff9834..2192a7eb4 100644 --- a/Sources/Streamer/Toolkit/Extensions/Fetcher.swift +++ b/Sources/Streamer/Toolkit/Extensions/Fetcher.swift @@ -48,21 +48,3 @@ extension Fetcher { return title } } - -/// Creates a `Fetcher` from an archive or a single file. -/// -/// This is used as a support for backward compatibility in the old parser APIs, the `Streamer` -/// implements its own algorithm for creating the leaf fetcher, with a recovery mechanism -/// to handle user password. -@available(*, unavailable) -func makeFetcher(for url: URL) throws -> Fetcher { - guard (try? url.checkResourceIsReachable()) == true else { - throw Publication.OpeningError.notFound - } - - do { - return try ArchiveFetcher(archive: DefaultArchiveFactory().open(url: url, password: nil).get()) - } catch { - return FileFetcher(href: "/\(url.lastPathComponent)", path: url) - } -} diff --git a/Sources/Streamer/Toolkit/Logger.swift b/Sources/Streamer/Toolkit/Logger.swift deleted file mode 100644 index 88805f06f..000000000 --- a/Sources/Streamer/Toolkit/Logger.swift +++ /dev/null @@ -1,13 +0,0 @@ -// -// Copyright 2023 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 R2Shared - -@available(*, unavailable, message: "Use `R2Shared.R2EnableLog` instead") -public func R2StreamerEnableLog(withMinimumSeverityLevel level: SeverityLevel, customLogger: LoggerType = LoggerStub()) { - R2EnableLog(withMinimumSeverityLevel: level, customLogger: customLogger) -} From 782fc8423bd9e83eb305e5af6d5540ca2456d589 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Menu?= Date: Mon, 20 Nov 2023 15:50:21 +0100 Subject: [PATCH 02/11] Refactor HREF normalization and models (#358) --- .github/workflows/checks.yml | 16 +- CHANGELOG.md | 16 +- Documentation/Migration Guide.md | 49 +++ Makefile | 2 +- README.md | 12 +- .../Adapters/GCDWebServer/GCDHTTPServer.swift | 71 ++-- Sources/Internal/Extensions/Array.swift | 4 + Sources/Internal/Extensions/String.swift | 7 + Sources/Internal/Extensions/URL.swift | 36 ++ .../LCPContentProtection.swift | 2 +- Sources/LCP/LCPAcquisition.swift | 4 +- Sources/LCP/LCPRenewDelegate.swift | 6 +- Sources/LCP/LCPService.swift | 6 +- .../Container/EPUBLicenseContainer.swift | 3 +- .../Container/LCPLLicenseContainer.swift | 9 +- .../License/Container/LicenseContainer.swift | 4 +- .../Container/ReadiumLicenseContainer.swift | 3 +- .../Container/ZIPLicenseContainer.swift | 11 +- Sources/LCP/License/License.swift | 17 +- .../LCP/License/Model/Components/Link.swift | 9 +- .../LCP/License/Model/LicenseDocument.swift | 4 +- .../LCP/License/Model/StatusDocument.swift | 4 +- Sources/LCP/Services/CRLService.swift | 2 +- Sources/LCP/Services/DeviceService.swift | 2 +- Sources/LCP/Services/LicensesService.swift | 17 +- .../Audiobook/PublicationMediaLoader.swift | 4 +- .../CBZ/CBZNavigatorViewController.swift | 32 +- .../EPUB/CSS/HTMLFontFamilyDeclaration.swift | 22 +- Sources/Navigator/EPUB/CSS/ReadiumCSS.swift | 10 +- .../Navigator/EPUB/EPUBFixedSpreadView.swift | 4 +- .../EPUB/EPUBNavigatorViewController.swift | 38 +- .../EPUB/EPUBNavigatorViewModel.swift | 77 ++-- .../EPUB/EPUBReflowableSpreadView.swift | 12 +- Sources/Navigator/EPUB/EPUBSpread.swift | 11 +- Sources/Navigator/EPUB/EPUBSpreadView.swift | 14 +- Sources/Navigator/PDF/PDFDocumentHolder.swift | 4 +- .../PDF/PDFNavigatorViewController.swift | 112 ++---- Sources/OPDS/OPDS2Parser.swift | 47 ++- Sources/Shared/Fetcher/ArchiveFetcher.swift | 22 +- Sources/Shared/Fetcher/Fetcher.swift | 4 +- Sources/Shared/Fetcher/FileFetcher.swift | 76 ++-- Sources/Shared/Fetcher/HTTPFetcher.swift | 29 +- .../Fetcher/Resource/CachingResource.swift | 2 +- .../Fetcher/Resource/DataResource.swift | 2 +- .../Fetcher/Resource/FailureResource.swift | 2 +- .../Fetcher/Resource/FileResource.swift | 13 +- .../Fetcher/Resource/LazyResource.swift | 2 +- .../Fetcher/Resource/ProxyResource.swift | 2 +- .../Shared/Fetcher/Resource/Resource.swift | 2 +- .../Shared/Publication/Accessibility.swift | 20 +- .../Shared/Publication/Asset/FileAsset.swift | 25 +- Sources/Shared/Publication/Contributor.swift | 22 +- .../Shared/Publication/HREFNormalizer.swift | 50 +++ Sources/Shared/Publication/Link.swift | 120 +++++-- Sources/Shared/Publication/Locator.swift | 51 +-- Sources/Shared/Publication/Manifest.swift | 61 ++-- .../Publication/ManifestTransformer.swift | 109 ++++++ Sources/Shared/Publication/Metadata.swift | 114 +++--- Sources/Shared/Publication/Properties.swift | 17 +- Sources/Shared/Publication/Publication.swift | 77 +--- .../Publication/PublicationCollection.swift | 28 +- .../ContentProtectionService+WS.swift | 12 +- .../HTMLResourceContentIterator.swift | 25 +- .../Cover/GeneratedCoverService.swift | 10 +- Sources/Shared/Publication/Subject.swift | 10 +- Sources/Shared/Toolkit/Archive/Archive.swift | 20 +- .../Toolkit/Archive/ExplodedArchive.swift | 56 ++- Sources/Shared/Toolkit/Archive/Minizip.swift | 34 +- Sources/Shared/Toolkit/HREF.swift | 79 +---- .../Toolkit/HTTP/DefaultHTTPClient.swift | 6 +- Sources/Shared/Toolkit/HTTP/HTTPClient.swift | 25 +- Sources/Shared/Toolkit/HTTP/HTTPRequest.swift | 27 +- Sources/Shared/Toolkit/HTTP/HTTPServer.swift | 25 +- .../Toolkit/Media Type/MediaTypeSniffer.swift | 16 +- .../Media Type/MediaTypeSnifferContent.swift | 14 +- .../Media Type/MediaTypeSnifferContext.swift | 2 +- Sources/Shared/Toolkit/PDF/CGPDF.swift | 8 +- Sources/Shared/Toolkit/PDF/PDFDocument.swift | 10 +- Sources/Shared/Toolkit/PDF/PDFKit.swift | 8 +- .../URL/Absolute URL/AbsoluteURL.swift | 106 ++++++ .../Toolkit/URL/Absolute URL/FileURL.swift | 70 ++++ .../Toolkit/URL/Absolute URL/HTTPURL.swift | 45 +++ .../URL/Absolute URL/UnknownAbsoluteURL.swift | 26 ++ Sources/Shared/Toolkit/URL/AnyURL.swift | 128 +++++++ Sources/Shared/Toolkit/URL/RelativeURL.swift | 106 ++++++ .../Toolkit/{ => URL}/URITemplate.swift | 4 +- .../Shared/Toolkit/URL/URLConvertible.swift | 32 ++ .../Shared/Toolkit/URL/URLExtensions.swift | 32 ++ Sources/Shared/Toolkit/URL/URLProtocol.swift | 128 +++++++ Sources/Shared/Toolkit/URL/URLQuery.swift | 48 +++ Sources/Shared/Toolkit/XML/XML.swift | 6 +- .../Streamer/Parser/Audio/AudioParser.swift | 2 +- .../Parser/EPUB/EPUBContainerParser.swift | 14 +- .../Parser/EPUB/EPUBEncryptionParser.swift | 15 +- .../Parser/EPUB/EPUBMetadataParser.swift | 6 +- Sources/Streamer/Parser/EPUB/EPUBParser.swift | 18 +- .../EPUB/Extensions/URLExtensions.swift | 24 ++ Sources/Streamer/Parser/EPUB/NCXParser.swift | 10 +- .../EPUB/NavigationDocumentParser.swift | 16 +- Sources/Streamer/Parser/EPUB/OPFParser.swift | 46 +-- Sources/Streamer/Parser/EPUB/SMILParser.swift | 213 ----------- .../Streamer/Parser/Image/ImageParser.swift | 4 +- Sources/Streamer/Parser/PDF/PDFParser.swift | 2 +- .../Streamer/Parser/PublicationParser.swift | 2 +- .../Parser/Readium/ReadiumWebPubParser.swift | 15 +- .../Streamer/Server/PublicationServer.swift | 330 +----------------- .../Streamer/Toolkit/Extensions/Fetcher.swift | 31 +- Support/Carthage/.xcodegen | 78 ++++- .../Readium.xcodeproj/project.pbxproj | 148 ++++---- TestApp/.gitignore | 1 + TestApp/Integrations/Local/TestApp.xctestplan | 57 +++ TestApp/Integrations/Local/project+lcp.yml | 9 + TestApp/Integrations/Local/project.yml | 9 + TestApp/Makefile | 4 +- TestApp/Sources/App/AppModule.swift | 16 +- TestApp/Sources/Common/Publication.swift | 2 +- TestApp/Sources/Data/Database.swift | 27 +- TestApp/Sources/Data/Highlight.swift | 14 +- .../Library/DRM/LCPLibraryService.swift | 4 +- TestApp/Sources/Library/LibraryService.swift | 10 +- .../Sources/OPDS/OPDSGroupTableViewCell.swift | 2 +- .../OPDS/OPDSPublicationTableViewCell.swift | 2 +- .../Audiobook/AudiobookViewController.swift | 4 +- .../Reader/Common/TTS/TTSViewModel.swift | 2 +- .../Common/VisualReaderViewController.swift | 15 +- .../Reader/EPUB/EPUBViewController.swift | 6 +- .../EPUB/CSS/ReadiumCSSTests.swift | 2 +- .../Fetcher/ArchiveFetcherTests.swift | 40 +-- .../Fetcher/FileFetcherTests.swift | 32 +- .../Resource/BufferedResourceTests.swift | 2 +- Tests/SharedTests/Fixtures.swift | 7 +- .../Publication/Asset/FileAssetTests.swift | 6 +- .../Publication/HREFNormalizerTests.swift | 181 ++++++++++ Tests/SharedTests/Publication/LinkTests.swift | 163 +++------ .../Publication/ManifestTests.swift | 199 +++-------- .../Publication/MetadataTests.swift | 85 ----- .../Publication/PropertiesTests.swift | 9 +- .../Publication/PublicationTests.swift | 16 +- .../HTMLResourceContentIteratorTests.swift | 20 +- .../Services/Cover/CoverServiceTests.swift | 4 +- .../Toolkit/Archive/Archive+ZIPTests.swift | 40 +-- .../Archive/ExplodedArchiveTests.swift | 20 +- .../Toolkit/DocumentTypesTests.swift | 2 +- Tests/SharedTests/Toolkit/HREFTests.swift | 130 ------- .../Media Type/MediaTypeSnifferTests.swift | 2 +- .../URL/Absolute URL/FileURLTests.swift | 239 +++++++++++++ .../URL/Absolute URL/HTTPURLTests.swift | 227 ++++++++++++ .../UnknownAbsoluteURLTests.swift | 216 ++++++++++++ .../SharedTests/Toolkit/URL/AnyURLTests.swift | 150 ++++++++ .../Toolkit/URL/RelativeURLTests.swift | 227 ++++++++++++ .../Toolkit/URL/URLQueryTests.swift | 45 +++ Tests/StreamerTests/Extensions.swift | 4 +- Tests/StreamerTests/Fixtures.swift | 7 +- .../Parser/Audio/AudioParserTests.swift | 18 +- .../EPUB/EPUBContainerParserTests.swift | 2 +- .../EPUB/EPUBEncryptionParserTests.swift | 14 +- .../Parser/EPUB/EPUBMetadataParserTests.swift | 1 - .../Parser/EPUB/NCXParserTests.swift | 2 +- .../EPUB/NavigationDocumentParserTests.swift | 2 +- .../Parser/EPUB/OPFParserTests.swift | 67 ++-- .../Services/EPUBPositionsServiceTests.swift | 2 +- .../Parser/Image/ImageParserTests.swift | 32 +- .../Parser/PublicationParsingTests.swift | 12 +- .../Readium/ReadiumWebPubParserTests.swift | 44 +-- .../Toolkit/Extensions/FetcherTests.swift | 14 +- 165 files changed, 3774 insertions(+), 2340 deletions(-) create mode 100644 Sources/Internal/Extensions/URL.swift create mode 100644 Sources/Shared/Publication/HREFNormalizer.swift create mode 100644 Sources/Shared/Publication/ManifestTransformer.swift create mode 100644 Sources/Shared/Toolkit/URL/Absolute URL/AbsoluteURL.swift create mode 100644 Sources/Shared/Toolkit/URL/Absolute URL/FileURL.swift create mode 100644 Sources/Shared/Toolkit/URL/Absolute URL/HTTPURL.swift create mode 100644 Sources/Shared/Toolkit/URL/Absolute URL/UnknownAbsoluteURL.swift create mode 100644 Sources/Shared/Toolkit/URL/AnyURL.swift create mode 100644 Sources/Shared/Toolkit/URL/RelativeURL.swift rename Sources/Shared/Toolkit/{ => URL}/URITemplate.swift (93%) create mode 100644 Sources/Shared/Toolkit/URL/URLConvertible.swift create mode 100644 Sources/Shared/Toolkit/URL/URLExtensions.swift create mode 100644 Sources/Shared/Toolkit/URL/URLProtocol.swift create mode 100644 Sources/Shared/Toolkit/URL/URLQuery.swift create mode 100644 Sources/Streamer/Parser/EPUB/Extensions/URLExtensions.swift delete mode 100644 Sources/Streamer/Parser/EPUB/SMILParser.swift create mode 100644 TestApp/Integrations/Local/TestApp.xctestplan create mode 100644 Tests/SharedTests/Publication/HREFNormalizerTests.swift delete mode 100644 Tests/SharedTests/Toolkit/HREFTests.swift create mode 100644 Tests/SharedTests/Toolkit/URL/Absolute URL/FileURLTests.swift create mode 100644 Tests/SharedTests/Toolkit/URL/Absolute URL/HTTPURLTests.swift create mode 100644 Tests/SharedTests/Toolkit/URL/Absolute URL/UnknownAbsoluteURLTests.swift create mode 100644 Tests/SharedTests/Toolkit/URL/AnyURLTests.swift create mode 100644 Tests/SharedTests/Toolkit/URL/RelativeURLTests.swift create mode 100644 Tests/SharedTests/Toolkit/URL/URLQueryTests.swift diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index e2991fea8..2d0204b55 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -7,17 +7,17 @@ on: env: platform: ${{ 'iOS Simulator' }} - device: ${{ 'iPhone 12' }} + device: ${{ 'iPhone 15' }} commit_sha: ${{ github.sha }} + DEVELOPER_DIR: /Applications/Xcode_15.0.1.app/Contents/Developer jobs: build: name: Build - runs-on: macos-12 + runs-on: macos-13 if: ${{ !github.event.pull_request.draft }} env: scheme: ${{ 'Readium-Package' }} - DEVELOPER_DIR: /Applications/Xcode_13.4.1.app/Contents/Developer steps: - name: Checkout @@ -40,7 +40,7 @@ jobs: lint: name: Lint - runs-on: macos-12 + runs-on: macos-13 if: ${{ !github.event.pull_request.draft }} env: scripts: ${{ 'Sources/Navigator/EPUB/Scripts' }} @@ -63,7 +63,7 @@ jobs: int-dev: name: Integration (Local) - runs-on: macos-12 + runs-on: macos-13 if: ${{ !github.event.pull_request.draft }} defaults: run: @@ -84,7 +84,7 @@ jobs: int-spm: name: Integration (Swift Package Manager) - runs-on: macos-12 + runs-on: macos-13 if: ${{ !github.event.pull_request.draft }} defaults: run: @@ -111,7 +111,7 @@ jobs: int-carthage: name: Integration (Carthage) - runs-on: macos-12 + runs-on: macos-13 if: ${{ !github.event.pull_request.draft }} defaults: run: @@ -141,7 +141,7 @@ jobs: int-cocoapods: name: Integration (CocoaPods) if: github.event_name == 'push' - runs-on: macos-12 + runs-on: macos-13 defaults: run: working-directory: TestApp diff --git a/CHANGELOG.md b/CHANGELOG.md index 46f73cac4..3c5a95fce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,21 @@ 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 + +* Many APIs now expect one of the new URL types (`RelativeURL`, `AbsoluteURL`, `HTTPURL` and `FileURL`). This is helpful because: + * It validates at compile time that we provide a URL that is supported. + * The API's capabilities are better documented, e.g. a download API could look like this : `download(url: HTTPURL) -> FileURL`. + +#### Shared + +* `Link` and `Locator`'s `href` are normalized as valid URLs to improve interoperability with the Readium Web toolkits. + * **You MUST migrate your database if you were persisting HREFs and Locators**. Take a look at [the migration guide](Documentation/Migration%20Guide.md) for guidance. +* Links are not resolved to the `self` URL of a manifest anymore. However, you can still normalize the HREFs yourselves by calling `Manifest.normalizeHREFsToSelf()`. +* `Publication.localizedTitle` is now optional, as we cannot guarantee a publication will always have a title. + ## [2.6.1] diff --git a/Documentation/Migration Guide.md b/Documentation/Migration Guide.md index fbbc5db43..349b149dd 100644 --- a/Documentation/Migration Guide.md +++ b/Documentation/Migration Guide.md @@ -2,6 +2,55 @@ All migration steps necessary in reading apps to upgrade to major versions of the Swift Readium toolkit will be documented in this file. +## Unreleased + +### Migration of HREFs and Locators (bookmarks, annotations, etc.) + + :warning: This requires a database migration in your application, if you were persisting `Locator` objects. + + In Readium v2.x, a `Link` or `Locator`'s `href` could be either: + + * a valid absolute URL for a streamed publication, e.g. `https://domain.com/isbn/dir/my%20chapter.html`, + * a percent-decoded path for a local archive such as an EPUB, e.g. `/dir/my chapter.html`. + * Note that it was relative to the root of the archive (`/`). + + To improve the interoperability with other Readium toolkits (in particular the Readium Web Toolkits, which only work in a streaming context) **Readium v3 now generates and expects valid URLs** for `Locator` and `Link`'s `href`. + + * `https://domain.com/isbn/dir/my%20chapter.html` is left unchanged, as it was already a valid URL. + * `/dir/my chapter.html` becomes the relative URL path `dir/my%20chapter.html` + * We dropped the `/` prefix to avoid issues when resolving to a base URL. + * Special characters are percent-encoded. + + **You must migrate the HREFs or Locators stored in your database** when upgrading to Readium 3. To assist you, two helpers are provided: `AnyURL(legacyHREF:)` and `Locator(legacyJSONString:)`. + + Here's an example of a [GRDB migration](https://swiftpackageindex.com/groue/grdb.swift/master/documentation/grdb/migrations) that can serve as inspiration: + + ```swift + migrator.registerMigration("normalizeHREFs") { db in + let normalizedRows: [(id: Int, href: String, locator: String)] = + try Row.fetchAll(db, sql: "SELECT id, href, locator FROM bookmarks") + .compactMap { row in + guard + let normalizedHREF = AnyURL(legacyHREF: row["href"])?.string, + let normalizedLocator = try Locator(legacyJSONString: row["locator"])?.jsonString + else { + return nil + } + return (row["id"], normalizedHREF, normalizedLocator) + } + + let updateStmt = try db.makeStatement(sql: "UPDATE bookmarks SET href = :href, locator = :locator WHERE id = :id") + for (id, href, locator) in normalizedRows { + try updateStmt.execute(arguments: [ + "id": id, + "href": href + "locator": locator + ]) + } +} +``` + + ## 2.5.0 In the following migration steps, only the `ReadiumInternal` one is mandatory with 2.5.0. diff --git a/Makefile b/Makefile index c3a2fb418..f7a287057 100644 --- a/Makefile +++ b/Makefile @@ -23,7 +23,7 @@ scripts: .PHONY: test test: # To limit to a particular test suite: -only-testing:R2SharedTests - xcodebuild test -scheme "Readium-Package" -destination "platform=iOS Simulator,name=iPhone 12" | xcbeautify -q + xcodebuild test -scheme "Readium-Package" -destination "platform=iOS Simulator,name=iPhone 15" | xcbeautify -q .PHONY: lint-format lint-format: diff --git a/README.md b/README.md index 3f8b1db56..0524a5c20 100644 --- a/README.md +++ b/README.md @@ -16,12 +16,12 @@ A [Test App](TestApp) demonstrates how to integrate the Readium Swift toolkit in -| Readium | iOS | Swift compiler | Xcode | -|-----------|------|----------------|-------| -| `develop` | 11.0 | 5.6.1 | 13.4 | -| 2.5.1 | 11.0 | 5.6.1 | 13.4 | -| 2.5.0 | 10.0 | 5.6.1 | 13.4 | -| 2.4.0 | 10.0 | 5.3.2 | 12.4 | +| Readium | iOS | Swift compiler | Xcode | +|-----------|------|----------------|--------| +| `develop` | 11.0 | 5.9 | 15.0.1 | +| 2.5.1 | 11.0 | 5.6.1 | 13.4 | +| 2.5.0 | 10.0 | 5.6.1 | 13.4 | +| 2.4.0 | 10.0 | 5.3.2 | 12.4 | ## Using Readium diff --git a/Sources/Adapters/GCDWebServer/GCDHTTPServer.swift b/Sources/Adapters/GCDWebServer/GCDHTTPServer.swift index fdb197f2c..5b0a87a2c 100644 --- a/Sources/Adapters/GCDWebServer/GCDHTTPServer.swift +++ b/Sources/Adapters/GCDWebServer/GCDHTTPServer.swift @@ -12,6 +12,7 @@ import UIKit public enum GCDHTTPServerError: Error { case failedToStartServer(cause: Error) case serverNotStarted + case invalidEndpoint(HTTPServerEndpoint) case nullServerURL } @@ -24,14 +25,14 @@ public class GCDHTTPServer: HTTPServer, Loggable { private let server = GCDWebServer() /// Mapping between endpoints and their handlers. - private var handlers: [HTTPServerEndpoint: (HTTPServerRequest) -> Resource] = [:] + private var handlers: [HTTPURL: (HTTPServerRequest) -> Resource] = [:] /// Mapping between endpoints and resource transformers. - private var transformers: [HTTPServerEndpoint: [ResourceTransformer]] = [:] + private var transformers: [HTTPURL: [ResourceTransformer]] = [:] private enum State { case stopped - case started(port: UInt, baseURL: URL) + case started(port: UInt, baseURL: HTTPURL) } private var state: State = .stopped @@ -112,12 +113,12 @@ public class GCDHTTPServer: HTTPServer, Loggable { } queue.async { [self] in - var path = request.path.removingPrefix("/") - path = path.removingPercentEncoding ?? path - // Remove anchors and query params - let pathWithoutAnchor = path.components(separatedBy: .init(charactersIn: "#?")).first ?? path + guard let url = request.url.httpURL else { + completion(FailureResource(link: Link(href: request.url.absoluteString), error: .notFound(nil))) + return + } - func transform(resource: Resource, at endpoint: HTTPServerEndpoint) -> Resource { + func transform(resource: Resource, at endpoint: HTTPURL) -> Resource { guard let transformers = transformers[endpoint], !transformers.isEmpty else { return resource } @@ -129,15 +130,15 @@ public class GCDHTTPServer: HTTPServer, Loggable { } for (endpoint, handler) in handlers { - if endpoint == pathWithoutAnchor { - let resource = handler(HTTPServerRequest(url: request.url, href: nil)) + if endpoint == url.removingQuery().removingFragment() { + let resource = handler(HTTPServerRequest(url: url, href: nil)) completion(transform(resource: resource, at: endpoint)) return - } else if path.hasPrefix(endpoint.addingSuffix("/")) { + } else if let href = endpoint.relativize(url) { let resource = handler(HTTPServerRequest( - url: request.url, - href: path.removingPrefix(endpoint.removingSuffix("/")) + url: url, + href: href )) completion(transform(resource: resource, at: endpoint)) return @@ -150,34 +151,46 @@ public class GCDHTTPServer: HTTPServer, Loggable { // MARK: HTTPServer - public func serve(at endpoint: HTTPServerEndpoint, handler: @escaping (HTTPServerRequest) -> Resource) throws -> URL { + public func serve(at endpoint: HTTPServerEndpoint, handler: @escaping (HTTPServerRequest) -> Resource) throws -> HTTPURL { try queue.sync(flags: .barrier) { if case .stopped = state { try start() } - guard case let .started(port: _, baseURL: baseURL) = state else { - throw GCDHTTPServerError.serverNotStarted - } - handlers[endpoint] = handler - - return baseURL.appendingPathComponent(endpoint) + let url = try url(for: endpoint) + handlers[url] = handler + return url } } - public func transformResources(at endpoint: HTTPServerEndpoint, with transformer: @escaping ResourceTransformer) { - queue.sync(flags: .barrier) { - var trs = transformers[endpoint] ?? [] + public func transformResources(at endpoint: HTTPServerEndpoint, with transformer: @escaping ResourceTransformer) throws { + try queue.sync(flags: .barrier) { + let url = try url(for: endpoint) + var trs = transformers[url] ?? [] trs.append(transformer) - transformers[endpoint] = trs + transformers[url] = trs } } - public func remove(at endpoint: HTTPServerEndpoint) { - queue.sync(flags: .barrier) { - handlers.removeValue(forKey: endpoint) - transformers.removeValue(forKey: endpoint) + public func remove(at endpoint: HTTPServerEndpoint) throws { + try queue.sync(flags: .barrier) { + let url = try url(for: endpoint) + handlers.removeValue(forKey: url) + transformers.removeValue(forKey: url) + } + } + + private func url(for endpoint: HTTPServerEndpoint) throws -> HTTPURL { + guard case let .started(port: _, baseURL: baseURL) = state else { + throw GCDHTTPServerError.serverNotStarted + } + guard + let endpointPath = RelativeURL(string: endpoint.addingSuffix("/")), + let endpointURL = baseURL.resolve(endpointPath) + else { + throw GCDHTTPServerError.invalidEndpoint(endpoint) } + return endpointURL } // MARK: Server lifecycle @@ -230,7 +243,7 @@ public class GCDHTTPServer: HTTPServer, Loggable { throw GCDHTTPServerError.failedToStartServer(cause: error) } - guard let baseURL = server.serverURL else { + guard let baseURL = server.serverURL?.httpURL else { stop() throw GCDHTTPServerError.nullServerURL } diff --git a/Sources/Internal/Extensions/Array.swift b/Sources/Internal/Extensions/Array.swift index c9b0974cd..5a6d9bced 100644 --- a/Sources/Internal/Extensions/Array.swift +++ b/Sources/Internal/Extensions/Array.swift @@ -57,6 +57,10 @@ public extension Array where Element: Hashable { array.removeAll { other in other == element } return array } + + @inlinable mutating func remove(_ element: Element) { + removeAll { other in other == element } + } } public extension Array where Element: Equatable { diff --git a/Sources/Internal/Extensions/String.swift b/Sources/Internal/Extensions/String.swift index 6284cd5ad..7360ca56f 100644 --- a/Sources/Internal/Extensions/String.swift +++ b/Sources/Internal/Extensions/String.swift @@ -84,4 +84,11 @@ public extension String { } return index } + + func orNilIfEmpty() -> String? { + guard !isEmpty else { + return nil + } + return self + } } diff --git a/Sources/Internal/Extensions/URL.swift b/Sources/Internal/Extensions/URL.swift new file mode 100644 index 000000000..f72e1153f --- /dev/null +++ b/Sources/Internal/Extensions/URL.swift @@ -0,0 +1,36 @@ +// +// Copyright 2023 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 + +public extension URL { + /// Removes the fragment portion of the receiver and returns it. + mutating func removeFragment() -> String? { + var fragment: String? + guard let result = copy({ + fragment = $0.fragment + $0.fragment = nil + }) else { + return nil + } + self = result + return fragment + } + + /// Creates a copy of the receiver after removing its fragment portion. + func removingFragment() -> URL? { + copy { $0.fragment = nil } + } + + /// Creates a copy of the receiver after modifying its components. + func copy(_ changes: (inout URLComponents) -> Void) -> URL? { + guard var components = URLComponents(url: self, resolvingAgainstBaseURL: true) else { + return nil + } + changes(&components) + return components.url + } +} diff --git a/Sources/LCP/Content Protection/LCPContentProtection.swift b/Sources/LCP/Content Protection/LCPContentProtection.swift index cc028a2df..639b3760d 100644 --- a/Sources/LCP/Content Protection/LCPContentProtection.swift +++ b/Sources/LCP/Content Protection/LCPContentProtection.swift @@ -34,7 +34,7 @@ final class LCPContentProtection: ContentProtection, Loggable { ?? self.authentication service.retrieveLicense( - from: file.url, + from: file.file, authentication: authentication, allowUserInteraction: allowUserInteraction, sender: sender diff --git a/Sources/LCP/LCPAcquisition.swift b/Sources/LCP/LCPAcquisition.swift index c926e042b..e1ffa49a6 100644 --- a/Sources/LCP/LCPAcquisition.swift +++ b/Sources/LCP/LCPAcquisition.swift @@ -15,7 +15,7 @@ public final class LCPAcquisition: Loggable, Cancellable { public struct Publication { /// Path to the downloaded publication. /// You must move this file to the user library's folder. - public let localURL: URL + public let localURL: FileURL /// Filename that should be used for the publication when importing it in the user library. public let suggestedFilename: String @@ -54,7 +54,7 @@ public final class LCPAcquisition: Loggable, Cancellable { completion(result) - if case let .success(publication) = result, (try? publication.localURL.checkResourceIsReachable()) == true { + if case let .success(publication) = result, (try? publication.localURL.exists()) == true { log(.warning, "The acquired LCP publication file was not moved in the completion closure. It will be removed from the file system.") } } diff --git a/Sources/LCP/LCPRenewDelegate.swift b/Sources/LCP/LCPRenewDelegate.swift index c383cbd1d..cbd8f7119 100644 --- a/Sources/LCP/LCPRenewDelegate.swift +++ b/Sources/LCP/LCPRenewDelegate.swift @@ -21,7 +21,7 @@ public protocol LCPRenewDelegate { /// /// You should present the URL in a `SFSafariViewController` and call the `completion` callback when the browser /// is dismissed by the user. - func presentWebPage(url: URL, completion: @escaping (CancellableResult) -> Void) + func presentWebPage(url: HTTPURL, completion: @escaping (CancellableResult) -> Void) } /// Default `LCPRenewDelegate` implementation using standard views. @@ -41,8 +41,8 @@ public class LCPDefaultRenewDelegate: NSObject, LCPRenewDelegate { completion(.success(nil)) } - public func presentWebPage(url: URL, completion: @escaping (CancellableResult) -> Void) { - let safariVC = SFSafariViewController(url: url) + public func presentWebPage(url: HTTPURL, completion: @escaping (CancellableResult) -> Void) { + let safariVC = SFSafariViewController(url: url.url) safariVC.modalPresentationStyle = modalPresentationStyle safariVC.presentationController?.delegate = self safariVC.delegate = self diff --git a/Sources/LCP/LCPService.swift b/Sources/LCP/LCPService.swift index 57ec577c5..53c79bec3 100644 --- a/Sources/LCP/LCPService.swift +++ b/Sources/LCP/LCPService.swift @@ -47,7 +47,7 @@ public final class LCPService: Loggable { } /// Returns whether the given `file` is protected by LCP. - public func isLCPProtected(_ file: URL) -> Bool { + public func isLCPProtected(_ file: FileURL) -> Bool { warnIfMainThread() return makeLicenseContainerSync(for: file)?.containsLicense() == true } @@ -56,7 +56,7 @@ public final class LCPService: Loggable { /// /// You can cancel the on-going download with `acquisition.cancel()`. @discardableResult - public func acquirePublication(from lcpl: URL, onProgress: @escaping (LCPAcquisition.Progress) -> Void = { _ in }, completion: @escaping (CancellableResult) -> Void) -> LCPAcquisition { + public func acquirePublication(from lcpl: FileURL, onProgress: @escaping (LCPAcquisition.Progress) -> Void = { _ in }, completion: @escaping (CancellableResult) -> Void) -> LCPAcquisition { licenses.acquirePublication(from: lcpl, onProgress: onProgress, completion: completion) } @@ -73,7 +73,7 @@ public final class LCPService: Loggable { /// - sender: Free object that can be used by reading apps to give some UX context when /// presenting dialogs with `LCPAuthenticating`. public func retrieveLicense( - from publication: URL, + from publication: FileURL, authentication: LCPAuthenticating = LCPDialogAuthentication(), allowUserInteraction: Bool = true, sender: Any? = nil, diff --git a/Sources/LCP/License/Container/EPUBLicenseContainer.swift b/Sources/LCP/License/Container/EPUBLicenseContainer.swift index 756849207..137267658 100644 --- a/Sources/LCP/License/Container/EPUBLicenseContainer.swift +++ b/Sources/LCP/License/Container/EPUBLicenseContainer.swift @@ -5,10 +5,11 @@ // import Foundation +import R2Shared /// Access a License Document stored in an EPUB archive, under META-INF/license.lcpl. final class EPUBLicenseContainer: ZIPLicenseContainer { - init(epub: URL) { + init(epub: FileURL) { super.init(zip: epub, pathInZIP: "META-INF/license.lcpl") } } diff --git a/Sources/LCP/License/Container/LCPLLicenseContainer.swift b/Sources/LCP/License/Container/LCPLLicenseContainer.swift index 2529160de..c5da6d4a7 100644 --- a/Sources/LCP/License/Container/LCPLLicenseContainer.swift +++ b/Sources/LCP/License/Container/LCPLLicenseContainer.swift @@ -5,12 +5,13 @@ // import Foundation +import R2Shared /// Access to a License Document packaged as a standalone LCPL file. final class LCPLLicenseContainer: LicenseContainer { - private let lcpl: URL + private let lcpl: FileURL - init(lcpl: URL) { + init(lcpl: FileURL) { self.lcpl = lcpl } @@ -19,7 +20,7 @@ final class LCPLLicenseContainer: LicenseContainer { } func read() throws -> Data { - guard let data = try? Data(contentsOf: lcpl) else { + guard let data = try? Data(contentsOf: lcpl.url) else { throw LCPError.licenseContainer(.readFailed(path: ".")) } return data @@ -27,7 +28,7 @@ final class LCPLLicenseContainer: LicenseContainer { func write(_ license: LicenseDocument) throws { do { - try license.data.write(to: lcpl, options: .atomic) + try license.data.write(to: lcpl.url, options: .atomic) } catch { throw LCPError.licenseContainer(.writeFailed(path: ".")) } diff --git a/Sources/LCP/License/Container/LicenseContainer.swift b/Sources/LCP/License/Container/LicenseContainer.swift index 982d3bdd7..21a2a4fa8 100644 --- a/Sources/LCP/License/Container/LicenseContainer.swift +++ b/Sources/LCP/License/Container/LicenseContainer.swift @@ -18,13 +18,13 @@ protocol LicenseContainer { func write(_ license: LicenseDocument) throws } -func makeLicenseContainer(for file: URL, mimetypes: [String] = []) -> Deferred { +func makeLicenseContainer(for file: FileURL, mimetypes: [String] = []) -> Deferred { deferred(on: .global(qos: .background)) { success, _, _ in success(makeLicenseContainerSync(for: file, mimetypes: mimetypes)) } } -func makeLicenseContainerSync(for file: URL, mimetypes: [String] = []) -> LicenseContainer? { +func makeLicenseContainerSync(for file: FileURL, mimetypes: [String] = []) -> LicenseContainer? { guard let mediaType = MediaType.of(file, mediaTypes: mimetypes, fileExtensions: []) else { return nil } diff --git a/Sources/LCP/License/Container/ReadiumLicenseContainer.swift b/Sources/LCP/License/Container/ReadiumLicenseContainer.swift index 3dd7b0eac..726899319 100644 --- a/Sources/LCP/License/Container/ReadiumLicenseContainer.swift +++ b/Sources/LCP/License/Container/ReadiumLicenseContainer.swift @@ -5,10 +5,11 @@ // import Foundation +import R2Shared /// Access a License Document stored in a webpub, audiobook or LCPDF package. final class ReadiumLicenseContainer: ZIPLicenseContainer { - init(path: URL) { + init(path: FileURL) { super.init(zip: path, pathInZIP: "license.lcpl") } } diff --git a/Sources/LCP/License/Container/ZIPLicenseContainer.swift b/Sources/LCP/License/Container/ZIPLicenseContainer.swift index 392f20dfd..eedfc37d2 100644 --- a/Sources/LCP/License/Container/ZIPLicenseContainer.swift +++ b/Sources/LCP/License/Container/ZIPLicenseContainer.swift @@ -5,28 +5,29 @@ // import Foundation +import R2Shared import ZIPFoundation /// Access to a License Document stored in a ZIP archive. /// Meant to be subclassed to customize the pathInZIP property, eg. EPUBLicenseContainer. class ZIPLicenseContainer: LicenseContainer { - private let zip: URL + private let zip: FileURL private let pathInZIP: String - init(zip: URL, pathInZIP: String) { + init(zip: FileURL, pathInZIP: String) { self.zip = zip self.pathInZIP = pathInZIP } func containsLicense() -> Bool { - guard let archive = Archive(url: zip, accessMode: .read) else { + guard let archive = Archive(url: zip.url, accessMode: .read) else { return false } return archive[pathInZIP] != nil } func read() throws -> Data { - guard let archive = Archive(url: zip, accessMode: .read) else { + guard let archive = Archive(url: zip.url, accessMode: .read) else { throw LCPError.licenseContainer(.openFailed) } guard let entry = archive[pathInZIP] else { @@ -46,7 +47,7 @@ class ZIPLicenseContainer: LicenseContainer { } func write(_ license: LicenseDocument) throws { - guard let archive = Archive(url: zip, accessMode: .update) else { + guard let archive = Archive(url: zip.url, accessMode: .update) else { throw LCPError.licenseContainer(.openFailed) } diff --git a/Sources/LCP/License/License.swift b/Sources/LCP/License/License.swift index 6f044671f..f05538883 100644 --- a/Sources/LCP/License/License.swift +++ b/Sources/LCP/License/License.swift @@ -192,7 +192,7 @@ extension License: LCPLicense { func renewWithWebPage(_ link: Link) throws -> Deferred { guard let statusURL = try? license.url(for: .status, preferredType: .lcpStatusDocument), - let url = link.url + let url = link.url() else { throw LCPError.licenseInteractionNotAvailable } @@ -216,13 +216,13 @@ extension License: LCPLicense { : Deferred.success(nil) } - func makeRenewURL(from endDate: Date?) throws -> URL { + func makeRenewURL(from endDate: Date?) throws -> HTTPURL { var params = device.asQueryParameters if let end = endDate { params["end"] = end.iso8601 } - guard let url = link.url(with: params) else { + guard let url = link.url(parameters: params) else { throw LCPError.licenseInteractionNotAvailable } return url @@ -263,8 +263,13 @@ extension License: LCPLicense { } func returnPublication(completion: @escaping (LCPError?) -> Void) { - guard let status = documents.status, - let url = try? status.url(for: .return, preferredType: .lcpStatusDocument, with: device.asQueryParameters) + guard + let status = documents.status, + let url = try? status.url( + for: .return, + preferredType: .lcpStatusDocument, + parameters: device.asQueryParameters + ) else { completion(LCPError.licenseInteractionNotAvailable) return @@ -299,7 +304,7 @@ public extension LCPRenewDelegate { Deferred { preferredEndDate(maximum: maximum, completion: $0) } } - func presentWebPage(url: URL) -> Deferred { + func presentWebPage(url: HTTPURL) -> Deferred { Deferred { presentWebPage(url: url, completion: $0) } } } diff --git a/Sources/LCP/License/Model/Components/Link.swift b/Sources/LCP/License/Model/Components/Link.swift index d4c34f10e..0d1c9296d 100644 --- a/Sources/LCP/License/Model/Components/Link.swift +++ b/Sources/LCP/License/Model/Components/Link.swift @@ -50,20 +50,19 @@ public struct Link { /// Gets the valid URL if possible, applying the given template context as query parameters if the link is templated. /// eg. http://url{?id,name} + [id: x, name: y] -> http://url?id=x&name=y - func url(with parameters: [String: LosslessStringConvertible]) -> URL? { + func url(parameters: [String: LosslessStringConvertible] = [:]) -> HTTPURL? { var href = href if templated { href = URITemplate(href).expand(with: parameters.mapValues { String(describing: $0) }) } - return URL(string: href) + return HTTPURL(string: href) } /// Expands the href without any template context. - var url: URL? { - url(with: [:]) - } + @available(*, unavailable, message: "Use url() instead") + var url: URL? { fatalError() } var mediaType: MediaType { type.flatMap { MediaType.of(mediaType: $0) } ?? .binary diff --git a/Sources/LCP/License/Model/LicenseDocument.swift b/Sources/LCP/License/Model/LicenseDocument.swift index a2c62bd70..e10e36442 100644 --- a/Sources/LCP/License/Model/LicenseDocument.swift +++ b/Sources/LCP/License/Model/LicenseDocument.swift @@ -99,11 +99,11 @@ public struct LicenseDocument { /// are found, the first link with the `rel` and an empty `type` will be returned. /// /// - Throws: `LCPError.invalidLink` if the URL can't be built. - func url(for rel: Rel, preferredType: MediaType? = nil, with parameters: [String: LosslessStringConvertible] = [:]) throws -> URL { + func url(for rel: Rel, preferredType: MediaType? = nil, parameters: [String: LosslessStringConvertible] = [:]) throws -> HTTPURL { let link = link(for: rel, type: preferredType) ?? links.firstWithRelAndNoType(rel.rawValue) - guard let url = link?.url(with: parameters) else { + guard let url = link?.url(parameters: parameters) else { throw ParsingError.url(rel: rel.rawValue) } diff --git a/Sources/LCP/License/Model/StatusDocument.swift b/Sources/LCP/License/Model/StatusDocument.swift index e874fb70e..7f9ee3560 100644 --- a/Sources/LCP/License/Model/StatusDocument.swift +++ b/Sources/LCP/License/Model/StatusDocument.swift @@ -104,11 +104,11 @@ public struct StatusDocument { /// are found, the first link with the `rel` and an empty `type` will be returned. /// /// - Throws: `LCPError.invalidLink` if the URL can't be built. - func url(for rel: Rel, preferredType: MediaType? = nil, with parameters: [String: LosslessStringConvertible] = [:]) throws -> URL { + func url(for rel: Rel, preferredType: MediaType? = nil, parameters: [String: LosslessStringConvertible] = [:]) throws -> HTTPURL { let link = link(for: rel, type: preferredType) ?? linkWithNoType(for: rel) - guard let url = link?.url(with: parameters) else { + guard let url = link?.url(parameters: parameters) else { throw ParsingError.url(rel: rel.rawValue) } diff --git a/Sources/LCP/Services/CRLService.swift b/Sources/LCP/Services/CRLService.swift index de2b66d85..0a3f7d0cb 100644 --- a/Sources/LCP/Services/CRLService.swift +++ b/Sources/LCP/Services/CRLService.swift @@ -44,7 +44,7 @@ final class CRLService { /// Fetches the updated Certificate Revocation List from EDRLab. private func fetch(timeout: TimeInterval? = nil) -> Deferred { - let url = URL(string: "http://crl.edrlab.telesec.de/rl/EDRLab_CA.crl")! + let url = HTTPURL(string: "http://crl.edrlab.telesec.de/rl/EDRLab_CA.crl")! return httpClient.fetch(HTTPRequest(url: url, timeoutInterval: timeout)) .mapError { _ in LCPError.crlFetching } diff --git a/Sources/LCP/Services/DeviceService.swift b/Sources/LCP/Services/DeviceService.swift index 7d8f38115..f21d607b9 100644 --- a/Sources/LCP/Services/DeviceService.swift +++ b/Sources/LCP/Services/DeviceService.swift @@ -50,7 +50,7 @@ final class DeviceService { guard !registered else { return .success(nil) } - guard let url = link.url(with: self.asQueryParameters) else { + guard let url = link.url(parameters: self.asQueryParameters) else { throw LCPError.licenseInteractionNotAvailable } diff --git a/Sources/LCP/Services/LicensesService.swift b/Sources/LCP/Services/LicensesService.swift index e657fa9c4..3ed22fd76 100644 --- a/Sources/LCP/Services/LicensesService.swift +++ b/Sources/LCP/Services/LicensesService.swift @@ -32,7 +32,7 @@ final class LicensesService: Loggable { self.passphrases = passphrases } - func retrieve(from publication: URL, authentication: LCPAuthenticating?, allowUserInteraction: Bool, sender: Any?) -> Deferred { + func retrieve(from publication: FileURL, authentication: LCPAuthenticating?, allowUserInteraction: Bool, sender: Any?) -> Deferred { makeLicenseContainer(for: publication) .flatMap { container in guard let container = container, container.containsLicense() else { @@ -95,7 +95,7 @@ final class LicensesService: Loggable { } } - func acquirePublication(from lcpl: URL, onProgress: @escaping (LCPAcquisition.Progress) -> Void, completion: @escaping (CancellableResult) -> Void) -> LCPAcquisition { + func acquirePublication(from lcpl: FileURL, onProgress: @escaping (LCPAcquisition.Progress) -> Void, completion: @escaping (CancellableResult) -> Void) -> LCPAcquisition { let acquisition = LCPAcquisition(onProgress: onProgress, completion: completion) readLicense(from: lcpl).resolve { result in @@ -118,7 +118,7 @@ final class LicensesService: Loggable { return acquisition } - private func readLicense(from lcpl: URL) -> Deferred { + private func readLicense(from lcpl: FileURL) -> Deferred { makeLicenseContainer(for: lcpl) .tryMap { container in guard let container = container, container.containsLicense() else { @@ -172,7 +172,7 @@ final class LicensesService: Loggable { } /// Injects the given License Document into the `file` acquired using `downloadTask`. - private func injectLicense(_ license: LicenseDocument, in download: HTTPDownload) -> Deferred { + private func injectLicense(_ license: LicenseDocument, in download: HTTPDownload) -> Deferred { var mimetypes: [String] = [ download.mediaType.string, ] @@ -181,7 +181,7 @@ final class LicensesService: Loggable { } return makeLicenseContainer(for: download.location, mimetypes: mimetypes) - .tryMap(on: .global(qos: .background)) { container -> URL in + .tryMap(on: .global(qos: .background)) { container -> FileURL in guard let container = container else { throw LCPError.licenseContainer(.openFailed) } @@ -193,8 +193,8 @@ final class LicensesService: Loggable { } /// Returns the suggested filename to be used when importing a publication. - private func suggestedFilename(for file: URL, license: LicenseDocument) -> String { - let fileExtension: String = { + private func suggestedFilename(for file: FileURL, license: LicenseDocument) -> String { + let fileExtension: String? = { let publicationLink = license.link(for: .publication) if var mediaType = MediaType.of(file, mediaType: publicationLink?.type) { mediaType = mediaTypesMapping[mediaType] ?? mediaType @@ -203,7 +203,8 @@ final class LicensesService: Loggable { return file.pathExtension } }() + let suffix = fileExtension?.addingPrefix(".") ?? "" - return "\(license.id).\(fileExtension)" + return "\(license.id)\(suffix)" } } diff --git a/Sources/Navigator/Audiobook/PublicationMediaLoader.swift b/Sources/Navigator/Audiobook/PublicationMediaLoader.swift index 54f30b7bd..6377bd5a5 100644 --- a/Sources/Navigator/Audiobook/PublicationMediaLoader.swift +++ b/Sources/Navigator/Audiobook/PublicationMediaLoader.swift @@ -35,8 +35,8 @@ final class PublicationMediaLoader: NSObject, AVAssetResourceLoaderDelegate { /// Creates a new `AVURLAsset` to serve the given `link`. func makeAsset(for link: Link) throws -> AVURLAsset { - let originalURL = link.url(relativeTo: publication.baseURL) ?? URL(fileURLWithPath: link.href) - guard var components = URLComponents(url: originalURL, resolvingAgainstBaseURL: true) else { + let originalURL = link.url(relativeTo: publication.baseURL) + guard var components = URLComponents(url: originalURL.url, resolvingAgainstBaseURL: true) else { throw AssetError.invalidHREF(link.href) } diff --git a/Sources/Navigator/CBZ/CBZNavigatorViewController.swift b/Sources/Navigator/CBZ/CBZNavigatorViewController.swift index ec556cfb1..f7f08559f 100644 --- a/Sources/Navigator/CBZ/CBZNavigatorViewController.swift +++ b/Sources/Navigator/CBZ/CBZNavigatorViewController.swift @@ -26,7 +26,7 @@ open class CBZNavigatorViewController: UIViewController, VisualNavigator, Loggab private let server: HTTPServer? private let publicationEndpoint: HTTPServerEndpoint? - private let publicationBaseURL: URL + private let publicationBaseURL: HTTPURL public convenience init( publication: Publication, @@ -39,7 +39,7 @@ open class CBZNavigatorViewController: UIViewController, VisualNavigator, Loggab } let publicationEndpoint: HTTPServerEndpoint? - let baseURL: URL + let baseURL: HTTPURL if let url = publication.baseURL { publicationEndpoint = nil baseURL = url @@ -58,20 +58,9 @@ open class CBZNavigatorViewController: UIViewController, VisualNavigator, Loggab ) } - @available(*, deprecated, message: "See the 2.5.0 migration guide to migrate the HTTP server") + @available(*, unavailable, message: "See the 2.5.0 migration guide to migrate the HTTP server") public convenience init(publication: Publication, initialLocation: Locator? = nil) { - precondition(!publication.isRestricted, "The provided publication is restricted. Check that any DRM was properly unlocked using a Content Protection.") - guard let baseURL = publication.baseURL else { - preconditionFailure("No base URL provided for the publication. Add it to the HTTP server.") - } - - self.init( - publication: publication, - initialLocation: initialLocation, - httpServer: nil, - publicationEndpoint: nil, - publicationBaseURL: baseURL - ) + fatalError() } private init( @@ -79,12 +68,12 @@ open class CBZNavigatorViewController: UIViewController, VisualNavigator, Loggab initialLocation: Locator?, httpServer: HTTPServer?, publicationEndpoint: HTTPServerEndpoint?, - publicationBaseURL: URL + publicationBaseURL: HTTPURL ) { self.publication = publication server = httpServer self.publicationEndpoint = publicationEndpoint - self.publicationBaseURL = URL(string: publicationBaseURL.absoluteString.addingSuffix("/"))! + self.publicationBaseURL = publicationBaseURL initialIndex = { guard let initialLocation = initialLocation, let initialIndex = publication.readingOrder.firstIndex(withHREF: initialLocation.href) else { @@ -108,7 +97,7 @@ open class CBZNavigatorViewController: UIViewController, VisualNavigator, Loggab deinit { if let endpoint = publicationEndpoint { - server?.remove(at: endpoint) + try? server?.remove(at: endpoint) } } @@ -180,13 +169,12 @@ open class CBZNavigatorViewController: UIViewController, VisualNavigator, Loggab } private func imageViewController(at index: Int) -> ImageViewController? { - guard publication.readingOrder.indices.contains(index), - let url = publication.readingOrder[index].url(relativeTo: publicationBaseURL) - else { + guard publication.readingOrder.indices.contains(index) else { return nil } - return ImageViewController(index: index, url: url) + let url = publication.readingOrder[index].url(relativeTo: publicationBaseURL) + return ImageViewController(index: index, url: url.url) } // MARK: - Navigator diff --git a/Sources/Navigator/EPUB/CSS/HTMLFontFamilyDeclaration.swift b/Sources/Navigator/EPUB/CSS/HTMLFontFamilyDeclaration.swift index 6e2383d00..9f64747de 100644 --- a/Sources/Navigator/EPUB/CSS/HTMLFontFamilyDeclaration.swift +++ b/Sources/Navigator/EPUB/CSS/HTMLFontFamilyDeclaration.swift @@ -21,14 +21,14 @@ public protocol HTMLFontFamilyDeclaration { /// /// Use `servingFile` to convert a file URL into an http one to make a local /// file available to the web views. - func inject(in html: String, servingFile: (URL) throws -> URL) throws -> String + func inject(in html: String, servingFile: (FileURL) throws -> HTTPURL) throws -> String } /// A type-erasing `HTMLFontFamilyDeclaration` object public struct AnyHTMLFontFamilyDeclaration: HTMLFontFamilyDeclaration { private let _fontFamily: () -> FontFamily private let _alternates: () -> [FontFamily] - private let _inject: (String, (URL) throws -> URL) throws -> String + private let _inject: (String, (FileURL) throws -> HTTPURL) throws -> String public var fontFamily: FontFamily { _fontFamily() } public var alternates: [FontFamily] { _alternates() } @@ -39,7 +39,7 @@ public struct AnyHTMLFontFamilyDeclaration: HTMLFontFamilyDeclaration { _inject = { try declaration.inject(in: $0, servingFile: $1) } } - public func inject(in html: String, servingFile: (URL) throws -> URL) throws -> String { + public func inject(in html: String, servingFile: (FileURL) throws -> HTTPURL) throws -> String { try _inject(html, servingFile) } } @@ -65,7 +65,7 @@ public struct CSSFontFamilyDeclaration: HTMLFontFamilyDeclaration { self.fontFaces = fontFaces } - public func inject(in html: String, servingFile: (URL) throws -> URL) throws -> String { + public func inject(in html: String, servingFile: (FileURL) throws -> HTTPURL) throws -> String { var injections = try fontFaces.flatMap { try $0.injections(for: html, servingFile: servingFile) } @@ -89,14 +89,14 @@ public struct CSSFontFace { /// /// `preload` indicates whether this source will be declared for preloading /// in the HTML using ``. - private typealias Source = (file: URL, preload: Bool) + private typealias Source = (file: FileURL, preload: Bool) public var style: CSSFontStyle? public var weight: CSSFontWeight? private var sources: [Source] public init( - file: URL, + file: FileURL, preload: Bool = false, style: CSSFontStyle? = nil, weight: CSSFontWeight? = nil @@ -111,26 +111,26 @@ public struct CSSFontFace { /// /// - Parameter preload: Indicates whether this source will be declared for /// preloading in the HTML using ``. - public func addingSource(file: URL, preload: Bool = false) -> Self { + public func addingSource(file: FileURL, preload: Bool = false) -> Self { var copy = self copy.sources.append((file, preload)) return copy } - func injections(for html: String, servingFile: (URL) throws -> URL) throws -> [HTMLInjection] { + func injections(for html: String, servingFile: (FileURL) throws -> HTTPURL) throws -> [HTMLInjection] { try sources .filter(\.preload) .map { source in let file = try servingFile(source.file) - return .link(href: file.absoluteString, rel: "preload", as: "font", crossOrigin: "") + return .link(href: file.string, rel: "preload", as: "font", crossOrigin: "") } } - func css(for fontFamily: String, servingFile: (URL) throws -> URL) throws -> String { + func css(for fontFamily: String, servingFile: (FileURL) throws -> HTTPURL) throws -> String { let urls = try sources.map { try servingFile($0.file) } var descriptors: [String: String] = [ "font-family": "\"\(fontFamily)\"", - "src": urls.map { "url(\"\($0.absoluteString)\")" }.joined(separator: ", "), + "src": urls.map { "url(\"\($0.string)\")" }.joined(separator: ", "), ] if let style = style { diff --git a/Sources/Navigator/EPUB/CSS/ReadiumCSS.swift b/Sources/Navigator/EPUB/CSS/ReadiumCSS.swift index 735c09ac4..e854359e6 100644 --- a/Sources/Navigator/EPUB/CSS/ReadiumCSS.swift +++ b/Sources/Navigator/EPUB/CSS/ReadiumCSS.swift @@ -15,7 +15,7 @@ struct ReadiumCSS { var userProperties: CSSUserProperties = .init() /// Base URL of the Readium CSS assets. - var baseURL: URL + var baseURL: HTTPURL var fontFamilyDeclarations: [AnyHTMLFontFamilyDeclaration] = [] } @@ -114,17 +114,17 @@ extension ReadiumCSS: HTMLInjectable { let hasStyles = hasStyles(html) var stylesheetsFolder = baseURL if let folder = layout.stylesheets.folder { - stylesheetsFolder.appendPathComponent(folder, isDirectory: true) + stylesheetsFolder = stylesheetsFolder.appendingPath(folder, isDirectory: true) } inj.append(.stylesheetLink( - href: stylesheetsFolder.appendingPathComponent("ReadiumCSS-before.css").absoluteString, + href: stylesheetsFolder.appendingPath("ReadiumCSS-before.css", isDirectory: false).string, prepend: true )) if !hasStyles { - inj.append(.stylesheetLink(href: stylesheetsFolder.appendingPathComponent("ReadiumCSS-default.css").absoluteString)) + inj.append(.stylesheetLink(href: stylesheetsFolder.appendingPath("ReadiumCSS-default.css", isDirectory: false).string)) } - inj.append(.stylesheetLink(href: stylesheetsFolder.appendingPathComponent("ReadiumCSS-after.css").absoluteString)) + inj.append(.stylesheetLink(href: stylesheetsFolder.appendingPath("ReadiumCSS-after.css", isDirectory: false).string)) // Fix Readium CSS issue with the positioning of