Skip to content

Commit d16e645

Browse files
committed
fix(code-placeholders): narrow arithmetic guards and preserve literal data
1 parent dc283dc commit d16e645

4 files changed

Lines changed: 427 additions & 1039 deletions

File tree

‎.github/workflows/checks.yml‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -614,6 +614,22 @@ jobs:
614614
- name: Install ripgrep
615615
run: command -v rg || (sudo apt-get update && sudo apt-get install -y ripgrep)
616616

617+
- name: Verify shell placeholder compilation in Bash
618+
if: matrix.shard == 1
619+
working-directory: apps/sim
620+
env:
621+
SHELL_PLACEHOLDERS_REPORT_PATH: ${{ runner.temp }}/shell-placeholders.json
622+
run: bun scripts/test-shell-placeholders-e2e.ts
623+
624+
- name: Upload shell placeholder execution report
625+
if: failure() && matrix.shard == 1
626+
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
627+
with:
628+
name: shell-placeholders
629+
path: ${{ runner.temp }}/shell-placeholders.json
630+
if-no-files-found: warn
631+
retention-days: 7
632+
617633
- name: Run tests
618634
env:
619635
NODE_OPTIONS: '--no-warnings --max-old-space-size=8192'

‎apps/sim/lib/execution/code-placeholders/compiler.test.ts‎

