@@ -9,6 +9,8 @@ const DEFAULT_MAX_DEPTH = 128;
99// reconstruction and pollute the shared process.
1010const PROTOTYPE_POLLUTION_KEY = "__proto__" ;
1111
12+ const MAX_ARRAY_HOLES = 1024 ;
13+
1214export function flattenAttributes (
1315 obj : unknown ,
1416 prefix ?: string ,
@@ -271,41 +273,52 @@ export function unflattenAttributes(
271273 }
272274
273275 const result : Record < string , unknown > = { } ;
276+ let everyTopLevelKeyIsArrayIndex = true ;
277+ const entries : { segments : string [ ] ; parsed : ParsedSegment [ ] ; value : unknown } [ ] = [ ] ;
274278
275279 for ( const [ key , value ] of Object . entries ( obj ) ) {
276280 if ( filteredKeys ?. includes ( key ) ) {
277281 continue ;
278282 }
279283
280- const parts = key . split ( "." ) . reduce (
281- ( acc , part ) => {
282- if ( part . startsWith ( "[" ) && part . endsWith ( "]" ) ) {
283- // Handle array indices more precisely
284- const match = part . match ( / ^ \[ ( \d + ) \] $ / ) ;
285- if ( match && match [ 1 ] ) {
286- acc . push ( parseInt ( match [ 1 ] ) ) ;
287- } else {
288- // Remove brackets for non-numeric array keys
289- acc . push ( part . slice ( 1 , - 1 ) ) ;
290- }
291- } else {
292- acc . push ( part ) ;
293- }
294- return acc ;
295- } ,
296- [ ] as ( string | number ) [ ]
297- ) ;
284+ const segments = key . split ( "." ) ;
285+ const parsed = segments . map ( parseSegment ) ;
298286
299287 // Skip keys that exceed max depth to prevent memory exhaustion
300- if ( parts . length > maxDepth ) {
288+ if ( parsed . length > maxDepth ) {
301289 continue ;
302290 }
303291
304292 // Skip any key whose path could walk into Object.prototype.
305- if ( parts . includes ( PROTOTYPE_POLLUTION_KEY ) ) {
293+ if ( parsed . some ( ( segment ) => segment . key === PROTOTYPE_POLLUTION_KEY ) ) {
306294 continue ;
307295 }
308296
297+ entries . push ( { segments, parsed, value } ) ;
298+ }
299+
300+ const maxArrayIndex = Object . keys ( obj ) . length + MAX_ARRAY_HOLES ;
301+ const oversizedArrayPaths = new Set < string > ( ) ;
302+ for ( const { segments, parsed } of entries ) {
303+ parsed . forEach ( ( segment , i ) => {
304+ if ( segment . index !== undefined && segment . index > maxArrayIndex ) {
305+ oversizedArrayPaths . add ( segments . slice ( 0 , i ) . join ( "." ) ) ;
306+ }
307+ } ) ;
308+ }
309+
310+ for ( const { segments, parsed, value } of entries ) {
311+ const parts = parsed . map ( ( segment , i ) =>
312+ segment . index !== undefined &&
313+ ! ( oversizedArrayPaths . size > 0 && oversizedArrayPaths . has ( segments . slice ( 0 , i ) . join ( "." ) ) )
314+ ? segment . index
315+ : segment . key
316+ ) ;
317+
318+ if ( typeof parts [ 0 ] !== "number" ) {
319+ everyTopLevelKeyIsArrayIndex = false ;
320+ }
321+
309322 let current : any = result ;
310323 for ( let i = 0 ; i < parts . length - 1 ; i ++ ) {
311324 const part = parts [ i ] ;
@@ -342,7 +355,11 @@ export function unflattenAttributes(
342355 // Convert the result to an array if all top-level keys are numeric indices.
343356 // Guard against an empty result (e.g. every key was skipped as unsafe), which
344357 // would otherwise produce Array(-Infinity) and throw.
345- if ( Object . keys ( result ) . length > 0 && Object . keys ( result ) . every ( ( k ) => / ^ \d + $ / . test ( k ) ) ) {
358+ if (
359+ everyTopLevelKeyIsArrayIndex &&
360+ Object . keys ( result ) . length > 0 &&
361+ Object . keys ( result ) . every ( ( k ) => / ^ \d + $ / . test ( k ) )
362+ ) {
346363 const maxIndex = Math . max ( ...Object . keys ( result ) . map ( ( k ) => parseInt ( k ) ) ) ;
347364 const arrayResult = Array ( maxIndex + 1 ) ;
348365 for ( const key in result ) {
@@ -355,6 +372,19 @@ export function unflattenAttributes(
355372 return result ;
356373}
357374
375+ type ParsedSegment = { key : string ; index ?: number } ;
376+
377+ function parseSegment ( segment : string ) : ParsedSegment {
378+ const match = segment . match ( / ^ \[ ( \d + ) \] $ / ) ;
379+ if ( match && match [ 1 ] ) {
380+ return { key : match [ 1 ] , index : parseInt ( match [ 1 ] ) } ;
381+ }
382+ if ( segment . startsWith ( "[" ) && segment . endsWith ( "]" ) ) {
383+ return { key : segment . slice ( 1 , - 1 ) } ;
384+ }
385+ return { key : segment } ;
386+ }
387+
358388function rehydrateCircular ( value : any ) : any {
359389 if ( value === CIRCULAR_REFERENCE_SENTINEL ) {
360390 return "[Circular Reference]" ;
0 commit comments