@@ -7,30 +7,156 @@ const disallowedKeys = new Set([
77 'constructor'
88] ) ;
99
10- const isValidPath = pathSegments => ! pathSegments . some ( segment => disallowedKeys . has ( segment ) ) ;
10+ const digits = new Set ( '0123456789' ) ;
1111
1212function getPathSegments ( path ) {
13- const pathArray = path . split ( '.' ) ;
1413 const parts = [ ] ;
14+ let currentSegment = '' ;
15+ let currentPart = 'start' ;
16+ let isIgnoring = false ;
17+
18+ for ( const character of path ) {
19+ switch ( character ) {
20+ case '\\' :
21+ if ( currentPart === 'index' ) {
22+ throw new Error ( 'Invalid character in an index' ) ;
23+ }
24+
25+ if ( currentPart === 'indexEnd' ) {
26+ throw new Error ( 'Invalid character after an index' ) ;
27+ }
28+
29+ if ( isIgnoring ) {
30+ currentSegment += character ;
31+ }
32+
33+ currentPart = 'property' ;
34+ isIgnoring = ! isIgnoring ;
35+ break ;
1536
16- for ( let i = 0 ; i < pathArray . length ; i ++ ) {
17- let p = pathArray [ i ] ;
37+ case '.' :
38+ if ( currentPart === 'index' ) {
39+ throw new Error ( 'Invalid character in an index' ) ;
40+ }
41+
42+ if ( currentPart === 'indexEnd' ) {
43+ currentPart = 'property' ;
44+ break ;
45+ }
46+
47+ if ( isIgnoring ) {
48+ isIgnoring = false ;
49+ currentSegment += character ;
50+ break ;
51+ }
52+
53+ if ( disallowedKeys . has ( currentSegment ) ) {
54+ return [ ] ;
55+ }
56+
57+ parts . push ( currentSegment ) ;
58+ currentSegment = '' ;
59+ currentPart = 'property' ;
60+ break ;
61+
62+ case '[' :
63+ if ( currentPart === 'index' ) {
64+ throw new Error ( 'Invalid character in an index' ) ;
65+ }
66+
67+ if ( currentPart === 'indexEnd' ) {
68+ currentPart = 'index' ;
69+ break ;
70+ }
71+
72+ if ( isIgnoring ) {
73+ isIgnoring = false ;
74+ currentSegment += character ;
75+ break ;
76+ }
77+
78+ if ( currentPart === 'property' ) {
79+ if ( disallowedKeys . has ( currentSegment ) ) {
80+ return [ ] ;
81+ }
82+
83+ parts . push ( currentSegment ) ;
84+ currentSegment = '' ;
85+ }
86+
87+ currentPart = 'index' ;
88+ break ;
1889
19- while ( p [ p . length - 1 ] === '\\' && pathArray [ i + 1 ] !== undefined ) {
20- p = p . slice ( 0 , - 1 ) + '.' ;
21- p += pathArray [ ++ i ] ;
90+ case ']' :
91+ if ( currentPart === 'index' ) {
92+ parts . push ( Number . parseInt ( currentSegment , 10 ) ) ;
93+ currentSegment = '' ;
94+ currentPart = 'indexEnd' ;
95+ break ;
96+ }
97+
98+ if ( currentPart === 'indexEnd' ) {
99+ throw new Error ( 'Invalid character after an index' ) ;
100+ }
101+
102+ // Falls through
103+
104+ default :
105+ if ( currentPart === 'index' && ! digits . has ( character ) ) {
106+ throw new Error ( 'Invalid character in an index' ) ;
107+ }
108+
109+ if ( currentPart === 'indexEnd' ) {
110+ throw new Error ( 'Invalid character after an index' ) ;
111+ }
112+
113+ if ( currentPart === 'start' ) {
114+ currentPart = 'property' ;
115+ }
116+
117+ if ( isIgnoring ) {
118+ isIgnoring = false ;
119+ currentSegment += '\\' ;
120+ }
121+
122+ currentSegment += character ;
22123 }
124+ }
23125
24- parts . push ( p ) ;
126+ if ( isIgnoring ) {
127+ currentSegment += '\\' ;
25128 }
26129
27- if ( ! isValidPath ( parts ) ) {
28- return [ ] ;
130+ if ( currentPart === 'property' ) {
131+ if ( disallowedKeys . has ( currentSegment ) ) {
132+ return [ ] ;
133+ }
134+
135+ parts . push ( currentSegment ) ;
136+ } else if ( currentPart === 'index' ) {
137+ throw new Error ( 'Index was not closed' ) ;
138+ } else if ( currentPart === 'start' ) {
139+ parts . push ( '' ) ;
29140 }
30141
31142 return parts ;
32143}
33144
145+ function isStringIndex ( object , key ) {
146+ if ( typeof key !== 'number' && Array . isArray ( object ) ) {
147+ const index = Number . parseInt ( key , 10 ) ;
148+ return Number . isInteger ( index ) && object [ index ] === object [ key ] ;
149+ }
150+
151+ return false ;
152+ }
153+
154+ function assertNotStringIndex ( object , key ) {
155+ if ( isStringIndex ( object , key ) ) {
156+ throw new Error ( 'Cannot use string index' ) ;
157+ }
158+ }
159+
34160module . exports = {
35161 get ( object , path , value ) {
36162 if ( ! isObject ( object ) || typeof path !== 'string' ) {
@@ -43,12 +169,18 @@ module.exports = {
43169 }
44170
45171 for ( let i = 0 ; i < pathArray . length ; i ++ ) {
46- object = object [ pathArray [ i ] ] ;
172+ const key = pathArray [ i ] ;
173+
174+ if ( isStringIndex ( object , key ) ) {
175+ object = i === pathArray . length - 1 ? undefined : null ;
176+ } else {
177+ object = object [ key ] ;
178+ }
47179
48180 if ( object === undefined || object === null ) {
49181 // `object` is either `undefined` or `null` so we want to stop the loop, and
50182 // if this is not the last bit of the path, and
51- // if it did 't return `undefined`
183+ // if it didn 't return `undefined`
52184 // it would return `null` if `object` is `null`
53185 // but we want `get({foo: null}, 'foo.bar')` to equal `undefined`, or the supplied value, not `null`
54186 if ( i !== pathArray . length - 1 ) {
@@ -72,9 +204,10 @@ module.exports = {
72204
73205 for ( let i = 0 ; i < pathArray . length ; i ++ ) {
74206 const p = pathArray [ i ] ;
207+ assertNotStringIndex ( object , p ) ;
75208
76209 if ( ! isObject ( object [ p ] ) ) {
77- object [ p ] = { } ;
210+ object [ p ] = Number . isInteger ( pathArray [ i + 1 ] ) ? [ ] : { } ;
78211 }
79212
80213 if ( i === pathArray . length - 1 ) {
@@ -96,6 +229,7 @@ module.exports = {
96229
97230 for ( let i = 0 ; i < pathArray . length ; i ++ ) {
98231 const p = pathArray [ i ] ;
232+ assertNotStringIndex ( object , p ) ;
99233
100234 if ( i === pathArray . length - 1 ) {
101235 delete object [ p ] ;
@@ -123,7 +257,7 @@ module.exports = {
123257 // eslint-disable-next-line unicorn/no-for-loop
124258 for ( let i = 0 ; i < pathArray . length ; i ++ ) {
125259 if ( isObject ( object ) ) {
126- if ( ! ( pathArray [ i ] in object ) ) {
260+ if ( ! ( pathArray [ i ] in object && ! isStringIndex ( object , pathArray [ i ] ) ) ) {
127261 return false ;
128262 }
129263
0 commit comments