Skip to content

Commit c6b12d0

Browse files
committed
url: fix surrogate handling in encodeAuth()
Also factor out common parts in querystring and url. PR-URL: #11161 Reviewed-By: Joyee Cheung <[email protected]> Reviewed-By: James M Snell <[email protected]> Reviewed-By: Brian White <[email protected]>
1 parent b738cbc commit c6b12d0

File tree

6 files changed

+104
-92
lines changed

6 files changed

+104
-92
lines changed

lib/internal/querystring.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
'use strict';
2+
3+
const hexTable = new Array(256);
4+
for (var i = 0; i < 256; ++i)
5+
hexTable[i] = '%' + ((i < 16 ? '0' : '') + i.toString(16)).toUpperCase();
6+
7+
// Instantiating this is faster than explicitly calling `Object.create(null)`
8+
// to get a "clean" empty object (tested with v8 v4.9).
9+
function StorageObject() {}
10+
StorageObject.prototype = Object.create(null);
11+
12+
module.exports = {
13+
hexTable,
14+
StorageObject
15+
};

lib/internal/url.js

Lines changed: 1 addition & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
'use strict';
22

33
const util = require('util');
4+
const { StorageObject } = require('internal/querystring');
45
const binding = process.binding('url');
56
const context = Symbol('context');
67
const cannotBeBase = Symbol('cannot-be-base');
@@ -22,9 +23,6 @@ const IteratorPrototype = Object.getPrototypeOf(
2223
Object.getPrototypeOf([][Symbol.iterator]())
2324
);
2425

