mirror of
https://github.com/Aminark/legado-ios.git
synced 2026-10-20 19:21:22 +08:00
92 lines
3.4 KiB
Swift
92 lines
3.4 KiB
Swift
import Foundation
|
||
|
||
public enum ReplaceRuleImportError: Error, LocalizedError, Equatable {
|
||
case emptyDocument
|
||
case documentTooLarge(Int)
|
||
case invalidDocument(String)
|
||
case emptyPattern(String)
|
||
|
||
public var errorDescription: String? {
|
||
switch self {
|
||
case .emptyDocument: "净化规则内容为空"
|
||
case .documentTooLarge(let maximum): "净化规则文件超过 \(maximum / 1_024 / 1_024) MB 上限"
|
||
case .invalidDocument(let detail): "无法解析净化规则 JSON:\(detail)"
|
||
case .emptyPattern(let name): "净化规则“\(name)”没有匹配内容"
|
||
}
|
||
}
|
||
}
|
||
|
||
public struct ReplaceRuleImporter: Sendable {
|
||
public static let maximumDocumentBytes = 10 * 1_024 * 1_024
|
||
|
||
public init() {}
|
||
|
||
public func decode(_ data: Data) throws -> [ReplaceRule] {
|
||
guard data.count <= Self.maximumDocumentBytes else {
|
||
throw ReplaceRuleImportError.documentTooLarge(Self.maximumDocumentBytes)
|
||
}
|
||
let bom = Data([0xEF, 0xBB, 0xBF])
|
||
let cleaned = data.starts(with: bom) ? Data(data.dropFirst(3)) : data
|
||
guard !cleaned.isEmpty else { throw ReplaceRuleImportError.emptyDocument }
|
||
do {
|
||
let decoder = JSONDecoder()
|
||
let rules = if let array = try? decoder.decode([ReplaceRule].self, from: cleaned) {
|
||
array
|
||
} else {
|
||
[try decoder.decode(ReplaceRule.self, from: cleaned)]
|
||
}
|
||
for rule in rules { try ReplaceRuleValidator.validate(rule) }
|
||
return rules
|
||
} catch let error as ReplaceRuleImportError {
|
||
throw error
|
||
} catch {
|
||
throw ReplaceRuleImportError.invalidDocument(error.localizedDescription)
|
||
}
|
||
}
|
||
|
||
public func merge(_ incoming: [ReplaceRule], into existing: [ReplaceRule]) -> [ReplaceRule] {
|
||
var result = existing
|
||
var indexes = Dictionary(uniqueKeysWithValues: result.enumerated().map { ($1.id, $0) })
|
||
for rule in incoming {
|
||
if let index = indexes[rule.id] {
|
||
result[index] = rule
|
||
} else {
|
||
indexes[rule.id] = result.count
|
||
result.append(rule)
|
||
}
|
||
}
|
||
return result.sorted { $0.sortOrder < $1.sortOrder }
|
||
}
|
||
}
|
||
|
||
public protocol ReplaceRuleRepository: Sendable {
|
||
func load() async throws -> [ReplaceRule]
|
||
func save(_ rules: [ReplaceRule]) async throws
|
||
}
|
||
|
||
public actor FileReplaceRuleRepository: ReplaceRuleRepository {
|
||
private let fileURL: URL
|
||
|
||
public init(fileURL: URL) { self.fileURL = fileURL }
|
||
|
||
public func load() throws -> [ReplaceRule] {
|
||
guard FileManager.default.fileExists(atPath: fileURL.path) else { return [] }
|
||
return try JSONDecoder().decode([ReplaceRule].self, from: Data(contentsOf: fileURL))
|
||
}
|
||
|
||
public func save(_ rules: [ReplaceRule]) throws {
|
||
let directory = fileURL.deletingLastPathComponent()
|
||
try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
|
||
let encoder = JSONEncoder()
|
||
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
|
||
try encoder.encode(rules).write(to: fileURL, options: .atomic)
|
||
}
|
||
}
|
||
|
||
public actor InMemoryReplaceRuleRepository: ReplaceRuleRepository {
|
||
private var rules: [ReplaceRule]
|
||
public init(rules: [ReplaceRule] = []) { self.rules = rules }
|
||
public func load() -> [ReplaceRule] { rules }
|
||
public func save(_ rules: [ReplaceRule]) { self.rules = rules }
|
||
}
|