mirror of
https://github.com/shadowsocks/ShadowsocksX-NG.git
synced 2026-11-08 10:34:51 +00:00
Replace the custom encrypted container with AgeSwift 0.1.0 so standard age tools can inspect sensitive configuration independently of the app. Use independent Keychain X25519 identities for catalog.age and proxy-listening.age. Preserve atomic commits and confirmed recovery; reject historical NG2 formats without migration or cleanup.
57 lines
2.3 KiB
Swift
57 lines
2.3 KiB
Swift
import Foundation
|
|
|
|
/// 敏感配置文件的原子写盘基线:目录 0700(含自愈)、临时文件
|
|
/// 创建即 0600、写全校验后原子替换;任一步失败保留原文件。catalog.age、
|
|
/// activation.json 与其他运行时文件共用同一物理安全基线。
|
|
enum AtomicFileWriter {
|
|
enum WriteError: Error, Equatable {
|
|
case failure(detail: String)
|
|
}
|
|
|
|
/// 隐藏暂存名约定 `.<name>.tmp-<uuid>` 的唯一出处:write 内部使用,同目录
|
|
/// 内的 symlink 暂存也由此派生,保证泄漏的暂存项能被 `isResidueName` 识别。
|
|
static func temporaryURL(for targetURL: URL) -> URL {
|
|
targetURL.deletingLastPathComponent()
|
|
.appendingPathComponent(".\(targetURL.lastPathComponent).tmp-\(UUID().uuidString)")
|
|
}
|
|
|
|
/// 目录项是否为本写入器的暂存残留(宽松匹配:0700 私有目录内过扫无害)。
|
|
static func isResidueName(_ name: String) -> Bool {
|
|
name.hasPrefix(".") && name.contains(".tmp-")
|
|
}
|
|
|
|
/// 新建的父目录同样使用 0700;已有目标目录修复权限后才允许写入。
|
|
static func prepareDirectory(at directory: URL) throws {
|
|
try FileManager.default.createDirectory(
|
|
at: directory, withIntermediateDirectories: true, attributes: [.posixPermissions: 0o700])
|
|
try FileManager.default.setAttributes([.posixPermissions: 0o700], ofItemAtPath: directory.path)
|
|
}
|
|
|
|
/// 把 `data` 原子写入 `fileURL`(含父目录创建与 0700 基线自愈)。
|
|
static func write(_ data: Data, to fileURL: URL) throws {
|
|
do {
|
|
try prepareDirectory(at: fileURL.deletingLastPathComponent())
|
|
} catch {
|
|
throw WriteError.failure(detail: String(describing: error))
|
|
}
|
|
let temporaryURL = temporaryURL(for: fileURL)
|
|
if !FileManager.default.createFile(
|
|
atPath: temporaryURL.path,
|
|
contents: data,
|
|
attributes: [.posixPermissions: 0o600]
|
|
) {
|
|
throw WriteError.failure(detail: "无法创建临时文件 \(temporaryURL.path)")
|
|
}
|
|
do {
|
|
if FileManager.default.fileExists(atPath: fileURL.path) {
|
|
_ = try FileManager.default.replaceItemAt(fileURL, withItemAt: temporaryURL)
|
|
} else {
|
|
try FileManager.default.moveItem(at: temporaryURL, to: fileURL)
|
|
}
|
|
} catch {
|
|
try? FileManager.default.removeItem(at: temporaryURL)
|
|
throw WriteError.failure(detail: String(describing: error))
|
|
}
|
|
}
|
|
}
|