fix(hooks): the args-heredoc guard judges a split delimiter as written out, also inside a string
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
1731e41629
commit
7ab4100489
3 changed files with 200 additions and 11 deletions
|
|
@ -302,7 +302,8 @@ const commandView = commandPositionView(deobfuscated.replace(/\x1B\[[0-9;]*m/g,
|
|||
// same trim the body lines get, so a CR (a CRLF line), VT, FF or other
|
||||
// character trim() strips around the word does not hide it.
|
||||
// After a quoted opener that passes, a later opener is skipped only when its
|
||||
// word is cut by an unpaired quote and is readable otherwise: the shell meets
|
||||
// word is cut by an unpaired quote and is readable otherwise and not split by
|
||||
// a backslash-newline (fix round 4.5b2, part A, decision 126): the shell meets
|
||||
// the same unpaired quote, stops with a parse error and runs nothing after it,
|
||||
// so a typed argument such as fix "<<" handling passes. Every other later
|
||||
// opener is checked, because the accepted opener may sit inside a string and
|
||||
|
|
@ -313,9 +314,12 @@ const commandView = commandPositionView(deobfuscated.replace(/\x1B\[[0-9;]*m/g,
|
|||
// odd count of that quote from the line start to the opener's first <),
|
||||
// as in grep -n '<<' or a commit message holding "<<". A cut word with
|
||||
// no string open before it stays unreadable and is blocked as unquoted.
|
||||
// The skip is safe: the quote has no partner, so the word's body is
|
||||
// never read as a heredoc body, and each opener is read on its own, so
|
||||
// skipping it hides no later opener.
|
||||
// The quote has no partner, so the outer shell never reads the word's
|
||||
// body as a heredoc body; a second shell (bash -c, eval) may, and the
|
||||
// regex reading still reads such a << when the delimiter follows it. An
|
||||
// opener whose raw span holds a backslash-newline has no regex match, so
|
||||
// it is never skipped (fix round 4.5b2, part A, decision 123). Each
|
||||
// opener is read on its own, so skipping one hides no later opener.
|
||||
// - A < after an odd run of backslashes (counted on the raw text, a line
|
||||
// break ends the run) starts no opener, in the regex reading and in the
|
||||
// shell reading alike, when a quote scan of the text before the run ends
|
||||
|
|
@ -426,10 +430,6 @@ function argsQuoteScan(text, from, to, openers, state) {
|
|||
p += 3; // a here-string is text
|
||||
continue;
|
||||
}
|
||||
if (!openers) {
|
||||
p += 2;
|
||||
continue;
|
||||
}
|
||||
// The opener record at p (openers are in text order).
|
||||
let o = null;
|
||||
for (let lo = 0, hi = openers.length - 1; lo <= hi && !o;) {
|
||||
|
|
@ -487,10 +487,18 @@ function argsEscapedLt(text, i, lim, openers) {
|
|||
return b % 2 === 1 && argsQuoteScan(text, 0, s, openers, lim.state) === 'none';
|
||||
}
|
||||
|
||||
// An opener whose raw span holds a backslash-newline. The regex reading cannot
|
||||
// see such an opener, so it is never skipped, as string text or as body text
|
||||
// (fix round 4.5b2, part A, decisions 123 and 126).
|
||||
function argsSplit(text, o) {
|
||||
return text.slice(o.index, o.end).includes('\\\n');
|
||||
}
|
||||
|
||||
// A cut opener (its quote has no partner after it) whose quote closes a
|
||||
// string opened earlier on the raw line holding the opener's first <: an
|
||||
// odd count of that quote character from the line start to the <.
|
||||
function argsCutInString(text, o) {
|
||||
if (argsSplit(text, o)) return false;
|
||||
const q = text[o.end];
|
||||
const lineStart = text.lastIndexOf('\n', o.index) + 1;
|
||||
return text.slice(lineStart, o.index).split(q).length % 2 === 0;
|
||||
|
|
@ -616,8 +624,27 @@ if (command.replace(/\\\n/g, '').includes(ARGS_DELIM)) {
|
|||
}
|
||||
let bodyFrom = Infinity;
|
||||
for (const o of argsShell) {
|
||||
if (o.index >= bodyFrom && o.cutOnly) continue;
|
||||
if (o.index >= bodyFrom && o.cutOnly && !argsSplit(command, o)) continue;
|
||||
if (o.cutOnly && argsCutInString(command, o)) continue;
|
||||
// A delimiter split by a backslash-newline whose word has a suffix is
|
||||
// judged as the regex reading judges the same opener written out (fix
|
||||
// round 4.5b2, part A).
|
||||
if (!o.unreadable && argsSplit(command, o)) {
|
||||
const w = o.word.trim();
|
||||
if (w.startsWith(ARGS_DELIM) && w.length > ARGS_DELIM.length) {
|
||||
const m = new RegExp(`^<<-?\\s*(['"\\\\]?)${ARGS_DELIM}(['"]?)`).exec(command.slice(o.index, o.end).replace(/\\\n/g, ''));
|
||||
if (!m) continue;
|
||||
const snl = command.indexOf('\n', o.end);
|
||||
if (snl === -1) continue;
|
||||
const sbody = argsLines.slice(command.slice(0, snl).split('\n').length);
|
||||
if (!sbody.some((line) => line.trim() !== '')) continue;
|
||||
const [, sopen, sclose] = m;
|
||||
const squoted = sopen === '\\' ? sclose === '' : sopen !== '' && sclose === sopen;
|
||||
if (squoted && closesOnceAtEnd) continue;
|
||||
argsVerdict = [!squoted, false];
|
||||
break;
|
||||
}
|
||||
}
|
||||
// The word is compared trimmed, as the body lines are: a word of D plus
|
||||
// characters trim() strips (a CR from a CRLF line, a VT, an FF, a
|
||||
// no-break space) is closed by a line equal to it, which trims to D.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue