Browse Source

fix: imitate JSON.stringify better in jsonDump

#1421
tophf 4 years ago
parent
commit
43de3d0f7f
3 changed files with 42 additions and 14 deletions
  1. 2 0
      src/injected/web/safe-globals-web.js
  2. 23 12
      src/injected/web/util-web.js
  3. 17 2
      test/injected/helpers.test.js

+ 2 - 0
src/injected/web/safe-globals-web.js

@@ -23,6 +23,7 @@ export let
   off,
   on,
   openWindow,
+  safeIsFinite,
   // Symbol
   scopeSym,
   toStringTag,
@@ -112,6 +113,7 @@ export const VAULT = (() => {
     ProxySafe = res[i += 1] || srcFF.Proxy,
     ResponseSafe = res[i += 1] || src.Response,
     fire = res[i += 1] || src.dispatchEvent,
+    safeIsFinite = res[i += 1] || srcFF.isFinite, // Firefox defines `isFinite` on `global`
     off = res[i += 1] || src.removeEventListener,
     on = res[i += 1] || src.addEventListener,
     openWindow = res[i += 1] || src.open,

+ 23 - 12
src/injected/web/util-web.js

@@ -1,9 +1,6 @@
 import { INJECT_CONTENT } from '../util';
 import bridge from './bridge';
 
-// Firefox defines `isFinite` on `global` not on `window`
-const { isFinite } = global; // eslint-disable-line no-restricted-properties
-const { toString: numberToString } = 0;
 // Reference: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/JSON#Polyfill
 const escMap = {
   __proto__: null,
@@ -15,21 +12,27 @@ const escMap = {
   '\r': '\\r',
   '\t': '\\t',
 };
-const escRE = /[\\"\u0000-\u001F\u2028\u2029]/g; // eslint-disable-line no-control-regex
-const escFunc = m => escMap[m] || `\\u${(m::charCodeAt(0) + 0x10000)::numberToString(16)::slice(1)}`;
+// TODO: handle \u2028\u2029 when Chrome's JSON.stringify starts to escape them
+const escRE = /[\\"\u0000-\u001F]/g; // eslint-disable-line no-control-regex
+const hex = '0123456789ABCDEF';
+const escCharCode = num => `\\u00${
+  hex[num >> 4] // eslint-disable-line no-bitwise
+}${
+  hex[num % 16]
+}`;
+const escFunc = m => escMap[m] || escCharCode(m::charCodeAt(0));
 /**
  * When running in the page context we must beware of sites that override Array#toJSON
  * leading to an invalid result, which is why our jsonDump() ignores toJSON.
  * Thus, we use the native JSON.stringify() only in the content script context and only until
  * a userscript is injected into this context (due to `@inject-into` and/or a CSP problem).
  */
-export const jsonDump = value => {
-  if (value == null) return 'null';
+export const jsonDump = (value, stack = []) => {
   let res;
-  switch (typeof value) {
+  switch (value === null ? (res = 'null') : typeof value) {
   case 'bigint':
   case 'number':
-    res = isFinite(value) ? `${value}` : 'null';
+    res = safeIsFinite(value) ? `${value}` : 'null';
     break;
   case 'boolean':
     res = `${value}`;
@@ -38,14 +41,21 @@ export const jsonDump = value => {
     res = `"${value::replace(escRE, escFunc)}"`;
     break;
   case 'object':
+    if (stack::indexOf(value) >= 0) {
+      throw new ErrorSafe('Converting circular structure to JSON');
+    }
+    setOwnProp(stack, stack.length, value);
     if (ArrayIsArray(value)) {
       res = '[';
-      value::forEach(v => { res += `${res.length > 1 ? ',' : ''}${jsonDump(v) ?? 'null'}`; });
+      // Must enumerate all values to include holes in sparse arrays
+      for (let i = 0, len = value.length; i < len; i += 1) {
+        res += `${i ? ',' : ''}${jsonDump(value[i], stack) ?? 'null'}`;
+      }
       res += ']';
     } else {
       res = '{';
-      objectKeys(value)::forEach(key => {
-        const v = jsonDump(value[key]);
+      objectKeys(value)::forEach((key) => {
+        const v = jsonDump(value[key], stack);
         // JSON.stringify skips keys with `undefined` or incompatible values
         if (v !== undefined) {
           res += `${res.length > 1 ? ',' : ''}${jsonDump(key)}:${v}`;
@@ -53,6 +63,7 @@ export const jsonDump = value => {
       });
       res += '}';
     }
+    stack.length -= 1;
     break;
   default:
   }

+ 17 - 2
test/injected/helpers.test.js

@@ -2,24 +2,39 @@ import test from 'tape';
 import { jsonDump } from '#/injected/web/util-web';
 
 test('jsonDump', (t) => {
+  const sameChildObj = { foo: 1 };
   // eslint-disable-next-line no-restricted-syntax
   for (const obj of [
     1,
     null,
     false,
+    undefined,
+    Infinity,
+    NaN,
     'abc',
     {},
     [],
-    [1, 2, 3],
+    [1, 2, 3, undefined, , 4], // eslint-disable-line no-sparse-arrays
     {
       a: 1, b: '2', c: true, d: 'aaa',
     },
     {
       a: [1, 2, 3],
-      b: { a: 'b' },
+      b: { a: '\\"\x01foo\r\t"\u2028\u2029' },
+      skipped: undefined,
+      unsupported: new Set(),
+    }, {
+      sameChild1: sameChildObj,
+      sameChild2: sameChildObj,
+      sameChild3: [sameChildObj],
     },
   ]) {
     t.equal(jsonDump(obj), JSON.stringify(obj));
   }
+  t.throws(() => {
+    const cyclic = {};
+    cyclic.foo = [1, 2, 3, { cyclic }];
+    jsonDump(cyclic);
+  }, /Converting circular structure to JSON/, 'circular');
   t.end();
 });