@@ -21,7 +21,6 @@ const {
2121 StringPrototypeCodePointAt,
2222 StringPrototypeIncludes,
2323 StringPrototypeIndexOf,
24- StringPrototypeReplaceAll,
2524 StringPrototypeSlice,
2625 StringPrototypeStartsWith,
2726 StringPrototypeToWellFormed,
@@ -39,6 +38,7 @@ const { inspect } = require('internal/util/inspect');
3938const {
4039 encodeStr,
4140 hexTable,
41+ isHexTable,
4242} = require ( 'internal/querystring' ) ;
4343
4444const {
@@ -1302,18 +1302,36 @@ function unescapeFormComponent(s) {
13021302 }
13031303}
13041304
1305+ function hasPercentHex ( s ) {
1306+ const end = s . length - 2 ;
1307+ for ( let i = 0 ; i < end ; i ++ ) {
1308+ if ( StringPrototypeCharCodeAt ( s , i ) === 37 && // '%'
1309+ isHexTable [ StringPrototypeCharCodeAt ( s , i + 1 ) ] === 1 &&
1310+ isHexTable [ StringPrototypeCharCodeAt ( s , i + 2 ) ] === 1 ) {
1311+ return true ;
1312+ }
1313+ }
1314+ return false ;
1315+ }
1316+
1317+ /* eslint-disable node-core/prefer-primordials */
13051318function decodeFormComponent ( qs , start , end ) {
13061319 if ( start >= end ) {
13071320 return '' ;
13081321 }
1309- const s = StringPrototypeSlice ( qs , start , end ) ;
1310- const plus = StringPrototypeIndexOf ( s , '+' ) ;
1311- const pct = StringPrototypeIndexOf ( s , '%' ) ;
1322+ const s = qs . slice ( start , end ) ;
1323+ const plus = s . indexOf ( '+' ) ;
1324+ const pct = s . indexOf ( '%' ) ;
13121325 if ( plus === - 1 && pct === - 1 ) {
13131326 return s ;
13141327 }
1315- const replaced = plus === - 1 ? s : StringPrototypeReplaceAll ( s , '+' , ' ' ) ;
1316- return pct === - 1 ? replaced : unescapeFormComponent ( replaced ) ;
1328+ const replaced = plus === - 1 ? s : s . replaceAll ( '+' , ' ' ) ;
1329+ // Only percent-decode when a complete %HH sequence exists. A lone '%' or
1330+ // '%©' must be left intact so later serialization can encode the raw bytes.
1331+ if ( pct === - 1 || ! hasPercentHex ( replaced ) ) {
1332+ return replaced ;
1333+ }
1334+ return unescapeFormComponent ( replaced ) ;
13171335}
13181336
13191337// application/x-www-form-urlencoded parser
@@ -1326,30 +1344,29 @@ function parseParams(qs) {
13261344 }
13271345
13281346 const out = [ ] ;
1329- // indexOf is SIMD-accelerated. When the query has no '+' or '%', every
1330- // component is a slice of the input and we can skip decodeFormComponent.
1331- const decode =
1332- ( StringPrototypeIndexOf ( qs , '+' , i ) === - 1 &&
1333- StringPrototypeIndexOf ( qs , '%' , i ) === - 1 ) ?
1334- StringPrototypeSlice :
1335- decodeFormComponent ;
1347+ // Native indexOf/slice/push outperform primordials on this tight loop.
1348+ const encoded = qs . indexOf ( '+' , i ) !== - 1 || qs . indexOf ( '%' , i ) !== - 1 ;
13361349 while ( i < len ) {
1337- let amp = StringPrototypeIndexOf ( qs , '&' , i ) ;
1350+ let amp = qs . indexOf ( '&' , i ) ;
13381351 if ( amp === - 1 ) {
13391352 amp = len ;
13401353 }
13411354 if ( amp !== i ) {
1342- const eq = StringPrototypeIndexOf ( qs , '=' , i ) ;
1355+ const eq = qs . indexOf ( '=' , i ) ;
13431356 if ( eq === - 1 || eq > amp ) {
1344- ArrayPrototypePush ( out , decode ( qs , i , amp ) , '' ) ;
1357+ out . push ( encoded ? decodeFormComponent ( qs , i , amp ) : qs . slice ( i , amp ) , '' ) ;
13451358 } else {
1346- ArrayPrototypePush ( out , decode ( qs , i , eq ) , decode ( qs , eq + 1 , amp ) ) ;
1359+ out . push (
1360+ encoded ? decodeFormComponent ( qs , i , eq ) : qs . slice ( i , eq ) ,
1361+ encoded ? decodeFormComponent ( qs , eq + 1 , amp ) : qs . slice ( eq + 1 , amp ) ,
1362+ ) ;
13471363 }
13481364 }
13491365 i = amp + 1 ;
13501366 }
13511367 return out ;
13521368}
1369+ /* eslint-enable node-core/prefer-primordials */
13531370
13541371// Adapted from querystring's implementation.
13551372// Ref: https://url.spec.whatwg.org/#concept-urlencoded-byte-serializer
0 commit comments