From 0d653719615e37a158bbf91b07340555e38a170f Mon Sep 17 00:00:00 2001 From: dushyant Date: Mon, 6 Jul 2026 14:07:07 +0530 Subject: [PATCH 1/3] util: fix OSC 8 hyperlink stripping in stripVTControlCharacters The bundled ansi-regex OSC pattern used a restrictive URI character class that failed when URIs contained RFC 3986-valid characters such as parentheses. Match OSC sequences generically as ESC ] ... ST, aligned with ansi-regex v6.2.0. Co-authored-by: Cursor --- lib/internal/util/inspect.js | 12 +++++------- test/parallel/test-util-stripvtcontrolcharacters.js | 5 +++++ 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/lib/internal/util/inspect.js b/lib/internal/util/inspect.js index c959c056fece45..b733367b8fd25e 100644 --- a/lib/internal/util/inspect.js +++ b/lib/internal/util/inspect.js @@ -282,16 +282,14 @@ const meta = [ ]; // Regex used for ansi escape code splitting -// Ref: https://github.com/chalk/ansi-regex/blob/f338e1814144efb950276aac84135ff86b72dc8e/index.js +// Ref: https://github.com/chalk/ansi-regex/blob/72bc570/index.js // License: MIT by Sindre Sorhus // Matches all ansi escape code sequences in a string const ansi = new RegExp( - '[\\u001B\\u009B][[\\]()#;?]*' + - '(?:(?:(?:(?:;[-a-zA-Z\\d\\/\\#&.:=?%@~_]+)*' + - '|[a-zA-Z\\d]+(?:;[-a-zA-Z\\d\\/\\#&.:=?%@~_]*)*)?' + - '(?:\\u0007|\\u001B\\u005C|\\u009C))' + - '|(?:(?:\\d{1,4}(?:;\\d{0,4})*)?' + - '[\\dA-PR-TZcf-nq-uy=><~]))', 'g', + '(?:\\u001B\\][\\s\\S]*?(?:\\u0007|\\u001B\\u005C|\\u009C))' + + '|[\\u001B\\u009B][[\\]()#;?]*' + + '(?:(?:\\d{1,4}(?:;\\d{0,4})*)?' + + '[\\dA-PR-TZcf-nq-uy=><~])', 'g', ); let getStringWidth; diff --git a/test/parallel/test-util-stripvtcontrolcharacters.js b/test/parallel/test-util-stripvtcontrolcharacters.js index a33d18d26dbc51..393ee7fa7ebe76 100644 --- a/test/parallel/test-util-stripvtcontrolcharacters.js +++ b/test/parallel/test-util-stripvtcontrolcharacters.js @@ -18,6 +18,11 @@ for (const ST of ['\u0007', '\u001B\u005C', '\u009C']) { tests.push( [`\u001B]8;;mailto:no-replay@mail.com${ST}mail\u001B]8;;${ST}`, 'mail'], [`\u001B]8;k=v;https://example-a.com/?a_b=1&c=2#tit%20le${ST}click\u001B]8;;${ST}`, 'click'], + [`\u001B]8;;https://example.com/(foo${ST}label\u001B]8;;${ST}`, 'label'], + [`\u001B]8;;https://example.com/foo)bar${ST}label\u001B]8;;${ST}`, 'label'], + [`\u001B]8;;https://example.com/!foo${ST}label\u001B]8;;${ST}`, 'label'], + [`\u001B]8;;https://example.com/foo+bar${ST}label\u001B]8;;${ST}`, 'label'], + [`\u001B]8;;https://example.com/[foo]${ST}label\u001B]8;;${ST}`, 'label'], ); } From 3cc8f897e3214ae98e400aec966d8c70cfffbcd6 Mon Sep 17 00:00:00 2001 From: Dushyant Singh Hada Date: Wed, 8 Jul 2026 10:26:25 +0530 Subject: [PATCH 2/3] util: use [;:] as CSI parameter separator, matching ansi-regex v6.2.0 Co-authored-by: Archkon <180910180+Archkon@users.noreply.github.com> Signed-off-by: dushyant --- lib/internal/util/inspect.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/internal/util/inspect.js b/lib/internal/util/inspect.js index b733367b8fd25e..8e14c4de8def8f 100644 --- a/lib/internal/util/inspect.js +++ b/lib/internal/util/inspect.js @@ -288,7 +288,7 @@ const meta = [ const ansi = new RegExp( '(?:\\u001B\\][\\s\\S]*?(?:\\u0007|\\u001B\\u005C|\\u009C))' + '|[\\u001B\\u009B][[\\]()#;?]*' + - '(?:(?:\\d{1,4}(?:;\\d{0,4})*)?' + + '(?:(?:\\d{1,4}(?:[;:]\\d{0,4})*)?' + '[\\dA-PR-TZcf-nq-uy=><~])', 'g', ); From 9da6ef26c5bfc36f2a9c42e2a43bda2ef0e5d9a3 Mon Sep 17 00:00:00 2001 From: dushyant Date: Wed, 8 Jul 2026 10:56:10 +0530 Subject: [PATCH 3/3] util: extend stripVTControlCharacters regression tests Signed-off-by: dushyant --- .../test-util-stripvtcontrolcharacters.js | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/test/parallel/test-util-stripvtcontrolcharacters.js b/test/parallel/test-util-stripvtcontrolcharacters.js index 393ee7fa7ebe76..0d23dd926aa579 100644 --- a/test/parallel/test-util-stripvtcontrolcharacters.js +++ b/test/parallel/test-util-stripvtcontrolcharacters.js @@ -23,9 +23,26 @@ for (const ST of ['\u0007', '\u001B\u005C', '\u009C']) { [`\u001B]8;;https://example.com/!foo${ST}label\u001B]8;;${ST}`, 'label'], [`\u001B]8;;https://example.com/foo+bar${ST}label\u001B]8;;${ST}`, 'label'], [`\u001B]8;;https://example.com/[foo]${ST}label\u001B]8;;${ST}`, 'label'], + [`\u001B]8;;https://example.com/foo$bar${ST}label\u001B]8;;${ST}`, 'label'], + [`\u001B]8;;https://example.com/foo'bar${ST}label\u001B]8;;${ST}`, 'label'], + [`\u001B]8;;https://example.com/foo*bar${ST}label\u001B]8;;${ST}`, 'label'], + [`\u001B]8;;https://example.com/foo,bar${ST}label\u001B]8;;${ST}`, 'label'], ); } +// Colon-delimited CSI sub-parameters (SGR) should be stripped like the +// semicolon-delimited form. +tests.push( + ['\u001B[38:2:255:0:0mHello\u001B[0m', 'Hello'], + ['\u001B[4:3mUnderline\u001B[4:0m', 'Underline'], +); + +// Malformed/truncated OSC sequences without any string terminator are left +// unmatched rather than being partially stripped. +tests.push( + ['\u001B]8;;https://example.com/no-terminator', 'ttps://example.com/no-terminator'], +); + test('util.stripVTControlCharacters', (t) => { for (const [before, expected] of tests) { t.assert.strictEqual(util.stripVTControlCharacters(before), expected);