From 76e3a74669b01b56447d179780022a6ee52f6123 Mon Sep 17 00:00:00 2001 From: Yusuke Hayashi Date: Wed, 29 Jul 2026 15:31:16 +0900 Subject: [PATCH] fs: apply nocase to literal glob exclude patterns On case-insensitive platforms include patterns match entries regardless of case, but literal (non-magic) exclude patterns were matched case-sensitively because exclude matchers are created with nocaseMagicOnly. Exclusion is a pure string match with no filesystem lookups, so disable nocaseMagicOnly for exclude matchers. Fixes: https://github.com/nodejs/node/issues/58991 Refs: https://github.com/nodejs/node/pull/63446 Signed-off-by: Yusuke Hayashi --- lib/internal/fs/glob.js | 5 ++++- test/parallel/test-fs-glob.mjs | 28 ++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/lib/internal/fs/glob.js b/lib/internal/fs/glob.js index a0e7837db7c3..8f1e4b952772 100644 --- a/lib/internal/fs/glob.js +++ b/lib/internal/fs/glob.js @@ -354,7 +354,10 @@ class Glob { // consistent comparison before instantiating matchers. const matchers = exclude .map((pattern) => resolve(this.#root, pattern)) - .map((pattern) => createMatcher(pattern)); + // Exclude matching is a pure string comparison with no filesystem + // lookups, so unlike include patterns, literal patterns must also + // match case-insensitively on case-insensitive platforms. + .map((pattern) => createMatcher(pattern, { nocaseMagicOnly: false })); this.#isExcluded = (value) => matchers.some((matcher) => matcher.match(value)); this.#results.setup(this.#root, this.#isExcluded); diff --git a/test/parallel/test-fs-glob.mjs b/test/parallel/test-fs-glob.mjs index bd95bce7d0e3..fa9a814b770d 100644 --- a/test/parallel/test-fs-glob.mjs +++ b/test/parallel/test-fs-glob.mjs @@ -669,3 +669,31 @@ describe('globSync - ENOTDIR', function() { } }); }); + +// gh-58991: exclude patterns must apply the same case-sensitivity as include +// patterns. On case-insensitive filesystems include patterns match entries +// regardless of case, so literal exclude patterns must match that behavior. +const skipCaseTests = { skip: !common.isWindows && !common.isMacOS }; +describe('glob - exclude case-insensitive consistency', skipCaseTests, function() { + test('literal exclude ignores case (sync)', () => { + assert.deepStrictEqual( + globSync('a/b', { cwd: fixtureDir, exclude: ['A/B'] }), []); + }); + test('literal exclude matches differently-cased results (sync)', () => { + assert.deepStrictEqual( + globSync('A/b', { cwd: fixtureDir, exclude: ['a/b'] }), []); + }); + test('literal exclude ignores case (async)', async () => { + const promisified = promisify(glob); + assert.deepStrictEqual( + await promisified('a/b', { cwd: fixtureDir, exclude: ['A/B'] }), []); + }); + test('literal exclude ignores case (promise)', async () => { + const actual = []; + for await (const entry of asyncGlob( + 'a/b', { cwd: fixtureDir, exclude: ['A/B'] })) { + actual.push(entry); + } + assert.deepStrictEqual(actual, []); + }); +});