diff --git a/scripts/hooks/block-no-verify.js b/scripts/hooks/block-no-verify.js index 75755260f..1eff156d7 100644 --- a/scripts/hooks/block-no-verify.js +++ b/scripts/hooks/block-no-verify.js @@ -815,18 +815,25 @@ function commandRole(words, budget) { return { kind: 'opaque', stdin: true }; } +// A brace group or subshell writes what its own commands print into the pipe: +// `{ echo 'git push --no-verify'; } | sh` feeds sh like `echo ... | sh`. The +// writes form one stream, so they are also read joined in order: +// `{ printf 'git pu'; printf 'sh --no-verify'; } | sh`. +function groupSources(scope, budget) { + const written = []; + for (const inner of scanShell(scope.body, budget).commands) { + if (!inner.pipeTo) written.push(...pipelineSources(inner, budget)); + } + if (written.length > 1) written.push({ text: written.map(source => source.text).join(''), opaque: written.some(source => source.opaque) }); + return written; +} + // Literal producers only. Unmodeled transformations remain conservative rather // than executing a formatter, interpreter, shell or user-supplied command. function pipelineSources(command, budget) { const sources = []; for (let current = command; current; current = current.pipeFrom) { - // A brace group or subshell writes what its own commands print into the - // pipe: `{ echo 'git push --no-verify'; } | sh` feeds sh like `echo ... | sh`. - if (current.closedScope) { - for (const inner of scanShell(current.closedScope.body, budget).commands) { - if (!inner.pipeTo) sources.push(...pipelineSources(inner, budget)); - } - } + if (current.closedScope) sources.push(...groupSources(current.closedScope, budget)); budget.spend(current.words.length + 1); const { words } = executableWords(current.words, budget); for (const word of words) budget.spend(word.value.length + 3); diff --git a/tests/hooks/block-no-verify.test.js b/tests/hooks/block-no-verify.test.js index 5dac62720..1bbec6131 100644 --- a/tests/hooks/block-no-verify.test.js +++ b/tests/hooks/block-no-verify.test.js @@ -239,6 +239,8 @@ const executingPayloads = [ ['pipe subshell data to bash', "(echo 'git push --no-verify'; printf x) | bash"], ['pipe nested group data to sh', "{ (printf '%s\\n' 'git commit -n -m x'); } | sh"], ['pipe group pipeline data to sh', "{ echo 'git push --no-verify' | grep git; } | sh"], + ['pipe consecutive group writes to sh', "{ printf 'git pu'; printf 'sh --no-verify'; } | sh"], + ['pipe echo -n and echo group writes to bash', "{ echo -n 'git commit '; echo '--no-verify -m x'; } | bash"], ['execute data through xargs and bash -c', "printf '%s\\n' 'git commit -n -m x' | xargs -I CMD bash -c CMD"], ['execute command substitution text through bash', "echo '$(git commit -n -m x)' | bash"], ['execute bash here-string', "bash <<< 'git commit -n -m x'"],