mirror of
https://github.com/shadowsocks/ShadowsocksX-NG.git
synced 2026-11-13 09:33:45 +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 文件时两个用例均 失败并点名违规文件,清理后复验为绿。
100 lines
3.8 KiB
Swift
100 lines
3.8 KiB
Swift
import Foundation
|
|
|
|
/// A sheet-local draft bound to saved facts, never part of the browsing snapshot.
|
|
struct CustomRuleDraft: Equatable, Sendable, Identifiable {
|
|
enum Kind: CaseIterable, Sendable { case ipAddress, cidr, domainSuffix, domainExact }
|
|
let id: UUID
|
|
let editingID: UUID?
|
|
let version: String
|
|
var kind: Kind = .domainSuffix
|
|
var content = ""
|
|
var action: RuleAction = .proxy
|
|
|
|
func normalizedMatch() throws -> RuleMatch {
|
|
let value = content.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
switch kind {
|
|
case .ipAddress, .cidr:
|
|
guard
|
|
kind == .ipAddress
|
|
? !value.contains("/")
|
|
: value.split(separator: "/", omittingEmptySubsequences: false).count == 2,
|
|
!value.hasPrefix("/"), !value.hasSuffix("/")
|
|
else { throw RuleMatchError.invalidCIDR(value) }
|
|
return try value.contains(":") ? RuleMatch(ipv6CIDR: value) : RuleMatch(ipv4CIDR: value)
|
|
case .domainExact: return try RuleMatch(domainExact: value)
|
|
case .domainSuffix: return try RuleMatch(nationalDomainSuffix: value)
|
|
}
|
|
}
|
|
}
|
|
|
|
struct CustomRulePreview: Equatable, Sendable {
|
|
enum Failure: Equatable, Sendable {
|
|
case incompleteCollection, busy, staleDraft
|
|
case invalidInput(CustomRuleDraft.Kind)
|
|
case duplicate, fixedLocalConflict, fixedPolicyDisablement
|
|
}
|
|
var failure: Failure?
|
|
var rule: CustomRule?
|
|
var row: RulesRow?
|
|
var inheritsDisablement = false
|
|
var document: CustomRuleDocument?
|
|
var displayContent: String? { rule?.identity.match.editingContent }
|
|
}
|
|
|
|
enum CustomRuleSaveResult: Equatable, Sendable {
|
|
case unavailable(CustomRulePreview.Failure)
|
|
case committed(CustomRuleUpdateOutcome)
|
|
}
|
|
|
|
extension RuleMatch {
|
|
var editingKind: CustomRuleDraft.Kind {
|
|
switch self {
|
|
case .domainExact: .domainExact
|
|
case .domainSuffix: .domainSuffix
|
|
case .ipv4CIDR(let value): value.hasSuffix("/32") ? .ipAddress : .cidr
|
|
case .ipv6CIDR(let value): value.hasSuffix("/128") ? .ipAddress : .cidr
|
|
}
|
|
}
|
|
|
|
var editingContent: String {
|
|
if editingKind == .ipAddress { return String(browsingContent.split(separator: "/")[0]) }
|
|
return browsingContent
|
|
}
|
|
}
|
|
|
|
extension RulesCollection {
|
|
func previewCustomRule(_ draft: CustomRuleDraft) -> CustomRulePreview {
|
|
guard let document = userDocument else {
|
|
return CustomRulePreview(failure: .incompleteCollection)
|
|
}
|
|
let match: RuleMatch
|
|
do { match = try draft.normalizedMatch() } catch {
|
|
return CustomRulePreview(failure: .invalidInput(draft.kind))
|
|
}
|
|
let previous = document.rules.first { $0.id == draft.editingID }
|
|
guard draft.editingID == nil || previous != nil else {
|
|
return CustomRulePreview(failure: .staleDraft)
|
|
}
|
|
let rule = CustomRule(
|
|
id: draft.id, action: draft.action, match: match, source: previous?.source)
|
|
let others = document.rules.filter { $0.id != draft.editingID }
|
|
guard !others.contains(where: { $0.identity == rule.identity }) else {
|
|
return CustomRulePreview(failure: .duplicate, rule: rule)
|
|
}
|
|
guard CustomRuleValidator.fixedLocalRejection(for: rule) == nil else {
|
|
return CustomRulePreview(failure: .fixedLocalConflict, rule: rule)
|
|
}
|
|
var disabled = document.disabledIdentities
|
|
let inherits = previous.map { disabled.contains($0.identity) } ?? false
|
|
if inherits {
|
|
guard rule.action != .direct || !RuleCoverage.fixedLocalMatches.contains(rule.identity.match)
|
|
else { return CustomRulePreview(failure: .fixedPolicyDisablement, rule: rule) }
|
|
disabled.insert(rule.identity)
|
|
}
|
|
let updated = CustomRuleDocument(rules: others + [rule], disabledIdentities: disabled)
|
|
return CustomRulePreview(
|
|
rule: rule, row: previewRow(identity: rule.identity, document: updated),
|
|
inheritsDisablement: inherits && previous?.identity != rule.identity, document: updated)
|
|
}
|
|
}
|