( command: string, parsed: ParseEntry[], )
| 115 | * shell-quote's correct parsing of ambiguous input can be exploited. |
| 116 | */ |
| 117 | export function hasMalformedTokens( |
| 118 | command: string, |
| 119 | parsed: ParseEntry[], |
| 120 | ): boolean { |
| 121 | // Check for unterminated quotes in the original command. shell-quote drops |
| 122 | // an unmatched quote without leaving any trace in the tokens, so this must |
| 123 | // inspect the raw string. Walk with bash semantics: backslash escapes the |
| 124 | // next char outside single-quotes; no escapes inside single-quotes. |
| 125 | let inSingle = false |
| 126 | let inDouble = false |
| 127 | let doubleCount = 0 |
| 128 | let singleCount = 0 |
| 129 | for (let i = 0; i < command.length; i++) { |
| 130 | const c = command[i] |
| 131 | if (c === '\\' && !inSingle) { |
| 132 | i++ |
| 133 | continue |
| 134 | } |
| 135 | if (c === '"' && !inSingle) { |
| 136 | doubleCount++ |
| 137 | inDouble = !inDouble |
| 138 | } else if (c === "'" && !inDouble) { |
| 139 | singleCount++ |
| 140 | inSingle = !inSingle |
| 141 | } |
| 142 | } |
| 143 | if (doubleCount % 2 !== 0 || singleCount % 2 !== 0) return true |
| 144 | |
| 145 | for (const entry of parsed) { |
| 146 | if (typeof entry !== 'string') continue |
| 147 | |
| 148 | // Check for unbalanced curly braces |
| 149 | const openBraces = (entry.match(/{/g) || []).length |
| 150 | const closeBraces = (entry.match(/}/g) || []).length |
| 151 | if (openBraces !== closeBraces) return true |
| 152 | |
| 153 | // Check for unbalanced parentheses |
| 154 | const openParens = (entry.match(/\(/g) || []).length |
| 155 | const closeParens = (entry.match(/\)/g) || []).length |
| 156 | if (openParens !== closeParens) return true |
| 157 | |
| 158 | // Check for unbalanced square brackets |
| 159 | const openBrackets = (entry.match(/\[/g) || []).length |
| 160 | const closeBrackets = (entry.match(/\]/g) || []).length |
| 161 | if (openBrackets !== closeBrackets) return true |
| 162 | |
| 163 | // Check for unbalanced double quotes |
| 164 | // Count quotes that aren't escaped (preceded by backslash) |
| 165 | // A token with an odd number of unescaped quotes is malformed |
| 166 | // eslint-disable-next-line custom-rules/no-lookbehind-regex -- gated by hasCommandSeparator check at caller, runs on short per-token strings |
| 167 | const doubleQuotes = entry.match(/(?<!\\)"/g) || [] |
| 168 | if (doubleQuotes.length % 2 !== 0) return true |
| 169 | |
| 170 | // Check for unbalanced single quotes |
| 171 | // eslint-disable-next-line custom-rules/no-lookbehind-regex -- same as above |
| 172 | const singleQuotes = entry.match(/(?<!\\)'/g) || [] |
| 173 | if (singleQuotes.length % 2 !== 0) return true |
| 174 | } |
no outgoing calls
no test coverage detected