- 给全部 78 个 Swift 源文件添加详细的中文注释(文件级、类级、方法级) - 删除 LegacyRDReaderController/ 死代码目录(16 文件 4592 行) - 根目录翻页容器文件移入 ReaderView/ 目录 - Resources/ 移入 EPUBCore/Resources/(与使用者归属一致) - RDEPUBTextIndexTable.swift 移入 EPUBTextRendering/(消除反向依赖) - RDURLReaderController.swift 移入 EPUBUI/(入口控制器归入 UI 层) - 更新 podspec 资源路径
117 lines
4.7 KiB
Swift
117 lines
4.7 KiB
Swift
// RDEPUBParser+Archive.swift
|
||
// EPUB 归档解压与 container.xml 解析
|
||
// 负责将 EPUB ZIP 文件解压到缓存目录(~/Library/Caches/ssreaderview-epub/),
|
||
// 并解析 META-INF/container.xml 获取 OPF 文件路径。
|
||
|
||
import Foundation
|
||
import ZIPFoundation
|
||
|
||
extension RDEPUBParser {
|
||
/// 解析 container.xml,提取 OPF(Package Document)的相对路径
|
||
/// - Parameter containerURL: container.xml 的文件路径
|
||
/// - Returns: OPF 文件的相对路径(如 "OEBPS/content.opf")
|
||
func parseContainerRootFile(at containerURL: URL) throws -> String {
|
||
guard let parser = XMLParser(contentsOf: containerURL) else {
|
||
throw RDEPUBParserError.invalidXML(containerURL)
|
||
}
|
||
|
||
let delegate = ContainerXMLParserDelegate()
|
||
parser.shouldProcessNamespaces = false
|
||
parser.delegate = delegate
|
||
|
||
guard parser.parse() else {
|
||
throw parser.parserError ?? RDEPUBParserError.invalidXML(containerURL)
|
||
}
|
||
|
||
guard let rootFilePath = delegate.rootFilePath, !rootFilePath.isEmpty else {
|
||
throw RDEPUBParserError.missingRootFile
|
||
}
|
||
|
||
return rootFilePath
|
||
}
|
||
|
||
/// 解压 EPUB ZIP 文件到缓存目录(幂等:已存在则跳过)
|
||
/// 目录名由 slug + 文件大小 + 修改时间戳组成,避免冲突
|
||
/// - Parameter epubURL: EPUB 文件路径
|
||
/// - Returns: 解压目标目录 URL
|
||
func extractArchiveIfNeeded(epubURL: URL) throws -> URL {
|
||
let fileManager = FileManager.default
|
||
let extractionURL = temporaryExtractionDirectory(for: epubURL)
|
||
|
||
if fileManager.fileExists(atPath: extractionURL.path) {
|
||
return extractionURL
|
||
}
|
||
|
||
guard let archive = Archive(url: epubURL, accessMode: .read) else {
|
||
throw RDEPUBParserError.archiveOpenFailed(epubURL)
|
||
}
|
||
|
||
try fileManager.createDirectory(at: extractionURL, withIntermediateDirectories: true)
|
||
|
||
for entry in archive {
|
||
let destinationURL = extractionURL.appendingPathComponent(entry.path)
|
||
switch entry.type {
|
||
case .directory:
|
||
try fileManager.createDirectory(at: destinationURL, withIntermediateDirectories: true)
|
||
case .file:
|
||
try fileManager.createDirectory(at: destinationURL.deletingLastPathComponent(), withIntermediateDirectories: true)
|
||
_ = try archive.extract(entry, to: destinationURL)
|
||
case .symlink:
|
||
continue
|
||
}
|
||
}
|
||
|
||
return extractionURL
|
||
}
|
||
|
||
/// 计算 EPUB 的临时解压目录路径
|
||
/// 路径格式:~/Library/Caches/ssreaderview-epub/{slug}-{fileSize}-{modifiedTimestamp}/
|
||
func temporaryExtractionDirectory(for epubURL: URL) -> URL {
|
||
let baseURL = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first?
|
||
.appendingPathComponent("ssreaderview-epub", isDirectory: true)
|
||
?? FileManager.default.temporaryDirectory.appendingPathComponent("ssreaderview-epub", isDirectory: true)
|
||
let fileAttributes = try? FileManager.default.attributesOfItem(atPath: epubURL.path)
|
||
let fileSize = (fileAttributes?[.size] as? NSNumber)?.stringValue ?? "0"
|
||
let modifiedAt = (fileAttributes?[.modificationDate] as? Date)?.timeIntervalSince1970 ?? 0
|
||
let slug = epubURL.deletingPathExtension().lastPathComponent
|
||
.replacingOccurrences(of: " ", with: "-")
|
||
let signature = String(format: "%.0f", modifiedAt)
|
||
return baseURL.appendingPathComponent("\(slug)-\(fileSize)-\(signature)", isDirectory: true)
|
||
}
|
||
|
||
/// 重置解析器状态(清除解压路径和出版物数据)
|
||
func reset() {
|
||
extractionRootURL = nil
|
||
opfURL = nil
|
||
resetPublicationState()
|
||
}
|
||
|
||
/// 重置出版物相关状态(metadata、manifest、spine、目录)
|
||
func resetPublicationState() {
|
||
metadata = RDEPUBMetadata()
|
||
manifest = [:]
|
||
spine = []
|
||
tableOfContents = []
|
||
}
|
||
}
|
||
|
||
/// container.xml 的 SAX 解析代理,提取 <rootfile> 元素的 full-path 属性
|
||
private final class ContainerXMLParserDelegate: NSObject, XMLParserDelegate {
|
||
/// 解析得到的 OPF 文件相对路径
|
||
private(set) var rootFilePath: String?
|
||
|
||
func parser(
|
||
_ parser: XMLParser,
|
||
didStartElement elementName: String,
|
||
namespaceURI: String?,
|
||
qualifiedName qName: String?,
|
||
attributes attributeDict: [String: String] = [:]
|
||
) {
|
||
let name = XMLName.localName(from: qName ?? elementName)
|
||
guard name == "rootfile", rootFilePath == nil else {
|
||
return
|
||
}
|
||
rootFilePath = attributeDict["full-path"]?.trimmingCharacters(in: .whitespacesAndNewlines)
|
||
}
|
||
}
|