188 lines
		
	
	
	
		
			5.1 KiB
			
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			188 lines
		
	
	
	
		
			5.1 KiB
			
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
// Copyright 2021 Signal Messenger, LLC
 | 
						|
// SPDX-License-Identifier: AGPL-3.0-only
 | 
						|
 | 
						|
import { isPlainObject } from 'lodash';
 | 
						|
import * as log from '../logging/log';
 | 
						|
 | 
						|
import { isIterable } from '../util/iterables';
 | 
						|
 | 
						|
/**
 | 
						|
 * IPC arguments are serialized with the [structured clone algorithm][0], but we can only
 | 
						|
 * save some data types to disk.
 | 
						|
 *
 | 
						|
 * This cleans the data so it's roughly JSON-serializable, though it does not handle
 | 
						|
 * every case. You can see the expected behavior in the tests. Notably, we try to convert
 | 
						|
 * protobufjs numbers to JavaScript numbers, and we don't touch ArrayBuffers.
 | 
						|
 *
 | 
						|
 * [0]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm
 | 
						|
 */
 | 
						|
export function cleanDataForIpc(data: unknown): {
 | 
						|
  // `any`s are dangerous but it's difficult (impossible?) to type this with generics.
 | 
						|
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
 | 
						|
  cleaned: any;
 | 
						|
  pathsChanged: Array<string>;
 | 
						|
} {
 | 
						|
  const pathsChanged: Array<string> = [];
 | 
						|
  const cleaned = cleanDataInner(data, 'root', pathsChanged, 0);
 | 
						|
  return { cleaned, pathsChanged };
 | 
						|
}
 | 
						|
 | 
						|
// These type definitions are lifted from [this GitHub comment][1].
 | 
						|
//
 | 
						|
// [1]: https://github.com/Microsoft/TypeScript/issues/3496#issuecomment-128553540
 | 
						|
type CleanedDataValue =
 | 
						|
  | string
 | 
						|
  | number
 | 
						|
  | boolean
 | 
						|
  | null
 | 
						|
  | undefined
 | 
						|
  | Uint8Array
 | 
						|
  | CleanedObject
 | 
						|
  | CleanedArray;
 | 
						|
/* eslint-disable no-restricted-syntax */
 | 
						|
interface CleanedObject {
 | 
						|
  [x: string]: CleanedDataValue;
 | 
						|
}
 | 
						|
// eslint-disable-next-line @typescript-eslint/no-empty-interface
 | 
						|
interface CleanedArray extends Array<CleanedDataValue> {}
 | 
						|
/* eslint-enable no-restricted-syntax */
 | 
						|
 | 
						|
