Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
223 changes: 222 additions & 1 deletion apps/sim/lib/execution/code-placeholders/compiler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1053,6 +1053,80 @@ describe('code placeholder compiler', () => {
'total=$(( $(( 1 + 1 )) + {{KEY}} ))',
'cat <<PAYLOAD\n$(( {{KEY}} * 2 ))\nPAYLOAD',
'cat <<PAYLOAD\n$[ {{KEY}} * 2 ]\nPAYLOAD',
'echo $(( $(cat <<EOF\n{{KEY}}\nEOF\n) + 1 ))',
"echo $(( $(cat <<'EOF'\n{{KEY}}\nEOF\n) + 1 ))",
"echo $(( $(cat <<-'EOF'\n\t{{KEY}}\n\tEOF\n) + 1 ))",
'[[ {{KEY}} -eq 0 ]] && echo zero',
'[[ "{{KEY}}" -eq 0 ]] && echo zero',
'declare -i n; declare -a n; n={{KEY}}',
'declare -i n; unset -f n; n={{KEY}}',
'if [[ 0 -lt {{KEY}} ]]; then echo positive; fi',
'[[ -n x && ( "{{KEY}}" -ge 1 ) ]]',
'[[ $(printf "%s" "{{KEY}}") -ne 0 ]]',
'[[ $(printf "%s" "{{KEY}}"; echo 1) -le 0 ]]',
'[[ $(cat <<EOF\n{{KEY}}\nEOF\n) -gt 0 ]]',
'let "x={{KEY}}"',
'let x={{KEY}}+1',
'declare -i x="{{KEY}}"',
'typeset -i x={{KEY}}',
'f() { local -i x="{{KEY}}"; }',
'declare -ai values=("{{KEY}}")',
'declare -x -i x="{{KEY}}"',
'let x="$(printf "%s" "{{KEY}}")"',
'declare -i x="$(printf "%s" "{{KEY}}")"',
'>/dev/null let x="{{KEY}}"',
'2>/dev/null let x="{{KEY}}"',
'builtin let x="{{KEY}}"',
'command let x="{{KEY}}"',
'declare "-i" n="{{KEY}}"',
// biome-ignore lint/suspicious/noTemplateCurlyInString: shell parameter expansion, not a JS template
'[[ ${missing:-{{KEY}}} -eq 0 ]]',
'declare -i n; echo "$(declare +i n)"; n="{{KEY}}"',
// biome-ignore lint/suspicious/noTemplateCurlyInString: shell parameter expansion, not a JS template
'printf "%s" "${missing:-{text}"; let x="{{KEY}}"',
'declare -i n; n="$(printf "%s" "{{KEY}}")"',
// biome-ignore lint/suspicious/noTemplateCurlyInString: shell parameter expansion, not a JS template
'declare -i n; n="${missing:-{{KEY}}}"',
'declare -i n; declare n="{{KEY}}"',
'> /dev/null let x="{{KEY}}"',
'let x="$(cat <<EOF\n{{KEY}}\nEOF\n)"',
'unset "a[{{KEY}}]"',
'unset a[{{KEY}}]',
// biome-ignore lint/suspicious/noTemplateCurlyInString: shell parameter expansion, not a JS template
'declare -A m=([key]=abc); printf "%s\\n" "${m[key]:{{KEY}}}"',
'echo $(( 1 < {{KEY}} ))',
'let x=1 >/dev/null {{KEY}}',
'a=([{{KEY}}]=text)',
"unset 'a[{{KEY}}]'",
'le\\\nt "x={{KEY}}"',
'local -A a; a[{{KEY}}]=1',
'declare -A m; unset m; m[{{KEY}}]=1',
'declare -A m; unset m; declare -a m; m[{{KEY}}]=1',
'(declare -A m); m[{{KEY}}]=1',
'a[{{KEY}}]=1',
'a[{{KEY}}]+=1',
'declare -i n; n="{{KEY}}"',
'declare -i n\nn="{{KEY}}"',
// biome-ignore lint/suspicious/noTemplateCurlyInString: shell parameter expansion, not a JS template
'a=(1 2); echo "${a[{{KEY}}]}"',
// biome-ignore lint/suspicious/noTemplateCurlyInString: shell parameter expansion, not a JS template
'a=(1 2); echo "${a[0]:{{KEY}}}"',
// biome-ignore lint/suspicious/noTemplateCurlyInString: shell parameter expansion, not a JS template
'a=(1 2); echo "${a[0]:0:{{KEY}}}"',
// biome-ignore lint/suspicious/noTemplateCurlyInString: shell parameter expansion, not a JS template
's=abc; echo "${s:{{KEY}}}"',
// biome-ignore lint/suspicious/noTemplateCurlyInString: shell parameter expansion, not a JS template
's=abc; echo "${s:0:{{KEY}}}"',
// biome-ignore lint/suspicious/noTemplateCurlyInString: shell parameter expansion, not a JS template
's=abc; echo "${s: -1:{{KEY}}}"',
// biome-ignore lint/suspicious/noTemplateCurlyInString: shell parameter expansion, not a JS template
'set -- a b; echo "${@:{{KEY}}}"',
// biome-ignore lint/suspicious/noTemplateCurlyInString: shell parameter expansion, not a JS template
's=abc; printf "%s\\n" "${missing:-"${s:{{KEY}}}"}"',
// biome-ignore lint/suspicious/noTemplateCurlyInString: shell parameter expansion, not a JS template
'cat <<PAYLOAD\n${a[{{KEY}}]}\nPAYLOAD',
// biome-ignore lint/suspicious/noTemplateCurlyInString: shell parameter expansion, not a JS template
'cat <<PAYLOAD\n${s:{{KEY}}}\nPAYLOAD',
])('rejects shell placeholders whose values enter arithmetic: %s', async (code) => {
await expect(
compileCodePlaceholders({
Expand All @@ -1064,7 +1138,7 @@ describe('code placeholder compiler', () => {
})

it('preserves shell literal arithmetic text and leaves completed arithmetic frames', async () => {
const value = 'values[$(printf injected >&2)]'
const value = 'values[1]'
const compiled = await compileCodePlaceholders({
code: [
'printf "%s\\n" "{{KEY}}"',
Expand All @@ -1087,6 +1161,153 @@ describe('code placeholder compiler', () => {
)
})

it.each([
// biome-ignore lint/suspicious/noTemplateCurlyInString: shell parameter expansion, not a JS template
'printf "%s\\n" "${missing:-https://{{KEY}}}"',
// biome-ignore lint/suspicious/noTemplateCurlyInString: shell parameter expansion, not a JS template
'printf "%s\\n" "${missing:-items[{{KEY}}]}"',
// biome-ignore lint/suspicious/noTemplateCurlyInString: shell parameter expansion, not a JS template
's=abc; printf "%s\\n" "${s#[{{KEY}}]}"',
// biome-ignore lint/suspicious/noTemplateCurlyInString: shell parameter expansion, not a JS template
's=abc; printf "%s\\n" "${s/[{{KEY}}]/x}"',
'f() { local -i n; }; n="{{KEY}}"',
'PREFIX="$(printf "%s" "{{KEY}}")" let total=2',
'printf "%s" a[{{KEY}}]=1',
'declare -i n; printf "%s" n={{KEY}}',
'declare -i n; declare +i n; n="{{KEY}}"',
'declare -i n; n=1 printf "%s" "{{KEY}}"',
'declare -i n; n=5 text="{{KEY}}"',
'declare -i +i n="{{KEY}}"',
'declare -i n; echo "$(declare +i n; n=\'{{KEY}}\'; printf "%s" "$n")"',
'$(let x=1 <<EOF\n{{KEY}}\nEOF\n)',
// biome-ignore lint/suspicious/noTemplateCurlyInString: shell parameter expansion, not a JS template
'declare -A m; m[{{KEY}}]=x; printf "%s\\n" "${m[{{KEY}}]}"',
// biome-ignore lint/suspicious/noTemplateCurlyInString: shell parameter expansion, not a JS template
'declare -A m; printf "%s\\n" "${m[{{KEY}}]}"',
'declare -iA m; m[{{KEY}}]=1',
'read -p "items[{{KEY}}]" name',
'a=([{{KEY}}])',
// biome-ignore lint/suspicious/noTemplateCurlyInString: shell parameter expansion, not a JS template
'printf "%s\\n" "${missing:- # {{KEY}}}"',
'declare -A m; declare -A m; m[{{KEY}}]=1',
'let x=1 >"{{KEY}}"',
'let x=1 > {{KEY}}',
'declare -i n=1 >"{{KEY}}"',
'[[ "{{KEY}}" == "-eq" ]]',
])('compiles parameter and prefix text that is not an arithmetic operand: %s', async (code) => {
// These forms use the value as pattern, default, or a shielded prefix — never arithmetic — so a
// conservative scanner must not reject them. Each would fail compilation if wrongly rejected.
await expect(
compileCodePlaceholders({
code,
language: CodeLanguage.Shell,
environmentVariables: { KEY: 'values[1]' },
})
).resolves.toBeDefined()
})

it('keeps literal single quotes inside a double-quoted default expansion', async () => {
const compiled = await compileCodePlaceholders({
// biome-ignore lint/suspicious/noTemplateCurlyInString: shell parameter expansion, not a JS template
code: 'printf "%s\\n" "${missing:-\'{{KEY}}\'}"',
language: CodeLanguage.Shell,
environmentVariables: { KEY: 'hello world' },
})
expect(executeShell(compiled.code, compiled.bindings)).toBe("'hello world'\n")
})

it('compiles shell placeholders beside arithmetic that never evaluates them', async () => {
const value = 'values[1]'
const compiled = await compileCodePlaceholders({
code: [
'[ "{{KEY}}" -eq 0 ] 2>/dev/null || printf "%s\\n" not-zero',
'[[ "{{KEY}}" == values* && 1 -eq 1 ]] && printf "%s\\n" matched',
'let total=1+1; printf "%s\\n" "{{KEY}}"',
'f() { local copy="{{KEY}}"; printf "%s\\n" "$copy"; }; f',
'declare copy="{{KEY}}"; printf "%s\\n" "$copy"',
// biome-ignore lint/suspicious/noTemplateCurlyInString: shell parameter expansion, not a JS template
'printf "%s\\n" "${missing:-{{KEY}}}"',
// biome-ignore lint/suspicious/noTemplateCurlyInString: shell parameter expansion, not a JS template
'a=(x y); printf "%s\\n" "${a[1]}{{KEY}}"',
'printf "%s\\n" "let {{KEY}}"',
'printf "%s\\n" "items[{{KEY}}]"',
'PREFIX="{{KEY}}" let total=2',
'# declare -i n',
'n="{{KEY}}"; printf "%s\\n" "$n"',
'let total=1 <<EOF',
'{{KEY}}',
'EOF',
'printf "%s\\n" "$total"',
'cat <<PAYLOAD',
'{{KEY}} $(( 1 + 1 ))',
'PAYLOAD',
].join('\n'),
language: CodeLanguage.Shell,
environmentVariables: { KEY: value },
})

expect(executeShell(`{\n${compiled.code}\n} 2>&1`, compiled.bindings)).toBe(
`not-zero\nmatched\n${value}\n${value}\n${value}\n${value}\ny${value}\n` +
`let ${value}\nitems[${value}]\n${value}\n1\n${value} 2\n`
)
})

it.each([
['an expansion in a subscript', 'a[$(cmd)]', 'an array subscript with an expansion'],
[
'a quoted bracket ending the walk early',
'a["]$(cmd)"]',
'an array subscript with an expansion',
],
['a backtick in a subscript', 'a[`cmd`]', 'an array subscript with an expansion'],
['a process substitution in a subscript', 'a[<(cmd)]', 'an array subscript with an expansion'],
['an escaped expansion in a subscript', 'a[\\$x]', 'an array subscript with an expansion'],
['an unclosed subscript', 'a[', 'an unbalanced array subscript'],
['a stray closing bracket', '$(cmd)]', 'an unbalanced array subscript'],
['an open quote in a subscript', 'a["]', 'an unbalanced array subscript'],
])('refuses a shell value with %s wherever it lands', async (_case, value, reason) => {
for (const code of ['printf "%s\\n" "{{KEY}}"', "cat <<'EOF'\n{{KEY}}\nEOF"]) {
await expect(
compileCodePlaceholders({
code,
language: CodeLanguage.Shell,
environmentVariables: { KEY: value },
})
).rejects.toThrow(reason)
}
})

it.each(['values[1]', '["a", "b"]', '{"items": [1, 2]}', '$HOME and $(date)', "it's [done]"])(
'compiles a shell value whose subscripts hold no expansion: %s',
async (value) => {
const compiled = await compileCodePlaceholders({
code: 'printf "%s\\n" "{{KEY}}"',
language: CodeLanguage.Shell,
environmentVariables: { KEY: value },
})
expect(executeShell(compiled.code, compiled.bindings)).toBe(`${value}\n`)
}
)

it('refuses arithmetic positions whose values are safe alone', async () => {
// Code can supply the brackets, or split a subscript across adjacent placeholders, so the value
// guard cannot judge these alone; the position scan refuses them.
await expect(
compileCodePlaceholders({
code: 'total=$(( a[{{KEY}}] ))',
language: CodeLanguage.Shell,
environmentVariables: { KEY: '$(cmd)' },
})
).rejects.toThrow('is not supported in shell arithmetic')
await expect(
compileCodePlaceholders({
code: 'total=$(( {{LEFT}}{{RIGHT}} ))',
language: CodeLanguage.Shell,
environmentVariables: { LEFT: 'a', RIGHT: '1' },
})
).rejects.toThrow('is not supported in shell arithmetic')
})

it('discovers shell arithmetic placeholders without compiling missing values', async () => {
const code = 'total=$(( {{MISSING}} + {{KEY}} ))'
await expect(analyzeCodePlaceholders(code, CodeLanguage.Shell)).resolves.toEqual([
Expand Down
8 changes: 4 additions & 4 deletions apps/sim/lib/execution/code-placeholders/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -327,12 +327,12 @@ export function createOffsetRangeLookup(
* The placeholders lying wholly inside `[start, end]`. Occurrences are ordered and never
* overlap, so the matches are one contiguous run found by bisection.
*/
export function occurrencesWithin(
occurrences: readonly CodePlaceholderOccurrence[],
export function occurrencesWithin<T extends Pick<CodePlaceholderOccurrence, 'start' | 'end'>>(
occurrences: readonly T[],
start: number,
end: number
): CodePlaceholderOccurrence[] {
const matches: CodePlaceholderOccurrence[] = []
): T[] {
const matches: T[] = []
for (
let index = partitionPoint(occurrences, (occurrence) => occurrence.start < start);
index < occurrences.length && occurrences[index].end <= end;
Expand Down
Loading
Loading