ReadViewSDK/Sources/RDReaderView/EPUBCore/RDEPUBParser+Archive.swift
2026-05-21 19:40:51 +08:00

99 lines
3.6 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 {
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
}
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)
}
}