Files

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 }
}