Skip to content
File

Blob: src/node/internal/internal_path.ts

typescript1515 lines
1// Copyright (c) 2026 Cloudflare, Inc.
2// Licensed under the Apache 2.0 license found in the LICENSE file or at:
3// https://opensource.org/licenses/Apache-2.0
4 
5// Copyright Joyent, Inc. and other Node contributors.
6//
7// Permission is hereby granted, free of charge, to any person obtaining a
8// copy of this software and associated documentation files (the
9// "Software"), to deal in the Software without restriction, including
10// without limitation the rights to use, copy, modify, merge, publish,
11// distribute, sublicense, and/or sell copies of the Software, and to permit
12// persons to whom the Software is furnished to do so, subject to the
13// following conditions:
14//
15// The above copyright notice and this permission notice shall be included
16// in all copies or substantial portions of the Software.
17//
18// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
19// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
20// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
21// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
22// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
23// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
24// USE OR OTHER DEALINGS IN THE SOFTWARE.
25 
26import {
27 CHAR_DOT,
28 CHAR_FORWARD_SLASH,
29 CHAR_COLON,
30 CHAR_UPPERCASE_A,
31 CHAR_UPPERCASE_Z,
32 CHAR_LOWERCASE_A,
33 CHAR_LOWERCASE_Z,
34 CHAR_BACKWARD_SLASH,
35 CHAR_QUESTION_MARK,
36} from 'node-internal:constants';
37 
38import { validateObject, validateString } from 'node-internal:validators';
39 
40function isPathSeparator(code: number): boolean {
41 return code === CHAR_FORWARD_SLASH || code === CHAR_BACKWARD_SLASH;
42}
43 
44function isPosixPathSeparator(code: number): boolean {
45 return code === CHAR_FORWARD_SLASH;
46}
47 
48function isWindowsDeviceRoot(code: number): boolean {
49 return (
50 (code >= CHAR_UPPERCASE_A && code <= CHAR_UPPERCASE_Z) ||
51 (code >= CHAR_LOWERCASE_A && code <= CHAR_LOWERCASE_Z)
52 );
53}
54 
55// Resolves . and .. elements in a path with directory names
56function normalizeString(
57 path: string,
58 allowAboveRoot: boolean,
59 separator: string,
60 isPathSeparator: (code: number) => boolean
61): string {
62 let res = '';
63 let lastSegmentLength = 0;
64 let lastSlash = -1;
65 let dots = 0;
66 let code = 0;
67 for (let i = 0; i <= path.length; ++i) {
68 if (i < path.length) code = path.charCodeAt(i);
69 else if (isPathSeparator(code)) break;
70 else code = CHAR_FORWARD_SLASH;
71 
72 if (isPathSeparator(code)) {
73 if (lastSlash === i - 1 || dots === 1) {
74 // NOOP
75 } else if (dots === 2) {
76 if (
77 res.length < 2 ||
78 lastSegmentLength !== 2 ||
79 res.charCodeAt(res.length - 1) !== CHAR_DOT ||
80 res.charCodeAt(res.length - 2) !== CHAR_DOT
81 ) {
82 if (res.length > 2) {
83 const lastSlashIndex = res.lastIndexOf(separator);
84 if (lastSlashIndex === -1) {
85 res = '';
86 lastSegmentLength = 0;
87 } else {
88 res = res.slice(0, lastSlashIndex);
89 lastSegmentLength = res.length - 1 - res.lastIndexOf(separator);
90 }
91 lastSlash = i;
92 dots = 0;
93 continue;
94 } else if (res.length !== 0) {
95 res = '';
96 lastSegmentLength = 0;
97 lastSlash = i;
98 dots = 0;
99 continue;
100 }
101 }
102 if (allowAboveRoot) {
103 res += res.length > 0 ? `${separator}..` : '..';
104 lastSegmentLength = 2;
105 }
106 } else {
107 if (res.length > 0)
108 res += `${separator}${path.slice(lastSlash + 1, i)}`;
109 else res = path.slice(lastSlash + 1, i);
110 lastSegmentLength = i - lastSlash - 1;
111 }
112 lastSlash = i;
113 dots = 0;
114 } else if (code === CHAR_DOT && dots !== -1) {
115 ++dots;
116 } else {
117 dots = -1;
118 }
119 }
120 return res;
121}
122 
123function formatExt(ext: string): string {
124 return ext ? `${ext[0] === '.' ? '' : '.'}${ext}` : '';
125}
126 
127/**
128 * @param {string} sep
129 * @param {{
130 * dir?: string;
131 * root?: string;
132 * base?: string;
133 * name?: string;
134 * ext?: string;
135 * }} pathObject
136 * @returns {string}
137 */
138 
139type PathObject = {
140 dir?: string;
141 root?: string;
142 base?: string;
143 name?: string;
144 ext?: string;
145};
146 
147function _format(sep: string, pathObject: PathObject): string {
148 validateObject(pathObject, 'pathObject');
149 const dir = pathObject.dir || pathObject.root;
150 const base =
151 pathObject.base ||
152 `${pathObject.name || ''}${formatExt(pathObject.ext as string)}`;
153 if (!dir) {
154 return base;
155 }
156 return dir === pathObject.root ? `${dir}${base}` : `${dir}${sep}${base}`;
157}
158 
159const win32 = {
160 resolve(...args: string[]): string {
161 let resolvedDevice = '';
162 let resolvedTail = '';
163 let resolvedAbsolute = false;
164 
165 for (let i = args.length - 1; i >= -1; i--) {
166 let path;
167 if (i >= 0) {
168 path = args[i];
169 validateString(path, `paths[${i}]`);
170 
171 // Skip empty entries
172 if (path.length === 0) {
173 continue;
174 }
175 } else if (resolvedDevice.length === 0) {
176 path = '/';
177 } else {
178 // Windows has the concept of drive-specific current working
179 // directories. If we've resolved a drive letter but not yet an
180 // absolute path, get cwd for that drive, or the process cwd if
181 // the drive cwd is not available. We're sure the device is not
182 // a UNC path at this points, because UNC paths are always absolute.
183 path = '/';
184 
185 // Verify that a cwd was found and that it actually points
186 // to our drive. If not, default to the drive's root.
187 if (
188 path.slice(0, 2).toLowerCase() !== resolvedDevice.toLowerCase() &&
189 path.charCodeAt(2) === CHAR_BACKWARD_SLASH
190 ) {
191 path = `${resolvedDevice}\\`;
192 }
193 }
194 
195 const len = path.length;
196 let rootEnd = 0;
197 let device = '';
198 let isAbsolute = false;
199 const code = path.charCodeAt(0);
200 
201 // Try to match a root
202 if (len === 1) {
203 if (isPathSeparator(code)) {
204 // `path` contains just a path separator
205 rootEnd = 1;
206 isAbsolute = true;
207 }
208 } else if (isPathSeparator(code)) {
209 // Possible UNC root
210 
211 // If we started with a separator, we know we at least have an
212 // absolute path of some kind (UNC or otherwise)
213 isAbsolute = true;
214 
215 if (isPathSeparator(path.charCodeAt(1))) {
216 // Matched double path separator at beginning
217 let j = 2;
218 let last = j;
219 // Match 1 or more non-path separators
220 while (j < len && !isPathSeparator(path.charCodeAt(j))) {
221 j++;
222 }
223 if (j < len && j !== last) {
224 const firstPart = path.slice(last, j);
225 // Matched!
226 last = j;
227 // Match 1 or more path separators
228 while (j < len && isPathSeparator(path.charCodeAt(j))) {
229 j++;
230 }
231 if (j < len && j !== last) {
232 // Matched!
233 last = j;
234 // Match 1 or more non-path separators
235 while (j < len && !isPathSeparator(path.charCodeAt(j))) {
236 j++;
237 }
238 if (j === len || j !== last) {
239 // We matched a UNC root
240 device = `\\\\${firstPart}\\${path.slice(last, j)}`;
241 rootEnd = j;
242 }
243 }
244 }
245 } else {
246 rootEnd = 1;
247 }
248 } else if (
249 isWindowsDeviceRoot(code) &&
250 path.charCodeAt(1) === CHAR_COLON
251 ) {
252 // Possible device root
253 device = path.slice(0, 2);
254 rootEnd = 2;
255 if (len > 2 && isPathSeparator(path.charCodeAt(2))) {
256 // Treat separator following drive name as an absolute path
257 // indicator
258 isAbsolute = true;
259 rootEnd = 3;
260 }
261 }
262 
263 if (device.length > 0) {
264 if (resolvedDevice.length > 0) {
265 if (device.toLowerCase() !== resolvedDevice.toLowerCase())
266 // This path points to another device so it is not applicable
267 continue;
268 } else {
269 resolvedDevice = device;
270 }
271 }
272 
273 if (resolvedAbsolute) {
274 if (resolvedDevice.length > 0) break;
275 } else {
276 resolvedTail = `${path.slice(rootEnd)}\\${resolvedTail}`;
277 resolvedAbsolute = isAbsolute;
278 if (isAbsolute && resolvedDevice.length > 0) {
279 break;
280 }
281 }
282 }
283 
284 // At this point the path should be resolved to a full absolute path,
285 // but handle relative paths to be safe (might happen when process.cwd()
286 // fails)
287 
288 // Normalize the tail path
289 resolvedTail = normalizeString(
290 resolvedTail,
291 !resolvedAbsolute,
292 '\\',
293 isPathSeparator
294 );
295 
296 return resolvedAbsolute
297 ? `${resolvedDevice}\\${resolvedTail}`
298 : `${resolvedDevice}${resolvedTail}` || '.';
299 },
300 
301 normalize(path: string): string {
302 validateString(path, 'path');
303 const len = path.length;
304 if (len === 0) return '.';
305 let rootEnd = 0;
306 let device;
307 let isAbsolute = false;
308 const code = path.charCodeAt(0);
309 
310 // Try to match a root
311 if (len === 1) {
312 // `path` contains just a single char, exit early to avoid
313 // unnecessary work
314 return isPosixPathSeparator(code) ? '\\' : path;
315 }
316 if (isPathSeparator(code)) {
317 // Possible UNC root
318 
319 // If we started with a separator, we know we at least have an absolute
320 // path of some kind (UNC or otherwise)
321 isAbsolute = true;
322 
323 if (isPathSeparator(path.charCodeAt(1))) {
324 // Matched double path separator at beginning
325 let j = 2;
326 let last = j;
327 // Match 1 or more non-path separators
328 while (j < len && !isPathSeparator(path.charCodeAt(j))) {
329 j++;
330 }
331 if (j < len && j !== last) {
332 const firstPart = path.slice(last, j);
333 // Matched!
334 last = j;
335 // Match 1 or more path separators
336 while (j < len && isPathSeparator(path.charCodeAt(j))) {
337 j++;
338 }
339 if (j < len && j !== last) {
340 // Matched!
341 last = j;
342 // Match 1 or more non-path separators
343 while (j < len && !isPathSeparator(path.charCodeAt(j))) {
344 j++;
345 }
346 if (j === len) {
347 // We matched a UNC root only
348 // Return the normalized version of the UNC root since there
349 // is nothing left to process
350 return `\\\\${firstPart}\\${path.slice(last)}\\`;
351 }
352 if (j !== last) {
353 // We matched a UNC root with leftovers
354 device = `\\\\${firstPart}\\${path.slice(last, j)}`;
355 rootEnd = j;
356 }
357 }
358 }
359 } else {
360 rootEnd = 1;
361 }
362 } else if (isWindowsDeviceRoot(code) && path.charCodeAt(1) === CHAR_COLON) {
363 // Possible device root
364 device = path.slice(0, 2);
365 rootEnd = 2;
366 if (len > 2 && isPathSeparator(path.charCodeAt(2))) {
367 // Treat separator following drive name as an absolute path
368 // indicator
369 isAbsolute = true;
370 rootEnd = 3;
371 }
372 }
373 
374 let tail =
375 rootEnd < len
376 ? normalizeString(
377 path.slice(rootEnd),
378 !isAbsolute,
379 '\\',
380 isPathSeparator
381 )
382 : '';
383 if (tail.length === 0 && !isAbsolute) tail = '.';
384 if (tail.length > 0 && isPathSeparator(path.charCodeAt(len - 1)))
385 tail += '\\';
386 if (device === undefined) {
387 return isAbsolute ? `\\${tail}` : tail;
388 }
389 return isAbsolute ? `${device}\\${tail}` : `${device}${tail}`;
390 },
391 
392 isAbsolute(path: string): boolean {
393 validateString(path, 'path');
394 const len = path.length;
395 if (len === 0) return false;
396 
397 const code = path.charCodeAt(0);
398 return (
399 isPathSeparator(code) ||
400 // Possible device root
401 (len > 2 &&
402 isWindowsDeviceRoot(code) &&
403 path.charCodeAt(1) === CHAR_COLON &&
404 isPathSeparator(path.charCodeAt(2)))
405 );
406 },
407 
408 join(...args: string[]): string {
409 if (args.length === 0) return '.';
410 
411 let joined: string | undefined;
412 let firstPart: string | undefined;
413 for (let i = 0; i < args.length; ++i) {
414 const arg = args[i];
415 validateString(arg, 'path');
416 if (arg.length > 0) {
417 if (joined === undefined) {
418 joined = arg;
419 firstPart = arg;
420 } else joined += `\\${arg}`;
421 }
422 }
423 
424 if (joined === undefined || firstPart === undefined) return '.';
425 
426 // Make sure that the joined path doesn't start with two slashes, because
427 // normalize() will mistake it for a UNC path then.
428 //
429 // This step is skipped when it is very clear that the user actually
430 // intended to point at a UNC path. This is assumed when the first
431 // non-empty string arguments starts with exactly two slashes followed by
432 // at least one more non-slash character.
433 //
434 // Note that for normalize() to treat a path as a UNC path it needs to
435 // have at least 2 components, so we don't filter for that here.
436 // This means that the user can use join to construct UNC paths from
437 // a server name and a share name; for example:
438 // path.join('//server', 'share') -> '\\\\server\\share\\')
439 let needsReplace = true;
440 let slashCount = 0;
441 if (isPathSeparator(firstPart.charCodeAt(0))) {
442 ++slashCount;
443 const firstLen = firstPart.length;
444 if (firstLen > 1 && isPathSeparator(firstPart.charCodeAt(1))) {
445 ++slashCount;
446 if (firstLen > 2) {
447 if (isPathSeparator(firstPart.charCodeAt(2))) ++slashCount;
448 else {
449 // We matched a UNC path in the first part
450 needsReplace = false;
451 }
452 }
453 }
454 }
455 if (needsReplace) {
456 // Find any more consecutive slashes we need to replace
457 while (
458 slashCount < joined.length &&
459 isPathSeparator(joined.charCodeAt(slashCount))
460 ) {
461 slashCount++;
462 }
463 
464 // Replace the slashes if needed
465 if (slashCount >= 2) joined = `\\${joined.slice(slashCount)}`;
466 }
467 
468 return win32.normalize(joined);
469 },
470 
471 relative(from: string, to: string): string {
472 validateString(from, 'from');
473 validateString(to, 'to');
474 
475 if (from === to) return '';
476 
477 const fromOrig = win32.resolve(from);
478 const toOrig = win32.resolve(to);
479 
480 if (fromOrig === toOrig) return '';
481 
482 from = fromOrig.toLowerCase();
483 to = toOrig.toLowerCase();
484 
485 if (from === to) return '';
486 
487 if (fromOrig.length !== from.length || toOrig.length !== to.length) {
488 const fromSplit = fromOrig.split('\\');
489 const toSplit = toOrig.split('\\');
490 if (fromSplit[fromSplit.length - 1] === '') {
491 fromSplit.pop();
492 }
493 if (toSplit[toSplit.length - 1] === '') {
494 toSplit.pop();
495 }
496 
497 const fromLen = fromSplit.length;
498 const toLen = toSplit.length;
499 const length = fromLen < toLen ? fromLen : toLen;
500 
501 let i;
502 for (i = 0; i < length; i++) {
503 if (fromSplit[i]?.toLowerCase() !== toSplit[i]?.toLowerCase()) {
504 break;
505 }
506 }
507 
508 if (i === 0) {
509 return toOrig;
510 } else if (i === length) {
511 if (toLen > length) {
512 return toSplit.slice(i).join('\\');
513 }
514 if (fromLen > length) {
515 return '..\\'.repeat(fromLen - 1 - i) + '..';
516 }
517 return '';
518 }
519 
520 return '..\\'.repeat(fromLen - i) + toSplit.slice(i).join('\\');
521 }
522 
523 // Trim any leading backslashes
524 let fromStart = 0;
525 while (
526 fromStart < from.length &&
527 from.charCodeAt(fromStart) === CHAR_BACKWARD_SLASH
528 ) {
529 fromStart++;
530 }
531 // Trim trailing backslashes (applicable to UNC paths only)
532 let fromEnd = from.length;
533 while (
534 fromEnd - 1 > fromStart &&
535 from.charCodeAt(fromEnd - 1) === CHAR_BACKWARD_SLASH
536 ) {
537 fromEnd--;
538 }
539 const fromLen = fromEnd - fromStart;
540 
541 // Trim any leading backslashes
542 let toStart = 0;
543 while (
544 toStart < to.length &&
545 to.charCodeAt(toStart) === CHAR_BACKWARD_SLASH
546 ) {
547 toStart++;
548 }
549 // Trim trailing backslashes (applicable to UNC paths only)
550 let toEnd = to.length;
551 while (
552 toEnd - 1 > toStart &&
553 to.charCodeAt(toEnd - 1) === CHAR_BACKWARD_SLASH
554 ) {
555 toEnd--;
556 }
557 const toLen = toEnd - toStart;
558 
559 // Compare paths to find the longest common path from root
560 const length = fromLen < toLen ? fromLen : toLen;
561 let lastCommonSep = -1;
562 let i = 0;
563 for (; i < length; i++) {
564 const fromCode = from.charCodeAt(fromStart + i);
565 if (fromCode !== to.charCodeAt(toStart + i)) break;
566 else if (fromCode === CHAR_BACKWARD_SLASH) lastCommonSep = i;
567 }
568 
569 // We found a mismatch before the first common path separator was seen, so
570 // return the original `to`.
571 if (i !== length) {
572 if (lastCommonSep === -1) return toOrig;
573 } else {
574 if (toLen > length) {
575 if (to.charCodeAt(toStart + i) === CHAR_BACKWARD_SLASH) {
576 // We get here if `from` is the exact base path for `to`.
577 // For example: from='C:\\foo\\bar'; to='C:\\foo\\bar\\baz'
578 return toOrig.slice(toStart + i + 1);
579 }
580 if (i === 2) {
581 // We get here if `from` is the device root.
582 // For example: from='C:\\'; to='C:\\foo'
583 return toOrig.slice(toStart + i);
584 }
585 }
586 if (fromLen > length) {
587 if (from.charCodeAt(fromStart + i) === CHAR_BACKWARD_SLASH) {
588 // We get here if `to` is the exact base path for `from`.
589 // For example: from='C:\\foo\\bar'; to='C:\\foo'
590 lastCommonSep = i;
591 } else if (i === 2) {
592 // We get here if `to` is the device root.
593 // For example: from='C:\\foo\\bar'; to='C:\\'
594 lastCommonSep = 3;
595 }
596 }
597 if (lastCommonSep === -1) lastCommonSep = 0;
598 }
599 
600 let out = '';
601 // Generate the relative path based on the path difference between `to` and
602 // `from`
603 for (i = fromStart + lastCommonSep + 1; i <= fromEnd; ++i) {
604 if (i === fromEnd || from.charCodeAt(i) === CHAR_BACKWARD_SLASH) {
605 out += out.length === 0 ? '..' : '\\..';
606 }
607 }
608 
609 toStart += lastCommonSep;
610 
611 // Lastly, append the rest of the destination (`to`) path that comes after
612 // the common path parts
613 if (out.length > 0) return `${out}${toOrig.slice(toStart, toEnd)}`;
614 
615 if (toOrig.charCodeAt(toStart) === CHAR_BACKWARD_SLASH) ++toStart;
616 return toOrig.slice(toStart, toEnd);
617 },
618 
619 toNamespacedPath(path: string): string {
620 // Note: this will *probably* throw somewhere.
621 if (typeof path !== 'string' || path.length === 0) return path;
622 
623 const resolvedPath = win32.resolve(path);
624 
625 if (resolvedPath.length <= 2) return path;
626 
627 if (resolvedPath.charCodeAt(0) === CHAR_BACKWARD_SLASH) {
628 // Possible UNC root
629 if (resolvedPath.charCodeAt(1) === CHAR_BACKWARD_SLASH) {
630 const code = resolvedPath.charCodeAt(2);
631 if (code !== CHAR_QUESTION_MARK && code !== CHAR_DOT) {
632 // Matched non-long UNC root, convert the path to a long UNC path
633 return `\\\\?\\UNC\\${resolvedPath.slice(2)}`;
634 }
635 }
636 } else if (
637 isWindowsDeviceRoot(resolvedPath.charCodeAt(0)) &&
638 resolvedPath.charCodeAt(1) === CHAR_COLON &&
639 resolvedPath.charCodeAt(2) === CHAR_BACKWARD_SLASH
640 ) {
641 // Matched device root, convert the path to a long UNC path
642 return `\\\\?\\${resolvedPath}`;
643 }
644 
645 return resolvedPath;
646 },
647 
648 dirname(path: string): string {
649 validateString(path, 'path');
650 const len = path.length;
651 if (len === 0) return '.';
652 let rootEnd = -1;
653 let offset = 0;
654 const code = path.charCodeAt(0);
655 
656 if (len === 1) {
657 // `path` contains just a path separator, exit early to avoid
658 // unnecessary work or a dot.
659 return isPathSeparator(code) ? path : '.';
660 }
661 
662 // Try to match a root
663 if (isPathSeparator(code)) {
664 // Possible UNC root
665 
666 rootEnd = offset = 1;
667 
668 if (isPathSeparator(path.charCodeAt(1))) {
669 // Matched double path separator at beginning
670 let j = 2;
671 let last = j;
672 // Match 1 or more non-path separators
673 while (j < len && !isPathSeparator(path.charCodeAt(j))) {
674 j++;
675 }
676 if (j < len && j !== last) {
677 // Matched!
678 last = j;
679 // Match 1 or more path separators
680 while (j < len && isPathSeparator(path.charCodeAt(j))) {
681 j++;
682 }
683 if (j < len && j !== last) {
684 // Matched!
685 last = j;
686 // Match 1 or more non-path separators
687 while (j < len && !isPathSeparator(path.charCodeAt(j))) {
688 j++;
689 }
690 if (j === len) {
691 // We matched a UNC root only
692 return path;
693 }
694 if (j !== last) {
695 // We matched a UNC root with leftovers
696 
697 // Offset by 1 to include the separator after the UNC root to
698 // treat it as a "normal root" on top of a (UNC) root
699 rootEnd = offset = j + 1;
700 }
701 }
702 }
703 }
704 // Possible device root
705 } else if (isWindowsDeviceRoot(code) && path.charCodeAt(1) === CHAR_COLON) {
706 rootEnd = len > 2 && isPathSeparator(path.charCodeAt(2)) ? 3 : 2;
707 offset = rootEnd;
708 }
709 
710 let end = -1;
711 let matchedSlash = true;
712 for (let i = len - 1; i >= offset; --i) {
713 if (isPathSeparator(path.charCodeAt(i))) {
714 if (!matchedSlash) {
715 end = i;
716 break;
717 }
718 } else {
719 // We saw the first non-path separator
720 matchedSlash = false;
721 }
722 }
723 
724 if (end === -1) {
725 if (rootEnd === -1) return '.';
726 
727 end = rootEnd;
728 }
729 return path.slice(0, end);
730 },
731 
732 basename(path: string, suffix?: string): string {
733 if (suffix !== undefined) validateString(suffix, 'suffix');
734 validateString(path, 'path');
735 let start = 0;
736 let end = -1;
737 let matchedSlash = true;
738 
739 // Check for a drive letter prefix so as not to mistake the following
740 // path separator as an extra separator at the end of the path that can be
741 // disregarded
742 if (
743 path.length >= 2 &&
744 isWindowsDeviceRoot(path.charCodeAt(0)) &&
745 path.charCodeAt(1) === CHAR_COLON
746 ) {
747 start = 2;
748 }
749 
750 if (
751 suffix !== undefined &&
752 suffix.length > 0 &&
753 suffix.length <= path.length
754 ) {
755 if (suffix === path) return '';
756 let extIdx = suffix.length - 1;
757 let firstNonSlashEnd = -1;
758 for (let i = path.length - 1; i >= start; --i) {
759 const code = path.charCodeAt(i);
760 if (isPathSeparator(code)) {
761 // If we reached a path separator that was not part of a set of path
762 // separators at the end of the string, stop now
763 if (!matchedSlash) {
764 start = i + 1;
765 break;
766 }
767 } else {
768 if (firstNonSlashEnd === -1) {
769 // We saw the first non-path separator, remember this index in case
770 // we need it if the extension ends up not matching
771 matchedSlash = false;
772 firstNonSlashEnd = i + 1;
773 }
774 if (extIdx >= 0) {
775 // Try to match the explicit extension
776 if (code === suffix.charCodeAt(extIdx)) {
777 if (--extIdx === -1) {
778 // We matched the extension, so mark this as the end of our path
779 // component
780 end = i;
781 }
782 } else {
783 // Extension does not match, so our result is the entire path
784 // component
785 extIdx = -1;
786 end = firstNonSlashEnd;
787 }
788 }
789 }
790 }
791 
792 if (start === end) end = firstNonSlashEnd;
793 else if (end === -1) end = path.length;
794 return path.slice(start, end);
795 }
796 for (let i = path.length - 1; i >= start; --i) {
797 if (isPathSeparator(path.charCodeAt(i))) {
798 // If we reached a path separator that was not part of a set of path
799 // separators at the end of the string, stop now
800 if (!matchedSlash) {
801 start = i + 1;
802 break;
803 }
804 } else if (end === -1) {
805 // We saw the first non-path separator, mark this as the end of our
806 // path component
807 matchedSlash = false;
808 end = i + 1;
809 }
810 }
811 
812 if (end === -1) return '';
813 return path.slice(start, end);
814 },
815 
816 extname(path: string): string {
817 validateString(path, 'path');
818 let start = 0;
819 let startDot = -1;
820 let startPart = 0;
821 let end = -1;
822 let matchedSlash = true;
823 // Track the state of characters (if any) we see before our first dot and
824 // after any path separator we find
825 let preDotState = 0;
826 
827 // Check for a drive letter prefix so as not to mistake the following
828 // path separator as an extra separator at the end of the path that can be
829 // disregarded
830 
831 if (
832 path.length >= 2 &&
833 path.charCodeAt(1) === CHAR_COLON &&
834 isWindowsDeviceRoot(path.charCodeAt(0))
835 ) {
836 start = startPart = 2;
837 }
838 
839 for (let i = path.length - 1; i >= start; --i) {
840 const code = path.charCodeAt(i);
841 if (isPathSeparator(code)) {
842 // If we reached a path separator that was not part of a set of path
843 // separators at the end of the string, stop now
844 if (!matchedSlash) {
845 startPart = i + 1;
846 break;
847 }
848 continue;
849 }
850 if (end === -1) {
851 // We saw the first non-path separator, mark this as the end of our
852 // extension
853 matchedSlash = false;
854 end = i + 1;
855 }
856 if (code === CHAR_DOT) {
857 // If this is our first dot, mark it as the start of our extension
858 if (startDot === -1) startDot = i;
859 else if (preDotState !== 1) preDotState = 1;
860 } else if (startDot !== -1) {
861 // We saw a non-dot and non-path separator before our dot, so we should
862 // have a good chance at having a non-empty extension
863 preDotState = -1;
864 }
865 }
866 
867 if (
868 startDot === -1 ||
869 end === -1 ||
870 // We saw a non-dot character immediately before the dot
871 preDotState === 0 ||
872 // The (right-most) trimmed path component is exactly '..'
873 (preDotState === 1 && startDot === end - 1 && startDot === startPart + 1)
874 ) {
875 return '';
876 }
877 return path.slice(startDot, end);
878 },
879 
880 format(path: PathObject): string {
881 return _format('\\', path);
882 },
883 
884 parse(path: string): PathObject {
885 validateString(path, 'path');
886 
887 const ret = { root: '', dir: '', base: '', ext: '', name: '' };
888 if (path.length === 0) return ret;
889 
890 const len = path.length;
891 let rootEnd = 0;
892 let code = path.charCodeAt(0);
893 
894 if (len === 1) {
895 if (isPathSeparator(code)) {
896 // `path` contains just a path separator, exit early to avoid
897 // unnecessary work
898 ret.root = ret.dir = path;
899 return ret;
900 }
901 ret.base = ret.name = path;
902 return ret;
903 }
904 // Try to match a root
905 if (isPathSeparator(code)) {
906 // Possible UNC root
907 
908 rootEnd = 1;
909 if (isPathSeparator(path.charCodeAt(1))) {
910 // Matched double path separator at beginning
911 let j = 2;
912 let last = j;
913 // Match 1 or more non-path separators
914 while (j < len && !isPathSeparator(path.charCodeAt(j))) {
915 j++;
916 }
917 if (j < len && j !== last) {
918 // Matched!
919 last = j;
920 // Match 1 or more path separators
921 while (j < len && isPathSeparator(path.charCodeAt(j))) {
922 j++;
923 }
924 if (j < len && j !== last) {
925 // Matched!
926 last = j;
927 // Match 1 or more non-path separators
928 while (j < len && !isPathSeparator(path.charCodeAt(j))) {
929 j++;
930 }
931 if (j === len) {
932 // We matched a UNC root only
933 rootEnd = j;
934 } else if (j !== last) {
935 // We matched a UNC root with leftovers
936 rootEnd = j + 1;
937 }
938 }
939 }
940 }
941 } else if (isWindowsDeviceRoot(code) && path.charCodeAt(1) === CHAR_COLON) {
942 // Possible device root
943 if (len <= 2) {
944 // `path` contains just a drive root, exit early to avoid
945 // unnecessary work
946 ret.root = ret.dir = path;
947 return ret;
948 }
949 rootEnd = 2;
950 if (isPathSeparator(path.charCodeAt(2))) {
951 if (len === 3) {
952 // `path` contains just a drive root, exit early to avoid
953 // unnecessary work
954 ret.root = ret.dir = path;
955 return ret;
956 }
957 rootEnd = 3;
958 }
959 }
960 if (rootEnd > 0) ret.root = path.slice(0, rootEnd);
961 
962 let startDot = -1;
963 let startPart = rootEnd;
964 let end = -1;
965 let matchedSlash = true;
966 let i = path.length - 1;
967 
968 // Track the state of characters (if any) we see before our first dot and
969 // after any path separator we find
970 let preDotState = 0;
971 
972 // Get non-dir info
973 for (; i >= rootEnd; --i) {
974 code = path.charCodeAt(i);
975 if (isPathSeparator(code)) {
976 // If we reached a path separator that was not part of a set of path
977 // separators at the end of the string, stop now
978 if (!matchedSlash) {
979 startPart = i + 1;
980 break;
981 }
982 continue;
983 }
984 if (end === -1) {
985 // We saw the first non-path separator, mark this as the end of our
986 // extension
987 matchedSlash = false;
988 end = i + 1;
989 }
990 if (code === CHAR_DOT) {
991 // If this is our first dot, mark it as the start of our extension
992 if (startDot === -1) startDot = i;
993 else if (preDotState !== 1) preDotState = 1;
994 } else if (startDot !== -1) {
995 // We saw a non-dot and non-path separator before our dot, so we should
996 // have a good chance at having a non-empty extension
997 preDotState = -1;
998 }
999 }
1000 
1001 if (end !== -1) {
1002 if (
1003 startDot === -1 ||
1004 // We saw a non-dot character immediately before the dot
1005 preDotState === 0 ||
1006 // The (right-most) trimmed path component is exactly '..'
1007 (preDotState === 1 &&
1008 startDot === end - 1 &&
1009 startDot === startPart + 1)
1010 ) {
1011 ret.base = ret.name = path.slice(startPart, end);
1012 } else {
1013 ret.name = path.slice(startPart, startDot);
1014 ret.base = path.slice(startPart, end);
1015 ret.ext = path.slice(startDot, end);
1016 }
1017 }
1018 
1019 // If the directory is the root, use the entire root as the `dir` including
1020 // the trailing slash if any (`C:\abc` -> `C:\`). Otherwise, strip out the
1021 // trailing slash (`C:\abc\def` -> `C:\abc`).
1022 if (startPart > 0 && startPart !== rootEnd)
1023 ret.dir = path.slice(0, startPart - 1);
1024 else ret.dir = ret.root;
1025 
1026 return ret;
1027 },
1028 
1029 matchesGlob(_path: string, _pattern: string): boolean {
1030 throw new Error('path.win32.matchesGlob() is not implemented.');
1031 },
1032 
1033 sep: '\\',
1034 delimiter: ';',
1035 win32: null as object | null,
1036 posix: null as object | null,
1037};
1038 
1039const posix = {
1040 /**
1041 * path.resolve([from ...], to)
1042 * @param {...string} args
1043 * @returns {string}
1044 */
1045 resolve(...args: string[]): string {
1046 let resolvedPath = '';
1047 let resolvedAbsolute = false;
1048 
1049 for (let i = args.length - 1; i >= 0 && !resolvedAbsolute; i--) {
1050 const path = args[i];
1051 validateString(path, `paths[${i}]`);
1052 
1053 // Skip empty entries
1054 if (path.length === 0) {
1055 continue;
1056 }
1057 
1058 resolvedPath = `${path}/${resolvedPath}`;
1059 resolvedAbsolute = path.charCodeAt(0) === CHAR_FORWARD_SLASH;
1060 }
1061 
1062 if (!resolvedAbsolute) {
1063 const cwd = '/';
1064 resolvedPath = `${cwd}/${resolvedPath}`;
1065 resolvedAbsolute = true;
1066 }
1067 
1068 // At this point the path should be resolved to a full absolute path, but
1069 // handle relative paths to be safe (might happen when process.cwd() fails)
1070 
1071 // Normalize the path
1072 resolvedPath = normalizeString(
1073 resolvedPath,
1074 !resolvedAbsolute,
1075 '/',
1076 isPosixPathSeparator
1077 );
1078 
1079 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
1080 if (resolvedAbsolute) {
1081 return `/${resolvedPath}`;
1082 }
1083 return resolvedPath.length > 0 ? resolvedPath : '.';
1084 },
1085 
1086 /**
1087 * @param {string} path
1088 * @returns {string}
1089 */
1090 normalize(path: string): string {
1091 validateString(path, 'path');
1092 
1093 if (path.length === 0) return '.';
1094 
1095 const isAbsolute = path.charCodeAt(0) === CHAR_FORWARD_SLASH;
1096 const trailingSeparator =
1097 path.charCodeAt(path.length - 1) === CHAR_FORWARD_SLASH;
1098 
1099 // Normalize the path
1100 path = normalizeString(path, !isAbsolute, '/', isPosixPathSeparator);
1101 
1102 if (path.length === 0) {
1103 if (isAbsolute) return '/';
1104 return trailingSeparator ? './' : '.';
1105 }
1106 if (trailingSeparator) path += '/';
1107 
1108 return isAbsolute ? `/${path}` : path;
1109 },
1110 
1111 /**
1112 * @param {string} path
1113 * @returns {boolean}
1114 */
1115 isAbsolute(path: string): boolean {
1116 validateString(path, 'path');
1117 return path.length > 0 && path.charCodeAt(0) === CHAR_FORWARD_SLASH;
1118 },
1119 
1120 /**
1121 * @param {...string} args
1122 * @returns {string}
1123 */
1124 join(...args: string[]): string {
1125 if (args.length === 0) return '.';
1126 let joined;
1127 for (let i = 0; i < args.length; ++i) {
1128 const arg = args[i];
1129 validateString(arg, 'path');
1130 if (arg.length > 0) {
1131 if (joined === undefined) joined = arg;
1132 else joined += `/${arg}`;
1133 }
1134 }
1135 if (joined === undefined) return '.';
1136 return posix.normalize(joined);
1137 },
1138 
1139 /**
1140 * @param {string} from
1141 * @param {string} to
1142 * @returns {string}
1143 */
1144 relative(from: string, to: string): string {
1145 validateString(from, 'from');
1146 validateString(to, 'to');
1147 
1148 if (from === to) return '';
1149 
1150 // Trim leading forward slashes.
1151 from = posix.resolve(from);
1152 to = posix.resolve(to);
1153 
1154 if (from === to) return '';
1155 
1156 // Trim any leading slashes
1157 let fromStart = 0;
1158 while (
1159 fromStart < from.length &&
1160 from.charCodeAt(fromStart) === CHAR_FORWARD_SLASH
1161 ) {
1162 fromStart++;
1163 }
1164 // Trim trailing slashes
1165 let fromEnd = from.length;
1166 while (
1167 fromEnd - 1 > fromStart &&
1168 from.charCodeAt(fromEnd - 1) === CHAR_FORWARD_SLASH
1169 ) {
1170 fromEnd--;
1171 }
1172 const fromLen = fromEnd - fromStart;
1173 
1174 // Trim any leading slashes
1175 let toStart = 0;
1176 while (
1177 toStart < to.length &&
1178 to.charCodeAt(toStart) === CHAR_FORWARD_SLASH
1179 ) {
1180 toStart++;
1181 }
1182 // Trim trailing slashes
1183 let toEnd = to.length;
1184 while (
1185 toEnd - 1 > toStart &&
1186 to.charCodeAt(toEnd - 1) === CHAR_FORWARD_SLASH
1187 ) {
1188 toEnd--;
1189 }
1190 const toLen = toEnd - toStart;
1191 
1192 // Compare paths to find the longest common path from root
1193 const length = fromLen < toLen ? fromLen : toLen;
1194 let lastCommonSep = -1;
1195 let i = 0;
1196 for (; i < length; i++) {
1197 const fromCode = from.charCodeAt(fromStart + i);
1198 if (fromCode !== to.charCodeAt(toStart + i)) break;
1199 else if (fromCode === CHAR_FORWARD_SLASH) lastCommonSep = i;
1200 }
1201 if (i === length) {
1202 if (toLen > length) {
1203 if (to.charCodeAt(toStart + i) === CHAR_FORWARD_SLASH) {
1204 // We get here if `from` is the exact base path for `to`.
1205 // For example: from='/foo/bar'; to='/foo/bar/baz'
1206 return to.slice(toStart + i + 1);
1207 }
1208 if (i === 0) {
1209 // We get here if `from` is the root
1210 // For example: from='/'; to='/foo'
1211 return to.slice(toStart + i);
1212 }
1213 } else if (fromLen > length) {
1214 if (from.charCodeAt(fromStart + i) === CHAR_FORWARD_SLASH) {
1215 // We get here if `to` is the exact base path for `from`.
1216 // For example: from='/foo/bar/baz'; to='/foo/bar'
1217 lastCommonSep = i;
1218 } else if (i === 0) {
1219 // We get here if `to` is the root.
1220 // For example: from='/foo/bar'; to='/'
1221 lastCommonSep = 0;
1222 }
1223 }
1224 }
1225 
1226 let out = '';
1227 // Generate the relative path based on the path difference between `to`
1228 // and `from`.
1229 for (i = fromStart + lastCommonSep + 1; i <= fromEnd; ++i) {
1230 if (i === fromEnd || from.charCodeAt(i) === CHAR_FORWARD_SLASH) {
1231 out += out.length === 0 ? '..' : '/..';
1232 }
1233 }
1234 
1235 // Lastly, append the rest of the destination (`to`) path that comes after
1236 // the common path parts.
1237 return `${out}${to.slice(toStart + lastCommonSep)}`;
1238 },
1239 
1240 /**
1241 * @param {string} path
1242 * @returns {string}
1243 */
1244 toNamespacedPath(path: string): string {
1245 // No-op on Posix systems
1246 return path;
1247 },
1248 
1249 /**
1250 * @param {string} path
1251 * @returns {string}
1252 */
1253 dirname(path: string): string {
1254 validateString(path, 'path');
1255 if (path.length === 0) return '.';
1256 const hasRoot = path.charCodeAt(0) === CHAR_FORWARD_SLASH;
1257 let end = -1;
1258 let matchedSlash = true;
1259 for (let i = path.length - 1; i >= 1; --i) {
1260 if (path.charCodeAt(i) === CHAR_FORWARD_SLASH) {
1261 if (!matchedSlash) {
1262 end = i;
1263 break;
1264 }
1265 } else {
1266 // We saw the first non-path separator
1267 matchedSlash = false;
1268 }
1269 }
1270 
1271 if (end === -1) return hasRoot ? '/' : '.';
1272 if (hasRoot && end === 1) return '//';
1273 return path.slice(0, end);
1274 },
1275 
1276 /**
1277 * @param {string} path
1278 * @param {string} [suffix]
1279 * @returns {string}
1280 */
1281 basename(path: string, suffix?: string): string {
1282 if (suffix !== undefined) validateString(suffix, 'ext');
1283 validateString(path, 'path');
1284 
1285 let start = 0;
1286 let end = -1;
1287 let matchedSlash = true;
1288 
1289 if (
1290 suffix !== undefined &&
1291 suffix.length > 0 &&
1292 suffix.length <= path.length
1293 ) {
1294 if (suffix === path) return '';
1295 let extIdx = suffix.length - 1;
1296 let firstNonSlashEnd = -1;
1297 for (let i = path.length - 1; i >= 0; --i) {
1298 const code = path.charCodeAt(i);
1299 if (code === CHAR_FORWARD_SLASH) {
1300 // If we reached a path separator that was not part of a set of path
1301 // separators at the end of the string, stop now
1302 if (!matchedSlash) {
1303 start = i + 1;
1304 break;
1305 }
1306 } else {
1307 if (firstNonSlashEnd === -1) {
1308 // We saw the first non-path separator, remember this index in case
1309 // we need it if the extension ends up not matching
1310 matchedSlash = false;
1311 firstNonSlashEnd = i + 1;
1312 }
1313 if (extIdx >= 0) {
1314 // Try to match the explicit extension
1315 if (code === suffix.charCodeAt(extIdx)) {
1316 if (--extIdx === -1) {
1317 // We matched the extension, so mark this as the end of our path
1318 // component
1319 end = i;
1320 }
1321 } else {
1322 // Extension does not match, so our result is the entire path
1323 // component
1324 extIdx = -1;
1325 end = firstNonSlashEnd;
1326 }
1327 }
1328 }
1329 }
1330 
1331 if (start === end) end = firstNonSlashEnd;
1332 else if (end === -1) end = path.length;
1333 return path.slice(start, end);
1334 }
1335 for (let i = path.length - 1; i >= 0; --i) {
1336 if (path.charCodeAt(i) === CHAR_FORWARD_SLASH) {
1337 // If we reached a path separator that was not part of a set of path
1338 // separators at the end of the string, stop now
1339 if (!matchedSlash) {
1340 start = i + 1;
1341 break;
1342 }
1343 } else if (end === -1) {
1344 // We saw the first non-path separator, mark this as the end of our
1345 // path component
1346 matchedSlash = false;
1347 end = i + 1;
1348 }
1349 }
1350 
1351 if (end === -1) return '';
1352 return path.slice(start, end);
1353 },
1354 
1355 /**
1356 * @param {string} path
1357 * @returns {string}
1358 */
1359 extname(path: string): string {
1360 validateString(path, 'path');
1361 let startDot = -1;
1362 let startPart = 0;
1363 let end = -1;
1364 let matchedSlash = true;
1365 // Track the state of characters (if any) we see before our first dot and
1366 // after any path separator we find
1367 let preDotState = 0;
1368 for (let i = path.length - 1; i >= 0; --i) {
1369 const code = path.charCodeAt(i);
1370 if (code === CHAR_FORWARD_SLASH) {
1371 // If we reached a path separator that was not part of a set of path
1372 // separators at the end of the string, stop now
1373 if (!matchedSlash) {
1374 startPart = i + 1;
1375 break;
1376 }
1377 continue;
1378 }
1379 if (end === -1) {
1380 // We saw the first non-path separator, mark this as the end of our
1381 // extension
1382 matchedSlash = false;
1383 end = i + 1;
1384 }
1385 if (code === CHAR_DOT) {
1386 // If this is our first dot, mark it as the start of our extension
1387 if (startDot === -1) startDot = i;
1388 else if (preDotState !== 1) preDotState = 1;
1389 } else if (startDot !== -1) {
1390 // We saw a non-dot and non-path separator before our dot, so we should
1391 // have a good chance at having a non-empty extension
1392 preDotState = -1;
1393 }
1394 }
1395 
1396 if (
1397 startDot === -1 ||
1398 end === -1 ||
1399 // We saw a non-dot character immediately before the dot
1400 preDotState === 0 ||
1401 // The (right-most) trimmed path component is exactly '..'
1402 (preDotState === 1 && startDot === end - 1 && startDot === startPart + 1)
1403 ) {
1404 return '';
1405 }
1406 return path.slice(startDot, end);
1407 },
1408 
1409 format: _format.bind(null, '/'),
1410 
1411 /**
1412 * @param {string} path
1413 * @returns {{
1414 * dir: string;
1415 * root: string;
1416 * base: string;
1417 * name: string;
1418 * ext: string;
1419 * }}
1420 */
1421 parse(path: string): PathObject {
1422 validateString(path, 'path');
1423 
1424 const ret = { root: '', dir: '', base: '', ext: '', name: '' };
1425 if (path.length === 0) return ret;
1426 const isAbsolute = path.charCodeAt(0) === CHAR_FORWARD_SLASH;
1427 let start;
1428 if (isAbsolute) {
1429 ret.root = '/';
1430 start = 1;
1431 } else {
1432 start = 0;
1433 }
1434 let startDot = -1;
1435 let startPart = 0;
1436 let end = -1;
1437 let matchedSlash = true;
1438 let i = path.length - 1;
1439 
1440 // Track the state of characters (if any) we see before our first dot and
1441 // after any path separator we find
1442 let preDotState = 0;
1443 
1444 // Get non-dir info
1445 for (; i >= start; --i) {
1446 const code = path.charCodeAt(i);
1447 if (code === CHAR_FORWARD_SLASH) {
1448 // If we reached a path separator that was not part of a set of path
1449 // separators at the end of the string, stop now
1450 if (!matchedSlash) {
1451 startPart = i + 1;
1452 break;
1453 }
1454 continue;
1455 }
1456 if (end === -1) {
1457 // We saw the first non-path separator, mark this as the end of our
1458 // extension
1459 matchedSlash = false;
1460 end = i + 1;
1461 }
1462 if (code === CHAR_DOT) {
1463 // If this is our first dot, mark it as the start of our extension
1464 if (startDot === -1) startDot = i;
1465 else if (preDotState !== 1) preDotState = 1;
1466 } else if (startDot !== -1) {
1467 // We saw a non-dot and non-path separator before our dot, so we should
1468 // have a good chance at having a non-empty extension
1469 preDotState = -1;
1470 }
1471 }
1472 
1473 if (end !== -1) {
1474 const start = startPart === 0 && isAbsolute ? 1 : startPart;
1475 if (
1476 startDot === -1 ||
1477 // We saw a non-dot character immediately before the dot
1478 preDotState === 0 ||
1479 // The (right-most) trimmed path component is exactly '..'
1480 (preDotState === 1 &&
1481 startDot === end - 1 &&
1482 startDot === startPart + 1)
1483 ) {
1484 ret.base = ret.name = path.slice(start, end);
1485 } else {
1486 ret.name = path.slice(start, startDot);
1487 ret.base = path.slice(start, end);
1488 ret.ext = path.slice(startDot, end);
1489 }
1490 }
1491 
1492 if (startPart > 0) ret.dir = path.slice(0, startPart - 1);
1493 else if (isAbsolute) ret.dir = '/';
1494 
1495 return ret;
1496 },
1497 
1498 matchesGlob(_path: string, _pattern: string): boolean {
1499 throw new Error('path.posix.matchesGlob() is not implemented.');
1500 },
1501 
1502 sep: '/',
1503 delimiter: ':',
1504 win32: null as object | null,
1505 posix: null as object | null,
1506};
1507 
1508posix.win32 = win32.win32 = win32;
1509posix.posix = win32.posix = posix;
1510 
1511// TODO(soon): Maybe we should export depending on the host operating system
1512// Ref: https://github.com/nodejs/node/blob/10addb0a208c0356a0358cfd2a7b80a0932cd108/lib/path.js#L1696
1513export default posix;
1514export { posix, win32 };