Lines changed: 1 addition & 233 deletions
Original file line numberDiff line numberDiff line change
@@ -1053,87 +1053,6 @@ describe('code placeholder compiler', () => {
10531053
'total=$(( $(( 1 + 1 )) + {{KEY}} ))',
10541054
'cat <<PAYLOAD\n$(( {{KEY}} * 2 ))\nPAYLOAD',
10551055
'cat <<PAYLOAD\n$[ {{KEY}} * 2 ]\nPAYLOAD',
1056-
'echo $(( $(cat <<EOF\n{{KEY}}\nEOF\n) + 1 ))',
1057-
"echo $(( $(cat <<'EOF'\n{{KEY}}\nEOF\n) + 1 ))",
1058-
"echo $(( $(cat <<-'EOF'\n\t{{KEY}}\n\tEOF\n) + 1 ))",
1059-
'[[ {{KEY}} -eq 0 ]] && echo zero',
1060-
'[[ "{{KEY}}" -eq 0 ]] && echo zero',
1061-
'declare -i n; declare -a n; n={{KEY}}',
1062-
'declare -i n; unset -f n; n={{KEY}}',
1063-
'declare -i n; n=1\\ {{KEY}}',
1064-
'declare -i n; cat <<PAYLOAD\n$(n={{KEY}})\nPAYLOAD',
1065-
// biome-ignore lint/suspicious/noTemplateCurlyInString: shell parameter expansion, not a JS template
1066-
'declare -A m <<PAYLOAD\n${m[{{KEY}}]}\nPAYLOAD',
1067-
// biome-ignore lint/suspicious/noTemplateCurlyInString: shell parameter expansion, not a JS template
1068-
'declare -A m "${m[{{KEY}}]}"',
1069-
'declare -i n; declare +i n <<PAYLOAD\n$(n={{KEY}})\nPAYLOAD',
1070-
'if [[ 0 -lt {{KEY}} ]]; then echo positive; fi',
1071-
'[[ -n x && ( "{{KEY}}" -ge 1 ) ]]',
1072-
'[[ $(printf "%s" "{{KEY}}") -ne 0 ]]',
1073-
'[[ $(printf "%s" "{{KEY}}"; echo 1) -le 0 ]]',
1074-
'[[ $(cat <<EOF\n{{KEY}}\nEOF\n) -gt 0 ]]',
1075-
'let "x={{KEY}}"',
1076-
'let x={{KEY}}+1',
1077-
'declare -i x="{{KEY}}"',
1078-
'typeset -i x={{KEY}}',
1079-
'f() { local -i x="{{KEY}}"; }',
1080-
'declare -ai values=("{{KEY}}")',
1081-
'declare -x -i x="{{KEY}}"',
1082-
'let x="$(printf "%s" "{{KEY}}")"',
1083-
'declare -i x="$(printf "%s" "{{KEY}}")"',
1084-
'>/dev/null let x="{{KEY}}"',
1085-
'2>/dev/null let x="{{KEY}}"',
1086-
'builtin let x="{{KEY}}"',
1087-
'command let x="{{KEY}}"',
1088-
'declare "-i" n="{{KEY}}"',
1089-
// biome-ignore lint/suspicious/noTemplateCurlyInString: shell parameter expansion, not a JS template
1090-
'[[ ${missing:-{{KEY}}} -eq 0 ]]',
1091-
'declare -i n; echo "$(declare +i n)"; n="{{KEY}}"',
1092-
// biome-ignore lint/suspicious/noTemplateCurlyInString: shell parameter expansion, not a JS template
1093-
'printf "%s" "${missing:-{text}"; let x="{{KEY}}"',
1094-
'declare -i n; n="$(printf "%s" "{{KEY}}")"',
1095-
// biome-ignore lint/suspicious/noTemplateCurlyInString: shell parameter expansion, not a JS template
1096-
'declare -i n; n="${missing:-{{KEY}}}"',
1097-
'declare -i n; declare n="{{KEY}}"',
1098-
'> /dev/null let x="{{KEY}}"',
1099-
'let x="$(cat <<EOF\n{{KEY}}\nEOF\n)"',
1100-
'unset "a[{{KEY}}]"',
1101-
'unset a[{{KEY}}]',
1102-
// biome-ignore lint/suspicious/noTemplateCurlyInString: shell parameter expansion, not a JS template
1103-
'declare -A m=([key]=abc); printf "%s\\n" "${m[key]:{{KEY}}}"',
1104-
'echo $(( 1 < {{KEY}} ))',
1105-
'let x=1 >/dev/null {{KEY}}',
1106-
'a=([{{KEY}}]=text)',
1107-
"unset 'a[{{KEY}}]'",
1108-
'le\\\nt "x={{KEY}}"',
1109-
'local -A a; a[{{KEY}}]=1',
1110-
'declare -A m; unset m; m[{{KEY}}]=1',
1111-
'declare -A m; unset m; declare -a m; m[{{KEY}}]=1',
1112-
'(declare -A m); m[{{KEY}}]=1',
1113-
'a[{{KEY}}]=1',
1114-
'a[{{KEY}}]+=1',
1115-
'declare -i n; n="{{KEY}}"',
1116-
'declare -i n\nn="{{KEY}}"',
1117-
// biome-ignore lint/suspicious/noTemplateCurlyInString: shell parameter expansion, not a JS template
1118-
'a=(1 2); echo "${a[{{KEY}}]}"',
1119-
// biome-ignore lint/suspicious/noTemplateCurlyInString: shell parameter expansion, not a JS template
1120-
'a=(1 2); echo "${a[0]:{{KEY}}}"',
1121-
// biome-ignore lint/suspicious/noTemplateCurlyInString: shell parameter expansion, not a JS template
1122-
'a=(1 2); echo "${a[0]:0:{{KEY}}}"',
1123-
// biome-ignore lint/suspicious/noTemplateCurlyInString: shell parameter expansion, not a JS template
1124-
's=abc; echo "${s:{{KEY}}}"',
1125-
// biome-ignore lint/suspicious/noTemplateCurlyInString: shell parameter expansion, not a JS template
1126-
's=abc; echo "${s:0:{{KEY}}}"',
1127-
// biome-ignore lint/suspicious/noTemplateCurlyInString: shell parameter expansion, not a JS template
1128-
's=abc; echo "${s: -1:{{KEY}}}"',
1129-
// biome-ignore lint/suspicious/noTemplateCurlyInString: shell parameter expansion, not a JS template
1130-
'set -- a b; echo "${@:{{KEY}}}"',
1131-
// biome-ignore lint/suspicious/noTemplateCurlyInString: shell parameter expansion, not a JS template
1132-
's=abc; printf "%s\\n" "${missing:-"${s:{{KEY}}}"}"',
1133-
// biome-ignore lint/suspicious/noTemplateCurlyInString: shell parameter expansion, not a JS template
1134-
'cat <<PAYLOAD\n${a[{{KEY}}]}\nPAYLOAD',
1135-
// biome-ignore lint/suspicious/noTemplateCurlyInString: shell parameter expansion, not a JS template
1136-
'cat <<PAYLOAD\n${s:{{KEY}}}\nPAYLOAD',
11371056
])('rejects shell placeholders whose values enter arithmetic: %s', async (code) => {
11381057
await expect(
11391058
compileCodePlaceholders({
@@ -1145,7 +1064,7 @@ describe('code placeholder compiler', () => {
11451064
})
11461065

11471066
it('preserves shell literal arithmetic text and leaves completed arithmetic frames', async () => {
1148-
const value = 'values[1]'
1067+
const value = 'values[$(printf injected >&2)]'
11491068
const compiled = await compileCodePlaceholders({
11501069
code: [
11511070
'printf "%s\\n" "{{KEY}}"',
@@ -1168,157 +1087,6 @@ describe('code placeholder compiler', () => {
11681087
)
11691088
})
11701089

1171-
it.each([
1172-
// biome-ignore lint/suspicious/noTemplateCurlyInString: shell parameter expansion, not a JS template
1173-
'printf "%s\\n" "${missing:-https://{{KEY}}}"',
1174-
// biome-ignore lint/suspicious/noTemplateCurlyInString: shell parameter expansion, not a JS template
1175-
'printf "%s\\n" "${missing:-items[{{KEY}}]}"',
1176-
// biome-ignore lint/suspicious/noTemplateCurlyInString: shell parameter expansion, not a JS template
1177-
's=abc; printf "%s\\n" "${s#[{{KEY}}]}"',
1178-
// biome-ignore lint/suspicious/noTemplateCurlyInString: shell parameter expansion, not a JS template
1179-
's=abc; printf "%s\\n" "${s/[{{KEY}}]/x}"',
1180-
'f() { local -i n; }; n="{{KEY}}"',
1181-
'PREFIX="$(printf "%s" "{{KEY}}")" let total=2',
1182-
'printf "%s" a[{{KEY}}]=1',
1183-
'declare -i n; printf "%s" n={{KEY}}',
1184-
'declare -i n; declare +i n; n="{{KEY}}"',
1185-
'declare -i n; n=1 printf "%s" "{{KEY}}"',
1186-
'declare -i n; n=5 text="{{KEY}}"',
1187-
'declare -i +i n="{{KEY}}"',
1188-
'declare -i n; echo "$(declare +i n; n=\'{{KEY}}\'; printf "%s" "$n")"',
1189-
'$(let x=1 <<EOF\n{{KEY}}\nEOF\n)',
1190-
// biome-ignore lint/suspicious/noTemplateCurlyInString: shell parameter expansion, not a JS template
1191-
'declare -A m; m[{{KEY}}]=x; printf "%s\\n" "${m[{{KEY}}]}"',
1192-
// biome-ignore lint/suspicious/noTemplateCurlyInString: shell parameter expansion, not a JS template
1193-
'declare -A m; printf "%s\\n" "${m[{{KEY}}]}"',
1194-
'declare -iA m; m[{{KEY}}]=1',
1195-
'read -p "items[{{KEY}}]" name',
1196-
'a=([{{KEY}}])',
1197-
// biome-ignore lint/suspicious/noTemplateCurlyInString: shell parameter expansion, not a JS template
1198-
'printf "%s\\n" "${missing:- # {{KEY}}}"',
1199-
'declare -A m; declare -A m; m[{{KEY}}]=1',
1200-
'let x=1 >"{{KEY}}"',
1201-
'let x=1 > {{KEY}}',
1202-
'declare -i n=1 >"{{KEY}}"',
1203-
'[[ "{{KEY}}" == "-eq" ]]',
1204-
'declare -i n; n=1 {{KEY}} hello',
1205-
'declare -A m=([{{KEY}}]=x)',
1206-
// biome-ignore lint/suspicious/noTemplateCurlyInString: shell parameter expansion, not a JS template
1207-
'declare -A m=([hello]=world); cat <<PAYLOAD\n${m[{{KEY}}]}\nPAYLOAD',
1208-
])('compiles parameter and prefix text that is not an arithmetic operand: %s', async (code) => {
1209-
// These forms use the value as pattern, default, or a shielded prefix — never arithmetic — so a
1210-
// conservative scanner must not reject them. Each would fail compilation if wrongly rejected.
1211-
await expect(
1212-
compileCodePlaceholders({
1213-
code,
1214-
language: CodeLanguage.Shell,
1215-
environmentVariables: { KEY: 'values[1]' },
1216-
})
1217-
).resolves.toBeDefined()
1218-
})
1219-
1220-
it('keeps literal single quotes inside a double-quoted default expansion', async () => {
1221-
const compiled = await compileCodePlaceholders({
1222-
// biome-ignore lint/suspicious/noTemplateCurlyInString: shell parameter expansion, not a JS template
1223-
code: 'printf "%s\\n" "${missing:-\'{{KEY}}\'}"',
1224-
language: CodeLanguage.Shell,
1225-
environmentVariables: { KEY: 'hello world' },
1226-
})
1227-
expect(executeShell(compiled.code, compiled.bindings)).toBe("'hello world'\n")
1228-
})
1229-
1230-
it('compiles shell placeholders beside arithmetic that never evaluates them', async () => {
1231-
const value = 'values[1]'
1232-
const compiled = await compileCodePlaceholders({
1233-
code: [
1234-
'[ "{{KEY}}" -eq 0 ] 2>/dev/null || printf "%s\\n" not-zero',
1235-
'[[ "{{KEY}}" == values* && 1 -eq 1 ]] && printf "%s\\n" matched',
1236-
'let total=1+1; printf "%s\\n" "{{KEY}}"',
1237-
'f() { local copy="{{KEY}}"; printf "%s\\n" "$copy"; }; f',
1238-
'declare copy="{{KEY}}"; printf "%s\\n" "$copy"',
1239-
// biome-ignore lint/suspicious/noTemplateCurlyInString: shell parameter expansion, not a JS template
1240-
'printf "%s\\n" "${missing:-{{KEY}}}"',
1241-
// biome-ignore lint/suspicious/noTemplateCurlyInString: shell parameter expansion, not a JS template
1242-
'a=(x y); printf "%s\\n" "${a[1]}{{KEY}}"',
1243-
'printf "%s\\n" "let {{KEY}}"',
1244-
'printf "%s\\n" "items[{{KEY}}]"',
1245-
'PREFIX="{{KEY}}" let total=2',
1246-
'# declare -i n',
1247-
'n="{{KEY}}"; printf "%s\\n" "$n"',
1248-
'let total=1 <<EOF',
1249-
'{{KEY}}',
1250-
'EOF',
1251-
'printf "%s\\n" "$total"',
1252-
'cat <<PAYLOAD',
1253-
'{{KEY}} $(( 1 + 1 ))',
1254-
'PAYLOAD',
1255-
].join('\n'),
1256-
language: CodeLanguage.Shell,
1257-
environmentVariables: { KEY: value },
1258-
})
1259-
1260-
expect(executeShell(`{\n${compiled.code}\n} 2>&1`, compiled.bindings)).toBe(
1261-
`not-zero\nmatched\n${value}\n${value}\n${value}\n${value}\ny${value}\n` +
1262-
`let ${value}\nitems[${value}]\n${value}\n1\n${value} 2\n`
1263-
)
1264-
})
1265-
1266-
it.each([
1267-
['an expansion in a subscript', 'a[$(cmd)]', 'an array subscript with an expansion'],
1268-
[
1269-
'a quoted bracket ending the walk early',
1270-
'a["]$(cmd)"]',
1271-
'an array subscript with an expansion',
1272-
],
1273-
['a backtick in a subscript', 'a[`cmd`]', 'an array subscript with an expansion'],
1274-
['a process substitution in a subscript', 'a[<(cmd)]', 'an array subscript with an expansion'],
1275-
['an escaped expansion in a subscript', 'a[\\$x]', 'an array subscript with an expansion'],
1276-
['an unclosed subscript', 'a[', 'an unbalanced array subscript'],
1277-
['a stray closing bracket', '$(cmd)]', 'an unbalanced array subscript'],
1278-
['an open quote in a subscript', 'a["]', 'an unbalanced array subscript'],
1279-
])('refuses a shell value with %s wherever it lands', async (_case, value, reason) => {
1280-
for (const code of ['printf "%s\\n" "{{KEY}}"', "cat <<'EOF'\n{{KEY}}\nEOF"]) {
1281-
await expect(
1282-
compileCodePlaceholders({
1283-
code,
1284-
language: CodeLanguage.Shell,
1285-
environmentVariables: { KEY: value },
1286-
})
1287-
).rejects.toThrow(reason)
1288-
}
1289-
})
1290-
1291-
it.each(['values[1]', '["a", "b"]', '{"items": [1, 2]}', '$HOME and $(date)', "it's [done]"])(
1292-
'compiles a shell value whose subscripts hold no expansion: %s',
1293-
async (value) => {
1294-
const compiled = await compileCodePlaceholders({
1295-
code: 'printf "%s\\n" "{{KEY}}"',
1296-
language: CodeLanguage.Shell,
1297-
environmentVariables: { KEY: value },
1298-
})
1299-
expect(executeShell(compiled.code, compiled.bindings)).toBe(`${value}\n`)
1300-
}
1301-
)
1302-
1303-
it('refuses arithmetic positions whose values are safe alone', async () => {
1304-
// Code can supply the brackets, or split a subscript across adjacent placeholders, so the value
1305-
// guard cannot judge these alone; the position scan refuses them.
1306-
await expect(
1307-
compileCodePlaceholders({
1308-
code: 'total=$(( a[{{KEY}}] ))',
1309-
language: CodeLanguage.Shell,
1310-
environmentVariables: { KEY: '$(cmd)' },
1311-
})
1312-
).rejects.toThrow('is not supported in shell arithmetic')
1313-
await expect(
1314-
compileCodePlaceholders({
1315-
code: 'total=$(( {{LEFT}}{{RIGHT}} ))',
1316-
language: CodeLanguage.Shell,
1317-
environmentVariables: { LEFT: 'a', RIGHT: '1' },
1318-
})
1319-
).rejects.toThrow('is not supported in shell arithmetic')
1320-
})
1321-
13221090
it('discovers shell arithmetic placeholders without compiling missing values', async () => {
13231091
const code = 'total=$(( {{MISSING}} + {{KEY}} ))'
13241092
await expect(analyzeCodePlaceholders(code, CodeLanguage.Shell)).resolves.toEqual([

0 commit comments

Comments
 (0)