mirror of
https://github.com/shadowsocks/ShadowsocksX-NG.git
synced 2026-11-12 06:35:06 +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 文件时两个用例均 失败并点名违规文件,清理后复验为绿。
288 lines
9.2 KiB
Swift
288 lines
9.2 KiB
Swift
import Foundation
|
|
|
|
/// Safe, durable facts for the most recent subscription refresh failure.
|
|
///
|
|
/// This type is deliberately a closed whitelist. It carries enough information
|
|
/// for a presentation edge to explain the failure, but never stores a URL,
|
|
/// token, credential reference, arbitrary platform description, or raw parser
|
|
/// detail in the catalog.
|
|
enum SubscriptionRefreshFailure: Codable, Equatable, Sendable {
|
|
enum TransportCategory: String, Codable, Equatable, Sendable {
|
|
case timedOut
|
|
case tls
|
|
case connection
|
|
case unknown
|
|
}
|
|
|
|
enum ContentTypeCategory: String, Codable, Equatable, Sendable {
|
|
case missing
|
|
case unsupported
|
|
}
|
|
|
|
enum RecordField: String, Codable, Equatable, Sendable {
|
|
case address
|
|
case port
|
|
case method
|
|
case password
|
|
case identity
|
|
}
|
|
|
|
enum CredentialCategory: String, Codable, Equatable, Sendable {
|
|
case missing
|
|
case read
|
|
case write
|
|
}
|
|
|
|
enum CommitCategory: String, Codable, Equatable, Sendable {
|
|
case persistence
|
|
case credentials
|
|
}
|
|
|
|
case invalidURL
|
|
case unsupportedScheme
|
|
case insecureRedirect
|
|
case transport(TransportCategory)
|
|
case httpStatus(code: Int)
|
|
case contentType(ContentTypeCategory)
|
|
case decodingFailure
|
|
case unsupportedSchemaVersion
|
|
case missingServers
|
|
case recordValidation(index: Int, field: RecordField)
|
|
case duplicateIdentity
|
|
case credential(category: CredentialCategory)
|
|
case commit(category: CommitCategory, rollback: CredentialRollbackStatus)
|
|
/// Failure text loaded from a pre-v4 document. The old detail is intentionally
|
|
/// discarded and cannot be written back.
|
|
case legacy
|
|
case unknown
|
|
|
|
private enum CodingKeys: String, CodingKey {
|
|
case kind
|
|
case category
|
|
case code
|
|
case index
|
|
case field
|
|
case rollback
|
|
}
|
|
|
|
private enum Kind: String, Codable {
|
|
case invalidURL
|
|
case unsupportedScheme
|
|
case insecureRedirect
|
|
case transport
|
|
case httpStatus
|
|
case contentType
|
|
case decodingFailure
|
|
case unsupportedSchemaVersion
|
|
case missingServers
|
|
case recordValidation
|
|
case duplicateIdentity
|
|
case credential
|
|
case commit
|
|
case legacy
|
|
case unknown
|
|
}
|
|
|
|
/// 无 payload 的失败值 ↔ kind 对照表:编解码共用,保持与 Kind 枚举一一对应。
|
|
private static let payloadlessKinds: [(kind: Kind, failure: SubscriptionRefreshFailure)] = [
|
|
(.invalidURL, .invalidURL),
|
|
(.unsupportedScheme, .unsupportedScheme),
|
|
(.insecureRedirect, .insecureRedirect),
|
|
(.decodingFailure, .decodingFailure),
|
|
(.unsupportedSchemaVersion, .unsupportedSchemaVersion),
|
|
(.missingServers, .missingServers),
|
|
(.duplicateIdentity, .duplicateIdentity),
|
|
(.legacy, .legacy),
|
|
(.unknown, .unknown),
|
|
]
|
|
|
|
init(from decoder: Decoder) throws {
|
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
|
let kind = try container.decode(Kind.self, forKey: .kind)
|
|
if let payloadless = Self.payloadlessKinds.first(where: { $0.kind == kind })?.failure {
|
|
self = payloadless
|
|
} else {
|
|
self = try Self.decodePayload(kind: kind, from: container)
|
|
}
|
|
}
|
|
|
|
/// 带 payload 的 kind → 关联值解码;无 payload 的 kind 由对照表处理,
|
|
/// 这里显式列出以保持对 Kind 枚举的穷举检查。
|
|
private static func decodePayload(
|
|
kind: Kind, from container: KeyedDecodingContainer<CodingKeys>
|
|
) throws -> SubscriptionRefreshFailure {
|
|
switch kind {
|
|
case .transport:
|
|
return .transport(try container.decode(TransportCategory.self, forKey: .category))
|
|
case .httpStatus:
|
|
return .httpStatus(code: try container.decode(Int.self, forKey: .code))
|
|
case .contentType:
|
|
return .contentType(try container.decode(ContentTypeCategory.self, forKey: .category))
|
|
case .recordValidation:
|
|
return .recordValidation(
|
|
index: try container.decode(Int.self, forKey: .index),
|
|
field: try container.decode(RecordField.self, forKey: .field))
|
|
case .credential:
|
|
return .credential(category: try container.decode(CredentialCategory.self, forKey: .category))
|
|
case .commit:
|
|
return .commit(
|
|
category: try container.decode(CommitCategory.self, forKey: .category),
|
|
rollback: try container.decode(CredentialRollbackStatus.self, forKey: .rollback))
|
|
case .invalidURL, .unsupportedScheme, .insecureRedirect, .decodingFailure,
|
|
.unsupportedSchemaVersion, .missingServers, .duplicateIdentity, .legacy, .unknown:
|
|
preconditionFailure("无 payload 的失败种类应由对照表处理")
|
|
}
|
|
}
|
|
|
|
func encode(to encoder: Encoder) throws {
|
|
var container = encoder.container(keyedBy: CodingKeys.self)
|
|
try container.encode(kind, forKey: .kind)
|
|
try encodePayload(to: &container)
|
|
}
|
|
|
|
private var kind: Kind {
|
|
if let kind = Self.payloadlessKinds.first(where: { $0.failure == self })?.kind {
|
|
return kind
|
|
}
|
|
switch self {
|
|
case .transport:
|
|
return .transport
|
|
case .httpStatus:
|
|
return .httpStatus
|
|
case .contentType:
|
|
return .contentType
|
|
case .recordValidation:
|
|
return .recordValidation
|
|
case .credential:
|
|
return .credential
|
|
case .commit:
|
|
return .commit
|
|
case .invalidURL, .unsupportedScheme, .insecureRedirect, .decodingFailure,
|
|
.unsupportedSchemaVersion, .missingServers, .duplicateIdentity, .legacy, .unknown:
|
|
preconditionFailure("无 payload 的失败种类应由对照表处理")
|
|
}
|
|
}
|
|
|
|
/// 仅 payload 字段;kind 已由 encode(to:) 写入。穷举保持编译期检查。
|
|
private func encodePayload(to container: inout KeyedEncodingContainer<CodingKeys>) throws {
|
|
switch self {
|
|
case .transport(let category):
|
|
try container.encode(category, forKey: .category)
|
|
case .httpStatus(let code):
|
|
try container.encode(code, forKey: .code)
|
|
case .contentType(let category):
|
|
try container.encode(category, forKey: .category)
|
|
case .recordValidation(let index, let field):
|
|
try container.encode(index, forKey: .index)
|
|
try container.encode(field, forKey: .field)
|
|
case .credential(let category):
|
|
try container.encode(category, forKey: .category)
|
|
case .commit(let category, let rollback):
|
|
try container.encode(category, forKey: .category)
|
|
try container.encode(rollback, forKey: .rollback)
|
|
case .invalidURL, .unsupportedScheme, .insecureRedirect, .decodingFailure,
|
|
.unsupportedSchemaVersion, .missingServers, .duplicateIdentity, .legacy, .unknown:
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Coarse credential recovery status allowed in persisted subscription state.
|
|
/// The full `CredentialRollbackOutcome` remains transient and stays in memory.
|
|
enum CredentialRollbackStatus: String, Codable, Equatable, Sendable {
|
|
case notNeeded
|
|
case restored
|
|
case incomplete
|
|
}
|
|
|
|
extension SubscriptionRefreshFailure {
|
|
/// The only conversion point for transport failures entering durable state.
|
|
static func from(fetchError: SubscriptionFetchError) -> Self {
|
|
switch fetchError {
|
|
case .invalidURL:
|
|
.invalidURL
|
|
case .unsupportedScheme:
|
|
.unsupportedScheme
|
|
case .insecureRedirect:
|
|
.insecureRedirect
|
|
case .transport(let detail):
|
|
.transport(transportCategory(for: detail))
|
|
case .httpStatus(let code):
|
|
.httpStatus(code: code)
|
|
case .contentType(let received):
|
|
.contentType(received == nil ? .missing : .unsupported)
|
|
}
|
|
}
|
|
|
|
/// The only conversion point for parser failures entering durable state.
|
|
static func from(parseError: SubscriptionParseError) -> Self {
|
|
switch parseError {
|
|
case .decodingFailure:
|
|
.decodingFailure
|
|
case .unsupportedSchemaVersion:
|
|
.unsupportedSchemaVersion
|
|
case .missingServers:
|
|
.missingServers
|
|
case .recordValidation(let index, let reason):
|
|
.recordValidation(index: index, field: recordField(for: reason))
|
|
case .duplicateServerID:
|
|
.duplicateIdentity
|
|
}
|
|
}
|
|
|
|
static func from(credentialError: CredentialStoreError, category: CredentialCategory) -> Self {
|
|
switch category {
|
|
case .missing:
|
|
.credential(category: .missing)
|
|
case .read:
|
|
.credential(category: .read)
|
|
case .write:
|
|
.credential(category: .write)
|
|
}
|
|
}
|
|
|
|
static func rollbackStatus(for outcome: CredentialRollbackOutcome) -> CredentialRollbackStatus {
|
|
switch outcome {
|
|
case .nothingToRestore:
|
|
.notNeeded
|
|
case .restored:
|
|
.restored
|
|
case .partial:
|
|
.incomplete
|
|
}
|
|
}
|
|
|
|
private static func transportCategory(for detail: String) -> TransportCategory {
|
|
let normalized = detail.lowercased()
|
|
if normalized.contains("timedout") || normalized.contains("timeout") {
|
|
return .timedOut
|
|
}
|
|
if normalized.contains("tls") || normalized.contains("secure") {
|
|
return .tls
|
|
}
|
|
if normalized.contains("connection") || normalized.contains("network")
|
|
|| normalized.contains("cannotconnect")
|
|
{
|
|
return .connection
|
|
}
|
|
return .unknown
|
|
}
|
|
|
|
private static func recordField(for reason: String) -> RecordField {
|
|
switch reason {
|
|
case let value where value.contains("server_port"):
|
|
.port
|
|
case let value where value.contains("server"):
|
|
.address
|
|
case let value where value.contains("method"):
|
|
.method
|
|
case let value where value.contains("password"):
|
|
.password
|
|
case let value where value.contains("id"):
|
|
.identity
|
|
default:
|
|
.identity
|
|
}
|
|
}
|
|
}
|