Skip to content

Commit 4da0a3a

Browse files
committed
fix: add depth limit and cycle detection to convertBigIntToString
1 parent ac2c476 commit 4da0a3a

File tree

2 files changed

+88
-3
lines changed

2 files changed

+88
-3
lines changed

packages/react-router-devtools/src/shared/bigint-util.test.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,4 +63,67 @@ describe("convertBigIntToString", () => {
6363
const result = convertBigIntToString(123)
6464
expect(result).toBe(123)
6565
})
66+
67+
it("should replace circular references with [Circular]", () => {
68+
const o: Record<string, unknown> = {}
69+
o.self = o
70+
const result = convertBigIntToString(o)
71+
expect(result).toEqual({ self: "[Circular]" })
72+
})
73+
74+
it("should replace circular reference at depth 1 with [Circular]", () => {
75+
const root: Record<string, unknown> = { a: 1 }
76+
root.nested = { b: 2, back: root }
77+
const result = convertBigIntToString(root)
78+
expect(result).toEqual({
79+
a: 1,
80+
nested: { b: 2, back: "[Circular]" },
81+
})
82+
})
83+
84+
it("should not replace shared (non-circular) references with [Circular]", () => {
85+
const shared = { x: 1, y: 2 }
86+
const root = { a: shared, b: shared }
87+
const result = convertBigIntToString(root)
88+
expect(result).toEqual({
89+
a: { x: 1, y: 2 },
90+
b: { x: 1, y: 2 },
91+
})
92+
})
93+
94+
it("should replace deep acyclic structure beyond maxDepth with [Max depth reached]", () => {
95+
let deep: Record<string, unknown> = {}
96+
const root = deep
97+
for (let i = 0; i < 60; i++) {
98+
deep.next = {}
99+
deep = deep.next as Record<string, unknown>
100+
}
101+
const result = convertBigIntToString(root)
102+
let current: unknown = result
103+
let depth = 0
104+
while (current !== null && typeof current === "object" && "next" in current) {
105+
current = (current as Record<string, unknown>).next
106+
depth++
107+
}
108+
expect(depth).toBe(50)
109+
expect(current).toBe("[Max depth reached]")
110+
})
111+
112+
it("should traverse deeper when maxDepth option is increased", () => {
113+
let deep: Record<string, unknown> = {}
114+
const root = deep
115+
for (let i = 0; i < 60; i++) {
116+
deep.next = {}
117+
deep = deep.next as Record<string, unknown>
118+
}
119+
const result = convertBigIntToString(root, { maxDepth: 100 })
120+
let current: unknown = result
121+
let depth = 0
122+
while (current !== null && typeof current === "object" && "next" in current) {
123+
current = (current as Record<string, unknown>).next
124+
depth++
125+
}
126+
expect(depth).toBe(60)
127+
expect(current).toEqual({})
128+
})
66129
})
Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,41 @@
11
// biome-ignore lint/suspicious/noExplicitAny: we don't know the data
22
export const bigIntReplacer = (_key: any, value: any) => (typeof value === "bigint" ? value.toString() : value)
33

4+
const DEFAULT_MAX_DEPTH = 50
5+
const CIRCULAR_PLACEHOLDER = "[Circular]"
6+
const MAX_DEPTH_PLACEHOLDER = "[Max depth reached]"
7+
48
// biome-ignore lint/suspicious/noExplicitAny: we don't know the data
5-
export const convertBigIntToString = (data: any): any => {
9+
function convertBigIntToStringInner(data: any, depth: number, seen: WeakSet<object>, maxDepth: number): any {
610
if (typeof data === "bigint") {
711
return data.toString()
812
}
913

1014
if (Array.isArray(data)) {
11-
return data.map((item) => convertBigIntToString(item))
15+
if (seen.has(data)) return CIRCULAR_PLACEHOLDER
16+
if (depth >= maxDepth) return MAX_DEPTH_PLACEHOLDER
17+
seen.add(data)
18+
const result = data.map((item) => convertBigIntToStringInner(item, depth + 1, seen, maxDepth))
19+
seen.delete(data)
20+
return result
1221
}
1322

1423
if (data !== null && typeof data === "object") {
15-
return Object.fromEntries(Object.entries(data).map(([key, value]) => [key, convertBigIntToString(value)]))
24+
if (seen.has(data)) return CIRCULAR_PLACEHOLDER
25+
if (depth >= maxDepth) return MAX_DEPTH_PLACEHOLDER
26+
seen.add(data)
27+
const result = Object.fromEntries(
28+
Object.entries(data).map(([key, value]) => [key, convertBigIntToStringInner(value, depth + 1, seen, maxDepth)]),
29+
)
30+
seen.delete(data)
31+
return result
1632
}
1733

1834
return data
1935
}
36+
37+
// biome-ignore lint/suspicious/noExplicitAny: we don't know the data
38+
export const convertBigIntToString = (data: any, options?: { maxDepth?: number }): any => {
39+
const maxDepth = options?.maxDepth ?? DEFAULT_MAX_DEPTH
40+
return convertBigIntToStringInner(data, 0, new WeakSet(), maxDepth)
41+
}

0 commit comments

Comments
 (0)