Skip to content

Commit 0eb4573

Browse files
fix(router-core): reject overlapping parameter affixes (#8154)
* fix(router-core): reject overlapping parameter affixes * perf(router-core): reduce affix overlap check size * test(router-core): assert affix match parameters * ci: apply automated fixes * perf(router-core): normalize route affixes * ci: apply automated fixes * add test case --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1 parent eb9ddac commit 0eb4573

2 files changed

Lines changed: 89 additions & 35 deletions

File tree

packages/router-core/src/new-process-route-tree.ts

Lines changed: 46 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -237,20 +237,13 @@ function parseSegments<TRouteLike extends RouteLike>(
237237
case SEGMENT_TYPE_PARAM:
238238
case SEGMENT_TYPE_OPTIONAL_PARAM:
239239
case SEGMENT_TYPE_WILDCARD: {
240-
const prefix_raw = path.substring(start, segment[1])
241-
const suffix_raw = path.substring(segment[4], end)
242-
const actuallyCaseSensitive =
243-
caseSensitive && !!(prefix_raw || suffix_raw)
244-
const prefix = !prefix_raw
245-
? undefined
246-
: actuallyCaseSensitive
247-
? prefix_raw
248-
: prefix_raw.toLowerCase()
249-
const suffix = !suffix_raw
250-
? undefined
251-
: actuallyCaseSensitive
252-
? suffix_raw
253-
: suffix_raw.toLowerCase()
240+
let prefix = path.substring(start, segment[1])
241+
let suffix = path.substring(segment[4], end)
242+
const actuallyCaseSensitive = caseSensitive && !!(prefix || suffix)
243+
if (!caseSensitive) {
244+
prefix = prefix.toLowerCase()
245+
suffix = suffix.toLowerCase()
246+
}
254247
const siblings =
255248
kind === SEGMENT_TYPE_PARAM
256249
? node.dynamic
@@ -357,15 +350,15 @@ function parseSegments<TRouteLike extends RouteLike>(
357350

358351
function sortDynamic(
359352
a: {
360-
prefix?: string
361-
suffix?: string
353+
prefix: string
354+
suffix: string
362355
caseSensitive: boolean
363356
parse: null | ((params: Record<string, string>) => unknown)
364357
priority: number
365358
},
366359
b: {
367-
prefix?: string
368-
suffix?: string
360+
prefix: string
361+
suffix: string
369362
caseSensitive: boolean
370363
parse: null | ((params: Record<string, string>) => unknown)
371364
priority: number
@@ -426,8 +419,8 @@ function createDynamicNode<T extends RouteLike>(
426419
| typeof SEGMENT_TYPE_OPTIONAL_PARAM,
427420
fullPath: string,
428421
caseSensitive: boolean,
429-
prefix?: string,
430-
suffix?: string,
422+
prefix: string,
423+
suffix: string,
431424
): DynamicSegmentNode<T> {
432425
return {
433426
kind,
@@ -462,8 +455,8 @@ type DynamicSegmentNode<T extends RouteLike> = SegmentNode<T> & {
462455
| typeof SEGMENT_TYPE_PARAM
463456
| typeof SEGMENT_TYPE_WILDCARD
464457
| typeof SEGMENT_TYPE_OPTIONAL_PARAM
465-
prefix?: string
466-
suffix?: string
458+
prefix: string
459+
suffix: string
467460
caseSensitive: boolean
468461
}
469462

@@ -818,12 +811,12 @@ function extractParams<T extends RouteLike>(
818811
if (node.kind === SEGMENT_TYPE_PARAM) {
819812
nodeParts ??= leaf.node.fullPath.split('/')
820813
const nodePart = nodeParts[segmentCount]!
821-
const preLength = node.prefix?.length ?? 0
814+
const preLength = node.prefix.length
822815
// we can't rely on the presence of prefix/suffix to know whether it's curly-braced or not, because `/{$param}/` is valid, but has no prefix/suffix
823816
const isCurlyBraced = nodePart.charCodeAt(preLength) === 123 // '{'
824817
// param name is extracted at match-time so that tree nodes that are identical except for param name can share the same node
825818
if (isCurlyBraced) {
826-
const sufLength = node.suffix?.length ?? 0
819+
const sufLength = node.suffix.length
827820
const name = nodePart.substring(
828821
preLength + 2,
829822
nodePart.length - sufLength - 1,
@@ -842,8 +835,8 @@ function extractParams<T extends RouteLike>(
842835
}
843836
nodeParts ??= leaf.node.fullPath.split('/')
844837
const nodePart = nodeParts[segmentCount]!
845-
const preLength = node.prefix?.length ?? 0
846-
const sufLength = node.suffix?.length ?? 0
838+
const preLength = node.prefix.length
839+
const sufLength = node.suffix.length
847840
const name = nodePart.substring(
848841
preLength + 3,
849842
nodePart.length - sufLength - 1,
@@ -856,8 +849,8 @@ function extractParams<T extends RouteLike>(
856849
} else if (node.kind === SEGMENT_TYPE_WILDCARD) {
857850
const n = node
858851
const value = path.substring(
859-
currentPathIndex + (n.prefix?.length ?? 0),
860-
path.length - (n.suffix?.length ?? 0),
852+
currentPathIndex + n.prefix.length,
853+
path.length - n.suffix.length,
861854
)
862855
const splat = decodeURIComponent(value)
863856
// TODO: Deprecate *
@@ -1061,9 +1054,17 @@ function getNodeMatch<T extends RouteLike>(
10611054
}
10621055
if (suffix) {
10631056
if (isBeyondPath) continue
1064-
const end = parts.slice(index).join('/').slice(-suffix.length)
1065-
const casePart = segment.caseSensitive ? end : end.toLowerCase()
1066-
if (casePart !== suffix) continue
1057+
const end = parts.slice(index).join('/')
1058+
const suffixPart = end.slice(-suffix.length)
1059+
const casePart = segment.caseSensitive
1060+
? suffixPart
1061+
: suffixPart.toLowerCase()
1062+
if (
1063+
casePart !== suffix ||
1064+
end.length - suffix.length < prefix.length
1065+
) {
1066+
continue
1067+
}
10671068
}
10681069
// wildcard matches consume the rest of the URL and cannot have children
10691070
stack.push({
@@ -1106,7 +1107,13 @@ function getNodeMatch<T extends RouteLike>(
11061107
? part!
11071108
: (lowerPart ??= part!.toLowerCase())
11081109
if (prefix && !casePart.startsWith(prefix)) continue
1109-
if (suffix && !casePart.endsWith(suffix)) continue
1110+
if (
1111+
suffix &&
1112+
casePart.indexOf(suffix, casePart.length - suffix.length) <
1113+
prefix.length
1114+
) {
1115+
continue
1116+
}
11101117
}
11111118
stack.push({
11121119
node: segment,
@@ -1132,7 +1139,13 @@ function getNodeMatch<T extends RouteLike>(
11321139
? part
11331140
: (lowerPart ??= part.toLowerCase())
11341141
if (prefix && !casePart.startsWith(prefix)) continue
1135-
if (suffix && !casePart.endsWith(suffix)) continue
1142+
if (
1143+
suffix &&
1144+
casePart.indexOf(suffix, casePart.length - suffix.length) <
1145+
prefix.length
1146+
) {
1147+
continue
1148+
}
11361149
}
11371150
stack.push({
11381151
node: segment,

packages/router-core/tests/new-process-route-tree.test.ts

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -627,6 +627,31 @@ describe('findRouteMatch', () => {
627627
'/A{$id}B',
628628
)
629629
})
630+
it('case sensitivity does not distinguish plain dynamic segments', () => {
631+
const tree = {
632+
id: '__root__',
633+
isRoot: true,
634+
fullPath: '/',
635+
path: '/',
636+
children: [
637+
{
638+
id: '/$first',
639+
fullPath: '/$first',
640+
path: '$first',
641+
options: { caseSensitive: false },
642+
},
643+
{
644+
id: '/$second',
645+
fullPath: '/$second',
646+
path: '$second',
647+
options: { caseSensitive: true },
648+
},
649+
],
650+
}
651+
const { processedTree } = processRouteTree(tree)
652+
653+
expect(findRouteMatch('/value', processedTree)?.route.id).toBe('/$first')
654+
})
630655
})
631656

632657
describe('basic matching', () => {
@@ -686,6 +711,22 @@ describe('findRouteMatch', () => {
686711
'/file{-$id}.txt',
687712
)
688713
})
714+
it.each([
715+
['/ab{$id}bc', '/abbc', { id: '' }],
716+
['/ab{-$id}bc', '/abbc', {}],
717+
['/ab{$}bc', '/abbc', { '*': '', _splat: '' }],
718+
['/ab{$}bc', '/abfoo/barbc', { '*': 'foo/bar', _splat: 'foo/bar' }],
719+
] as const)(
720+
'does not match overlapping affixes for %s',
721+
(route, path, rawParams) => {
722+
const tree = makeTree([route])
723+
expect(findRouteMatch('/abc', tree)).toBeNull()
724+
expect(findRouteMatch(`${path}x`, tree)).toBeNull()
725+
const match = findRouteMatch(path, tree)
726+
expect(match?.route.id).toBe(route)
727+
expect(match?.rawParams).toEqual(rawParams)
728+
},
729+
)
689730
})
690731

691732
it('optional at the end can still be omitted', () => {
@@ -1676,7 +1717,7 @@ describe('findRouteMatch', () => {
16761717
"wildcard": null,
16771718
},
16781719
],
1679-
"prefix": undefined,
1720+
"prefix": "",
16801721
"priority": 0,
16811722
"route": null,
16821723
"static": null,
@@ -1703,7 +1744,7 @@ describe('findRouteMatch', () => {
17031744
"wildcard": null,
17041745
},
17051746
},
1706-
"suffix": undefined,
1747+
"suffix": "",
17071748
"wildcard": null,
17081749
},
17091750
],

0 commit comments

Comments
 (0)