From 335ba3063a5df7d025effade213dfe69fc10614d Mon Sep 17 00:00:00 2001 From: Sheraff Date: Fri, 14 Nov 2025 19:41:55 +0100 Subject: [PATCH 1/2] refactor(router-core): simplify decodePath implementation, improve performance --- packages/router-core/src/utils.ts | 71 ++++++------------------ packages/router-core/tests/utils.test.ts | 2 +- 2 files changed, 18 insertions(+), 55 deletions(-) diff --git a/packages/router-core/src/utils.ts b/packages/router-core/src/utils.ts index cb679d2bebc..0b27637f56d 100644 --- a/packages/router-core/src/utils.ts +++ b/packages/router-core/src/utils.ts @@ -489,50 +489,12 @@ export function findLast( return undefined } -const DECODE_IGNORE_LIST = [ - '%25', // % - '%5C', // \ -] - -function splitAndDecode( - part: string, - decodeIgnore: Array, - startIndex = 0, -): string { - // decode the path / path segment by splitting it into parts defined by the ignore list. - // once these pieces have been decoded, join them back together to form the final decoded path segment with the ignored character in place. - // we walk through the ignore list linearly, breaking the segment up into pieces and decoding each piece individually. - // use index traversal to avoid making unnecessary copies of the array. - for (let i = startIndex; i < decodeIgnore.length; i++) { - const char = decodeIgnore[i]!.toUpperCase() - - // check if the part includes the current ignore character - // if it doesn't continue to the next ignore character - if (part.includes(char)) { - // split the part into pieces that needs to be checked and decoded - const partsToDecode = part.split(char) - const partsToJoin = new Array(partsToDecode.length) - - // now check and decode each piece individually taking into consideration the remaining ignored characters. - // since we are walking through the list linearly, we only need to consider ignore items not yet traversed. - for (let j = 0; j < partsToDecode.length; j++) { - const partToDecode = partsToDecode[j]! - // once we have traversed the entire ignore list, each decoded part is returned. - partsToJoin[j] = splitAndDecode(partToDecode, decodeIgnore, i + 1) - } - - // and join them back together to form the final decoded path segment with the ignored character in place. - return partsToJoin.join(char) - } - } - - // once we have reached the end of the ignore list, we start walking back returning each decoded part. - // should there be no matching characters, the path segment as a whole will be decoded. +function decodeSegment(segment: string): string { try { - return decodeURI(part) + return decodeURI(segment) } catch { // if the decoding fails, try to decode the various parts leaving the malformed tags in place - return part.replaceAll(/%[0-9A-F]{2}/g, (match) => { + return segment.replaceAll(/%[0-9A-F]{2}/gi, (match) => { try { return decodeURI(match) } catch { @@ -542,17 +504,18 @@ function splitAndDecode( } } -export function decodePath( - part: string, - decodeIgnore: Array = DECODE_IGNORE_LIST, -): string { - // if the path segment does not contain any encoded uri components return the path as is - if (part === '' || !/%[0-9A-Fa-f]{2}/g.test(part)) return part - - // ensure all encoded characters are uppercase - const normalizedPart = part.replaceAll(/%[0-9a-f]{2}/g, (match) => - match.toUpperCase(), - ) - - return splitAndDecode(normalizedPart, decodeIgnore) +export function decodePath(path: string, decodeIgnore?: Array): string { + const re = decodeIgnore + ? new RegExp(`${decodeIgnore.join('|')}`, 'gi') + : /%25|%5C/gi + let cursor = 0 + let result = '' + let match + while (null !== (match = re.exec(path))) { + const i = match.index + const chunk = match[0] + result += decodeSegment(path.slice(cursor, i)) + chunk + cursor = i + chunk.length + } + return result + decodeSegment(path.slice(cursor)) } diff --git a/packages/router-core/tests/utils.test.ts b/packages/router-core/tests/utils.test.ts index 4dec592a57c..932d368ca89 100644 --- a/packages/router-core/tests/utils.test.ts +++ b/packages/router-core/tests/utils.test.ts @@ -607,7 +607,7 @@ describe('decodePath', () => { const stringToCheckWithLowerCase = '/params-ps/named/foo%2Fabc/c%5C%2f%5cAh' const expectedResultWithLowerCase = - '/params-ps/named/foo%2Fabc/c%5C%2F%5CAh' + '/params-ps/named/foo%2Fabc/c%5C%2f%5cAh' expect(decodePath(stringToCheckWithLowerCase)).toBe( expectedResultWithLowerCase, ) From 07dbfea2183c9555aca92d1707ca54dc9c62808a Mon Sep 17 00:00:00 2001 From: Sheraff Date: Fri, 14 Nov 2025 19:57:44 +0100 Subject: [PATCH 2/2] simplify more --- packages/router-core/src/utils.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/router-core/src/utils.ts b/packages/router-core/src/utils.ts index 0b27637f56d..eb8d3025e78 100644 --- a/packages/router-core/src/utils.ts +++ b/packages/router-core/src/utils.ts @@ -505,6 +505,7 @@ function decodeSegment(segment: string): string { } export function decodePath(path: string, decodeIgnore?: Array): string { + if (!path) return path const re = decodeIgnore ? new RegExp(`${decodeIgnore.join('|')}`, 'gi') : /%25|%5C/gi @@ -512,10 +513,8 @@ export function decodePath(path: string, decodeIgnore?: Array): string { let result = '' let match while (null !== (match = re.exec(path))) { - const i = match.index - const chunk = match[0] - result += decodeSegment(path.slice(cursor, i)) + chunk - cursor = i + chunk.length + result += decodeSegment(path.slice(cursor, match.index)) + match[0] + cursor = re.lastIndex } - return result + decodeSegment(path.slice(cursor)) + return result + decodeSegment(cursor ? path.slice(cursor) : path) }