From a9fde3d71c502385e622d659c2618bfa9d03399d Mon Sep 17 00:00:00 2001 From: Jeff Widman Date: Thu, 15 Oct 2026 01:45:20 -0700 Subject: [PATCH] fix(arborist): skip normalization for canonical ASCII map keys Avoid NFKD normalization and lowercasing for ASCII strings without uppercase letters. Retain the existing path for uppercase and Unicode input, with regression coverage for equivalent keys, replacements, and non-string keys. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../arborist/lib/case-insensitive-map.js | 3 +- .../arborist/test/case-insensitive-map.js | 36 +++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/workspaces/arborist/lib/case-insensitive-map.js b/workspaces/arborist/lib/case-insensitive-map.js index afc6afbe0f98a..2820db298ea7a 100644 --- a/workspaces/arborist/lib/case-insensitive-map.js +++ b/workspaces/arborist/lib/case-insensitive-map.js @@ -13,7 +13,8 @@ module.exports = class CIMap extends Map { } #normKey (key) { - if (typeof key !== 'string') { + // ASCII keys without uppercase letters are already normalized. + if (typeof key !== 'string' || !/[A-Z\u0080-\uffff]/.test(key)) { return key } return key.normalize('NFKD').toLowerCase() diff --git a/workspaces/arborist/test/case-insensitive-map.js b/workspaces/arborist/test/case-insensitive-map.js index 7c63bd67b37ea..04c9123f6cd1d 100644 --- a/workspaces/arborist/test/case-insensitive-map.js +++ b/workspaces/arborist/test/case-insensitive-map.js @@ -45,3 +45,39 @@ t.test('do not get confused with undefined or weird values', t => { ]) t.end() }) + +t.test('preserves case folding and Unicode normalization', t => { + for (const [key, equivalent] of [ + ['@scope/package-name_1.0', '@SCOPE/PACKAGE-NAME_1.0'], + ['caf\u00e9', 'cafe\u0301'], + ['\u212a', 'k'], + ['\ufb03', 'ffi'], + ['\uff21', 'a'], + ['\ud835\udc00', 'a'], + ['\u0130', 'i\u0307'], + ]) { + const cmap = new CMap([[key, 1]]) + t.equal(cmap.get(equivalent), 1) + t.equal(cmap.has(equivalent), true) + cmap.set(equivalent, 2) + t.equal(cmap.size, 1) + t.equal(cmap.get(key), 2) + t.strictSame([...cmap.keys()], [equivalent]) + t.equal(cmap.delete(key), true) + t.equal(cmap.has(equivalent), false) + } + t.end() +}) + +t.test('preserves empty, undefined-valued, and non-string keys', t => { + const object = {} + const symbol = Symbol('key') + for (const key of ['', '\u0000', undefined, null, NaN, 0, object, symbol]) { + const cmap = new CMap([[key, undefined]]) + t.equal(cmap.has(key), true) + t.equal(cmap.get(key), undefined) + t.equal(cmap.delete(key), true) + t.equal(cmap.has(key), false) + } + t.end() +})