@@ -414,6 +414,58 @@ function normalizePathEntryForComparison(entry: string, platform: NodeJS.Platfor
414414 return platform === "win32" ? normalized . toLowerCase ( ) : normalized ;
415415}
416416
417+ function sanitizePathEntry ( entry : string , platform : NodeJS . Platform ) : string {
418+ return platform === "win32" ? entry . replaceAll ( '"' , "" ) : entry ;
419+ }
420+
421+ /**
422+ * Splits a PATH value, honouring Windows quoting. A quoted entry may itself
423+ * contain the delimiter — `C:\\bin;"C:\\my;dir"` is two directories, not three —
424+ * so splitting on the raw delimiter first would tear it in half and leave a
425+ * relative `dir` entry resolved against the child process's cwd.
426+ */
427+ function splitPathValue ( value : string , delimiter : string , platform : NodeJS . Platform ) : string [ ] {
428+ if ( platform !== "win32" ) return value . split ( delimiter ) ;
429+ // Unbalanced quotes are stray characters, not quoting: honouring them would
430+ // swallow every later entry into one. Fall back to a plain split there.
431+ if ( ( value . match ( / " / g) ?. length ?? 0 ) % 2 !== 0 ) return value . split ( delimiter ) ;
432+ const entries : string [ ] = [ ] ;
433+ let current = "" ;
434+ let quoted = false ;
435+ for ( const char of value ) {
436+ if ( char === '"' ) {
437+ quoted = ! quoted ;
438+ current += char ;
439+ continue ;
440+ }
441+ if ( char === delimiter && ! quoted ) {
442+ entries . push ( current ) ;
443+ current = "" ;
444+ continue ;
445+ }
446+ current += char ;
447+ }
448+ entries . push ( current ) ;
449+ return entries ;
450+ }
451+
452+ /**
453+ * Re-quotes an entry containing the delimiter. Stripping its quotes and joining
454+ * would hand consumers a value they split back into the wrong directories.
455+ */
456+ function quotePathEntryIfNeeded (
457+ entry : string ,
458+ delimiter : string ,
459+ platform : NodeJS . Platform ,
460+ ) : string {
461+ return platform === "win32" && entry . includes ( delimiter ) ? `"${ entry } "` : entry ;
462+ }
463+
464+ /** A bare drive letter is drive-relative, so it would add the child's cwd to the search. */
465+ function isUsablePathEntry ( entry : string , platform : NodeJS . Platform ) : boolean {
466+ return platform !== "win32" || ! / ^ [ A - Z a - z ] : $ / . test ( entry ) ;
467+ }
468+
417469export function mergePathValues (
418470 preferredPath : string | undefined ,
419471 inheritedPath : string | undefined ,
@@ -426,19 +478,21 @@ export function mergePathValues(
426478 for ( const rawValue of [ preferredPath , inheritedPath ] ) {
427479 if ( ! rawValue ) continue ;
428480
429- for ( const entry of rawValue . split ( delimiter ) ) {
430- const trimmed = entry . trim ( ) ;
431- if ( trimmed . length === 0 ) continue ;
481+ for ( const entry of splitPathValue ( rawValue , delimiter , platform ) ) {
482+ const sanitized = sanitizePathEntry ( entry . trim ( ) , platform ) ;
483+ if ( sanitized . length === 0 || ! isUsablePathEntry ( sanitized , platform ) ) continue ;
432484
433- const normalized = normalizePathEntryForComparison ( trimmed , platform ) ;
485+ const normalized = normalizePathEntryForComparison ( sanitized , platform ) ;
434486 if ( normalized . length === 0 || seen . has ( normalized ) ) continue ;
435487
436488 seen . add ( normalized ) ;
437- merged . push ( trimmed ) ;
489+ merged . push ( sanitized ) ;
438490 }
439491 }
440492
441- return merged . length > 0 ? merged . join ( delimiter ) : undefined ;
493+ return merged . length > 0
494+ ? merged . map ( ( entry ) => quotePathEntryIfNeeded ( entry , delimiter , platform ) ) . join ( delimiter )
495+ : undefined ;
442496}
443497
444498function readEnvPath ( env : NodeJS . ProcessEnv ) : string | undefined {
0 commit comments