Skip to content

Commit 8dbd562

Browse files
TimothyGuitaloacasas
authored andcommitted
url: fix surrogate handling in encodeAuth()
Also factor out common parts in querystring and url. Backport-of: #11161
1 parent 7e37628 commit 8dbd562

File tree

6 files changed

+104
-92
lines changed

6 files changed

+104
-92
lines changed

lib/internal/querystring.js

+15
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

+1-71
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;
@@ -1212,7 +1143,6 @@ exports.URL = URL;
12121143
exports.URLSearchParams = URLSearchParams;
12131144
exports.domainToASCII = domainToASCII;
12141145
exports.domainToUnicode = domainToUnicode;
1215-
exports.encodeAuth = encodeAuth;
12161146
exports.urlToOptions = urlToOptions;
12171147
exports.formatSymbol = kFormat;
12181148
exports.searchParamsSymbol = searchParams;

lib/querystring.js

+3-12
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
// ' ( ) *
@@ -263,7 +254,7 @@ const defEqCodes = [61]; // =
263254

264255
// Parse a key/val string.
265256
function parse(qs, sep, eq, options) {
266-
const obj = new ParsedQueryString();
257+
const obj = new StorageObject();
267258

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

lib/url.js

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

node.gyp

+1
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@
9292
'lib/internal/process/stdio.js',
9393
'lib/internal/process/warning.js',
9494
'lib/internal/process.js',
95+
'lib/internal/querystring.js',
9596
'lib/internal/readline.js',
9697
'lib/internal/repl.js',
9798
'lib/internal/socket_list.js',

test/parallel/test-url.js

+9
Original file line numberDiff line numberDiff line change
@@ -1231,6 +1231,15 @@ const formatTests = {
12311231
protocol: 'file',
12321232
pathname: '/home/user',
12331233
path: '/home/user'
1234+
},
1235+
1236+
// surrogate in auth
1237+
'http://%F0%9F%98%[email protected]/': {
1238+
href: 'http://%F0%9F%98%[email protected]/',
1239+
protocol: 'http:',
1240+
auth: '\uD83D\uDE00',
1241+
hostname: 'www.example.com',
1242+
pathname: '/'
12341243
}
12351244
};
12361245
for (const u in formatTests) {

0 commit comments

Comments
 (0)