mirror of
https://github.com/shadowsocks/ShadowsocksX-NG.git
synced 2026-11-14 02:04:13 +00:00
Domain/ 根目录原有 62 个平铺源文件,无法从目录读出主题划分。 按「目录即主题」重组为 11 个目录(Rules/Custom 为嵌套子目录):Catalog / Subscription / Rules / Runtime / Activation / Settings / SystemProxy / Plugin / Credentials / LegacyImport / Diagnostics。纯移动:类型名、模块名 与 import 均未改动,Domain/ 根下不再有散落文件。目录口径取自 GLOSSARY.md 的章节划分(配置与订阅、代理规则、代理控制与网络、设置、 凭据与诊断),而非新造分类。 配套改动: - project.yml:wrapper 与 helper 两个 target 按文件引用的 15 条显式路径 随目录改名,并补注释固定这一约定——这 15 条指向不存在的文件时, xcodegen generate 以 "missing source directory" 直接失败(实测,不静默)。 - SkeletonTests 新增 DomainLayoutTests:断言 Domain/ 根下无平铺 .swift、 树下只有 .swift。后者防的是与 App/ 同源的坑——Domain/ 整目录是 app 与 Core 的 source path,任何非源码文件都会被 XcodeGen 收进 Resources 阶段 并封进签名后的 bundle。 - README 工程结构说明同步;docs/design 与 docs/research 属历史文档不改。 验证:xcodegen generate 后 app 164 / Core 163 源文件数与整理前一致;Agent 恰好 9 个、Helper 恰好 6 个 Domain 文件(无缺失无多出);app 与 Core 各覆盖 全部 62 个。clean build 0 error;task test 1024 用例全通过(整理前 1022, +2 为新增守卫);task format 未改写任何被移动文件(证明纯移动);task lint 与整理前同为 21 条内容型 warning;packaging-gate.sh ALL CHECKS PASSED。 布局守卫已做变异验证:临时投放松散 .swift 与非 .swift 文件时两个用例均 失败并点名违规文件,清理后复验为绿。
126 lines
4.8 KiB
Swift
126 lines
4.8 KiB
Swift
import Foundation
|
|
|
|
/// 系统代理写入失败(issue #71):helper 内 SC 操作失败与 GUI 侧 XPC 失败共用
|
|
/// 同一族,可直接作为 XPC 应答 payload 编解码(Codable)。
|
|
enum SystemProxyError: Error, Equatable, Sendable, Codable {
|
|
case preferencesUnavailable
|
|
case preferencesBusy
|
|
case noCurrentNetworkSet
|
|
case noNetworkLocations
|
|
case noProxyServices
|
|
case unreadableService(String)
|
|
case invalidStoredConfiguration(String)
|
|
case cannotWriteService(String)
|
|
case commitFailed(String)
|
|
case applyFailed(String)
|
|
case invalidRequest(String)
|
|
case helperUnavailable(String)
|
|
}
|
|
|
|
/// A service identity includes its network location because service IDs can appear in
|
|
/// different locations during cleanup-all-locations scans.
|
|
struct SystemProxyServiceIdentifier: Equatable, Hashable, Sendable {
|
|
let locationID: String
|
|
let serviceID: String
|
|
}
|
|
|
|
/// One service's complete Proxies dictionary as a pure value.
|
|
struct SystemProxyServiceState: Equatable, Sendable {
|
|
let identifier: SystemProxyServiceIdentifier
|
|
/// `nil` means that the service has no Proxies entity configuration.
|
|
let configuration: Data?
|
|
var name: String = ""
|
|
}
|
|
|
|
/// One planned Proxies-entity write. `nil` clears the complete entity.
|
|
struct SystemProxyPlannedWrite: Equatable, Sendable {
|
|
let identifier: SystemProxyServiceIdentifier
|
|
let configuration: Data?
|
|
}
|
|
|
|
/// Result of an apply plan. `unchanged` avoids an unnecessary SCPreferences commit.
|
|
enum SystemProxyWriteOutcome: Equatable, Sendable, Codable {
|
|
case written
|
|
case unchanged
|
|
}
|
|
|
|
/// Pure configuration planning. Apply rewrites every differing service in the active
|
|
/// location, preserving unmodeled Proxies keys. Cleanup is unconditional: the complete
|
|
/// Proxies dictionary goes for every service in every scanned location, regardless of
|
|
/// which application or account wrote it (issue #71).
|
|
enum SystemProxyPlanner {
|
|
struct ApplyPlan: Equatable, Sendable {
|
|
let writes: [SystemProxyPlannedWrite]
|
|
|
|
var outcome: SystemProxyWriteOutcome {
|
|
writes.isEmpty ? .unchanged : .written
|
|
}
|
|
}
|
|
|
|
struct ClearPlan: Equatable, Sendable {
|
|
let writes: [SystemProxyPlannedWrite]
|
|
}
|
|
|
|
static func makeApplyPlan(
|
|
services: [SystemProxyServiceState], configuration: SystemProxyConfiguration
|
|
) throws -> ApplyPlan {
|
|
var writes: [SystemProxyPlannedWrite] = []
|
|
for service in services {
|
|
let original = try dictionary(
|
|
from: service.configuration, serviceID: service.identifier.serviceID)
|
|
let projected = SystemProxyPropertyList.applying(configuration, to: original)
|
|
let normalizedOriginal =
|
|
SystemProxyPropertyList.semanticallyNormalized(original) as NSDictionary
|
|
let normalizedDesired =
|
|
SystemProxyPropertyList.semanticallyNormalized(projected) as NSDictionary
|
|
guard !normalizedOriginal.isEqual(normalizedDesired) else { continue }
|
|
let desired = try propertyListData(
|
|
from: projected,
|
|
serviceID: service.identifier.serviceID)
|
|
writes.append(SystemProxyPlannedWrite(identifier: service.identifier, configuration: desired))
|
|
}
|
|
return ApplyPlan(writes: writes)
|
|
}
|
|
|
|
/// 无条件清理计划:凡持有 Proxies 实体的服务都整字典移除,不校验来源,
|
|
/// 也不保存或查询端点签名、所有权标记或历史快照(issue #71)。
|
|
static func makeClearPlan(services: [SystemProxyServiceState]) -> ClearPlan {
|
|
let writes =
|
|
services
|
|
.filter { $0.configuration != nil }
|
|
.map { SystemProxyPlannedWrite(identifier: $0.identifier, configuration: nil) }
|
|
return ClearPlan(writes: writes)
|
|
}
|
|
|
|
/// Property-list semantic equality; dictionary key order does not matter.
|
|
static func equivalent(_ lhs: Data?, _ rhs: Data?) -> Bool {
|
|
guard let lhs, let rhs else { return lhs == nil && rhs == nil }
|
|
guard
|
|
let left = try? PropertyListSerialization.propertyList(from: lhs, options: [], format: nil)
|
|
as? NSDictionary,
|
|
let right = try? PropertyListSerialization.propertyList(from: rhs, options: [], format: nil)
|
|
as? NSDictionary
|
|
else { return lhs == rhs }
|
|
return left.isEqual(right)
|
|
}
|
|
|
|
static func dictionary(from data: Data?, serviceID: String) throws -> [String: Any] {
|
|
guard let data else { return [:] }
|
|
guard
|
|
let propertyList = try? PropertyListSerialization.propertyList(
|
|
from: data, options: [], format: nil),
|
|
let dictionary = propertyList as? [String: Any]
|
|
else { throw SystemProxyError.invalidStoredConfiguration(serviceID) }
|
|
return dictionary
|
|
}
|
|
|
|
static func propertyListData(from dictionary: [String: Any], serviceID: String) throws -> Data {
|
|
do {
|
|
return try PropertyListSerialization.data(
|
|
fromPropertyList: dictionary, format: .binary, options: 0)
|
|
} catch {
|
|
throw SystemProxyError.unreadableService(serviceID)
|
|
}
|
|
}
|
|
}
|