From 0ba3e9ba4119d8d65825d679db85b987f6f1ebde Mon Sep 17 00:00:00 2001 From: Yarchik Date: Thu, 2 Jul 2026 10:50:39 +0100 Subject: [PATCH] fix(util): escape null byte as \000 in quote() to avoid octal-escape merge In bash ANSI-C quoting ($'...'), \0 is a variable-length octal escape that greedily consumes up to three following octal digits. Emitting a NUL as \0 therefore merges with subsequent digits: quote('\0' + '123') produced $'\0123', which bash reads as octal \012 (newline) followed by '3' instead of NUL followed by '123'. Emitting a fixed three-digit \000 keeps the escape unambiguous, since bash octal escapes are at most three digits. --- build/util.cjs | 2 +- src/util.ts | 2 +- test/util.test.js | 6 +++++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/build/util.cjs b/build/util.cjs index 1df79370d1..873ceb19d2 100644 --- a/build/util.cjs +++ b/build/util.cjs @@ -64,7 +64,7 @@ function preferLocalBin(env, ...dirs) { function quote(arg) { if (arg === "") return `$''`; if (/^[\w/.\-+@:=,%]+$/.test(arg)) return arg; - return `$'` + arg.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/\f/g, "\\f").replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t").replace(/\v/g, "\\v").replace(/\0/g, "\\0") + `'`; + return `$'` + arg.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/\f/g, "\\f").replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t").replace(/\v/g, "\\v").replace(/\0/g, "\\000") + `'`; } function quotePowerShell(arg) { if (arg === "") return `''`; diff --git a/src/util.ts b/src/util.ts index 1a272905ba..76b5ce0e87 100644 --- a/src/util.ts +++ b/src/util.ts @@ -86,7 +86,7 @@ export function quote(arg: string): string { .replace(/\r/g, '\\r') .replace(/\t/g, '\\t') .replace(/\v/g, '\\v') - .replace(/\0/g, '\\0') + + .replace(/\0/g, '\\000') + `'` ) } diff --git a/test/util.test.js b/test/util.test.js index fba510fcab..21da7c243a 100644 --- a/test/util.test.js +++ b/test/util.test.js @@ -70,13 +70,17 @@ describe('util', () => { test('quote()', () => { assert.ok(quote('string') === 'string') assert.ok(quote('') === `$''`) - assert.ok(quote(`'\f\n\r\t\v\0`) === `$'\\'\\f\\n\\r\\t\\v\\0'`) + assert.ok(quote(`'\f\n\r\t\v\0`) === `$'\\'\\f\\n\\r\\t\\v\\000'`) const allowed = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_/.-+@:=,%' assert.equal(quote(allowed), allowed) }) + test('quote() escapes null byte unambiguously', () => { + assert.equal(quote('\0' + '123'), `$'\\000123'`) + }) + test('quotePowerShell()', () => { assert.equal(quotePowerShell('string'), 'string') assert.equal(quotePowerShell(`'`), `''''`)