function cleanDataInner(
 | 
						|
  data: unknown,
 | 
						|
  path: string,
 | 
						|
  pathsChanged: Array<string>,
 | 
						|
  depth: number
 | 
						|
): CleanedDataValue {
 | 
						|
  if (depth > 10) {
 | 
						|
    log.error(
 | 
						|
      `cleanDataInner: Reached maximum depth ${depth}; path is ${path}`
 | 
						|
    );
 | 
						|
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
 | 
						|
    return { cleaned: data as any, pathsChanged };
 | 
						|
  }
 | 
						|
 | 
						|
  switch (typeof data) {
 | 
						|
    case 'undefined':
 | 
						|
    case 'boolean':
 | 
						|
    case 'number':
 | 
						|
    case 'string':
 | 
						|
      return data;
 | 
						|
    case 'bigint':
 | 
						|
      pathsChanged.push(path);
 | 
						|
      return data.toString();
 | 
						|
    case 'function':
 | 
						|
      // For backwards compatibility with previous versions of this function, we clean
 | 
						|
      //   functions but don't mark them as cleaned.
 | 
						|
      return undefined;
 | 
						|
    case 'object': {
 | 
						|
      // eslint-disable-next-line eqeqeq
 | 
						|
      if (data === null) {
 | 
						|
        return null;
 | 
						|
      }
 | 
						|
 | 
						|
      if (Array.isArray(data)) {
 | 
						|
        const result: CleanedArray = [];
 | 
						|
        data.forEach((item, index) => {
 | 
						|
          const indexPath = `${path}.${index}`;
 | 
						|
          if (item == null) {
 | 
						|
            pathsChanged.push(indexPath);
 | 
						|
          } else {
 | 
						|
            result.push(
 | 
						|
              cleanDataInner(item, indexPath, pathsChanged, depth + 1)
 | 
						|
            );
 | 
						|
          }
 | 
						|
        });
 | 
						|
        return result;
 | 
						|
      }
 | 
						|
 | 
						|
      if (data instanceof Map) {
 | 
						|
        const result: CleanedObject = {};
 | 
						|
        pathsChanged.push(path);
 | 
						|
        data.forEach((value, key) => {
 | 
						|
          if (typeof key === 'string') {
 | 
						|
            result[key] = cleanDataInner(
 | 
						|
              value,
 | 
						|
              `${path}.<map value at ${key}>`,
 | 
						|
              pathsChanged,
 | 
						|
              depth + 1
 | 
						|
            );
 | 
						|
          } else {
 | 
						|
            pathsChanged.push(`${path}.<map key ${String(key)}>`);
 | 
						|
          }
 | 
						|
        });
 | 
						|
        return result;
 | 
						|
      }
 | 
						|
 | 
						|
      if (data instanceof Date) {
 | 
						|
        pathsChanged.push(path);
 | 
						|
        return Number.isNaN(data.valueOf()) ? undefined : data.toISOString();
 | 
						|
      }
 | 
						|
 | 
						|
      if (data instanceof ArrayBuffer) {
 | 
						|
        pathsChanged.push(path);
 | 
						|
        return undefined;
 | 
						|
      }
 | 
						|
 | 
						|
      if (data instanceof Uint8Array) {
 | 
						|
        return data;
 | 
						|
      }
 | 
						|
 | 
						|
      const dataAsRecord = data as Record<string, unknown>;
 | 
						|
 | 
						|
      if (
 | 
						|
        'toNumber' in dataAsRecord &&
 | 
						|
        typeof dataAsRecord.toNumber === 'function'
 | 
						|
      ) {
 | 
						|
        // We clean this just in case `toNumber` returns something bogus.
 | 
						|
        return cleanDataInner(
 | 
						|
          dataAsRecord.toNumber(),
 | 
						|
          path,
 | 
						|
          pathsChanged,
 | 
						|
          depth + 1
 | 
						|
        );
 | 
						|
      }
 | 
						|
 | 
						|
      if (isIterable(dataAsRecord)) {
 | 
						|
        const result: CleanedArray = [];
 | 
						|
        let index = 0;
 | 
						|
        pathsChanged.push(path);
 | 
						|
        for (const value of dataAsRecord) {
 | 
						|
          result.push(
 | 
						|
            cleanDataInner(
 | 
						|
              value,
 | 
						|
              `${path}.<iterator index ${index}>`,
 | 
						|
              pathsChanged,
 | 
						|
              depth + 1
 | 
						|
            )
 | 
						|
          );
 | 
						|
          index += 1;
 | 
						|
        }
 | 
						|
        return result;
 | 
						|
      }
 | 
						|
 | 
						|
      // We'll still try to clean non-plain objects, but we want to mark that they've
 | 
						|
      //   changed.
 | 
						|
      if (!isPlainObject(data)) {
 | 
						|
        pathsChanged.push(path);
 | 
						|
      }
 | 
						|
 | 
						|
      const result: CleanedObject = {};
 | 
						|
 | 
						|
      // Conveniently, `Object.entries` removes symbol keys.
 | 
						|
      Object.entries(dataAsRecord).forEach(([key, value]) => {
 | 
						|
        result[key] = cleanDataInner(
 | 
						|
          value,
 | 
						|
          `${path}.${key}`,
 | 
						|
          pathsChanged,
 | 
						|
          depth + 1
 | 
						|
        );
 | 
						|
      });
 | 
						|
 | 
						|
      return result;
 | 
						|
    }
 | 
						|
    default: {
 | 
						|
      pathsChanged.push(path);
 | 
						|
      return undefined;
 | 
						|
    }
 | 
						|
  }
 | 
						|
}
 |