(path: string)
| 615 | } |
| 616 | |
| 617 | export function decodePath(path: string) { |
| 618 | if (!path) return { path, handledProtocolRelativeURL: false } |
| 619 | |
| 620 | // Fast path: most paths are already decoded and safe. |
| 621 | // Only fall back to the slower scan/regex path when we see a '%' (encoded), |
| 622 | // a backslash (explicitly handled), a control character, or a protocol-relative |
| 623 | // prefix which needs collapsing. |
| 624 | // eslint-disable-next-line no-control-regex |
| 625 | if (!/[%\\\x00-\x1f\x7f]/.test(path) && !path.startsWith('//')) { |
| 626 | return { path, handledProtocolRelativeURL: false } |
| 627 | } |
| 628 | |
| 629 | const re = /%25|%5C/gi |
| 630 | let cursor = 0 |
| 631 | let result = '' |
| 632 | let match |
| 633 | while (null !== (match = re.exec(path))) { |
| 634 | result += decodeSegment(path.slice(cursor, match.index)) + match[0] |
| 635 | cursor = re.lastIndex |
| 636 | } |
| 637 | result = result + decodeSegment(cursor ? path.slice(cursor) : path) |
| 638 | |
| 639 | // Prevent open redirect via protocol-relative URLs (e.g. "//evil.com") |
| 640 | // After sanitizing control characters, paths like "/\r/evil.com" become "//evil.com" |
| 641 | // Collapse leading double slashes to a single slash |
| 642 | let handledProtocolRelativeURL = false |
| 643 | if (result.startsWith('//')) { |
| 644 | handledProtocolRelativeURL = true |
| 645 | result = '/' + result.replace(/^\/+/, '') |
| 646 | } |
| 647 | |
| 648 | return { path: result, handledProtocolRelativeURL } |
| 649 | } |
| 650 | |
| 651 | /** |
| 652 | * Encodes a path the same way `new URL()` would, but without the overhead of full URL parsing. |
no test coverage detected