mirror of
https://github.com/shadowsocks/ShadowsocksX-NG.git
synced 2026-11-08 10:54:50 +00:00
140 lines
5.1 KiB
Swift
140 lines
5.1 KiB
Swift
import Darwin
|
|
import Foundation
|
|
|
|
/// One derivation uses captured executable facts, never a live mutable mapping.
|
|
struct PluginCatalogSnapshot: PluginExecutableResolving, Equatable, Sendable {
|
|
enum Source: String, Codable, Sendable {
|
|
case managed
|
|
case user
|
|
case unknown
|
|
}
|
|
|
|
enum Availability: Equatable, Sendable {
|
|
case available
|
|
case missing
|
|
case notExecutable
|
|
case unreadable
|
|
}
|
|
|
|
struct Entry: Equatable, Sendable {
|
|
let program: String
|
|
let path: String?
|
|
let source: Source
|
|
let availability: Availability
|
|
let managedInfo: ManagedPluginInfo?
|
|
var arguments: [String] = []
|
|
var mode: PluginTransportMode = .tcpOnly
|
|
var recordID: UUID?
|
|
}
|
|
|
|
let entries: [Entry]
|
|
let generation: Int
|
|
let mappingsUnreadable: Bool
|
|
var managedRecords: [String: Entry] = [:]
|
|
var managedArguments: [String: [String]] = [:]
|
|
|
|
init(
|
|
configuration: PluginConfiguration,
|
|
managed: BundleManagedPluginProvider = BundleManagedPluginProvider(),
|
|
generation: Int = 0,
|
|
mappingsUnreadable: Bool = false
|
|
) {
|
|
let managedPlugins = ManagedPluginCatalog.plugins(in: managed.bundleURL)
|
|
let managedNames = managedPlugins.flatMap(\.names)
|
|
let userByName = Dictionary(
|
|
uniqueKeysWithValues: configuration.userPlugins.flatMap { record in
|
|
record.names.map { ($0, record) }
|
|
})
|
|
let programs = Set(managedNames).union(userByName.keys).sorted()
|
|
self.entries = programs.map { program in
|
|
let info = managedPlugins.first { $0.names.contains(program) }
|
|
let user = userByName[program]
|
|
let source: Source = mappingsUnreadable ? .unknown : (user == nil ? .managed : .user)
|
|
let path =
|
|
user?.path
|
|
?? managed.bundleURL
|
|
.appendingPathComponent("Contents/Helpers/Plugins")
|
|
.appendingPathComponent(info?.executableName ?? program).path
|
|
let arguments =
|
|
user?.arguments
|
|
?? info.map {
|
|
$0.defaultArguments + (configuration.managedArguments[$0.program] ?? [])
|
|
} ?? []
|
|
return Entry(
|
|
program: program, path: path, source: source,
|
|
availability: mappingsUnreadable
|
|
? .unreadable : Self.availability(at: path, fileManager: managed.fileManager),
|
|
managedInfo: source == .managed ? info : nil, arguments: arguments,
|
|
mode: user?.mode ?? info.flatMap { configuration.managedModes[$0.program] } ?? .tcpOnly,
|
|
recordID: user?.id)
|
|
}
|
|
self.managedRecords = Dictionary(
|
|
uniqueKeysWithValues: managedPlugins.map { info in
|
|
let path = managed.bundleURL.appendingPathComponent("Contents/Helpers/Plugins")
|
|
.appendingPathComponent(info.executableName).path
|
|
return (
|
|
info.program,
|
|
Entry(
|
|
program: info.program, path: path, source: .managed,
|
|
availability: mappingsUnreadable
|
|
? .unreadable : Self.availability(at: path, fileManager: managed.fileManager),
|
|
managedInfo: info,
|
|
arguments: info.defaultArguments + (configuration.managedArguments[info.program] ?? []),
|
|
mode: configuration.managedModes[info.program] ?? .tcpOnly)
|
|
)
|
|
})
|
|
self.managedArguments = configuration.managedArguments
|
|
self.generation = generation
|
|
self.mappingsUnreadable = mappingsUnreadable
|
|
}
|
|
|
|
init(entries: [Entry], generation: Int = 0, mappingsUnreadable: Bool = false) {
|
|
self.entries = entries
|
|
self.generation = generation
|
|
self.mappingsUnreadable = mappingsUnreadable
|
|
}
|
|
|
|
private static func availability(at path: String, fileManager: FileManager) -> Availability {
|
|
do {
|
|
let target = URL(fileURLWithPath: path).resolvingSymlinksInPath().path
|
|
let attributes = try fileManager.attributesOfItem(atPath: target)
|
|
if attributes[.type] as? FileAttributeType == .typeSymbolicLink {
|
|
// Foundation leaves broken or unresolvable links unchanged. stat follows
|
|
// the target and preserves the distinction between absence and read failure.
|
|
var facts = stat()
|
|
if stat(path, &facts) != 0 {
|
|
return errno == ENOENT || errno == ENOTDIR ? .missing : .unreadable
|
|
} else {
|
|
return
|
|
(facts.st_mode & S_IFMT) == S_IFDIR
|
|
|| !fileManager.isExecutableFile(atPath: path) ? .notExecutable : .available
|
|
}
|
|
} else if attributes[.type] as? FileAttributeType == .typeDirectory
|
|
|| !fileManager.isExecutableFile(atPath: path)
|
|
{
|
|
return .notExecutable
|
|
} else {
|
|
return .available
|
|
}
|
|
} catch {
|
|
let failure = error as NSError
|
|
return
|
|
failure.domain == NSCocoaErrorDomain
|
|
&& failure.code == NSFileReadNoSuchFileError ? .missing : .unreadable
|
|
}
|
|
}
|
|
|
|
func entry(for program: String) -> Entry? {
|
|
entries.first { $0.program == program }
|
|
}
|
|
|
|
func resolve(program: String) -> ResolvedPlugin? {
|
|
guard !mappingsUnreadable, let entry = entry(for: program),
|
|
entry.availability == .available, let path = entry.path
|
|
else { return nil }
|
|
return ResolvedPlugin(path: path, arguments: entry.arguments, mode: entry.mode)
|
|
}
|
|
|
|
func catalogSnapshot() -> PluginCatalogSnapshot { self }
|
|
}
|