mirror of
https://github.com/shadowsocks/ShadowsocksX-NG.git
synced 2026-11-08 10:44:50 +00:00
169 lines
5.5 KiB
Swift
169 lines
5.5 KiB
Swift
import Foundation
|
|
|
|
struct UserPluginRecord: Codable, Equatable, Identifiable, Sendable {
|
|
var id: UUID = UUID()
|
|
var names: [String]
|
|
var path: String
|
|
var arguments: [String] = []
|
|
var mode: PluginTransportMode = .tcpOnly
|
|
|
|
enum CodingKeys: String, CodingKey {
|
|
case id, names, path, arguments
|
|
case mode = "plugin_mode"
|
|
}
|
|
|
|
init(
|
|
id: UUID = UUID(), names: [String], path: String, arguments: [String] = [],
|
|
mode: PluginTransportMode = .tcpOnly
|
|
) {
|
|
self.id = id
|
|
self.names = names
|
|
self.path = path
|
|
self.arguments = arguments
|
|
self.mode = mode
|
|
}
|
|
|
|
init(from decoder: any Decoder) throws {
|
|
let values = try decoder.container(keyedBy: CodingKeys.self)
|
|
id = try values.decode(UUID.self, forKey: .id)
|
|
names = try values.decode([String].self, forKey: .names)
|
|
path = try values.decode(String.self, forKey: .path)
|
|
arguments = try values.decode([String].self, forKey: .arguments)
|
|
mode =
|
|
values.contains(.mode) ? try values.decode(PluginTransportMode.self, forKey: .mode) : .tcpOnly
|
|
}
|
|
}
|
|
|
|
struct PluginConfiguration: Codable, Equatable, Sendable {
|
|
var userPlugins: [UserPluginRecord] = []
|
|
/// Keys are fixed managed-record identities, independent of their aliases.
|
|
var managedArguments: [String: [String]] = [:]
|
|
var managedModes: [String: PluginTransportMode] = [:]
|
|
|
|
enum CodingKeys: String, CodingKey { case userPlugins, managedArguments, managedModes }
|
|
|
|
init(
|
|
userPlugins: [UserPluginRecord] = [], managedArguments: [String: [String]] = [:],
|
|
managedModes: [String: PluginTransportMode] = [:]
|
|
) {
|
|
self.userPlugins = userPlugins
|
|
self.managedArguments = managedArguments
|
|
self.managedModes = managedModes
|
|
}
|
|
|
|
init(from decoder: any Decoder) throws {
|
|
let values = try decoder.container(keyedBy: CodingKeys.self)
|
|
userPlugins = try values.decode([UserPluginRecord].self, forKey: .userPlugins)
|
|
managedArguments = try values.decode([String: [String]].self, forKey: .managedArguments)
|
|
managedModes =
|
|
values.contains(.managedModes)
|
|
? try values.decode([String: PluginTransportMode].self, forKey: .managedModes) : [:]
|
|
}
|
|
|
|
func validate() throws {
|
|
var names = Set<String>()
|
|
var identities = Set<UUID>()
|
|
for record in userPlugins {
|
|
guard identities.insert(record.id).inserted, !record.names.isEmpty else {
|
|
throw PluginMappingError.invalidName
|
|
}
|
|
for name in record.names {
|
|
guard !name.isEmpty, name == name.trimmingCharacters(in: .whitespacesAndNewlines),
|
|
!name.contains("\0")
|
|
else { throw PluginMappingError.invalidName }
|
|
guard names.insert(name).inserted else { throw PluginMappingError.nameExists }
|
|
}
|
|
guard record.path.hasPrefix("/"), !record.path.contains("\0") else {
|
|
throw PluginMappingError.invalidPath
|
|
}
|
|
try Self.validateArguments(record.arguments)
|
|
}
|
|
for program in managedModes.keys {
|
|
guard ManagedPluginCatalog.plugins.contains(where: { $0.program == program }) else {
|
|
throw PluginMappingError.nameMissing
|
|
}
|
|
}
|
|
for (program, arguments) in managedArguments {
|
|
guard ManagedPluginCatalog.plugins.contains(where: { $0.program == program }) else {
|
|
throw PluginMappingError.nameMissing
|
|
}
|
|
try Self.validateArguments(arguments)
|
|
}
|
|
}
|
|
|
|
static func validateArguments(_ arguments: [String]) throws {
|
|
guard !arguments.contains(where: { $0.contains("\0") }) else {
|
|
throw PluginMappingError.invalidArguments
|
|
}
|
|
}
|
|
}
|
|
|
|
protocol PluginMappingStoring {
|
|
func load() throws -> PluginConfiguration
|
|
func save(_ configuration: PluginConfiguration) throws
|
|
}
|
|
|
|
enum PluginMappingError: Error, Equatable {
|
|
case unreadable
|
|
case unsupportedVersion
|
|
case invalidName
|
|
case invalidPath
|
|
case invalidArguments
|
|
case unavailableFile
|
|
case nameExists
|
|
case nameMissing
|
|
}
|
|
|
|
struct PluginMappingFileStore: PluginMappingStoring {
|
|
var fileURL = ApplicationSupportPaths.current.pluginMappingFileURL
|
|
|
|
private struct Version: Decodable { let version: Int }
|
|
private struct LegacyRecord: Decodable {
|
|
let mappings: [String: String]
|
|
}
|
|
private struct Record: Codable {
|
|
let version: Int
|
|
let configuration: PluginConfiguration
|
|
}
|
|
|
|
func load() throws -> PluginConfiguration {
|
|
let data: Data
|
|
do {
|
|
data = try Data(contentsOf: fileURL)
|
|
} catch let error as CocoaError where error.code == .fileReadNoSuchFile {
|
|
return PluginConfiguration()
|
|
} catch {
|
|
throw PluginMappingError.unreadable
|
|
}
|
|
do {
|
|
let decoder = JSONDecoder()
|
|
let configuration: PluginConfiguration
|
|
switch try decoder.decode(Version.self, from: data).version {
|
|
case 1:
|
|
let legacy = try decoder.decode(LegacyRecord.self, from: data)
|
|
configuration = PluginConfiguration(
|
|
userPlugins: legacy.mappings.sorted { $0.key < $1.key }.map {
|
|
UserPluginRecord(names: [$0.key], path: $0.value)
|
|
})
|
|
case 2:
|
|
configuration = try decoder.decode(Record.self, from: data).configuration
|
|
default: throw PluginMappingError.unsupportedVersion
|
|
}
|
|
try configuration.validate()
|
|
return configuration
|
|
} catch PluginMappingError.unsupportedVersion {
|
|
throw PluginMappingError.unsupportedVersion
|
|
} catch {
|
|
throw PluginMappingError.unreadable
|
|
}
|
|
}
|
|
|
|
func save(_ configuration: PluginConfiguration) throws {
|
|
try configuration.validate()
|
|
let encoder = JSONEncoder()
|
|
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
|
|
try AtomicFileWriter.write(
|
|
try encoder.encode(Record(version: 2, configuration: configuration)), to: fileURL)
|
|
}
|
|
}
|