import Foundation /// 加密 EPUB 资源数据提供者。 /// /// 宿主 App 实现该协议以支持“解压目录内单文件加密”的 EPUB: /// SDK 读取章节 HTML、图片、内联 CSS 等资源时会优先向 provider 索取解密后的数据, /// provider 返回 nil 表示该文件无需解密,SDK 回退为直接读取磁盘明文 /// (明文书、或加密书内的明文文件均走该兜底路径,因此对明文书零影响)。 /// /// 密钥推导与解密算法完全由宿主实现,SDK 不持有任何密钥。 /// /// 注意:方法会在排版 / 渲染的后台队列被同步调用,实现必须线程安全, /// 且除必要的文件 I/O 与解密计算外不得阻塞(例如不要在其中发起网络请求)。 public protocol RDEPUBResourceDataProvider: AnyObject { /// 返回指定文件解密后的数据。 /// - Parameter fileURL: 解压目录内资源文件的绝对路径 /// - Returns: 解密后的数据;返回 nil 表示该文件不需要解密,由 SDK 直读磁盘 func resourceData(at fileURL: URL) -> Data? } /// 加密资源访问登记表。 /// /// DTCoreText 在解析 `` 时由自己内部加载图片文件,拿不到 parser 实例, /// 因此这里以弱引用登记所有启用了 provider 的 parser, /// 供 `RDEPUBDecryptingImageAttachment` 按文件路径反查所属书籍的 provider。 enum RDEPUBResourceAccessRegistry { private struct WeakParserBox { weak var parser: RDEPUBParser? } private static let lock = NSLock() private static var boxes: [WeakParserBox] = [] /// 登记启用了 provider 的 parser(重复登记会被去重,弱引用失效项顺带清理) static func register(_ parser: RDEPUBParser) { lock.lock() defer { lock.unlock() } boxes.removeAll { $0.parser == nil || $0.parser === parser } boxes.append(WeakParserBox(parser: parser)) #if canImport(DTCoreText) RDEPUBDecryptingImageAttachment.registerTagClassIfNeeded() #endif } /// 按文件路径反查 provider 并返回解密数据; /// 仅当文件位于某个已登记 parser 的解压目录内才会命中 static func providedResourceData(at fileURL: URL) -> Data? { let targetPath = fileURL.resolvingSymlinksInPath().standardizedFileURL.path lock.lock() let parsers = boxes.compactMap(\.parser) lock.unlock() for parser in parsers { guard let rootURL = parser.extractionRootURL else { continue } let rootPath = rootURL.resolvingSymlinksInPath().standardizedFileURL.path guard targetPath.hasPrefix(rootPath + "/") else { continue } return parser.providedResourceData(at: fileURL) } return nil } }