From bd8029f698d4f1a25961f65608c2605bf13913e8 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 25 Oct 2018 21:31:42 -0400 Subject: [PATCH 1/8] reimplement JSON.stringify --- src/index.ts | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index 88d04d9..e55f162 100644 --- a/src/index.ts +++ b/src/index.ts @@ -202,7 +202,7 @@ function escape(char: string) { } function stringifyPrimitive(thing: any) { - if (typeof thing === 'string') return JSON.stringify(thing).replace(unsafe, escape); + if (typeof thing === 'string') return stringifyString(thing); if (thing === void 0) return 'void 0'; if (thing === 0 && 1 / thing < 0) return '-0'; const str = String(thing); @@ -220,4 +220,27 @@ function safeKey(key: string) { function safeProp(key: string) { return /^[_$a-zA-Z][_$a-zA-Z0-9]*$/.test(key) ? `.${key}` : `[${JSON.stringify(key)}]`; +} + +function stringifyString(str: string) { + let result = '"'; + + for (let i = 0; i < str.length; i += 1) { + const char = str[i]; + const code = char.charCodeAt(0); + + if (char === '"') { + result += '\\"'; + } else if (char in escaped) { + result += escaped[char]; + } else if ((code >= 0xD800 && code <= 0xDBFF) && i < str.length - 1) { + // escape lone surrogates + result += `\\\\u${code.toString(16).toUpperCase()}`; + } else { + result += char; + } + } + + result += '"'; + return result; } \ No newline at end of file From 207040a68ebeb8492d6297b4fa15209df4710c95 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 25 Oct 2018 22:33:35 -0400 Subject: [PATCH 2/8] fix lone surrogate handling, add tests --- src/index.ts | 28 +++++++++++++++++++++++----- test/test.ts | 9 +++++++++ 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/src/index.ts b/src/index.ts index e55f162..6d1192a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,20 @@ const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_$'; const reserved = /^(?:do|if|in|for|int|let|new|try|var|byte|case|char|else|enum|goto|long|this|void|with|await|break|catch|class|const|final|float|short|super|throw|while|yield|delete|double|export|import|native|return|switch|throws|typeof|boolean|default|extends|finally|package|private|abstract|continue|debugger|function|volatile|interface|protected|transient|implements|instanceof|synchronized)$/; const unsafe = /[<>\/\u2028\u2029]/g; -const escaped: Record = { '<': '\\u003C', '>' : '\\u003E', '/': '\\u002F', '\u2028': '\\u2028', '\u2029': '\\u2029' }; +const escaped: Record = { + '<': '\\u003C', + '>' : '\\u003E', + '/': '\\u002F', + '\\': '\\\\', + '\b': '\\b', + '\f': '\\f', + '\n': '\\n', + '\r': '\\r', + '\t': '\\t', + '\0': '\\u0000', + '\u2028': '\\u2028', + '\u2029': '\\u2029' +}; const objectProtoOwnPropertyNames = Object.getOwnPropertyNames(Object.prototype).sort().join('\0'); export default function devalue(value: any) { @@ -226,16 +239,21 @@ function stringifyString(str: string) { let result = '"'; for (let i = 0; i < str.length; i += 1) { - const char = str[i]; + const char = str.charAt(i); const code = char.charCodeAt(0); if (char === '"') { result += '\\"'; } else if (char in escaped) { result += escaped[char]; - } else if ((code >= 0xD800 && code <= 0xDBFF) && i < str.length - 1) { - // escape lone surrogates - result += `\\\\u${code.toString(16).toUpperCase()}`; + } else if ((code >= 0xD800 && code <= 0xDBFF)) { + const next = str.charCodeAt(i + 1); + if (next >= 0xDC00 && next <= 0xDFFF) { + result += char; + } else { + // lone surrogates + result += `\\u${code.toString(16).toUpperCase()}`; + } } else { result += char; } diff --git a/test/test.ts b/test/test.ts index 74e69bd..e061397 100644 --- a/test/test.ts +++ b/test/test.ts @@ -35,6 +35,15 @@ describe('devalue', () => { test('Map', new Map([['a', 'b']]), 'new Map([["a","b"]])'); }); + describe('strings', () => { + test('newline', 'a\nb', JSON.stringify('a\nb')); + test('double quotes', '"yar"', JSON.stringify('"yar"')); + test('lone surrogate', "\uD800", '"\\uD800"'); + test('surrogate pair', '𝌆', JSON.stringify('𝌆')); + test('nul', '\0', JSON.stringify('\0')); + test('backslash', '\\', JSON.stringify('\\')); + }); + describe('cycles', () => { let map = new Map(); map.set('self', map); From 0f2501cbe2d5d08fe9e5fc7dfd39a3ab25b932dc Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 26 Oct 2018 06:42:03 -0400 Subject: [PATCH 3/8] make suggested changes --- src/index.ts | 5 ++--- test/test.ts | 8 ++++++-- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/index.ts b/src/index.ts index 6d1192a..3d322b3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -11,7 +11,6 @@ const escaped: Record = { '\n': '\\n', '\r': '\\r', '\t': '\\t', - '\0': '\\u0000', '\u2028': '\\u2028', '\u2029': '\\u2029' }; @@ -246,10 +245,10 @@ function stringifyString(str: string) { result += '\\"'; } else if (char in escaped) { result += escaped[char]; - } else if ((code >= 0xD800 && code <= 0xDBFF)) { + } else if (code >= 0xD800 && code <= 0xDBFF) { const next = str.charCodeAt(i + 1); if (next >= 0xDC00 && next <= 0xDFFF) { - result += char; + result += char + str[++i]; } else { // lone surrogates result += `\\u${code.toString(16).toUpperCase()}`; diff --git a/test/test.ts b/test/test.ts index e061397..cbe0948 100644 --- a/test/test.ts +++ b/test/test.ts @@ -38,9 +38,13 @@ describe('devalue', () => { describe('strings', () => { test('newline', 'a\nb', JSON.stringify('a\nb')); test('double quotes', '"yar"', JSON.stringify('"yar"')); - test('lone surrogate', "\uD800", '"\\uD800"'); + test('lone low surrogate', "a\uDC00b", '"a\\uDC00b"'); + test('lone high surrogate', "a\uD800b", '"a\\uD800b"'); + test('two low surrogates', "a\uDC00\uDC00b", '"a\\uDC00\\uDC00b"'); + test('two high surrogates', "a\uD800\uD800b", '"a\\uD800\\uD800b"'); test('surrogate pair', '𝌆', JSON.stringify('𝌆')); - test('nul', '\0', JSON.stringify('\0')); + test('surrogate pair in wrong order', 'a\uDC00\uD800b', '"a\uDC00\uD800b"'); + test('nul', '\0', '"\0"'); test('backslash', '\\', JSON.stringify('\\')); }); From 0d7c36a41a2e3e5830279f56bfa21cee36f0ca79 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 26 Oct 2018 08:41:19 -0400 Subject: [PATCH 4/8] escape expected string --- test/test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test.ts b/test/test.ts index cbe0948..f9b3ef8 100644 --- a/test/test.ts +++ b/test/test.ts @@ -43,7 +43,7 @@ describe('devalue', () => { test('two low surrogates', "a\uDC00\uDC00b", '"a\\uDC00\\uDC00b"'); test('two high surrogates', "a\uD800\uD800b", '"a\\uD800\\uD800b"'); test('surrogate pair', '𝌆', JSON.stringify('𝌆')); - test('surrogate pair in wrong order', 'a\uDC00\uD800b', '"a\uDC00\uD800b"'); + test('surrogate pair in wrong order', 'a\uDC00\uD800b', '"a\\uDC00\\uD800b"'); test('nul', '\0', '"\0"'); test('backslash', '\\', JSON.stringify('\\')); }); From df1848a1065040145dd3a9832c5e40d62345fdb9 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 26 Oct 2018 08:41:35 -0400 Subject: [PATCH 5/8] escape surrogates except in [low, high] pair --- src/index.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/index.ts b/src/index.ts index 3d322b3..a2fce10 100644 --- a/src/index.ts +++ b/src/index.ts @@ -245,12 +245,14 @@ function stringifyString(str: string) { result += '\\"'; } else if (char in escaped) { result += escaped[char]; - } else if (code >= 0xD800 && code <= 0xDBFF) { + } else if (code >= 0xd800 && code <= 0xdfff) { const next = str.charCodeAt(i + 1); - if (next >= 0xDC00 && next <= 0xDFFF) { + + // If this is the beginning of a [low, high] surrogate pair, + // add the next two characters, otherwise escape + if (code <= 0xdbff && (next >= 0xdc00 && next <= 0xdfff)) { result += char + str[++i]; } else { - // lone surrogates result += `\\u${code.toString(16).toUpperCase()}`; } } else { From 081f528472f767a33773ce1b476de8a9e4fe928f Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 26 Oct 2018 08:42:07 -0400 Subject: [PATCH 6/8] remove some unused code --- src/index.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/index.ts b/src/index.ts index a2fce10..3ef6643 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,5 @@ const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_$'; const reserved = /^(?:do|if|in|for|int|let|new|try|var|byte|case|char|else|enum|goto|long|this|void|with|await|break|catch|class|const|final|float|short|super|throw|while|yield|delete|double|export|import|native|return|switch|throws|typeof|boolean|default|extends|finally|package|private|abstract|continue|debugger|function|volatile|interface|protected|transient|implements|instanceof|synchronized)$/; -const unsafe = /[<>\/\u2028\u2029]/g; const escaped: Record = { '<': '\\u003C', '>' : '\\u003E', @@ -19,8 +18,6 @@ const objectProtoOwnPropertyNames = Object.getOwnPropertyNames(Object.prototype) export default function devalue(value: any) { const counts = new Map(); - let n = 0; - function walk(thing: any) { if (typeof thing === 'function') { throw new Error(`Cannot stringify a function`); @@ -209,10 +206,6 @@ function isPrimitive(thing: any) { return Object(thing) !== thing; } -function escape(char: string) { - return escaped[char]; -} - function stringifyPrimitive(thing: any) { if (typeof thing === 'string') return stringifyString(thing); if (thing === void 0) return 'void 0'; From bb328eab97d5305c19a6b445fa1cf0791164fdb1 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 26 Oct 2018 09:32:34 -0400 Subject: [PATCH 7/8] escape U+0000 --- src/index.ts | 1 + test/test.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index 3ef6643..c28ee89 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,6 +10,7 @@ const escaped: Record = { '\n': '\\n', '\r': '\\r', '\t': '\\t', + '\0': '\\0', '\u2028': '\\u2028', '\u2029': '\\u2029' }; diff --git a/test/test.ts b/test/test.ts index f9b3ef8..f0efc95 100644 --- a/test/test.ts +++ b/test/test.ts @@ -44,7 +44,7 @@ describe('devalue', () => { test('two high surrogates', "a\uD800\uD800b", '"a\\uD800\\uD800b"'); test('surrogate pair', '𝌆', JSON.stringify('𝌆')); test('surrogate pair in wrong order', 'a\uDC00\uD800b', '"a\\uDC00\\uD800b"'); - test('nul', '\0', '"\0"'); + test('nul', '\0', '"\\0"'); test('backslash', '\\', JSON.stringify('\\')); }); From 9463b0f487b56d5f064fea3bd0104e0c0ef05a95 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 26 Oct 2018 09:32:48 -0400 Subject: [PATCH 8/8] up is down and black is white --- src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index c28ee89..50945fa 100644 --- a/src/index.ts +++ b/src/index.ts @@ -242,7 +242,7 @@ function stringifyString(str: string) { } else if (code >= 0xd800 && code <= 0xdfff) { const next = str.charCodeAt(i + 1); - // If this is the beginning of a [low, high] surrogate pair, + // If this is the beginning of a [high, low] surrogate pair, // add the next two characters, otherwise escape if (code <= 0xdbff && (next >= 0xdc00 && next <= 0xdfff)) { result += char + str[++i];