Skip to content

Commit 627ea84

Browse files
committed
fix: stop extracting patterns at command flags and revert command-validation changes
1 parent 8b4150e commit 627ea84

File tree

2 files changed

+173
-26
lines changed

2 files changed

+173
-26
lines changed

webview-ui/src/utils/command-parser.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ export function extractPatternsFromCommand(command: string): string[] {
4141
}
4242

4343
function isValidToken(token: string): boolean {
44-
return !!token && !token.match(/[/\\~:]/) && token !== "." && !token.match(/\.\w+$/)
44+
return !!token && !token.startsWith("-") && !token.match(/[/\\~:]/) && token !== "." && !token.match(/\.\w+$/)
4545
}
4646

4747
function extractFromTokens(tokens: string[], patterns: Set<string>): void {

webview-ui/src/utils/command-validation.ts

Lines changed: 172 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { parse } from "shell-quote"
22

3+
type ShellToken = string | { op: string } | { command: string }
4+
35
/**
46
* # Command Denylist Feature - Longest Prefix Match Strategy
57
*
@@ -70,38 +72,183 @@ import { parse } from "shell-quote"
7072
export function parseCommand(command: string): string[] {
7173
if (!command?.trim()) return []
7274

75+
// Split by newlines first (handle different line ending formats)
76+
// This regex splits on \r\n (Windows), \n (Unix), or \r (old Mac)
77+
const lines = command.split(/\r\n|\r|\n/)
78+
const allCommands: string[] = []
79+
80+
for (const line of lines) {
81+
// Skip empty lines
82+
if (!line.trim()) continue
83+
84+
// Process each line through the existing parsing logic
85+
const lineCommands = parseCommandLine(line)
86+
allCommands.push(...lineCommands)
87+
}
88+
89+
return allCommands
90+
}
91+
92+
/**
93+
* Parse a single line of commands (internal helper function)
94+
*/
95+
function parseCommandLine(command: string): string[] {
96+
if (!command?.trim()) return []
97+
98+
// Storage for replaced content
99+
const redirections: string[] = []
100+
const subshells: string[] = []
101+
const quotes: string[] = []
102+
const arrayIndexing: string[] = []
103+
const arithmeticExpressions: string[] = []
104+
const variables: string[] = []
105+
const parameterExpansions: string[] = []
106+
const processSubstitutions: string[] = []
107+
108+
// First handle PowerShell redirections by temporarily replacing them
109+
let processedCommand = command.replace(/\d*>&\d*/g, (match) => {
110+
redirections.push(match)
111+
return `__REDIR_${redirections.length - 1}__`
112+
})
113+
114+
// Handle arithmetic expressions: $((...)) pattern
115+
// Match the entire arithmetic expression including nested parentheses
116+
processedCommand = processedCommand.replace(/\$\(\([^)]*(?:\)[^)]*)*\)\)/g, (match) => {
117+
arithmeticExpressions.push(match)
118+
return `__ARITH_${arithmeticExpressions.length - 1}__`
119+
})
120+
121+
// Handle parameter expansions: ${...} patterns (including array indexing)
122+
// This covers ${var}, ${var:-default}, ${var:+alt}, ${#var}, ${var%pattern}, etc.
123+
processedCommand = processedCommand.replace(/\$\{[^}]+\}/g, (match) => {
124+
parameterExpansions.push(match)
125+
return `__PARAM_${parameterExpansions.length - 1}__`
126+
})
127+
128+
// Handle process substitutions: <(...) and >(...)
129+
processedCommand = processedCommand.replace(/[<>]\([^)]+\)/g, (match) => {
130+
processSubstitutions.push(match)
131+
return `__PROCSUB_${processSubstitutions.length - 1}__`
132+
})
133+
134+
// Handle simple variable references: $varname pattern
135+
// This prevents shell-quote from splitting $count into separate tokens
136+
processedCommand = processedCommand.replace(/\$[a-zA-Z_][a-zA-Z0-9_]*/g, (match) => {
137+
variables.push(match)
138+
return `__VAR_${variables.length - 1}__`
139+
})
140+
141+
// Handle special bash variables: $?, $!, $#, $$, $@, $*, $-, $0-$9
142+
processedCommand = processedCommand.replace(/\$[?!#$@*\-0-9]/g, (match) => {
143+
variables.push(match)
144+
return `__VAR_${variables.length - 1}__`
145+
})
146+
147+
// Then handle subshell commands
148+
processedCommand = processedCommand
149+
.replace(/\$\((.*?)\)/g, (_, inner) => {
150+
subshells.push(inner.trim())
151+
return `__SUBSH_${subshells.length - 1}__`
152+
})
153+
.replace(/`(.*?)`/g, (_, inner) => {
154+
subshells.push(inner.trim())
155+
return `__SUBSH_${subshells.length - 1}__`
156+
})
157+
158+
// Then handle quoted strings
159+
processedCommand = processedCommand.replace(/"[^"]*"/g, (match) => {
160+
quotes.push(match)
161+
return `__QUOTE_${quotes.length - 1}__`
162+
})
163+
164+
let tokens: ShellToken[]
73165
try {
74-
const parsed = parse(command)
75-
const commands: string[] = []
76-
let currentCommand: string[] = []
77-
78-
for (const token of parsed) {
79-
if (typeof token === "object" && "op" in token) {
80-
// Chain operator - split command
81-
if (["&&", "||", ";", "|"].includes(token.op)) {
82-
if (currentCommand.length > 0) {
83-
commands.push(currentCommand.join(" "))
84-
currentCommand = []
85-
}
86-
} else {
87-
// Other operators are part of the command
88-
currentCommand.push(token.op)
166+
tokens = parse(processedCommand) as ShellToken[]
167+
} catch (error: any) {
168+
// If shell-quote fails to parse, fall back to simple splitting
169+
console.warn("shell-quote parse error:", error.message, "for command:", processedCommand)
170+
171+
// Simple fallback: split by common operators
172+
const fallbackCommands = processedCommand
173+
.split(/(?:&&|\|\||;|\|)/)
174+
.map((cmd) => cmd.trim())
175+
.filter((cmd) => cmd.length > 0)
176+
177+
// Restore all placeholders for each command
178+
return fallbackCommands.map((cmd) => {
179+
let result = cmd
180+
// Restore quotes
181+
result = result.replace(/__QUOTE_(\d+)__/g, (_, i) => quotes[parseInt(i)])
182+
// Restore redirections
183+
result = result.replace(/__REDIR_(\d+)__/g, (_, i) => redirections[parseInt(i)])
184+
// Restore array indexing expressions
185+
result = result.replace(/__ARRAY_(\d+)__/g, (_, i) => arrayIndexing[parseInt(i)])
186+
// Restore arithmetic expressions
187+
result = result.replace(/__ARITH_(\d+)__/g, (_, i) => arithmeticExpressions[parseInt(i)])
188+
// Restore parameter expansions
189+
result = result.replace(/__PARAM_(\d+)__/g, (_, i) => parameterExpansions[parseInt(i)])
190+
// Restore process substitutions
191+
result = result.replace(/__PROCSUB_(\d+)__/g, (_, i) => processSubstitutions[parseInt(i)])
192+
// Restore variable references
193+
result = result.replace(/__VAR_(\d+)__/g, (_, i) => variables[parseInt(i)])
194+
return result
195+
})
196+
}
197+
198+
const commands: string[] = []
199+
let currentCommand: string[] = []
200+
201+
for (const token of tokens) {
202+
if (typeof token === "object" && "op" in token) {
203+
// Chain operator - split command
204+
if (["&&", "||", ";", "|"].includes(token.op)) {
205+
if (currentCommand.length > 0) {
206+
commands.push(currentCommand.join(" "))
207+
currentCommand = []
89208
}
90-
} else if (typeof token === "string") {
209+
} else {
210+
// Other operators (>, &) are part of the command
211+
currentCommand.push(token.op)
212+
}
213+
} else if (typeof token === "string") {
214+
// Check if it's a subshell placeholder
215+
const subshellMatch = token.match(/__SUBSH_(\d+)__/)
216+
if (subshellMatch) {
217+
if (currentCommand.length > 0) {
218+
commands.push(currentCommand.join(" "))
219+
currentCommand = []
220+
}
221+
commands.push(subshells[parseInt(subshellMatch[1])])
222+
} else {
91223
currentCommand.push(token)
92224
}
93225
}
226+
}
94227

95-
// Add any remaining command
96-
if (currentCommand.length > 0) {
97-
commands.push(currentCommand.join(" "))
98-
}
99-
100-
return commands
101-
} catch (_error) {
102-
// If shell-quote fails, fall back to simple splitting
103-
return [command]
228+
// Add any remaining command
229+
if (currentCommand.length > 0) {
230+
commands.push(currentCommand.join(" "))
104231
}
232+
233+
// Restore quotes and redirections
234+
return commands.map((cmd) => {
235+
let result = cmd
236+
// Restore quotes
237+
result = result.replace(/__QUOTE_(\d+)__/g, (_, i) => quotes[parseInt(i)])
238+
// Restore redirections
239+
result = result.replace(/__REDIR_(\d+)__/g, (_, i) => redirections[parseInt(i)])
240+
// Restore array indexing expressions
241+
result = result.replace(/__ARRAY_(\d+)__/g, (_, i) => arrayIndexing[parseInt(i)])
242+
// Restore arithmetic expressions
243+
result = result.replace(/__ARITH_(\d+)__/g, (_, i) => arithmeticExpressions[parseInt(i)])
244+
// Restore parameter expansions
245+
result = result.replace(/__PARAM_(\d+)__/g, (_, i) => parameterExpansions[parseInt(i)])
246+
// Restore process substitutions
247+
result = result.replace(/__PROCSUB_(\d+)__/g, (_, i) => processSubstitutions[parseInt(i)])
248+
// Restore variable references
249+
result = result.replace(/__VAR_(\d+)__/g, (_, i) => variables[parseInt(i)])
250+
return result
251+
})
105252
}
106253

107254
/**

0 commit comments

Comments
 (0)