Files
ShadowsocksX-NG/ShadowsocksX-NG2/Domain/ProxySettingsFileStore.swift
T
Qiu Yuzhou 4b2ddae99e refactor(paths): App Support 目录命名权归 RuntimePaths
六个 GUI 配置文件位置(catalog/settings/listen-settings/custom-rules/
activation 与 silent-launch 遗留迁移源)自拼 applicationSupport 目录的
写法收敛为 RuntimePaths 的具名函数,各 store 的 defaultFileURL 改一行
委托。RuntimePaths 宪章从「跨进程运行时契约」扩为「App Support 目录
全部文件清单」——目录里有过哪些文件从此一处可答。

评审候选⑥ 6c:目录改名从动六个文件缩为动一处。
2026-10-04 13:02:31 +08:00

175 lines
6.5 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import Foundation
/// #33 偏好存储。非敏感字段写入受保护的 JSON。GFWList URL 与 PAC 用户规则
/// 已随 issue #67 移除;未发布的 NG2 PAC 偏好不做迁移。
struct ProxySettingsFileStore: ProxySettingsStoring {
let fileURL: URL
let legacyListenFileURL: URL
init(
fileURL: URL = ProxySettingsFileStore.defaultFileURL(),
legacyListenFileURL: URL = ListenSettingsFileStore.defaultFileURL()
) {
self.fileURL = fileURL
self.legacyListenFileURL = legacyListenFileURL
}
static func defaultFileURL() -> URL {
RuntimePaths.settingsFileURL()
}
func load() throws -> ProxySettings {
guard FileManager.default.fileExists(atPath: fileURL.path) else {
do {
let listen = try ListenSettingsFileStore(fileURL: legacyListenFileURL).load()
return try validated(ProxySettings(listen: listen))
} catch let error as ListenSettingsStoreError {
throw ProxySettingsStoreError.legacyListenSettings(error)
} catch let error as ProxySettingsStoreError {
throw error
} catch {
throw ProxySettingsStoreError.ioFailure(detail: String(describing: error))
}
}
let data: Data
do {
data = try Data(contentsOf: fileURL)
} catch {
throw ProxySettingsStoreError.ioFailure(detail: String(describing: error))
}
let record: ProxySettingsRecord
do {
record = try JSONDecoder().decode(ProxySettingsRecord.self, from: data)
} catch {
throw ProxySettingsStoreError.corrupt(detail: String(describing: error))
}
return try settings(from: record)
}
func save(_ settings: ProxySettings) throws {
_ = try validated(settings)
do {
let data = try Self.jsonEncoder.encode(record(from: settings))
try AtomicFileWriter.write(data, to: fileURL)
} catch let error as ProxySettingsStoreError {
throw error
} catch let error as AtomicFileWriter.WriteError {
throw ProxySettingsStoreError.ioFailure(detail: String(describing: error))
} catch {
throw ProxySettingsStoreError.ioFailure(detail: String(describing: error))
}
}
static func restored(store: ProxySettingsFileStore = ProxySettingsFileStore())
-> RestoredProxySettings
{
do {
return RestoredProxySettings(settings: try store.load(), unreadableError: nil)
} catch let error as ProxySettingsStoreError {
RuntimeLog.emit(.listenSettingsUnreadable(detail: String(describing: error)))
return RestoredProxySettings(settings: ProxySettings(), unreadableError: error)
} catch {
let wrapped = ProxySettingsStoreError.ioFailure(detail: String(describing: error))
RuntimeLog.emit(.listenSettingsUnreadable(detail: String(describing: error)))
return RestoredProxySettings(settings: ProxySettings(), unreadableError: wrapped)
}
}
private func validated(_ settings: ProxySettings) throws -> ProxySettings {
let errors = settings.validationErrors
guard errors.isEmpty else { throw ProxySettingsStoreError.invalid(errors) }
return settings
}
private func settings(from record: ProxySettingsRecord) throws -> ProxySettings {
var listen = SslocalListenSettings()
listen.listenerMode = record.listenerMode
listen.socksPort = record.socksPort
listen.httpPort = record.httpPort
return try validated(
ProxySettings(
listen: listen,
proxyExceptions: record.proxyExceptions,
preferredMode: record.preferredMode,
ruleDefaultAction: record.ruleDefaultAction,
agentEnabled: record.agentEnabled,
systemProxyEnabled: record.systemProxyEnabled))
}
private func record(from settings: ProxySettings) -> ProxySettingsRecord {
var record = ProxySettingsRecord()
record.listenerMode = settings.listen.listenerMode
record.socksPort = settings.listen.socksPort
record.httpPort = settings.listen.httpPort
record.proxyExceptions = settings.proxyExceptions
record.preferredMode = settings.preferredMode
record.ruleDefaultAction = settings.ruleDefaultAction
record.agentEnabled = settings.agentEnabled
record.systemProxyEnabled = settings.systemProxyEnabled
return record
}
private static let jsonEncoder: JSONEncoder = {
let encoder = JSONEncoder()
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
return encoder
}()
}
private struct ProxySettingsRecord: Codable, Equatable, Sendable {
var listenerMode: ListenerMode = .localhost
var socksPort: Int = SslocalListenSettings.defaultSocksPort
var httpPort: Int = SslocalListenSettings.defaultHTTPPort
var proxyExceptions: String = ProxySettings.defaultProxyExceptions
var preferredMode: ProxyModeKind = .rule
var ruleDefaultAction: RuleDefaultAction = .proxyWhenUnmatched
var agentEnabled: Bool = false
var systemProxyEnabled: Bool = false
init() {}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
listenerMode =
try container.decodeIfPresent(ListenerMode.self, forKey: .listenerMode) ?? .localhost
socksPort =
try container.decodeIfPresent(Int.self, forKey: .socksPort)
?? SslocalListenSettings.defaultSocksPort
httpPort =
try container.decodeIfPresent(Int.self, forKey: .httpPort)
?? SslocalListenSettings.defaultHTTPPort
proxyExceptions =
try container.decodeIfPresent(String.self, forKey: .proxyExceptions)
?? ProxySettings.defaultProxyExceptions
// 旧 NG2 偏好里的 "pac" 等已删除值容错回落到规则模式(issue #67:
// 未发布的 PAC 偏好不做迁移,但解码失败会把全部保留字段连坐丢掉)。
if let rawMode = try container.decodeIfPresent(String.self, forKey: .preferredMode),
let mode = ProxyModeKind(rawValue: rawMode)
{
preferredMode = mode
} else {
preferredMode = .rule
}
// 规则模式子选项(issue #63):出厂「未匹配时代理」。
ruleDefaultAction =
try container.decodeIfPresent(RuleDefaultAction.self, forKey: .ruleDefaultAction)
?? .proxyWhenUnmatched
// 未发版无迁移:缺省 agent off、系统代理 off(ADR-0011)。
agentEnabled = try container.decodeIfPresent(Bool.self, forKey: .agentEnabled) ?? false
systemProxyEnabled =
try container.decodeIfPresent(Bool.self, forKey: .systemProxyEnabled) ?? false
}
}
extension ProxySettingsRecord {
fileprivate enum CodingKeys: String, CodingKey {
case listenerMode, socksPort, httpPort
case proxyExceptions
case preferredMode
case ruleDefaultAction
case agentEnabled, systemProxyEnabled
}
}