Skip to content

Commit

Permalink
Fix Node.js without ICU support (#1144)
Browse files Browse the repository at this point in the history
  • Loading branch information
ehmicky authored Aug 14, 2024
1 parent 607a0ff commit 074ea4a
Show file tree
Hide file tree
Showing 3 changed files with 43 additions and 8 deletions.
18 changes: 17 additions & 1 deletion lib/arguments/escape.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,23 @@ const escapeControlCharacter = character => {
// Some shells do not even have a way to print those characters in an escaped fashion.
// Therefore, we prioritize printing those safely, instead of allowing those to be copy-pasted.
// List of Unicode character categories: https://www.fileformat.info/info/unicode/category/index.htm
const SPECIAL_CHAR_REGEXP = /\p{Separator}|\p{Other}/gu;
const getSpecialCharRegExp = () => {
try {
// This throws when using Node.js without ICU support.
// When using a RegExp literal, this would throw at parsing-time, instead of runtime.
// eslint-disable-next-line prefer-regex-literals
return new RegExp('\\p{Separator}|\\p{Other}', 'gu');
} catch {
// Similar to the above RegExp, but works even when Node.js has been built without ICU support.
// Unlike the above RegExp, it only covers whitespaces and C0/C1 control characters.
// It does not cover some edge cases, such as Unicode reserved characters.
// See https://github.com/sindresorhus/execa/issues/1143
// eslint-disable-next-line no-control-regex
return /[\s\u0000-\u001F\u007F-\u009F\u00AD]/g;
}
};

const SPECIAL_CHAR_REGEXP = getSpecialCharRegExp();

// Accepted by $'...' in Bash.
// Exclude \a \e \v which are accepted in Bash but not in JavaScript (except \v) and JSON.
Expand Down
16 changes: 16 additions & 0 deletions test/arguments/escape-no-icu.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Mimics Node.js when built without ICU support
// See https://github.com/sindresorhus/execa/issues/1143
globalThis.RegExp = class extends RegExp {
constructor(regExpString, flags) {
if (flags?.includes('u') && regExpString.includes('\\p{')) {
throw new Error('Invalid property name');
}

super(regExpString, flags);
}

static isMocked = true;
};

// Execa computes the RegExp when first loaded, so we must delay this import
await import('./escape.js');
17 changes: 10 additions & 7 deletions test/arguments/escape.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,11 @@ test(testResultCommand, ' foo bar', 'foo', 'bar');
test(testResultCommand, ' baz quz', 'baz', 'quz');
test(testResultCommand, '');

const testEscapedCommand = async (t, commandArguments, expectedUnix, expectedWindows) => {
const expected = isWindows ? expectedWindows : expectedUnix;
// eslint-disable-next-line max-params
const testEscapedCommand = async (t, commandArguments, expectedUnix, expectedWindows, expectedUnixNoIcu = expectedUnix, expectedWindowsNoIcu = expectedWindows) => {
const expected = RegExp.isMocked
? (isWindows ? expectedWindowsNoIcu : expectedUnixNoIcu)
: (isWindows ? expectedWindows : expectedUnix);

t.like(
await t.throwsAsync(execa('fail.js', commandArguments)),
Expand Down Expand Up @@ -89,12 +92,12 @@ test('result.escapedCommand - \\x01', testEscapedCommand, ['\u0001'], '\'\\u0001
test('result.escapedCommand - \\x7f', testEscapedCommand, ['\u007F'], '\'\\u007f\'', '"\\u007f"');
test('result.escapedCommand - \\u0085', testEscapedCommand, ['\u0085'], '\'\\u0085\'', '"\\u0085"');
test('result.escapedCommand - \\u2000', testEscapedCommand, ['\u2000'], '\'\\u2000\'', '"\\u2000"');
test('result.escapedCommand - \\u200E', testEscapedCommand, ['\u200E'], '\'\\u200e\'', '"\\u200e"');
test('result.escapedCommand - \\u200E', testEscapedCommand, ['\u200E'], '\'\\u200e\'', '"\\u200e"', '\'\u200E\'', '"\u200E"');
test('result.escapedCommand - \\u2028', testEscapedCommand, ['\u2028'], '\'\\u2028\'', '"\\u2028"');
test('result.escapedCommand - \\u2029', testEscapedCommand, ['\u2029'], '\'\\u2029\'', '"\\u2029"');
test('result.escapedCommand - \\u5555', testEscapedCommand, ['\u5555'], '\'\u5555\'', '"\u5555"');
test('result.escapedCommand - \\uD800', testEscapedCommand, ['\uD800'], '\'\\ud800\'', '"\\ud800"');
test('result.escapedCommand - \\uE000', testEscapedCommand, ['\uE000'], '\'\\ue000\'', '"\\ue000"');
test('result.escapedCommand - \\uD800', testEscapedCommand, ['\uD800'], '\'\\ud800\'', '"\\ud800"', '\'\uD800\'', '"\uD800"');
test('result.escapedCommand - \\uE000', testEscapedCommand, ['\uE000'], '\'\\ue000\'', '"\\ue000"', '\'\uE000\'', '"\uE000"');
test('result.escapedCommand - \\U1D172', testEscapedCommand, ['\u{1D172}'], '\'\u{1D172}\'', '"\u{1D172}"');
test('result.escapedCommand - \\U1D173', testEscapedCommand, ['\u{1D173}'], '\'\\U1d173\'', '"\\U1d173"');
test('result.escapedCommand - \\U10FFFD', testEscapedCommand, ['\u{10FFFD}'], '\'\\U10fffd\'', '"\\U10fffd"');
test('result.escapedCommand - \\U1D173', testEscapedCommand, ['\u{1D173}'], '\'\\U1d173\'', '"\\U1d173"', '\'\u{1D173}\'', '"\u{1D173}"');
test('result.escapedCommand - \\U10FFFD', testEscapedCommand, ['\u{10FFFD}'], '\'\\U10fffd\'', '"\\U10fffd"', '\'\u{10FFFD}\'', '"\u{10FFFD}"');

0 comments on commit 074ea4a

Please sign in to comment.