2026-02-25 00:01:53 +00:00
|
|
|
import Foundation
|
|
|
|
|
|
|
|
|
|
enum ExecSystemRunCommandValidator {
|
|
|
|
|
struct ResolvedCommand {
|
|
|
|
|
let displayCommand: String
|
2026-03-19 13:51:17 +02:00
|
|
|
let evaluationRawCommand: String?
|
2026-02-25 00:01:53 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
enum ValidationResult {
|
|
|
|
|
case ok(ResolvedCommand)
|
|
|
|
|
case invalid(message: String)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static let posixOrPowerShellInlineWrapperNames = Set([
|
|
|
|
|
"ash",
|
|
|
|
|
"bash",
|
|
|
|
|
"dash",
|
|
|
|
|
"fish",
|
|
|
|
|
"ksh",
|
|
|
|
|
"powershell",
|
|
|
|
|
"pwsh",
|
|
|
|
|
"sh",
|
|
|
|
|
"zsh",
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
static func resolve(command: [String], rawCommand: String?) -> ValidationResult {
|
|
|
|
|
let normalizedRaw = self.normalizeRaw(rawCommand)
|
|
|
|
|
let shell = ExecShellWrapperParser.extract(command: command, rawCommand: nil)
|
|
|
|
|
let shellCommand = shell.isWrapper ? self.trimmedNonEmpty(shell.command) : nil
|
|
|
|
|
|
|
|
|
|
let envManipulationBeforeShellWrapper = self.hasEnvManipulationBeforeShellWrapper(command)
|
|
|
|
|
let shellWrapperPositionalArgv = self.hasTrailingPositionalArgvAfterInlineCommand(command)
|
|
|
|
|
let mustBindDisplayToFullArgv = envManipulationBeforeShellWrapper || shellWrapperPositionalArgv
|
2026-03-19 13:51:17 +02:00
|
|
|
let formattedArgv = ExecCommandFormatter.displayString(for: command)
|
|
|
|
|
let previewCommand: String? = if let shellCommand, !mustBindDisplayToFullArgv {
|
2026-02-25 00:27:31 +00:00
|
|
|
shellCommand
|
2026-02-25 00:01:53 +00:00
|
|
|
} else {
|
2026-03-19 13:51:17 +02:00
|
|
|
nil
|
2026-02-25 00:01:53 +00:00
|
|
|
}
|
|
|
|
|
|
2026-03-19 13:51:17 +02:00
|
|
|
if let raw = normalizedRaw, raw != formattedArgv, raw != previewCommand {
|
2026-02-25 00:01:53 +00:00
|
|
|
return .invalid(message: "INVALID_REQUEST: rawCommand does not match command")
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-19 13:51:17 +02:00
|
|
|
return .ok(ResolvedCommand(
|
|
|
|
|
displayCommand: formattedArgv,
|
|
|
|
|
evaluationRawCommand: self.allowlistEvaluationRawCommand(
|
|
|
|
|
normalizedRaw: normalizedRaw,
|
|
|
|
|
shellIsWrapper: shell.isWrapper,
|
|
|
|
|
previewCommand: previewCommand)))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static func allowlistEvaluationRawCommand(command: [String], rawCommand: String?) -> String? {
|
|
|
|
|
let normalizedRaw = self.normalizeRaw(rawCommand)
|
|
|
|
|
let shell = ExecShellWrapperParser.extract(command: command, rawCommand: nil)
|
|
|
|
|
let shellCommand = shell.isWrapper ? self.trimmedNonEmpty(shell.command) : nil
|
|
|
|
|
|
|
|
|
|
let envManipulationBeforeShellWrapper = self.hasEnvManipulationBeforeShellWrapper(command)
|
|
|
|
|
let shellWrapperPositionalArgv = self.hasTrailingPositionalArgvAfterInlineCommand(command)
|
|
|
|
|
let mustBindDisplayToFullArgv = envManipulationBeforeShellWrapper || shellWrapperPositionalArgv
|
|
|
|
|
let previewCommand: String? = if let shellCommand, !mustBindDisplayToFullArgv {
|
|
|
|
|
shellCommand
|
|
|
|
|
} else {
|
|
|
|
|
nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return self.allowlistEvaluationRawCommand(
|
|
|
|
|
normalizedRaw: normalizedRaw,
|
|
|
|
|
shellIsWrapper: shell.isWrapper,
|
|
|
|
|
previewCommand: previewCommand)
|
2026-02-25 00:01:53 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static func normalizeRaw(_ rawCommand: String?) -> String? {
|
|
|
|
|
let trimmed = rawCommand?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
|
|
|
|
return trimmed.isEmpty ? nil : trimmed
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static func trimmedNonEmpty(_ value: String?) -> String? {
|
|
|
|
|
let trimmed = value?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
|
|
|
|
return trimmed.isEmpty ? nil : trimmed
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-19 13:51:17 +02:00
|
|
|
private static func allowlistEvaluationRawCommand(
|
|
|
|
|
normalizedRaw: String?,
|
|
|
|
|
shellIsWrapper: Bool,
|
|
|
|
|
previewCommand: String?) -> String?
|
|
|
|
|
{
|
|
|
|
|
guard shellIsWrapper else {
|
|
|
|
|
return normalizedRaw
|
|
|
|
|
}
|
|
|
|
|
guard let normalizedRaw else {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
return normalizedRaw == previewCommand ? normalizedRaw : nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-19 14:27:05 +02:00
|
|
|
private static func hasEnvManipulationBeforeShellWrapper(_ argv: [String]) -> Bool {
|
2026-03-19 14:14:28 +02:00
|
|
|
return ExecWrapperResolution.hasEnvManipulationBeforeShellWrapper(argv)
|
2026-02-25 00:01:53 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static func hasTrailingPositionalArgvAfterInlineCommand(_ argv: [String]) -> Bool {
|
|
|
|
|
let wrapperArgv = self.unwrapShellWrapperArgv(argv)
|
|
|
|
|
guard let token0 = self.trimmedNonEmpty(wrapperArgv.first) else {
|
|
|
|
|
return false
|
|
|
|
|
}
|
2026-03-19 14:14:28 +02:00
|
|
|
let wrapper = ExecWrapperResolution.normalizeExecutableToken(token0)
|
2026-02-25 00:01:53 +00:00
|
|
|
guard self.posixOrPowerShellInlineWrapperNames.contains(wrapper) else {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-19 14:14:28 +02:00
|
|
|
let inlineCommandIndex = ExecWrapperResolution.resolveInlineCommandValueTokenIndex(
|
|
|
|
|
wrapperArgv,
|
|
|
|
|
normalizedWrapper: wrapper)
|
2026-02-25 00:01:53 +00:00
|
|
|
guard let inlineCommandIndex else {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
let start = inlineCommandIndex + 1
|
|
|
|
|
guard start < wrapperArgv.count else {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
return wrapperArgv[start...].contains { !$0.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static func unwrapShellWrapperArgv(_ argv: [String]) -> [String] {
|
2026-03-19 14:14:28 +02:00
|
|
|
ExecWrapperResolution.unwrapShellInspectionArgv(argv)
|
2026-02-25 00:01:53 +00:00
|
|
|
}
|
|
|
|
|
}
|