From 6c85e33197ba062da7de29d96959b1494ef97bb0 Mon Sep 17 00:00:00 2001 From: OrcaWin Date: Mon, 7 Sep 2026 23:27:37 -0700 Subject: [PATCH] perf: lazily index case-insensitive Windows environment keys (#19483) * perf: lazily index case-insensitive Windows environment keys * test(windows): pin env expansion fallback against the per-miss lookup oracle Adds zero-enumeration, first-case-variant-wins, prototype-chain and 4,000-case randomized differential coverage, and groups the new cases under their describe. --------- Co-authored-by: m4air Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com> --- .../windows-environment-expansion.test.ts | 80 +++++++++++++++++++ src/shared/windows-environment-expansion.ts | 31 +++---- 2 files changed, 97 insertions(+), 14 deletions(-) diff --git a/src/shared/windows-environment-expansion.test.ts b/src/shared/windows-environment-expansion.test.ts index 35268e60790..3b6f5fc27a8 100644 --- a/src/shared/windows-environment-expansion.test.ts +++ b/src/shared/windows-environment-expansion.test.ts @@ -4,6 +4,33 @@ import { expandWindowsPathEnvironmentVariables } from './windows-environment-expansion' +/** The per-miss `Object.keys().find()` form the lazy index replaced; the differential oracle. */ +function referenceExpand(value: string, env: Readonly>): string { + return value.replace(/%([^%]+)%/g, (match, name: string) => { + const exactValue = env[name] + if (typeof exactValue === 'string') { + return exactValue + } + const key = Object.keys(env).find((candidate) => candidate.toLowerCase() === name.toLowerCase()) + const fallback = key ? env[key] : undefined + return typeof fallback === 'string' ? fallback : match + }) +} + +function countingEnv(source: Record): { + env: Record + enumerations: () => number +} { + let enumerations = 0 + const env = new Proxy(source, { + ownKeys(target) { + enumerations += 1 + return Reflect.ownKeys(target) + } + }) + return { env, enumerations: () => enumerations } +} + describe('expandWindowsEnvironmentVariables', () => { it('expands names case-insensitively and preserves unknown variables', () => { expect( @@ -18,6 +45,59 @@ describe('expandWindowsEnvironmentVariables', () => { 'beforeafter' ) }) + + it('enumerates fallback keys once for a PATH containing repeated mixed-case variables', () => { + const { env, enumerations } = countingEnv({ ROOT: 'C:\\root', OTHER: 'unused' }) + const value = Array.from({ length: 1000 }, () => '%root%').join(';') + expect(expandWindowsEnvironmentVariables(value, env)).toBe( + Array(1000).fill('C:\\root').join(';') + ) + expect(enumerations()).toBe(1) + }) + + it('never enumerates when every name resolves by exact case', () => { + const { env, enumerations } = countingEnv({ ROOT: 'C:\\root', EMPTY: '' }) + expect(expandWindowsEnvironmentVariables('%ROOT%;%EMPTY%;plain', env)).toBe('C:\\root;;plain') + expect(enumerations()).toBe(0) + }) + + it('preserves exact casing authority and first fallback keys including undefined values', () => { + const env = { Root: undefined, ROOT: 'upper', root: 'lower' } + expect(expandWindowsEnvironmentVariables('%ROOT%:%root%:%rOoT%', env)).toBe( + 'upper:lower:%rOoT%' + ) + }) + + // Why: the fallback index keeps the first insertion-order key per lowercase name, so a later + // duplicate casing never wins even when the first one is the shadowed value. + it('resolves an inexact name to the first case variant, not the last', () => { + const env = { Path: 'first', PATH: 'second', pAtH: 'third' } + expect(expandWindowsEnvironmentVariables('%PaTh%', env)).toBe('first') + }) + + it('does not resolve names from the prototype chain', () => { + for (const name of ['__proto__', 'constructor', 'toString', 'hasOwnProperty']) { + expect(expandWindowsEnvironmentVariables(`%${name}%`, { ROOT: 'x' })).toBe(`%${name}%`) + } + }) + + it('matches the per-miss lookup oracle across randomized mixed-case environments', () => { + const names = ['PATH', 'Path', 'path', 'pAtH', 'ROOT', 'root', 'Temp', 'TEMP', 'MISSING'] + const values = ['a', '', 'C:\\x', undefined] + let seed = 0x9e3779b9 + const next = (): number => (seed = (seed * 1103515245 + 12345) & 0x7fffffff) / 0x7fffffff + for (let index = 0; index < 4000; index += 1) { + const env: Record = {} + for (let entry = 0; entry < Math.floor(next() * 5); entry += 1) { + env[names[Math.floor(next() * names.length)]!] = values[Math.floor(next() * values.length)] + } + const value = Array.from( + { length: Math.floor(next() * 6) }, + () => `%${names[Math.floor(next() * names.length)]}%` + ).join(';') + expect(expandWindowsEnvironmentVariables(value, env)).toBe(referenceExpand(value, env)) + } + }) }) describe('expandWindowsPathEnvironmentVariables', () => { diff --git a/src/shared/windows-environment-expansion.ts b/src/shared/windows-environment-expansion.ts index 27e6eb1124c..62e07a092e0 100644 --- a/src/shared/windows-environment-expansion.ts +++ b/src/shared/windows-environment-expansion.ts @@ -1,22 +1,25 @@ -function getEnvironmentVariable( - env: Readonly>, - name: string -): string | undefined { - const exactValue = env[name] - if (typeof exactValue === 'string') { - return exactValue - } - const key = Object.keys(env).find((candidate) => candidate.toLowerCase() === name.toLowerCase()) - const value = key ? env[key] : undefined - return typeof value === 'string' ? value : undefined -} - export function expandWindowsEnvironmentVariables( value: string, env: Readonly> ): string { + let keysByLowerName: Map | undefined return value.replace(/%([^%]+)%/g, (match, name: string) => { - return getEnvironmentVariable(env, name) ?? match + const exact = env[name] + if (typeof exact === 'string') { + return exact + } + if (!keysByLowerName) { + keysByLowerName = new Map() + for (const key of Object.keys(env)) { + const lower = key.toLowerCase() + if (!keysByLowerName.has(lower)) { + keysByLowerName.set(lower, key) + } + } + } + const key = keysByLowerName.get(name.toLowerCase()) + const replacement = key ? env[key] : undefined + return typeof replacement === 'string' ? replacement : match }) }