- 实现EPUB阅读器搜索功能及选中注释功能 - 优化CFI模块,修复代码审查发现的11个问题 - 实现大书远距目录跳转与后台补全优化方案 - 优化设置面板与章节运行时联动 - 重构及大量改进优化
124 lines
4.4 KiB
Swift
124 lines
4.4 KiB
Swift
|
|
import Foundation
|
|
import ZIPFoundation
|
|
|
|
extension RDEPUBParser {
|
|
|
|
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
|
|
}
|
|
|
|
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 {
|
|
guard let destinationURL = validatedExtractionDestination(for: entry.path, extractionRoot: extractionURL) else {
|
|
throw RDEPUBParserError.invalidArchiveEntryPath(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
|
|
}
|
|
|
|
private func validatedExtractionDestination(for entryPath: String, extractionRoot: URL) -> URL? {
|
|
|
|
if entryPath.hasPrefix("/") {
|
|
return nil
|
|
}
|
|
|
|
let components = entryPath.split(separator: "/", omittingEmptySubsequences: true)
|
|
if components.contains(where: { $0 == ".." }) {
|
|
return nil
|
|
}
|
|
let destinationURL = extractionRoot.appendingPathComponent(entryPath)
|
|
let standardizedDest = destinationURL.standardizedFileURL.path
|
|
let standardizedRoot = extractionRoot.standardizedFileURL.path
|
|
|
|
guard standardizedDest.hasPrefix(standardizedRoot) else {
|
|
return nil
|
|
}
|
|
return destinationURL
|
|
}
|
|
|
|
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()
|
|
}
|
|
|
|
func resetPublicationState() {
|
|
metadata = RDEPUBMetadata()
|
|
manifest = [:]
|
|
spine = []
|
|
tableOfContents = []
|
|
}
|
|
}
|
|
|
|
private final class ContainerXMLParserDelegate: NSObject, XMLParserDelegate {
|
|
|
|
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)
|
|
}
|
|
}
|