mirror of
https://github.com/affaan-m/ECC.git
synced 2026-11-10 10:06:00 +00:00
fix(hooks): decode the escapes of an ANSI-C quoted here-document delimiter
Bash decodes $'...' quoting, so <<$'E\x4fF' ends at an EOF line. The splitter kept the escape, ended the body at a literal E\x4fF line, and exposed the rest of the body as commands. The delimiter word is now read by readWord(), which decodes $'...' as Bash does, \' included.
This commit is contained in:
3 files changed
+65
-25
No files matched your search
+57
-25
@@ -1,5 +1,59 @@
|
||||
'use strict';
|
||||
|
||||
// $'...' quoting, in which a backslash also escapes the closing quote
|
||||
const ANSI_C_QUOTED_RE = /\$'((?:\\[\s\S]|[^'\\])*)'/y;
|
||||
const ANSI_C_ESCAPES = { a: '\x07', b: '\b', e: '\x1b', E: '\x1b', f: '\f', n: '\n', r: '\r', t: '\t', v: '\v' };
|
||||
|
||||
// The text of $'...' quoting with its escapes decoded, as Bash decodes them.
|
||||
function decodeAnsiC(text) {
|
||||
return text.replace(/\\(x[0-9A-Fa-f]{1,2}|u[0-9A-Fa-f]{1,4}|U[0-9A-Fa-f]{1,8}|[0-7]{1,3}|c[\s\S]|[\s\S])/g, (escape, code) => {
|
||||
if ('xuU'.includes(code[0]) && code.length > 1) {
|
||||
const value = parseInt(code.slice(1), 16);
|
||||
return value <= 0x10ffff ? String.fromCodePoint(value) : escape;
|
||||
}
|
||||
if (/^[0-7]/.test(code)) return String.fromCharCode(parseInt(code, 8) & 0xff);
|
||||
if (code[0] === 'c' && code.length > 1) return String.fromCharCode(code.charCodeAt(1) & 0x1f);
|
||||
return ANSI_C_ESCAPES[code] ?? (`\\'"?`.includes(code) ? code : escape);
|
||||
});
|
||||
}
|
||||
|
||||
// The shell word at `start`, up to an unquoted blank or operator character, with its quoting
|
||||
// removed, and whether any part of it was quoted. Null if a quote does not close.
|
||||
function readWord(command, start) {
|
||||
let word = '';
|
||||
let quote = null;
|
||||
let quoted = false;
|
||||
let i = start;
|
||||
for (; i < command.length; i++) {
|
||||
const ch = command[i];
|
||||
ANSI_C_QUOTED_RE.lastIndex = i;
|
||||
const ansiC = !quote && ch === '$' ? ANSI_C_QUOTED_RE.exec(command) : null;
|
||||
if (quote) {
|
||||
if (ch === quote) quote = null;
|
||||
else word += ch;
|
||||
} else if (ansiC) {
|
||||
// $'...' decodes its escapes, so $'E\x4fF' is EOF
|
||||
word += decodeAnsiC(ansiC[1]);
|
||||
quoted = true;
|
||||
i += ansiC[0].length - 1;
|
||||
} else if (ch === "'" || ch === '"') {
|
||||
quote = ch;
|
||||
quoted = true;
|
||||
} else if (ch === '$' && command[i + 1] === '"') {
|
||||
// $"..." is double quoting; the $ is not part of the word
|
||||
continue;
|
||||
} else if (ch === '\\' && i + 1 < command.length) {
|
||||
word += command[++i];
|
||||
quoted = true;
|
||||
} else if (/[\s|&;()<>]/.test(ch)) {
|
||||
break;
|
||||
} else {
|
||||
word += ch;
|
||||
}
|
||||
}
|
||||
return quote ? null : { word, quoted, end: i };
|
||||
}
|
||||
|
||||
// A here-document opener at `index`, but not a here-string (<<<): `<<` or `<<-`, then the
|
||||
// delimiter, a shell word whose quotes are removed (`<<'EOF'`, `<<E"OF"`, `<<$'EOF'`,
|
||||
// `<<EOF/END`, `<<123`, and `<<''`, whose body ends at the first empty line). Unless part of the
|
||||
@@ -10,31 +64,9 @@ function heredocAt(command, index) {
|
||||
const stripTabs = command[i] === '-';
|
||||
if (stripTabs) i++;
|
||||
while (command[i] === ' ' || command[i] === '\t') i++;
|
||||
let delimiter = '';
|
||||
let quote = null;
|
||||
let quoted = false;
|
||||
for (; i < command.length; i++) {
|
||||
const ch = command[i];
|
||||
if (quote) {
|
||||
if (ch === quote) quote = null;
|
||||
else delimiter += ch;
|
||||
} else if (ch === "'" || ch === '"') {
|
||||
quote = ch;
|
||||
quoted = true;
|
||||
} else if (ch === '$' && (command[i + 1] === "'" || command[i + 1] === '"')) {
|
||||
// $'...' and $"..." are quoting too; the $ is not part of the word
|
||||
continue;
|
||||
} else if (ch === '\\' && i + 1 < command.length) {
|
||||
delimiter += command[++i];
|
||||
quoted = true;
|
||||
} else if (/[\s|&;()<>]/.test(ch)) {
|
||||
break;
|
||||
} else {
|
||||
delimiter += ch;
|
||||
}
|
||||
}
|
||||
if ((!delimiter && !quoted) || quote) return null;
|
||||
return { text: command.slice(index, i), delimiter, stripTabs, expands: !quoted };
|
||||
const word = readWord(command, i);
|
||||
if (!word || (!word.word && !word.quoted)) return null;
|
||||
return { text: command.slice(index, word.end), delimiter: word.word, stripTabs, expands: !word.quoted };
|
||||
}
|
||||
|
||||
// The index after the `)` that closes the `(` at `index`, or -1. Quoted parentheses and escaped
|
||||
|
||||
@@ -246,6 +246,11 @@ function runTests() {
|
||||
assert.strictEqual(result.code, 0, `Expected exit code 0, got ${result.code}`);
|
||||
}) ? passed++ : failed++);
|
||||
|
||||
(test('allows a here-document body whose ANSI-C quoted delimiter has an escape', () => {
|
||||
const result = runScript("cat <<$'E\\x4fF'\nE\\x4fF\nnpm run dev\nEOF");
|
||||
assert.strictEqual(result.code, 0, `Expected exit code 0, got ${result.code}`);
|
||||
}) ? passed++ : failed++);
|
||||
|
||||
(test('allows a here-document body fed to a package manager command', () => {
|
||||
const result = runScript('npm --version <<EOF && cat\nnpm run dev\nEOF');
|
||||
assert.strictEqual(result.code, 0, `Expected exit code 0, got ${result.code}`);
|
||||
|
||||
@@ -141,6 +141,9 @@ test('a here-document delimiter is a shell word, read without its quotes', () =>
|
||||
assert.deepStrictEqual(splitShellSegments('cat <<E"OF"\nnpm run dev\nEOF\nls'), ['cat <<E"OF"', 'ls']);
|
||||
assert.deepStrictEqual(splitShellSegments('cat <<123\nnpm run dev\n123\nls'), ['cat <<123', 'ls']);
|
||||
assert.deepStrictEqual(splitShellSegments("cat <<$'EOF'\ntext\nEOF\nnpm run dev"), ["cat <<$'EOF'", 'npm run dev']);
|
||||
// $'...' decodes its escapes: $'E\x4fF' is EOF, and $'it\'s' is it's
|
||||
assert.deepStrictEqual(splitShellSegments("cat <<$'E\\x4fF'\nE\\x4fF\nnpm run dev\nEOF\nls"), ["cat <<$'E\\x4fF'", 'ls']);
|
||||
assert.deepStrictEqual(splitShellSegments("cat <<$'it\\'s'\nnpm run dev\nit's\nls"), ["cat <<$'it\\'s'", 'ls']);
|
||||
// An empty delimiter ends the body at the first empty line
|
||||
assert.deepStrictEqual(splitShellSegments("cat <<''\nnpm run dev\n\nls"), ["cat <<''", 'ls']);
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user