1/**
2 * Copyright (c) 650 Industries.
3 * Copyright (c) Meta Platforms, Inc. and affiliates.
4 *
5 * This source code is licensed under the MIT license found in the
6 * LICENSE file in the root directory of this source tree.
7 */
8
9/**
10 * Tries to stringify with JSON.stringify and toString, but catches exceptions
11 * (e.g. from circular objects) and always returns a string and never throws.
12 */
13export function createStringifySafeWithLimits(limits: {
14  maxDepth?: number;
15  maxStringLimit?: number;
16  maxArrayLimit?: number;
17  maxObjectKeysLimit?: number;
18}): (foo: any) => string {
19  const {
20    maxDepth = Number.POSITIVE_INFINITY,
21    maxStringLimit = Number.POSITIVE_INFINITY,
22    maxArrayLimit = Number.POSITIVE_INFINITY,
23    maxObjectKeysLimit = Number.POSITIVE_INFINITY,
24  } = limits;
25  const stack: any[] = [];
26  function replacer(this: unknown, _key: string, value: any): any {
27    while (stack.length && this !== stack[0]) {
28      stack.shift();
29    }
30
31    if (typeof value === 'string') {
32      const truncatedString = '...(truncated)...';
33      if (value.length > maxStringLimit + truncatedString.length) {
34        return value.substring(0, maxStringLimit) + truncatedString;
35      }
36      return value;
37    }
38    if (typeof value !== 'object' || value === null) {
39      return value;
40    }
41
42    let retval = value;
43    if (Array.isArray(value)) {
44      if (stack.length >= maxDepth) {
45        retval = `[ ... array with ${value.length} values ... ]`;
46      } else if (value.length > maxArrayLimit) {
47        retval = value
48          .slice(0, maxArrayLimit)
49          .concat([`... extra ${value.length - maxArrayLimit} values truncated ...`]);
50      }
51    } else {
52      // Add refinement after Array.isArray call.
53      if (typeof value !== 'object') {
54        throw new Error('This was already found earlier');
55      }
56      const keys = Object.keys(value);
57      if (stack.length >= maxDepth) {
58        retval = `{ ... object with ${keys.length} keys ... }`;
59      } else if (keys.length > maxObjectKeysLimit) {
60        // Return a sample of the keys.
61        retval = {};
62        for (const k of keys.slice(0, maxObjectKeysLimit)) {
63          retval[k] = value[k];
64        }
65        const truncatedKey = '...(truncated keys)...';
66        retval[truncatedKey] = keys.length - maxObjectKeysLimit;
67      }
68    }
69    stack.unshift(retval);
70    return retval;
71  }
72
73  return function stringifySafe(arg: any): string {
74    if (arg === undefined) {
75      return 'undefined';
76    } else if (arg === null) {
77      return 'null';
78    } else if (typeof arg === 'function') {
79      try {
80        return arg.toString();
81      } catch {
82        return '[function unknown]';
83      }
84    } else if (arg instanceof Error) {
85      return arg.name + ': ' + arg.message;
86    } else {
87      // Perform a try catch, just in case the object has a circular
88      // reference or stringify throws for some other reason.
89      try {
90        const ret = JSON.stringify(arg, replacer);
91        if (ret === undefined) {
92          return '["' + typeof arg + '" failed to stringify]';
93        }
94        return ret;
95      } catch {
96        if (typeof arg.toString === 'function') {
97          try {
98            // $FlowFixMe[incompatible-use] : toString shouldn't take any arguments in general.
99            return arg.toString();
100          } catch {}
101        }
102      }
103    }
104    return '["' + typeof arg + '" failed to stringify]';
105  };
106}
107
108const stringifySafe = createStringifySafeWithLimits({
109  maxDepth: 10,
110  maxStringLimit: 100,
111  maxArrayLimit: 50,
112  maxObjectKeysLimit: 50,
113});
114
115export default stringifySafe;
116