Files
ShadowsocksX-NG/ShadowsocksX-NG2/App/Application/ProxyControlWorkflow/TerminalCommandAddress.swift
T
Qiu Yuzhou fb428f12bc refactor(app): 按分层重组 App/ 源码目录
App/ 根目录原有 73 个散落源文件,已无法从目录读出任何分组信息。

按「目录即分层」重组为 Composition / Application / Presentation / UI /
PlatformEffects / Resources 六个目录,UI 层再按子视图分目录(Workspace、
Home、Servers(含 Plugin)、Subscriptions、Rules、Settings、Diagnostics、
StatusMenu)。纯移动:类型名、模块名与 import 均未改动,App/ 根层不再有
散落文件。

配套改动:

- project.yml:Core 测试镜像的 excludes 由裸文件名改为 `**/` 递归形式。
  XcodeGen 的 exclude 不跨 `/`,裸名只匹配 App 根层;MainApp.swift 下移后
  旧写法会静默失效,把 @main 组合根编进测试镜像。info.path 与
  entitlements.path 指向 App/Resources/;并排除 `**/*.md`——App/ 下任何
  非源码文件都会被 XcodeGen 收进 Resources 阶段并封进签名后的 bundle。
- SkeletonTests 的架构守卫由「默认扫描全部 App/ + 7 个豁免文件 + 1 个豁免
  前缀 + 4 个豁免目录」改为正面圈定 App/UI/,豁免登记全部删除;新增运行时
  适配器放进 Application/ 即自动落在扫描范围外,无需再登记。
- README 工程结构说明同步;根 .gitignore 的生成物路径随 Resources/ 归位。

验证:clean build 0 error;task test 1022 用例全通过(含两个架构守卫);
task format 无改动(证明纯移动);task lint 与移动前同为 21 条内容型
warning;packaging-gate.sh ALL CHECKS PASSED,确认 plist/entitlements 归位
后签名与 entitlements 语义不变。

注意:迁移时须先删旧的生成物再 xcodegen generate,否则遗留的 App/Info.plist
会被当作普通资源扫进 Resources 阶段。
2026-10-05 19:56:18 +08:00

