mirror of
https://github.com/shadowsocks/ShadowsocksX-NG.git
synced 2026-11-08 11:14:51 +00:00
348 lines
15 KiB
Swift
348 lines
15 KiB
Swift
import Foundation
|
||
|
||
/// The single application presentation edge. Domain and workflow modules expose
|
||
/// typed errors/facts; this file is the only place that turns them into text.
|
||
extension Error {
|
||
var presentableMessage: String {
|
||
AppPresentation.message(for: self)
|
||
}
|
||
}
|
||
|
||
enum AppPresentation {
|
||
static let unknownError = String(localized: "操作失败,请重试或检查设置")
|
||
}
|
||
|
||
extension AppPresentation {
|
||
// The presentation edge deliberately dispatches across the app's typed
|
||
// failure families; keeping this switch explicit makes the whitelist auditable.
|
||
// swiftlint:disable:next cyclomatic_complexity function_body_length
|
||
static func message(for error: Error) -> String {
|
||
switch error {
|
||
case let error as CatalogError:
|
||
return catalog(error)
|
||
case let error as ActivationFailure:
|
||
return activation(error)
|
||
case let error as ServerFormError:
|
||
return serverForm(error)
|
||
case let error as SsUriError:
|
||
return ssURI(error)
|
||
case let error as CredentialKeyStoreError:
|
||
return credential(error)
|
||
case let error as SubscriptionFormError:
|
||
return subscriptionForm(error)
|
||
case let error as SubscriptionFetchError:
|
||
return subscriptionFetch(error)
|
||
case let error as SubscriptionParseError:
|
||
return subscriptionParse(error)
|
||
case let error as SettingsWorkflowFailure:
|
||
return message(for: error)
|
||
case let error as RuntimeFailureFacts:
|
||
return message(for: error)
|
||
case let error as ImportLineFailureReason:
|
||
return importLine(error)
|
||
case let error as ImportSourceFailure:
|
||
return importSource(error)
|
||
case let error as LegacySkippedRecord.Reason:
|
||
return reason(error)
|
||
case let error as PortSettingError:
|
||
return port(error)
|
||
case let error as ProxyListeningStoreError:
|
||
return listeningStore(error)
|
||
case let error as LegacyImportError:
|
||
return legacyImport(error)
|
||
case let error as SystemProxyError:
|
||
return systemProxy(SystemProxyOperationFailure(error))
|
||
case let error as TextClipboardFailure:
|
||
return textClipboard(error)
|
||
case let error as TerminalCommandFailure:
|
||
switch error {
|
||
case .credentialUnreadable: return String(localized: "局域网身份验证密码不可读,请重试。")
|
||
case .addressUnavailable: return String(localized: "命令地址已失效,请重新选择 IP。")
|
||
}
|
||
case let error as ImageClipboardFailure:
|
||
return imageClipboard(error)
|
||
case let error as QrImageSaveFailure:
|
||
return qrImageSave(error)
|
||
case let error as DiagnosticReportFailure:
|
||
return diagnosticReport(error)
|
||
case let error as DiagnosticReportExportFailure:
|
||
return diagnosticReportExport(error)
|
||
case let error as ConfigurationGroupExportFailure:
|
||
return configurationGroupExport(error)
|
||
case let error as ConfigurationGroupFileExportFailure:
|
||
return error == .writeFailed
|
||
? String(localized: "SIP-008 文件写入失败") : String(localized: "URI 链接列表文本文件写入失败")
|
||
default:
|
||
return unknownError
|
||
}
|
||
}
|
||
|
||
// swiftlint:disable:next cyclomatic_complexity
|
||
static func message(for failure: SubscriptionRefreshFailure) -> String {
|
||
switch failure {
|
||
case .invalidURL:
|
||
return String(localized: "订阅地址无效")
|
||
case .unsupportedScheme:
|
||
return String(localized: "订阅地址必须是 HTTPS")
|
||
case .insecureRedirect:
|
||
return String(localized: "订阅地址重定向到了非 HTTPS 站点,已拒绝")
|
||
case .transport(let category):
|
||
switch category {
|
||
case .timedOut: return String(localized: "订阅获取超时")
|
||
case .tls: return String(localized: "订阅安全连接失败")
|
||
case .connection: return String(localized: "订阅连接失败")
|
||
case .unknown: return String(localized: "订阅获取失败")
|
||
}
|
||
case .httpStatus(let code):
|
||
return String(localized: "订阅服务器返回 HTTP \(code)")
|
||
case .contentType(let category):
|
||
return category == .missing
|
||
? String(localized: "订阅响应缺少 JSON Content-Type")
|
||
: String(localized: "订阅响应类型不是 JSON")
|
||
case .decodingFailure:
|
||
return String(localized: "订阅内容不是合法的 SIP-008 JSON 文档")
|
||
case .unsupportedSchemaVersion:
|
||
return String(localized: "订阅文档版本不受支持")
|
||
case .missingServers:
|
||
return String(localized: "订阅文档缺少 servers 列表")
|
||
case .recordValidation(let index, let field):
|
||
return String(localized: "订阅第 \(index + 1) 条服务器记录的\(recordFieldName(field))无效")
|
||
case .duplicateIdentity:
|
||
return String(localized: "订阅包含重复的服务器身份")
|
||
case .commit:
|
||
return String(localized: "订阅快照保存失败")
|
||
case .unknown:
|
||
return unknownError
|
||
}
|
||
}
|
||
|
||
static func message(for issue: SettingsFieldIssue) -> String {
|
||
switch issue {
|
||
case .port(_, let error):
|
||
return message(for: error)
|
||
}
|
||
}
|
||
|
||
static func message(for failure: SettingsWorkflowFailure) -> String {
|
||
switch failure {
|
||
case .store(let error): return listeningStore(error)
|
||
case .unknown: return unknownError
|
||
}
|
||
}
|
||
|
||
// 显式白名单 switch,与 message(for:) 同一可审计惯例。
|
||
// swiftlint:disable:next cyclomatic_complexity
|
||
private static func catalog(_ error: CatalogError) -> String {
|
||
switch error {
|
||
case .nodeNotFound: return String(localized: "节点不存在(可能已被删除)")
|
||
case .parentNotFound: return String(localized: "目标分组不存在")
|
||
case .parentNotAGroup: return String(localized: "目标位置不是分组")
|
||
case .notAServer: return String(localized: "该节点不是服务器")
|
||
case .notAGroup: return String(localized: "该节点不是分组")
|
||
case .duplicateID: return String(localized: "节点身份冲突")
|
||
case .crossSourcePlacement(_, let container):
|
||
return container == .subscription
|
||
? String(localized: "不能把手动节点移入订阅子树") : String(localized: "不能把订阅节点移入手动子树")
|
||
case .subscriptionNodeImmutable: return String(localized: "订阅节点由远端管理:结构、连接字段与名称只读")
|
||
case .subscriptionServerAtRoot: return String(localized: "订阅服务器不能移出其订阅分组")
|
||
case .cycleDetected: return String(localized: "不能把分组移动进它自己的子树")
|
||
case .indexOutOfRange: return String(localized: "插入位置超出范围")
|
||
case .invalidStructure: return String(localized: "配置目录结构不一致")
|
||
}
|
||
}
|
||
|
||
static func activation(_ error: ActivationFailure) -> String {
|
||
switch error {
|
||
case .noActiveTarget: return String(localized: "尚未激活任何服务器或分组,请先选择一个目标")
|
||
case .targetNotFound: return String(localized: "活动目标不存在,已清除目标;本地监听不受影响")
|
||
case .targetExpandsToNothing: return String(localized: "目标没有可激活的服务器")
|
||
case .invalidLeaf(let node, let reason):
|
||
return String(localized: "服务器不可用:\(leaf(reason))(\(shortNodeID(node)))")
|
||
}
|
||
}
|
||
|
||
private static func leaf(_ reason: LeafInvalidationReason) -> String {
|
||
switch reason {
|
||
case .invalidAddress: return String(localized: "地址无效")
|
||
case .invalidPort: return String(localized: "端口无效")
|
||
case .missingEncryptionMethod: return String(localized: "缺少加密方法")
|
||
case .unsupportedEncryptionMethod: return String(localized: "加密方法不受支持")
|
||
case .pluginNotProvided: return String(localized: "插件未提供")
|
||
case .missingPassword: return String(localized: "密码缺失或为空")
|
||
}
|
||
}
|
||
|
||
private static func serverForm(_ error: ServerFormError) -> String {
|
||
switch error {
|
||
case .invalidAddress: return String(localized: "服务器地址不能为空")
|
||
case .invalidPort: return String(localized: "端口必须是 1–65535 之间的整数")
|
||
case .missingEncryptionMethod: return String(localized: "必须选择加密方法")
|
||
case .unsupportedEncryptionMethod: return String(localized: "当前 sslocal 不支持该加密方法")
|
||
case .invalidPassword: return String(localized: "密码不能为空")
|
||
case .emptyName: return String(localized: "名称不能为空")
|
||
case .pluginUnknown: return String(localized: "本版本未提供所选插件")
|
||
}
|
||
}
|
||
|
||
private static func ssURI(_ error: SsUriError) -> String {
|
||
switch error {
|
||
case .notSsUri: return String(localized: "不是 ss:// 链接")
|
||
case .malformed: return String(localized: "无法解析 ss:// 链接")
|
||
}
|
||
}
|
||
|
||
private static func credential(_ error: CredentialKeyStoreError) -> String {
|
||
switch error {
|
||
case .keychainStatus: return String(localized: "钥匙串操作失败")
|
||
}
|
||
}
|
||
|
||
private static func subscriptionForm(_ error: SubscriptionFormError) -> String {
|
||
switch error {
|
||
case .invalidURL: return String(localized: "订阅地址必须是有效的 HTTPS URL")
|
||
case .notFound: return String(localized: "订阅不存在(可能已被删除)")
|
||
}
|
||
}
|
||
|
||
private static func subscriptionFetch(_ error: SubscriptionFetchError) -> String {
|
||
message(for: SubscriptionRefreshFailure.from(fetchError: error))
|
||
}
|
||
|
||
private static func subscriptionParse(_ error: SubscriptionParseError) -> String {
|
||
message(for: SubscriptionRefreshFailure.from(parseError: error))
|
||
}
|
||
|
||
private static func importLine(_ error: ImportLineFailureReason) -> String {
|
||
switch error {
|
||
case .decode(let error): return ssURI(error)
|
||
}
|
||
}
|
||
|
||
/// 统一导入的来源级失败:解析失败复用订阅解析文案族,提交失败复用
|
||
/// 提交错误经过统一呈现,其余点名具体原因。
|
||
private static func importSource(_ error: ImportSourceFailure) -> String {
|
||
switch error {
|
||
case .parse(let error): return subscriptionParse(error)
|
||
case .undecodableImage: return String(localized: "图片无法识别,请使用常见图片格式")
|
||
case .qrPayloadNotFound: return String(localized: "未识别到 ss:// 二维码")
|
||
case .noImportableLines: return String(localized: "没有可导入的 ss:// 链接")
|
||
case .commit(let error): return message(for: error)
|
||
}
|
||
}
|
||
|
||
private static func reason(_ error: LegacySkippedRecord.Reason) -> String {
|
||
switch error {
|
||
case .notDictionary: return String(localized: "记录不是字典")
|
||
case .invalidAddress: return String(localized: "服务器地址缺失或无效")
|
||
case .invalidPort: return String(localized: "服务器端口缺失或无效")
|
||
case .invalidEncryptionMethod: return String(localized: "加密方式缺失或无效")
|
||
case .missingPassword: return String(localized: "密码缺失或为空")
|
||
}
|
||
}
|
||
|
||
private static func port(_ error: PortSettingError) -> String {
|
||
switch error {
|
||
case .portOutOfRange(let endpoint, let portNumber):
|
||
let port = portNumber.formatted(.number.grouping(.never))
|
||
return String(
|
||
localized:
|
||
"\(endpoint.displayName) 端口 \(port) 无效,必须是 1025–65535 之间的整数"
|
||
)
|
||
case .duplicatePort(let endpoint, let otherEndpoint, let portNumber):
|
||
let port = portNumber.formatted(.number.grouping(.never))
|
||
return
|
||
String(
|
||
localized:
|
||
"\(endpoint.displayName) 端口与 \(otherEndpoint.displayName) 端口冲突(都是 \(port)),请为每个端点配置不同的端口"
|
||
)
|
||
}
|
||
}
|
||
|
||
private static func listeningStore(_ error: ProxyListeningStoreError) -> String {
|
||
switch error {
|
||
case .corrupt: return String(localized: "偏好文件损坏")
|
||
case .invalid(let errors): return errors.map(port).joined(separator: ";")
|
||
case .ioFailure: return String(localized: "偏好文件读写失败")
|
||
case .credentialFailure(let detail): return String(localized: "凭据操作失败:\(detail)")
|
||
case .encryptedFile: return String(localized: "监听配置无法解密或格式不受支持")
|
||
}
|
||
}
|
||
|
||
private static func legacyImport(_ error: LegacyImportError) -> String {
|
||
switch error {
|
||
case .noSnapshot: return String(localized: "没有发现可导入的 Legacy 配置")
|
||
case .alreadyCompleted: return String(localized: "Legacy 配置已经导入;如需再次导入,请明确选择再次导入")
|
||
case .malformedSnapshot: return String(localized: "Legacy 快照无效")
|
||
case .commitFailed: return String(localized: "Legacy 导入未完成,2.0 写入未完成")
|
||
}
|
||
}
|
||
|
||
private static func textClipboard(_ error: TextClipboardFailure) -> String {
|
||
switch error {
|
||
case .writeFailed: return String(localized: "剪贴板写入失败,请重试")
|
||
}
|
||
}
|
||
|
||
private static func imageClipboard(_ error: ImageClipboardFailure) -> String {
|
||
switch error {
|
||
case .writeFailed: return String(localized: "剪贴板写入失败,请重试")
|
||
}
|
||
}
|
||
|
||
private static func qrImageSave(_ error: QrImageSaveFailure) -> String {
|
||
switch error {
|
||
case .writeFailed: return String(localized: "二维码图片写入失败,请选择其他位置或重试")
|
||
}
|
||
}
|
||
|
||
private static func diagnosticReport(_ error: DiagnosticReportFailure) -> String {
|
||
switch error {
|
||
case .encodingFailed: return String(localized: "诊断报告无法安全构造,请重试")
|
||
}
|
||
}
|
||
|
||
private static func diagnosticReportExport(_ error: DiagnosticReportExportFailure) -> String {
|
||
switch error {
|
||
case .writeFailed: return String(localized: "诊断报告写入失败,请选择其他位置或重试")
|
||
}
|
||
}
|
||
|
||
private static func configurationGroupExport(_ error: ConfigurationGroupExportFailure) -> String {
|
||
switch error {
|
||
case .groupNotFound: return String(localized: "分组不存在(可能已被删除)")
|
||
case .targetIsNotGroup: return String(localized: "所选节点不是分组")
|
||
case .noServers: return String(localized: "分组中没有服务器配置")
|
||
case .invalidServer(_, let problem): return invalidServerExport(problem)
|
||
case .pluginOptionsWithoutProgram:
|
||
return String(localized: "服务器配置了插件参数但没有插件名称,无法导出 SIP-008 文件")
|
||
case .documentEncodingFailed:
|
||
return String(localized: "SIP-008 文档生成失败")
|
||
}
|
||
}
|
||
|
||
private static func invalidServerExport(_ problem: ConfigurationGroupExportFailure.RecordProblem)
|
||
-> String
|
||
{
|
||
switch problem {
|
||
case .invalidAddress: return String(localized: "服务器地址为空,无法导出 SIP-008 文件")
|
||
case .invalidPort: return String(localized: "服务器端口无效,无法导出 SIP-008 文件")
|
||
case .missingEncryptionMethod: return String(localized: "服务器缺少加密方式,无法导出 SIP-008 文件")
|
||
case .missingPassword: return String(localized: "服务器密码为空,无法导出 SIP-008 文件")
|
||
}
|
||
}
|
||
|
||
private static func recordFieldName(_ field: SubscriptionRefreshFailure.RecordField) -> String {
|
||
switch field {
|
||
case .address: return String(localized: "服务器地址")
|
||
case .port: return String(localized: "服务器端口")
|
||
case .method: return String(localized: "加密方法")
|
||
case .password: return String(localized: "密码")
|
||
case .identity: return String(localized: "身份")
|
||
}
|
||
}
|
||
|
||
private static func shortNodeID(_ id: NodeID) -> String {
|
||
String(id.rawValue.suffix(8))
|
||
}
|
||
}
|