25-
function StorageObject() {}
26-
StorageObject.prototype = Object.create(null);
27-
2826
class OpaqueOrigin {
2927
toString() {
3028
return 'null';
@@ -528,73 +526,6 @@ Object.defineProperties(URL.prototype, {
528526
}
529527
});
530528

531-
const hexTable = new Array(256);
532-
533-
for (var i = 0; i < 256; ++i)
534-
hexTable[i] = '%' + ((i < 16 ? '0' : '') + i.toString(16)).toUpperCase();
535-
function encodeAuth(str) {
536-
// faster encodeURIComponent alternative for encoding auth uri components
537-
var out = '';
538-
var lastPos = 0;
539-
for (var i = 0; i < str.length; ++i) {
540-
var c = str.charCodeAt(i);
541-
542-
// These characters do not need escaping:
543-
// ! - . _ ~
544-
// ' ( ) * :
545-
// digits
546-
// alpha (uppercase)
547-
// alpha (lowercase)
548-
if (c === 0x21 || c === 0x2D || c === 0x2E || c === 0x5F || c === 0x7E ||
549-
(c >= 0x27 && c <= 0x2A) ||
550-
(c >= 0x30 && c <= 0x3A) ||
551-
(c >= 0x41 && c <= 0x5A) ||
552-
(c >= 0x61 && c <= 0x7A)) {
553-
continue;
554-
}
555-
556-
if (i - lastPos > 0)
557-
out += str.slice(lastPos, i);
558-
559-
lastPos = i + 1;
560-
561-
// Other ASCII characters
562-
if (c < 0x80) {
563-
out += hexTable[c];
564-
continue;
565-
}
566-
567-
// Multi-byte characters ...
568-
if (c < 0x800) {
569-
out += hexTable[0xC0 | (c >> 6)] + hexTable[0x80 | (c & 0x3F)];
570-
continue;
571-
}
572-
if (c < 0xD800 || c >= 0xE000) {
573-
out += hexTable[0xE0 | (c >> 12)] +
574-
hexTable[0x80 | ((c >> 6) & 0x3F)] +
575-
hexTable[0x80 | (c & 0x3F)];
576-
continue;
577-
}
578-
// Surrogate pair
579-
++i;
580-
var c2;
581-
if (i < str.length)
582-
c2 = str.charCodeAt(i) & 0x3FF;
583-
else
584-
c2 = 0;
585-
c = 0x10000 + (((c & 0x3FF) << 10) | c2);
586-
out += hexTable[0xF0 | (c >> 18)] +
587-
hexTable[0x80 | ((c >> 12) & 0x3F)] +
588-
hexTable[0x80 | ((c >> 6) & 0x3F)] +
589-
hexTable[0x80 | (c & 0x3F)];
590-
}
591-
if (lastPos === 0)
592-
return str;
593-
if (lastPos < str.length)
594-
return out + str.slice(lastPos);
595-
return out;
596-
}
597-
598529
function update(url, params) {
599530
if (!url)
600531
return;
@@ -1254,7 +1185,6 @@ exports.URL = URL;
12541185
exports.URLSearchParams = URLSearchParams;
12551186
exports.domainToASCII = domainToASCII;
12561187
exports.domainToUnicode = domainToUnicode;
1257-
exports.encodeAuth = encodeAuth;
12581188
exports.urlToOptions = urlToOptions;
12591189
exports.formatSymbol = kFormat;
12601190
exports.searchParamsSymbol = searchParams;

lib/querystring.js

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
'use strict';
22

3+
const { Buffer } = require('buffer');
4+
const { StorageObject, hexTable } = require('internal/querystring');
35
const QueryString = module.exports = {
46
unescapeBuffer,
57
// `unescape()` is a JS global, so we need to use a different local name
@@ -14,13 +16,6 @@ const QueryString = module.exports = {
1416
parse,
1517
decode: parse
1618
};
17-
const Buffer = require('buffer').Buffer;
18-
19-
// This constructor is used to store parsed query string values. Instantiating
20-
// this is faster than explicitly calling `Object.create(null)` to get a
21-
// "clean" empty object (tested with v8 v4.9).
22-
function ParsedQueryString() {}
23-
ParsedQueryString.prototype = Object.create(null);
2419

2520
const unhexTable = [
2621
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 0 - 15
@@ -116,10 +111,6 @@ function qsUnescape(s, decodeSpaces) {
116111
}
117112

118113

119-
const hexTable = [];
120-
for (var i = 0; i < 256; ++i)
121-
hexTable[i] = '%' + ((i < 16 ? '0' : '') + i.toString(16)).toUpperCase();
122-
123114
// These characters do not need escaping when generating query strings:
124115
// ! - . _ ~
125116
// ' ( ) *
@@ -282,7 +273,7 @@ const defEqCodes = [61]; // =
282273

283274
// Parse a key/val string.
284275
function parse(qs, sep, eq, options) {
285-
const obj = new ParsedQueryString();
276+
const obj = new StorageObject();
286277

287278
if (typeof qs !== 'string' || qs.length === 0) {
288279
return obj;

lib/url.js

Lines changed: 75 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ function importPunycode() {
1010

1111
const { toASCII } = importPunycode();
1212

13+
const { StorageObject, hexTable } = require('internal/querystring');
1314
const internalUrl = require('internal/url');
14-
const encodeAuth = internalUrl.encodeAuth;
1515
exports.parse = urlParse;
1616
exports.resolve = urlResolve;
1717
exports.resolveObject = urlResolveObject;
@@ -76,12 +76,6 @@ const slashedProtocol = {
7676
};
7777
const querystring = require('querystring');
7878

79-
// This constructor is used to store parsed query string values. Instantiating
80-
// this is faster than explicitly calling `Object.create(null)` to get a
81-
// "clean" empty object (tested with v8 v4.9).
82-
function ParsedQueryString() {}
83-
ParsedQueryString.prototype = Object.create(null);
84-
8579
function urlParse(url, parseQueryString, slashesDenoteHost) {
8680
if (url instanceof Url) return url;
8781

@@ -190,7 +184,7 @@ Url.prototype.parse = function(url, parseQueryString, slashesDenoteHost) {
190184
}
191185
} else if (parseQueryString) {
192186
this.search = '';
193-
this.query = new ParsedQueryString();
187+
this.query = new StorageObject();
194188
}
195189
return this;
196190
}
@@ -380,7 +374,7 @@ Url.prototype.parse = function(url, parseQueryString, slashesDenoteHost) {
380374
} else if (parseQueryString) {
381375
// no query string, but parseQueryString still requested
382376
this.search = '';
383-
this.query = new ParsedQueryString();
377+
this.query = new StorageObject();
384378
}
385379

386380
var firstIdx = (questionIdx !== -1 &&
@@ -948,3 +942,75 @@ function spliceOne(list, index) {
948942
list[i] = list[k];
949943
list.pop();
950944
}
945+
946+
// These characters do not need escaping:
947+
// ! - . _ ~
948+
// ' ( ) * :
949+
// digits
950+
// alpha (uppercase)
951+
// alpha (lowercase)
952+
const noEscapeAuth = [
953+
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0x00 - 0x0F
954+
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0x10 - 0x1F
955+
0, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 0, // 0x20 - 0x2F
956+
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, // 0x30 - 0x3F
957+
0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 0x40 - 0x4F
958+
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, // 0x50 - 0x5F
959+
0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 0x60 - 0x6F
960+
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 0 // 0x70 - 0x7F
961+
];
962+
963+
function encodeAuth(str) {
964+
// faster encodeURIComponent alternative for encoding auth uri components
965+
var out = '';
966+
var lastPos = 0;
967+
for (var i = 0; i < str.length; ++i) {
968+
var c = str.charCodeAt(i);
969+
970+
// ASCII
971+
if (c < 0x80) {
972+
if (noEscapeAuth[c] === 1)
973+
continue;
974+
if (lastPos < i)
975+
out += str.slice(lastPos, i);
976+
lastPos = i + 1;
977+
out += hexTable[c];
978+
continue;
979+
}
980+
981+
if (lastPos < i)
982+
out += str.slice(lastPos, i);
983+
984+
// Multi-byte characters ...
985+
if (c < 0x800) {
986+
lastPos = i + 1;
987+
out += hexTable[0xC0 | (c >> 6)] + hexTable[0x80 | (c & 0x3F)];
988+
continue;
989+
}
990+
if (c < 0xD800 || c >= 0xE000) {
991+
lastPos = i + 1;
992+
out += hexTable[0xE0 | (c >> 12)] +
993+
hexTable[0x80 | ((c >> 6) & 0x3F)] +
994+
hexTable[0x80 | (c & 0x3F)];
995+
continue;
996+
}
997+
// Surrogate pair
998+
++i;
999+
var c2;
1000+
if (i < str.length)
1001+
c2 = str.charCodeAt(i) & 0x3FF;
1002+
else
1003+
c2 = 0;
1004+
lastPos = i + 1;
1005+
c = 0x10000 + (((c & 0x3FF) << 10) | c2);
1006+
out += hexTable[0xF0 | (c >> 18)] +
1007+
hexTable[0x80 | ((c >> 12) & 0x3F)] +
1008+
hexTable[0x80 | ((c >> 6) & 0x3F)] +
1009+
hexTable[0x80 | (c & 0x3F)];
1010+
}
1011+
if (lastPos === 0)
1012+
return str;
1013+
if (lastPos < str.length)
1014+
return out + str.slice(lastPos);
1015+
return out;
1016+
}

node.gyp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@
9393
'lib/internal/process/stdio.js',
9494
'lib/internal/process/warning.js',
9595
'lib/internal/process.js',
96+
'lib/internal/querystring.js',
9697
'lib/internal/readline.js',
9798
'lib/internal/repl.js',
9899
'lib/internal/socket_list.js',

test/parallel/test-url-format.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,15 @@ const formatTests = {
235235
protocol: 'file',
236236
pathname: '/home/user',
237237
path: '/home/user'
238+
},
239+
240+
// surrogate in auth
241+
'http://%F0%9F%98%[email protected]/': {
242+
href: 'http://%F0%9F%98%[email protected]/',
243+
protocol: 'http:',
244+
auth: '\uD83D\uDE00',
245+
hostname: 'www.example.com',
246+
pathname: '/'
238247
}
239248
};
240249
for (const u in formatTests) {

0 commit comments

Comments
 (0)