From 99be1fdad6bb6a865a8d77045f2e2a22a0f67e5e Mon Sep 17 00:00:00 2001 From: Blake Embrey Date: Wed, 11 Nov 2026 15:31:13 -0800 Subject: [PATCH 1/3] Backport encoding regex --- index.js | 5 ++++- test/test.js | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/index.js b/index.js index ce530f2..f3a68e2 100644 --- a/index.js +++ b/index.js @@ -16,10 +16,13 @@ module.exports = encodeUrl /** * RegExp to match non-URL code points, *after* encoding (i.e. not including "%") * and including invalid escape sequences. + * Group up to 1024 character and percent runs per replacement to reduce calls + * to encodeURI while bounding RegExp stack use. Character runs stay intact so + * surrogate pairs are never split between replacements. * @private */ -var ENCODE_CHARS_REGEXP = /(?:[^\x21\x23-\x3B\x3D\x3F-\x5F\x61-\x7A\x7C\x7E]|%(?:[^0-9A-Fa-f]|[0-9A-Fa-f][^0-9A-Fa-f]|$))+/g +var ENCODE_CHARS_REGEXP = /(?:[^\x21\x23-\x3B\x3D\x3F-\x5F\x61-\x7A\x7C\x7E]+|%+(?![0-9A-Fa-f]{2})){1,1024}/g /** * RegExp to match surrogate pairs and unpaired surrogates. diff --git a/test/test.js b/test/test.js index 55ec5a9..262185d 100644 --- a/test/test.js +++ b/test/test.js @@ -75,6 +75,56 @@ describe('encodeUrl(url)', function () { }) }) + describe('when url contains invalid percent sequences', function () { + it('should encode truncated sequences', function () { + assert.strictEqual(encodeUrl('/%A'), '/%25A') + assert.strictEqual(encodeUrl('/a%2'), '/a%252') + }) + + it('should preserve a valid sequence after an invalid one', function () { + assert.strictEqual(encodeUrl('/%%41'), '/%25%41') + assert.strictEqual(encodeUrl('/%2%20snow.html'), '/%252%20snow.html') + }) + + it('should preserve allowed characters after an invalid sequence', function () { + assert.strictEqual(encodeUrl('/%\\'), '/%25\\') + assert.strictEqual(encodeUrl('/%2\\'), '/%252\\') + assert.strictEqual(encodeUrl('/%^%|%['), '/%25^%25|%25[') + }) + }) + + describe('when url contains large inputs', function () { + var length = 10000000 + var cases = [ + ['plain characters', 'a', 'a'], + ['spaces', ' ', '%20'], + ['percent signs', '%', '%25'], + ['Unicode', '\u65E5', '%E6%97%A5'], + ['surrogate pairs', '\uD83D\uDC7B', '%F0%9F%91%BB'], + ['unpaired surrogates', '\uD800', '%EF%BF%BD'], + ['valid escapes', '%20', '%20'], + ['mixed characters', '% \u65E5', '%25%20%E6%97%A5'], + ['percent signs and surrogate pairs', '%\uD83D\uDC7B', '%25%F0%9F%91%BB'] + ] + + cases.forEach(function (entry) { + it('should handle a long run of ' + entry[0], function () { + var count = Math.floor(length / entry[1].length) + assert.strictEqual(encodeUrl(entry[1].repeat(count)), entry[2].repeat(count)) + }) + }) + + it('should preserve a valid escape after a long percent run', function () { + assert.strictEqual(encodeUrl('%'.repeat(length) + '41'), '%25'.repeat(length - 1) + '%41') + }) + + it('should preserve escapes and surrogate pairs across grouped runs', function () { + var input = '%\uD83D\uDC7B'.repeat(1025) + var expected = '%25%F0%9F%91%BB'.repeat(1025) + assert.strictEqual(encodeUrl(input + '%%41%2%20%A'), expected + '%25%41%252%20%25A') + }) + }) + describe('when url contains raw surrogate pairs', function () { it('should encode pair as UTF-8 byte sequences', function () { assert.strictEqual(encodeUrl('http://localhost/\uD83D\uDC7B snow.html'), 'http://localhost/%F0%9F%91%BB%20snow.html') From 8b489451da019cb2f946c94c3bff249ee52017af Mon Sep 17 00:00:00 2001 From: Blake Embrey Date: Wed, 11 Nov 2026 15:34:06 -0800 Subject: [PATCH 2/3] Fix test for old node --- test/test.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/test.js b/test/test.js index 262185d..10a7a1f 100644 --- a/test/test.js +++ b/test/test.js @@ -110,17 +110,17 @@ describe('encodeUrl(url)', function () { cases.forEach(function (entry) { it('should handle a long run of ' + entry[0], function () { var count = Math.floor(length / entry[1].length) - assert.strictEqual(encodeUrl(entry[1].repeat(count)), entry[2].repeat(count)) + assert.strictEqual(encodeUrl(new Array(count + 1).join(entry[1])), new Array(count + 1).join(entry[2])) }) }) it('should preserve a valid escape after a long percent run', function () { - assert.strictEqual(encodeUrl('%'.repeat(length) + '41'), '%25'.repeat(length - 1) + '%41') + assert.strictEqual(encodeUrl(new Array(length + 1).join('%') + '41'), new Array(length).join('%25') + '%41') }) it('should preserve escapes and surrogate pairs across grouped runs', function () { - var input = '%\uD83D\uDC7B'.repeat(1025) - var expected = '%25%F0%9F%91%BB'.repeat(1025) + var input = new Array(1026).join('%\uD83D\uDC7B') + var expected = new Array(1026).join('%25%F0%9F%91%BB') assert.strictEqual(encodeUrl(input + '%%41%2%20%A'), expected + '%25%41%252%20%25A') }) }) From 83e18e5b426cd21a8f8761321eb2b1444831d3c1 Mon Sep 17 00:00:00 2001 From: Blake Embrey Date: Wed, 11 Nov 2026 15:44:27 -0800 Subject: [PATCH 3/3] Fix timeout --- test/test.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/test.js b/test/test.js index 10a7a1f..3a85b0e 100644 --- a/test/test.js +++ b/test/test.js @@ -94,7 +94,9 @@ describe('encodeUrl(url)', function () { }) describe('when url contains large inputs', function () { - var length = 10000000 + this.timeout(30000) + + var length = 100000 var cases = [ ['plain characters', 'a', 'a'], ['spaces', ' ', '%20'],