196 lines
8.0 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
import SystemConfiguration
// MARK: - 命令地址投影(issue #72)
/// 首页命令地址候选(GLOSSARY.md「Terminal command address」)。身份 = BSD
/// 接口身份 + 规范化 IP;显示名与类型注记仅用于呈现,不参与身份,由呈现层
/// 自行组合(菜单项主行 IP、副标题「注记 - 名称」与收起状态说明行)。
struct TerminalCommandAddress: Equatable, Sendable {
/// BSD 接口名;回环固定为 lo0。
let bsdName: String
/// 规范化 IP 文本。
let address: String
/// 系统本地化接口名称,缺失回退 BSD 名;回环显示 lo0。
let displayName: String
/// IPv6 地址类型注记(与 ifconfig 同源:autoconf secured / autoconf /
/// dynamic);IPv4 或标志缺失为 nil。
let annotation: String?
init(bsdName: String, address: String, displayName: String, annotation: String? = nil) {
self.bsdName = bsdName
self.address = address
self.displayName = displayName
self.annotation = annotation
}
/// 跨刷新匹配既有选择的身份(不含显示名与注记;两者变化不使选择失效)。
var identity: TerminalCommandAddressIdentity {
TerminalCommandAddressIdentity(bsdName: bsdName, address: address)
}
}
/// 命令地址身份:BSD 接口名 + 规范化 IP。
struct TerminalCommandAddressIdentity: Equatable, Hashable, Sendable {
let bsdName: String
let address: String
}
/// 首页命令地址选择器投影(issue #72):可见性、候选与生效选择一次观察
/// 产出,所有字段来自同一时间点。
struct TerminalCommandAddressPicker: Equatable, Sendable {
/// 仅本机监听方式隐藏。
let isVisible: Bool
/// 用户可见候选(已过滤、已排序;至少含兼容回环项)。
let candidates: [TerminalCommandAddress]
/// 当前生效选择;恒为候选之一(选择失效自动回退默认回环)。
let selected: TerminalCommandAddress
}
// MARK: - 派生策略
/// 首页命令地址策略(issue #72):接口原始事实 + 已保存监听方式 → 用户可见
/// 候选与生效选择。纯派生:类型白名单、地址族过滤、链路本地/通配排除、名称
/// 回退、排序合并与失效回退全部在此。测试以原始事实替身驱动本策略,不直接
/// 注入已过滤候选。
enum TerminalCommandAddressPolicy {
/// 非回环接口的严格类型白名单:仅 SystemConfiguration 的 Wi‑Fi(IEEE80211)
/// 与 Ethernet 类型;不以接口名前缀或通用链路类型代替,无法确认属于允许
/// 类型的非回环接口排除。
private static let allowedInterfaceTypes: Set<String> = [
kSCNetworkInterfaceTypeIEEE80211 as String,
kSCNetworkInterfaceTypeEthernet as String,
]
/// 通配绑定地址不能用作命令目标。
private static let wildcardAddresses: Set<String> = ["0.0.0.0", "::"]
/// 一次观察的完整选择器事实。`selection` 不在候选中(地址消失、地址族
/// 不兼容或枚举失败)时回退当前监听方式的默认回环地址。
static func picker(
mode: ListenerMode,
interfaces: [LocalInterfaceFacts]?,
selection: TerminalCommandAddressIdentity?
) -> TerminalCommandAddressPicker {
let candidates = candidates(for: mode, interfaces: interfaces)
let selected = candidates.first { $0.identity == selection } ?? defaultLoopback(for: mode)
return TerminalCommandAddressPicker(
isVisible: mode != .localhost,
candidates: candidates,
selected: selected)
}
/// 候选列表:回环项置顶(双栈 127.0.0.1 先于 ::1),其余来自白名单接口;
/// 仅本机方式监听器只绑定 127.0.0.1,候选不含接口地址。枚举失败(nil)
/// 仅提供兼容回环候选。
static func candidates(
for mode: ListenerMode,
interfaces: [LocalInterfaceFacts]?
) -> [TerminalCommandAddress] {
let loopbacks = loopbackCandidates(for: mode)
guard mode != .localhost, let interfaces else { return loopbacks }
return loopbacks + interfaceCandidates(mode: mode, interfaces: interfaces)
}
/// 当前监听方式的默认回环地址(Domain 回环清单首项)。
static func defaultLoopback(for mode: ListenerMode) -> TerminalCommandAddress {
TerminalCommandAddress(
bsdName: "lo0", address: mode.proxyLoopbackAddress, displayName: "lo0", annotation: nil)
}
// MARK: - 派生细节
/// 回环候选(置顶)直接来自 Domain 的监听方式回环清单,展示名固定 lo0。
private static func loopbackCandidates(for mode: ListenerMode) -> [TerminalCommandAddress] {
mode.compatibleLoopbackAddresses.map {
TerminalCommandAddress(bsdName: "lo0", address: $0, displayName: "lo0", annotation: nil)
}
}
/// 待排序的接口分组:显示名 + BSD 名 + 该接口的地址候选。
private struct InterfaceGroup {
let name: String
let bsdName: String
let addresses: [TerminalCommandAddress]
}
private static func interfaceCandidates(
mode: ListenerMode,
interfaces: [LocalInterfaceFacts]
) -> [TerminalCommandAddress] {
var groups: [InterfaceGroup] = []
for interface in interfaces where !interface.isLoopback {
guard let type = interface.interfaceType, allowedInterfaceTypes.contains(type) else {
continue
}
// 显示名缺失回退 BSD 名;回退不放宽类型过滤。
let name = interface.localizedName ?? interface.bsdName
var seen = Set<String>()
let accepted = interface.addresses
.filter { isAcceptable($0, mode: mode) }
.filter { seen.insert($0.address).inserted } // 同一接口重复 IP 合并
let addresses =
accepted
.sorted { lhs, rhs in
// 同一接口先 IPv4 后 IPv6,再按地址稳定排序。
if lhs.family != rhs.family { return lhs.family == .ipv4 }
return lhs.address < rhs.address
}
.map {
TerminalCommandAddress(
bsdName: interface.bsdName,
address: $0.address,
displayName: name,
annotation: annotation(for: $0))
}
if !addresses.isEmpty {
groups.append(InterfaceGroup(name: name, bsdName: interface.bsdName, addresses: addresses))
}
}
return
groups
.sorted { lhs, rhs in
// 其余项按接口名称排序(大小写不敏感,BSD 回退名与本地化名混排);
// 同显示名以 BSD 名保持稳定。
let lhsName = lhs.name.lowercased()
let rhsName = rhs.name.lowercased()
return lhsName == rhsName ? lhs.bsdName < rhs.bsdName : lhsName < rhsName
}
.flatMap(\.addresses)
}
private static func isAcceptable(
_ address: LocalInterfaceAddress, mode: ListenerMode
) -> Bool {
guard !wildcardAddresses.contains(address.address) else { return false }
// IPv6 链路本地需要额外接口作用域,不作为命令目标;IPv4 链路本地允许。
if address.family == .ipv6, address.isIPv6LinkLocal { return false }
// RFC 4941 隐私临时地址会轮换,不适合作为其他设备长期使用的命令目标。
if address.v6Flags?.contains(.temporary) == true { return false }
switch (mode, address.family) {
case (.allIPv4Interfaces, .ipv4):
return true
case (.allIPv4AndIPv6Interfaces, _):
return true
case (.allIPv6Interfaces, .ipv6):
return true
default:
return false
}
}
/// IPv6 地址类型注记(与 ifconfig 同源的打印逻辑);无标志(标志查询
/// 失败或纯回环)不注记。
private static func annotation(for address: LocalInterfaceAddress) -> String? {
guard let flags = address.v6Flags else { return nil }
if flags.contains(.autoconf) {
if flags.contains(.secured) { return "autoconf secured" }
if flags.contains(.temporary) { return "autoconf temporary" }
return "autoconf"
}
if flags.contains(.secured) { return "secured" }
if flags.contains(.dynamic) { return "dynamic" }
return nil
}
}