Files
ShadowsocksX-NG/ShadowsocksX-NG2/Domain/Catalog/CatalogFileStore.swift
T

132 lines
4.6 KiB
Swift

import Foundation
/// 服务器库文件存储;生产和测试均通过整体加密存储写盘。
struct CatalogFileStore {
enum PersistenceError: Error, Equatable {
/// 文件存在但不是本模型可接受的目录文档:JSON 损坏、版本未知、结构不一致。
case corrupt(detail: String)
/// 读取或写入时的文件系统错误。
case ioFailure(detail: String)
}
private static let currentVersion = 7
private static let jsonEncoder: JSONEncoder = {
let encoder = JSONEncoder()
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
return encoder
}()
let fileURL: URL
private let library: ServerLibraryStorage
init(fileURL: URL, library: ServerLibraryStorage = ServerLibraryStorage()) {
self.fileURL = fileURL
self.library = library
}
func reset() throws -> CatalogDocument {
try library.reset(at: fileURL)
}
/// 默认密文位置:`~/Library/Application Support/ShadowsocksX-NG2/configuration/catalog.enc`。
static func defaultFileURL() -> URL {
ApplicationSupportPaths.current.catalogFileURL
}
/// 文件缺失 → 全新空文档;存在但损坏/版本未知/结构不一致 → `.corrupt`。
func load() throws -> CatalogDocument {
try load(allowingInteraction: false)
}
func load(allowingInteraction: Bool) throws -> CatalogDocument {
try library.load(at: fileURL, allowingInteraction: allowingInteraction)
}
/// 整体加密后原子替换,失败时保留原文件。
func save(_ document: CatalogDocument) throws {
try library.save(document, at: fileURL)
}
static func encode(_ document: CatalogDocument) throws -> Data {
try Self.validateFavorites(document.favoriteIDs, in: document.catalog)
let payload = CatalogFilePayload(
version: Self.currentVersion,
rootChildren: document.catalog.rootChildren,
entries: document.catalog.entries.values.sorted { $0.id.rawValue < $1.id.rawValue },
subscriptions: document.subscriptions, favoriteIDs: document.favoriteIDs)
let data: Data
do {
data = try Self.jsonEncoder.encode(payload)
} catch {
throw PersistenceError.ioFailure(detail: String(describing: error))
}
return data
}
static func decode(_ data: Data) throws -> CatalogDocument {
let payload: CatalogFilePayload
do {
payload = try JSONDecoder().decode(CatalogFilePayload.self, from: data)
} catch {
throw PersistenceError.corrupt(detail: String(describing: error))
}
guard payload.version == Self.currentVersion else {
throw PersistenceError.corrupt(detail: "unsupported version \(payload.version)")
}
var entries: [NodeID: CatalogEntry] = [:]
for entry in payload.entries {
guard entries[entry.id] == nil else {
throw PersistenceError.corrupt(detail: "duplicate node id \(entry.id.rawValue)")
}
entries[entry.id] = entry
}
let catalog: ConfigurationCatalog
do {
catalog = try ConfigurationCatalog.validated(
rootChildren: payload.rootChildren, entries: entries)
} catch let error as CatalogError {
throw PersistenceError.corrupt(detail: String(describing: error))
}
let subscriptions = payload.subscriptions
var seen = Set<NodeID>()
for record in subscriptions {
guard seen.insert(record.id).inserted else {
throw PersistenceError.corrupt(detail: "duplicate subscription \(record.id.rawValue)")
}
}
// 订阅固定分组必须存在于目录且来源正确;缺失即文档不一致。
for record in subscriptions {
guard
let entry = catalog.entry(for: record.groupID), case .group = entry.kind,
entry.source == .subscription
else {
throw PersistenceError.corrupt(
detail: "subscription group missing \(record.groupID.rawValue)")
}
}
let favoriteIDs = payload.favoriteIDs
try validateFavorites(favoriteIDs, in: catalog)
return CatalogDocument(
catalog: catalog, subscriptions: subscriptions, favoriteIDs: favoriteIDs)
}
private static func validateFavorites(_ ids: [NodeID], in catalog: ConfigurationCatalog) throws {
var seen = Set<NodeID>()
for id in ids {
guard seen.insert(id).inserted, catalog.entry(for: id) != nil else {
throw PersistenceError.corrupt(detail: "duplicate or missing favorite node \(id.rawValue)")
}
}
}
}
/// 当前服务器库明文载荷;只能在内存中编解码,由整体加密存储写盘。
private struct CatalogFilePayload: Codable {
var version: Int
var rootChildren: [NodeID]
var entries: [CatalogEntry]
var subscriptions: [SubscriptionRecord]
var favoriteIDs: [NodeID]
}