Add an assertion when updating conversations; update cleanData
This commit is contained in:
parent
73a304faba
commit
bc37b5c907
23 changed files with 749 additions and 79 deletions
|
@ -6,12 +6,18 @@
|
||||||
const { ipcRenderer } = require('electron');
|
const { ipcRenderer } = require('electron');
|
||||||
const url = require('url');
|
const url = require('url');
|
||||||
const i18n = require('./js/modules/i18n');
|
const i18n = require('./js/modules/i18n');
|
||||||
|
const {
|
||||||
|
getEnvironment,
|
||||||
|
setEnvironment,
|
||||||
|
parseEnvironment,
|
||||||
|
} = require('./ts/environment');
|
||||||
|
|
||||||
const config = url.parse(window.location.toString(), true).query;
|
const config = url.parse(window.location.toString(), true).query;
|
||||||
const { locale } = config;
|
const { locale } = config;
|
||||||
const localeMessages = ipcRenderer.sendSync('locale-data');
|
const localeMessages = ipcRenderer.sendSync('locale-data');
|
||||||
|
setEnvironment(parseEnvironment(config.environment));
|
||||||
|
|
||||||
window.getEnvironment = () => config.environment;
|
window.getEnvironment = getEnvironment;
|
||||||
window.getVersion = () => config.version;
|
window.getVersion = () => config.version;
|
||||||
window.getAppInstance = () => config.appInstance;
|
window.getAppInstance = () => config.appInstance;
|
||||||
|
|
||||||
|
|
|
@ -3,21 +3,25 @@
|
||||||
|
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const { app } = require('electron');
|
const { app } = require('electron');
|
||||||
|
const {
|
||||||
let environment;
|
Environment,
|
||||||
|
getEnvironment,
|
||||||
|
setEnvironment,
|
||||||
|
parseEnvironment,
|
||||||
|
} = require('../ts/environment');
|
||||||
|
|
||||||
// In production mode, NODE_ENV cannot be customized by the user
|
// In production mode, NODE_ENV cannot be customized by the user
|
||||||
if (!app.isPackaged) {
|
if (app.isPackaged) {
|
||||||
environment = process.env.NODE_ENV || 'development';
|
setEnvironment(Environment.Production);
|
||||||
} else {
|
} else {
|
||||||
environment = 'production';
|
setEnvironment(parseEnvironment(process.env.NODE_ENV || 'development'));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set environment vars to configure node-config before requiring it
|
// Set environment vars to configure node-config before requiring it
|
||||||
process.env.NODE_ENV = environment;
|
process.env.NODE_ENV = getEnvironment();
|
||||||
process.env.NODE_CONFIG_DIR = path.join(__dirname, '..', 'config');
|
process.env.NODE_CONFIG_DIR = path.join(__dirname, '..', 'config');
|
||||||
|
|
||||||
if (environment === 'production') {
|
if (getEnvironment() === Environment.Production) {
|
||||||
// harden production config against the local env
|
// harden production config against the local env
|
||||||
process.env.NODE_CONFIG = '';
|
process.env.NODE_CONFIG = '';
|
||||||
process.env.NODE_CONFIG_STRICT_MODE = true;
|
process.env.NODE_CONFIG_STRICT_MODE = true;
|
||||||
|
@ -30,9 +34,10 @@ if (environment === 'production') {
|
||||||
}
|
}
|
||||||
|
|
||||||
// We load config after we've made our modifications to NODE_ENV
|
// We load config after we've made our modifications to NODE_ENV
|
||||||
|
// eslint-disable-next-line import/order
|
||||||
const config = require('config');
|
const config = require('config');
|
||||||
|
|
||||||
config.environment = environment;
|
config.environment = getEnvironment();
|
||||||
config.enableHttp = process.env.SIGNAL_ENABLE_HTTP;
|
config.enableHttp = process.env.SIGNAL_ENABLE_HTTP;
|
||||||
|
|
||||||
// Log resulting env vars in use by config
|
// Log resulting env vars in use by config
|
||||||
|
|
|
@ -7,10 +7,16 @@ const { ipcRenderer } = require('electron');
|
||||||
const url = require('url');
|
const url = require('url');
|
||||||
const copyText = require('copy-text-to-clipboard');
|
const copyText = require('copy-text-to-clipboard');
|
||||||
const i18n = require('./js/modules/i18n');
|
const i18n = require('./js/modules/i18n');
|
||||||
|
const {
|
||||||
|
getEnvironment,
|
||||||
|
setEnvironment,
|
||||||
|
parseEnvironment,
|
||||||
|
} = require('./ts/environment');
|
||||||
|
|
||||||
const config = url.parse(window.location.toString(), true).query;
|
const config = url.parse(window.location.toString(), true).query;
|
||||||
const { locale } = config;
|
const { locale } = config;
|
||||||
const localeMessages = ipcRenderer.sendSync('locale-data');
|
const localeMessages = ipcRenderer.sendSync('locale-data');
|
||||||
|
setEnvironment(parseEnvironment(config.environment));
|
||||||
|
|
||||||
window.getVersion = () => config.version;
|
window.getVersion = () => config.version;
|
||||||
window.theme = config.theme;
|
window.theme = config.theme;
|
||||||
|
@ -21,7 +27,7 @@ window.copyText = copyText;
|
||||||
window.nodeSetImmediate = setImmediate;
|
window.nodeSetImmediate = setImmediate;
|
||||||
|
|
||||||
window.getNodeVersion = () => config.node_version;
|
window.getNodeVersion = () => config.node_version;
|
||||||
window.getEnvironment = () => config.environment;
|
window.getEnvironment = getEnvironment;
|
||||||
|
|
||||||
require('./ts/logging/set_up_renderer_logging');
|
require('./ts/logging/set_up_renderer_logging');
|
||||||
|
|
||||||
|
|
|
@ -23,6 +23,7 @@ const rimraf = require('rimraf');
|
||||||
const electronRemote = require('electron').remote;
|
const electronRemote = require('electron').remote;
|
||||||
|
|
||||||
const crypto = require('../../ts/Crypto');
|
const crypto = require('../../ts/Crypto');
|
||||||
|
const { getEnvironment } = require('../../ts/environment');
|
||||||
|
|
||||||
const { dialog, BrowserWindow } = electronRemote;
|
const { dialog, BrowserWindow } = electronRemote;
|
||||||
|
|
||||||
|
@ -1198,7 +1199,7 @@ function deleteAll(pattern) {
|
||||||
const ARCHIVE_NAME = 'messages.tar.gz';
|
const ARCHIVE_NAME = 'messages.tar.gz';
|
||||||
|
|
||||||
async function exportToDirectory(directory, options) {
|
async function exportToDirectory(directory, options) {
|
||||||
const env = window.getEnvironment();
|
const env = getEnvironment();
|
||||||
if (env !== 'test') {
|
if (env !== 'test') {
|
||||||
throw new Error('export is only supported in test mode');
|
throw new Error('export is only supported in test mode');
|
||||||
}
|
}
|
||||||
|
@ -1266,7 +1267,7 @@ async function importFromDirectory(directory, options) {
|
||||||
|
|
||||||
const archivePath = path.join(directory, ARCHIVE_NAME);
|
const archivePath = path.join(directory, ARCHIVE_NAME);
|
||||||
if (fs.existsSync(archivePath)) {
|
if (fs.existsSync(archivePath)) {
|
||||||
const env = window.getEnvironment();
|
const env = getEnvironment();
|
||||||
if (env !== 'test') {
|
if (env !== 'test') {
|
||||||
throw new Error('import is only supported in test mode');
|
throw new Error('import is only supported in test mode');
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,7 +29,7 @@
|
||||||
"publish-to-apt": "NAME=$npm_package_name VERSION=$npm_package_version ./aptly.sh",
|
"publish-to-apt": "NAME=$npm_package_name VERSION=$npm_package_version ./aptly.sh",
|
||||||
"test": "yarn test-node && yarn test-electron",
|
"test": "yarn test-node && yarn test-electron",
|
||||||
"test-electron": "yarn grunt test",
|
"test-electron": "yarn grunt test",
|
||||||
"test-node": "electron-mocha --require test/setup-test-node.js --recursive test/app test/modules ts/test-node ts/test-both",
|
"test-node": "electron-mocha --file test/setup-test-node.js --recursive test/app test/modules ts/test-node ts/test-both",
|
||||||
"test-node-coverage": "nyc --reporter=lcov --reporter=text mocha --recursive test/app test/modules ts/test-node ts/test-both",
|
"test-node-coverage": "nyc --reporter=lcov --reporter=text mocha --recursive test/app test/modules ts/test-node ts/test-both",
|
||||||
"eslint": "eslint .",
|
"eslint": "eslint .",
|
||||||
"lint": "yarn format --list-different && yarn eslint",
|
"lint": "yarn format --list-different && yarn eslint",
|
||||||
|
|
|
@ -11,14 +11,20 @@ const url = require('url');
|
||||||
const i18n = require('./js/modules/i18n');
|
const i18n = require('./js/modules/i18n');
|
||||||
const { ConfirmationModal } = require('./ts/components/ConfirmationModal');
|
const { ConfirmationModal } = require('./ts/components/ConfirmationModal');
|
||||||
const { makeGetter, makeSetter } = require('./preload_utils');
|
const { makeGetter, makeSetter } = require('./preload_utils');
|
||||||
|
const {
|
||||||
|
getEnvironment,
|
||||||
|
setEnvironment,
|
||||||
|
parseEnvironment,
|
||||||
|
} = require('./ts/environment');
|
||||||
|
|
||||||
const { nativeTheme } = remote.require('electron');
|
const { nativeTheme } = remote.require('electron');
|
||||||
|
|
||||||
const config = url.parse(window.location.toString(), true).query;
|
const config = url.parse(window.location.toString(), true).query;
|
||||||
const { locale } = config;
|
const { locale } = config;
|
||||||
const localeMessages = ipcRenderer.sendSync('locale-data');
|
const localeMessages = ipcRenderer.sendSync('locale-data');
|
||||||
|
setEnvironment(parseEnvironment(config.environment));
|
||||||
|
|
||||||
window.getEnvironment = () => config.environment;
|
window.getEnvironment = getEnvironment;
|
||||||
window.getVersion = () => config.version;
|
window.getVersion = () => config.version;
|
||||||
window.theme = config.theme;
|
window.theme = config.theme;
|
||||||
window.i18n = i18n.setup(locale, localeMessages);
|
window.i18n = i18n.setup(locale, localeMessages);
|
||||||
|
|
14
preload.js
14
preload.js
|
@ -11,6 +11,12 @@ try {
|
||||||
const client = require('libsignal-client');
|
const client = require('libsignal-client');
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const { installGetter, installSetter } = require('./preload_utils');
|
const { installGetter, installSetter } = require('./preload_utils');
|
||||||
|
const {
|
||||||
|
getEnvironment,
|
||||||
|
setEnvironment,
|
||||||
|
parseEnvironment,
|
||||||
|
Environment,
|
||||||
|
} = require('./ts/environment');
|
||||||
|
|
||||||
const { remote } = electron;
|
const { remote } = electron;
|
||||||
const { app } = remote;
|
const { app } = remote;
|
||||||
|
@ -19,9 +25,11 @@ try {
|
||||||
window.PROTO_ROOT = 'protos';
|
window.PROTO_ROOT = 'protos';
|
||||||
const config = require('url').parse(window.location.toString(), true).query;
|
const config = require('url').parse(window.location.toString(), true).query;
|
||||||
|
|
||||||
|
setEnvironment(parseEnvironment(config.environment));
|
||||||
|
|
||||||
let title = config.name;
|
let title = config.name;
|
||||||
if (config.environment !== 'production') {
|
if (getEnvironment() !== Environment.Production) {
|
||||||
title += ` - ${config.environment}`;
|
title += ` - ${getEnvironment()}`;
|
||||||
}
|
}
|
||||||
if (config.appInstance) {
|
if (config.appInstance) {
|
||||||
title += ` - ${config.appInstance}`;
|
title += ` - ${config.appInstance}`;
|
||||||
|
@ -37,7 +45,7 @@ try {
|
||||||
|
|
||||||
window.platform = process.platform;
|
window.platform = process.platform;
|
||||||
window.getTitle = () => title;
|
window.getTitle = () => title;
|
||||||
window.getEnvironment = () => config.environment;
|
window.getEnvironment = getEnvironment;
|
||||||
window.getAppInstance = () => config.appInstance;
|
window.getAppInstance = () => config.appInstance;
|
||||||
window.getVersion = () => config.version;
|
window.getVersion = () => config.version;
|
||||||
window.getExpiration = () => {
|
window.getExpiration = () => {
|
||||||
|
|
|
@ -7,10 +7,16 @@ const { ipcRenderer, remote } = require('electron');
|
||||||
|
|
||||||
const url = require('url');
|
const url = require('url');
|
||||||
const i18n = require('./js/modules/i18n');
|
const i18n = require('./js/modules/i18n');
|
||||||
|
const {
|
||||||
|
getEnvironment,
|
||||||
|
setEnvironment,
|
||||||
|
parseEnvironment,
|
||||||
|
} = require('./ts/environment');
|
||||||
|
|
||||||
const config = url.parse(window.location.toString(), true).query;
|
const config = url.parse(window.location.toString(), true).query;
|
||||||
const { locale } = config;
|
const { locale } = config;
|
||||||
const localeMessages = ipcRenderer.sendSync('locale-data');
|
const localeMessages = ipcRenderer.sendSync('locale-data');
|
||||||
|
setEnvironment(parseEnvironment(config.environment));
|
||||||
|
|
||||||
const { nativeTheme } = remote.require('electron');
|
const { nativeTheme } = remote.require('electron');
|
||||||
|
|
||||||
|
@ -33,7 +39,7 @@ window.subscribeToSystemThemeChange = fn => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
window.getEnvironment = () => config.environment;
|
window.getEnvironment = getEnvironment;
|
||||||
window.getVersion = () => config.version;
|
window.getVersion = () => config.version;
|
||||||
window.getAppInstance = () => config.appInstance;
|
window.getAppInstance = () => config.appInstance;
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,11 @@ const config = require('url').parse(window.location.toString(), true).query;
|
||||||
const { noop, uniqBy } = require('lodash');
|
const { noop, uniqBy } = require('lodash');
|
||||||
const pMap = require('p-map');
|
const pMap = require('p-map');
|
||||||
const { deriveStickerPackKey } = require('../ts/Crypto');
|
const { deriveStickerPackKey } = require('../ts/Crypto');
|
||||||
|
const {
|
||||||
|
getEnvironment,
|
||||||
|
setEnvironment,
|
||||||
|
parseEnvironment,
|
||||||
|
} = require('../ts/environment');
|
||||||
const { makeGetter } = require('../preload_utils');
|
const { makeGetter } = require('../preload_utils');
|
||||||
|
|
||||||
const { dialog } = remote;
|
const { dialog } = remote;
|
||||||
|
@ -21,9 +26,11 @@ const MAX_STICKER_DIMENSION = STICKER_SIZE;
|
||||||
const MAX_WEBP_STICKER_BYTE_LENGTH = 100 * 1024;
|
const MAX_WEBP_STICKER_BYTE_LENGTH = 100 * 1024;
|
||||||
const MAX_ANIMATED_STICKER_BYTE_LENGTH = 300 * 1024;
|
const MAX_ANIMATED_STICKER_BYTE_LENGTH = 300 * 1024;
|
||||||
|
|
||||||
|
setEnvironment(parseEnvironment(config.environment));
|
||||||
|
|
||||||
window.ROOT_PATH = window.location.href.startsWith('file') ? '../../' : '/';
|
window.ROOT_PATH = window.location.href.startsWith('file') ? '../../' : '/';
|
||||||
window.PROTO_ROOT = '../../protos';
|
window.PROTO_ROOT = '../../protos';
|
||||||
window.getEnvironment = () => config.environment;
|
window.getEnvironment = getEnvironment;
|
||||||
window.getVersion = () => config.version;
|
window.getVersion = () => config.version;
|
||||||
window.getGuid = require('uuid/v4');
|
window.getGuid = require('uuid/v4');
|
||||||
window.PQueue = require('p-queue').default;
|
window.PQueue = require('p-queue').default;
|
||||||
|
|
|
@ -3,6 +3,12 @@
|
||||||
|
|
||||||
/* eslint-disable no-console */
|
/* eslint-disable no-console */
|
||||||
|
|
||||||
|
const { setEnvironment, Environment } = require('../ts/environment');
|
||||||
|
|
||||||
|
before(() => {
|
||||||
|
setEnvironment(Environment.Test);
|
||||||
|
});
|
||||||
|
|
||||||
// To replicate logic we have on the client side
|
// To replicate logic we have on the client side
|
||||||
global.window = {
|
global.window = {
|
||||||
log: {
|
log: {
|
||||||
|
|
49
ts/environment.ts
Normal file
49
ts/environment.ts
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
// Many places rely on this enum being a string.
|
||||||
|
export enum Environment {
|
||||||
|
Development = 'development',
|
||||||
|
Production = 'production',
|
||||||
|
Staging = 'staging',
|
||||||
|
Test = 'test',
|
||||||
|
TestLib = 'test-lib',
|
||||||
|
}
|
||||||
|
|
||||||
|
let environment: undefined | Environment;
|
||||||
|
|
||||||
|
export function getEnvironment(): Environment {
|
||||||
|
if (environment === undefined) {
|
||||||
|
// This should never happen—we should always have initialized the environment by this
|
||||||
|
// point. It'd be nice to log here but the logger depends on the environment and we
|
||||||
|
// can't have circular dependencies.
|
||||||
|
return Environment.Production;
|
||||||
|
}
|
||||||
|
return environment;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the current environment. Should be called early in a process's life, and can only
|
||||||
|
* be called once.
|
||||||
|
*/
|
||||||
|
export function setEnvironment(env: Environment): void {
|
||||||
|
if (environment !== undefined) {
|
||||||
|
throw new Error('Environment has already been set');
|
||||||
|
}
|
||||||
|
environment = env;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ENVIRONMENTS_BY_STRING = new Map<string, Environment>([
|
||||||
|
['development', Environment.Development],
|
||||||
|
['production', Environment.Production],
|
||||||
|
['staging', Environment.Staging],
|
||||||
|
['test', Environment.Test],
|
||||||
|
['test-lib', Environment.TestLib],
|
||||||
|
]);
|
||||||
|
export function parseEnvironment(value: unknown): Environment {
|
||||||
|
if (typeof value !== 'string') {
|
||||||
|
return Environment.Production;
|
||||||
|
}
|
||||||
|
const result = ENVIRONMENTS_BY_STRING.get(value);
|
||||||
|
return result || Environment.Production;
|
||||||
|
}
|
33
ts/logging/log.ts
Normal file
33
ts/logging/log.ts
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import { noop } from 'lodash';
|
||||||
|
import { LogLevel } from './shared';
|
||||||
|
|
||||||
|
type LogAtLevelFnType = (
|
||||||
|
level: LogLevel,
|
||||||
|
...args: ReadonlyArray<unknown>
|
||||||
|
) => void;
|
||||||
|
|
||||||
|
let logAtLevel: LogAtLevelFnType = noop;
|
||||||
|
let hasInitialized = false;
|
||||||
|
|
||||||
|
type LogFn = (...args: ReadonlyArray<unknown>) => void;
|
||||||
|
export const fatal: LogFn = (...args) => logAtLevel(LogLevel.Fatal, ...args);
|
||||||
|
export const error: LogFn = (...args) => logAtLevel(LogLevel.Error, ...args);
|
||||||
|
export const warn: LogFn = (...args) => logAtLevel(LogLevel.Warn, ...args);
|
||||||
|
export const info: LogFn = (...args) => logAtLevel(LogLevel.Info, ...args);
|
||||||
|
export const debug: LogFn = (...args) => logAtLevel(LogLevel.Debug, ...args);
|
||||||
|
export const trace: LogFn = (...args) => logAtLevel(LogLevel.Trace, ...args);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the low-level logging interface. Should be called early in a process's life, and
|
||||||
|
* can only be called once.
|
||||||
|
*/
|
||||||
|
export function setLogAtLevel(log: LogAtLevelFnType): void {
|
||||||
|
if (hasInitialized) {
|
||||||
|
throw new Error('Logger has already been initialized');
|
||||||
|
}
|
||||||
|
logAtLevel = log;
|
||||||
|
hasInitialized = true;
|
||||||
|
}
|
|
@ -19,6 +19,7 @@ import {
|
||||||
getLogLevelString,
|
getLogLevelString,
|
||||||
isLogEntry,
|
isLogEntry,
|
||||||
} from './shared';
|
} from './shared';
|
||||||
|
import * as log from './log';
|
||||||
import { reallyJsonStringify } from '../util/reallyJsonStringify';
|
import { reallyJsonStringify } from '../util/reallyJsonStringify';
|
||||||
|
|
||||||
// To make it easier to visually scan logs, we make all levels the same length
|
// To make it easier to visually scan logs, we make all levels the same length
|
||||||
|
@ -33,13 +34,13 @@ function now() {
|
||||||
return date.toJSON();
|
return date.toJSON();
|
||||||
}
|
}
|
||||||
|
|
||||||
function log(...args: ReadonlyArray<unknown>) {
|
function consoleLog(...args: ReadonlyArray<unknown>) {
|
||||||
logAtLevel(LogLevel.Info, ...args);
|
logAtLevel(LogLevel.Info, ...args);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (window.console) {
|
if (window.console) {
|
||||||
console._log = console.log;
|
console._log = console.log;
|
||||||
console.log = log;
|
console.log = consoleLog;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The mechanics of preparing a log for publish
|
// The mechanics of preparing a log for publish
|
||||||
|
@ -126,13 +127,15 @@ function logAtLevel(level: LogLevel, ...args: ReadonlyArray<unknown>): void {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.setLogAtLevel(logAtLevel);
|
||||||
|
|
||||||
window.log = {
|
window.log = {
|
||||||
fatal: _.partial(logAtLevel, LogLevel.Fatal),
|
fatal: log.fatal,
|
||||||
error: _.partial(logAtLevel, LogLevel.Error),
|
error: log.error,
|
||||||
warn: _.partial(logAtLevel, LogLevel.Warn),
|
warn: log.warn,
|
||||||
info: _.partial(logAtLevel, LogLevel.Info),
|
info: log.info,
|
||||||
debug: _.partial(logAtLevel, LogLevel.Debug),
|
debug: log.debug,
|
||||||
trace: _.partial(logAtLevel, LogLevel.Trace),
|
trace: log.trace,
|
||||||
fetch,
|
fetch,
|
||||||
publish,
|
publish,
|
||||||
};
|
};
|
||||||
|
|
|
@ -16,15 +16,17 @@ import {
|
||||||
get,
|
get,
|
||||||
groupBy,
|
groupBy,
|
||||||
isFunction,
|
isFunction,
|
||||||
isObject,
|
|
||||||
last,
|
last,
|
||||||
map,
|
map,
|
||||||
|
omit,
|
||||||
set,
|
set,
|
||||||
} from 'lodash';
|
} from 'lodash';
|
||||||
|
|
||||||
import { arrayBufferToBase64, base64ToArrayBuffer } from '../Crypto';
|
import { arrayBufferToBase64, base64ToArrayBuffer } from '../Crypto';
|
||||||
import { CURRENT_SCHEMA_VERSION } from '../../js/modules/types/message';
|
import { CURRENT_SCHEMA_VERSION } from '../../js/modules/types/message';
|
||||||
import { createBatcher } from '../util/batcher';
|
import { createBatcher } from '../util/batcher';
|
||||||
|
import { assert } from '../util/assert';
|
||||||
|
import { cleanDataForIpc } from './cleanDataForIpc';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ConversationModelCollectionType,
|
ConversationModelCollectionType,
|
||||||
|
@ -231,7 +233,6 @@ const dataInterface: ClientInterface = {
|
||||||
// Client-side only, and test-only
|
// Client-side only, and test-only
|
||||||
|
|
||||||
_removeConversations,
|
_removeConversations,
|
||||||
_cleanData,
|
|
||||||
_jobs,
|
_jobs,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -251,55 +252,22 @@ const channelsAsUnknown = fromPairs(
|
||||||
|
|
||||||
const channels: ServerInterface = channelsAsUnknown;
|
const channels: ServerInterface = channelsAsUnknown;
|
||||||
|
|
||||||
// When IPC arguments are prepared for the cross-process send, they are serialized with
|
function _cleanData(
|
||||||
// the [structured clone algorithm][0]. We can't send some values, like BigNumbers and
|
data: unknown
|
||||||
// functions (both of which come from protobufjs), so we clean them up.
|
): ReturnType<typeof cleanDataForIpc>['cleaned'] {
|
||||||
// [0]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm
|
const { cleaned, pathsChanged } = cleanDataForIpc(data);
|
||||||
function _cleanData(data: any, path = 'root') {
|
|
||||||
if (data === null || data === undefined) {
|
|
||||||
window.log.warn(`_cleanData: null or undefined value at path ${path}`);
|
|
||||||
|
|
||||||
return data;
|
if (pathsChanged.length) {
|
||||||
|
window.log.info(
|
||||||
|
`_cleanData cleaned the following paths: ${pathsChanged.join(', ')}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
return cleaned;
|
||||||
typeof data === 'string' ||
|
}
|
||||||
typeof data === 'number' ||
|
|
||||||
typeof data === 'boolean'
|
|
||||||
) {
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
const keys = Object.keys(data);
|
function _cleanMessageData(data: MessageType): MessageType {
|
||||||
const max = keys.length;
|
return _cleanData(omit(data, ['dataMessage']));
|
||||||
for (let index = 0; index < max; index += 1) {
|
|
||||||
const key = keys[index];
|
|
||||||
const value = data[key];
|
|
||||||
|
|
||||||
if (value === null || value === undefined) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isFunction(value)) {
|
|
||||||
delete data[key];
|
|
||||||
} else if (isFunction(value.toNumber)) {
|
|
||||||
data[key] = value.toNumber();
|
|
||||||
} else if (Array.isArray(value)) {
|
|
||||||
data[key] = value.map((item, mapIndex) =>
|
|
||||||
_cleanData(item, `${path}.${key}.${mapIndex}`)
|
|
||||||
);
|
|
||||||
} else if (isObject(value)) {
|
|
||||||
data[key] = _cleanData(value, `${path}.${key}`);
|
|
||||||
} else if (
|
|
||||||
typeof value !== 'string' &&
|
|
||||||
typeof value !== 'number' &&
|
|
||||||
typeof value !== 'boolean'
|
|
||||||
) {
|
|
||||||
window.log.info(`_cleanData: key ${key} had type ${typeof value}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return data;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function _shutdown() {
|
async function _shutdown() {
|
||||||
|
@ -764,7 +732,12 @@ function updateConversation(data: ConversationType) {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateConversations(array: Array<ConversationType>) {
|
async function updateConversations(array: Array<ConversationType>) {
|
||||||
await channels.updateConversations(array);
|
const { cleaned, pathsChanged } = cleanDataForIpc(array);
|
||||||
|
assert(
|
||||||
|
!pathsChanged.length,
|
||||||
|
`Paths were cleaned: ${JSON.stringify(pathsChanged)}`
|
||||||
|
);
|
||||||
|
await channels.updateConversations(cleaned);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function removeConversation(
|
async function removeConversation(
|
||||||
|
@ -884,7 +857,9 @@ async function saveMessage(
|
||||||
data: MessageType,
|
data: MessageType,
|
||||||
{ forceSave, Message }: { forceSave?: boolean; Message: typeof MessageModel }
|
{ forceSave, Message }: { forceSave?: boolean; Message: typeof MessageModel }
|
||||||
) {
|
) {
|
||||||
const id = await channels.saveMessage(_cleanData(data), { forceSave });
|
const id = await channels.saveMessage(_cleanMessageData(data), {
|
||||||
|
forceSave,
|
||||||
|
});
|
||||||
Message.updateTimers();
|
Message.updateTimers();
|
||||||
|
|
||||||
return id;
|
return id;
|
||||||
|
@ -894,7 +869,10 @@ async function saveMessages(
|
||||||
arrayOfMessages: Array<MessageType>,
|
arrayOfMessages: Array<MessageType>,
|
||||||
{ forceSave }: { forceSave?: boolean } = {}
|
{ forceSave }: { forceSave?: boolean } = {}
|
||||||
) {
|
) {
|
||||||
await channels.saveMessages(_cleanData(arrayOfMessages), { forceSave });
|
await channels.saveMessages(
|
||||||
|
arrayOfMessages.map(message => _cleanMessageData(message)),
|
||||||
|
{ forceSave }
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function removeMessage(
|
async function removeMessage(
|
||||||
|
|
|
@ -393,7 +393,6 @@ export type ClientInterface = DataInterface & {
|
||||||
// Client-side only, and test-only
|
// Client-side only, and test-only
|
||||||
|
|
||||||
_removeConversations: (ids: Array<string>) => Promise<void>;
|
_removeConversations: (ids: Array<string>) => Promise<void>;
|
||||||
_cleanData: (data: any, path?: string) => any;
|
|
||||||
_jobs: { [id: string]: ClientJobType };
|
_jobs: { [id: string]: ClientJobType };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
163
ts/sql/cleanDataForIpc.ts
Normal file
163
ts/sql/cleanDataForIpc.ts
Normal file
|
@ -0,0 +1,163 @@
|
||||||
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import { isPlainObject } from 'lodash';
|
||||||
|
|
||||||
|
import { isIterable } from '../util/isIterable';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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);
|
||||||
|
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
|
||||||
|
| 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>
|
||||||
|
): CleanedDataValue {
|
||||||
|
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': {
|
||||||
|
if (data === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(data)) {
|
||||||
|
const result: CleanedArray = [];
|
||||||
|
data.forEach((item, index) => {
|
||||||
|
const indexPath = `${path}.${index}`;
|
||||||
|
if (item === undefined || item === null) {
|
||||||
|
pathsChanged.push(indexPath);
|
||||||
|
} else {
|
||||||
|
result.push(cleanDataInner(item, indexPath, pathsChanged));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
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
|
||||||
|
);
|
||||||
|
} 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isIterable(dataAsRecord)) {
|
||||||
|
const result: CleanedArray = [];
|
||||||
|
let index = 0;
|
||||||
|
pathsChanged.push(path);
|
||||||
|
// `for ... of` is the cleanest way to go through "generic" iterables without
|
||||||
|
// a helper library.
|
||||||
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
|
for (const value of dataAsRecord) {
|
||||||
|
result.push(
|
||||||
|
cleanDataInner(
|
||||||
|
value,
|
||||||
|
`${path}.<iterator index ${index}>`,
|
||||||
|
pathsChanged
|
||||||
|
)
|
||||||
|
);
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
pathsChanged.push(path);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
41
ts/test-both/environment.ts
Normal file
41
ts/test-both/environment.ts
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import { assert } from 'chai';
|
||||||
|
|
||||||
|
import { parseEnvironment, Environment } from '../environment';
|
||||||
|
|
||||||
|
describe('environment utilities', () => {
|
||||||
|
describe('parseEnvironment', () => {
|
||||||
|
it('returns Environment.Production for non-strings', () => {
|
||||||
|
assert.equal(parseEnvironment(undefined), Environment.Production);
|
||||||
|
assert.equal(parseEnvironment(0), Environment.Production);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns Environment.Production for invalid strings', () => {
|
||||||
|
assert.equal(parseEnvironment(''), Environment.Production);
|
||||||
|
assert.equal(parseEnvironment(' development '), Environment.Production);
|
||||||
|
assert.equal(parseEnvironment('PRODUCTION'), Environment.Production);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('parses "development" as Environment.Development', () => {
|
||||||
|
assert.equal(parseEnvironment('development'), Environment.Development);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('parses "production" as Environment.Production', () => {
|
||||||
|
assert.equal(parseEnvironment('production'), Environment.Production);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('parses "staging" as Environment.Staging', () => {
|
||||||
|
assert.equal(parseEnvironment('staging'), Environment.Staging);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('parses "test" as Environment.Test', () => {
|
||||||
|
assert.equal(parseEnvironment('test'), Environment.Test);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('parses "test-lib" as Environment.TestLib', () => {
|
||||||
|
assert.equal(parseEnvironment('test-lib'), Environment.TestLib);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
254
ts/test-both/sql/cleanDataForIpc_test.ts
Normal file
254
ts/test-both/sql/cleanDataForIpc_test.ts
Normal file
|
@ -0,0 +1,254 @@
|
||||||
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import { assert } from 'chai';
|
||||||
|
import { noop } from 'lodash';
|
||||||
|
|
||||||
|
import { cleanDataForIpc } from '../../sql/cleanDataForIpc';
|
||||||
|
|
||||||
|
describe('cleanDataForIpc', () => {
|
||||||
|
it('does nothing to JSON primitives', () => {
|
||||||
|
['', 'foo bar', 0, 123, true, false, null].forEach(value => {
|
||||||
|
assert.deepEqual(cleanDataForIpc(value), {
|
||||||
|
cleaned: value,
|
||||||
|
pathsChanged: [],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does nothing to undefined', () => {
|
||||||
|
// Though `undefined` is not technically JSON-serializable, we don't clean it because
|
||||||
|
// its key is dropped.
|
||||||
|
assert.deepEqual(cleanDataForIpc(undefined), {
|
||||||
|
cleaned: undefined,
|
||||||
|
pathsChanged: [],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('converts BigInts to strings', () => {
|
||||||
|
assert.deepEqual(cleanDataForIpc(BigInt(0)), {
|
||||||
|
cleaned: '0',
|
||||||
|
pathsChanged: ['root'],
|
||||||
|
});
|
||||||
|
assert.deepEqual(cleanDataForIpc(BigInt(123)), {
|
||||||
|
cleaned: '123',
|
||||||
|
pathsChanged: ['root'],
|
||||||
|
});
|
||||||
|
assert.deepEqual(cleanDataForIpc(BigInt(-123)), {
|
||||||
|
cleaned: '-123',
|
||||||
|
pathsChanged: ['root'],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('converts functions to `undefined` but does not mark them as cleaned, for backwards compatibility', () => {
|
||||||
|
assert.deepEqual(cleanDataForIpc(noop), {
|
||||||
|
cleaned: undefined,
|
||||||
|
pathsChanged: [],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('converts symbols to `undefined`', () => {
|
||||||
|
assert.deepEqual(cleanDataForIpc(Symbol('test')), {
|
||||||
|
cleaned: undefined,
|
||||||
|
pathsChanged: ['root'],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('converts ArrayBuffers to `undefined`', () => {
|
||||||
|
assert.deepEqual(cleanDataForIpc(new ArrayBuffer(2)), {
|
||||||
|
cleaned: undefined,
|
||||||
|
pathsChanged: ['root'],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('converts valid dates to ISO strings', () => {
|
||||||
|
assert.deepEqual(cleanDataForIpc(new Date(924588548000)), {
|
||||||
|
cleaned: '1999-04-20T06:09:08.000Z',
|
||||||
|
pathsChanged: ['root'],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('converts invalid dates to `undefined`', () => {
|
||||||
|
assert.deepEqual(cleanDataForIpc(new Date(NaN)), {
|
||||||
|
cleaned: undefined,
|
||||||
|
pathsChanged: ['root'],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('converts other iterables to arrays', () => {
|
||||||
|
assert.deepEqual(cleanDataForIpc(new Uint8Array([1, 2, 3])), {
|
||||||
|
cleaned: [1, 2, 3],
|
||||||
|
pathsChanged: ['root'],
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepEqual(cleanDataForIpc(new Float32Array([1, 2, 3])), {
|
||||||
|
cleaned: [1, 2, 3],
|
||||||
|
pathsChanged: ['root'],
|
||||||
|
});
|
||||||
|
|
||||||
|
function* generator() {
|
||||||
|
yield 1;
|
||||||
|
yield 2;
|
||||||
|
}
|
||||||
|
assert.deepEqual(cleanDataForIpc(generator()), {
|
||||||
|
cleaned: [1, 2],
|
||||||
|
pathsChanged: ['root'],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('deeply cleans arrays, removing `undefined` and `null`s', () => {
|
||||||
|
const result = cleanDataForIpc([
|
||||||
|
12,
|
||||||
|
Symbol('top level symbol'),
|
||||||
|
{ foo: 3, symb: Symbol('nested symbol 1') },
|
||||||
|
[45, Symbol('nested symbol 2')],
|
||||||
|
undefined,
|
||||||
|
null,
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert.deepEqual(result.cleaned, [
|
||||||
|
12,
|
||||||
|
undefined,
|
||||||
|
{
|
||||||
|
foo: 3,
|
||||||
|
symb: undefined,
|
||||||
|
},
|
||||||
|
[45, undefined],
|
||||||
|
]);
|
||||||
|
assert.sameMembers(result.pathsChanged, [
|
||||||
|
'root.1',
|
||||||
|
'root.2.symb',
|
||||||
|
'root.3.1',
|
||||||
|
'root.4',
|
||||||
|
'root.5',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('deeply cleans sets and converts them to arrays', () => {
|
||||||
|
const result = cleanDataForIpc(
|
||||||
|
new Set([
|
||||||
|
12,
|
||||||
|
Symbol('top level symbol'),
|
||||||
|
{ foo: 3, symb: Symbol('nested symbol 1') },
|
||||||
|
[45, Symbol('nested symbol 2')],
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.isArray(result.cleaned);
|
||||||
|
assert.sameDeepMembers(result.cleaned, [
|
||||||
|
12,
|
||||||
|
undefined,
|
||||||
|
{
|
||||||
|
foo: 3,
|
||||||
|
symb: undefined,
|
||||||
|
},
|
||||||
|
[45, undefined],
|
||||||
|
]);
|
||||||
|
assert.sameMembers(result.pathsChanged, [
|
||||||
|
'root',
|
||||||
|
'root.<iterator index 1>',
|
||||||
|
'root.<iterator index 2>.symb',
|
||||||
|
'root.<iterator index 3>.1',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('deeply cleans maps and converts them to objects', () => {
|
||||||
|
const result = cleanDataForIpc(
|
||||||
|
new Map<unknown, unknown>([
|
||||||
|
['key 1', 'value'],
|
||||||
|
[Symbol('symbol key'), 'dropped'],
|
||||||
|
['key 2', ['foo', Symbol('nested symbol')]],
|
||||||
|
[3, 'dropped'],
|
||||||
|
[BigInt(4), 'dropped'],
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.deepEqual(result.cleaned, {
|
||||||
|
'key 1': 'value',
|
||||||
|
'key 2': ['foo', undefined],
|
||||||
|
});
|
||||||
|
assert.sameMembers(result.pathsChanged, [
|
||||||
|
'root',
|
||||||
|
'root.<map key Symbol(symbol key)>',
|
||||||
|
'root.<map value at key 2>.1',
|
||||||
|
'root.<map key 3>',
|
||||||
|
'root.<map key 4>',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls `toNumber` when available', () => {
|
||||||
|
assert.deepEqual(
|
||||||
|
cleanDataForIpc([
|
||||||
|
{
|
||||||
|
toNumber() {
|
||||||
|
return 5;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
toNumber() {
|
||||||
|
return Symbol('bogus');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
{
|
||||||
|
cleaned: [5, undefined],
|
||||||
|
pathsChanged: ['root.1'],
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('deeply cleans objects with a `null` prototype', () => {
|
||||||
|
const value = Object.assign(Object.create(null), {
|
||||||
|
'key 1': 'value',
|
||||||
|
[Symbol('symbol key')]: 'dropped',
|
||||||
|
'key 2': ['foo', Symbol('nested symbol')],
|
||||||
|
});
|
||||||
|
const result = cleanDataForIpc(value);
|
||||||
|
|
||||||
|
assert.deepEqual(result.cleaned, {
|
||||||
|
'key 1': 'value',
|
||||||
|
'key 2': ['foo', undefined],
|
||||||
|
});
|
||||||
|
assert.sameMembers(result.pathsChanged, ['root.key 2.1']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('deeply cleans objects with a prototype of `Object.prototype`', () => {
|
||||||
|
const value = {
|
||||||
|
'key 1': 'value',
|
||||||
|
[Symbol('symbol key')]: 'dropped',
|
||||||
|
'key 2': ['foo', Symbol('nested symbol')],
|
||||||
|
};
|
||||||
|
const result = cleanDataForIpc(value);
|
||||||
|
|
||||||
|
assert.deepEqual(result.cleaned, {
|
||||||
|
'key 1': 'value',
|
||||||
|
'key 2': ['foo', undefined],
|
||||||
|
});
|
||||||
|
assert.sameMembers(result.pathsChanged, ['root.key 2.1']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('deeply cleans class instances', () => {
|
||||||
|
class Person {
|
||||||
|
public toBeDiscarded = Symbol('to be discarded');
|
||||||
|
|
||||||
|
constructor(public firstName: string, public lastName: string) {}
|
||||||
|
|
||||||
|
get name() {
|
||||||
|
return this.getName();
|
||||||
|
}
|
||||||
|
|
||||||
|
getName() {
|
||||||
|
return `${this.firstName} ${this.lastName}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const person = new Person('Selena', 'Gomez');
|
||||||
|
const result = cleanDataForIpc(person);
|
||||||
|
|
||||||
|
assert.deepEqual(result.cleaned, {
|
||||||
|
firstName: 'Selena',
|
||||||
|
lastName: 'Gomez',
|
||||||
|
toBeDiscarded: undefined,
|
||||||
|
});
|
||||||
|
assert.sameMembers(result.pathsChanged, ['root', 'root.toBeDiscarded']);
|
||||||
|
});
|
||||||
|
});
|
18
ts/test-both/util/assert_test.ts
Normal file
18
ts/test-both/util/assert_test.ts
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import * as chai from 'chai';
|
||||||
|
|
||||||
|
import { assert } from '../../util/assert';
|
||||||
|
|
||||||
|
describe('assert', () => {
|
||||||
|
it('does nothing if the assertion passes', () => {
|
||||||
|
assert(true, 'foo bar');
|
||||||
|
});
|
||||||
|
|
||||||
|
it("throws because we're in a test environment", () => {
|
||||||
|
chai.assert.throws(() => {
|
||||||
|
assert(false, 'foo bar');
|
||||||
|
}, 'foo bar');
|
||||||
|
});
|
||||||
|
});
|
50
ts/test-both/util/isIterable_test.ts
Normal file
50
ts/test-both/util/isIterable_test.ts
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import { assert } from 'chai';
|
||||||
|
|
||||||
|
import { isIterable } from '../../util/isIterable';
|
||||||
|
|
||||||
|
describe('isIterable', () => {
|
||||||
|
it('returns false for non-iterables', () => {
|
||||||
|
assert.isFalse(isIterable(undefined));
|
||||||
|
assert.isFalse(isIterable(null));
|
||||||
|
assert.isFalse(isIterable(123));
|
||||||
|
assert.isFalse(isIterable({ foo: 'bar' }));
|
||||||
|
assert.isFalse(
|
||||||
|
isIterable({
|
||||||
|
length: 2,
|
||||||
|
'0': 'fake',
|
||||||
|
'1': 'array',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns true for iterables', () => {
|
||||||
|
assert.isTrue(isIterable('strings are iterable'));
|
||||||
|
assert.isTrue(isIterable(['arrays too']));
|
||||||
|
assert.isTrue(isIterable(new Set('and sets')));
|
||||||
|
assert.isTrue(isIterable(new Map([['and', 'maps']])));
|
||||||
|
assert.isTrue(
|
||||||
|
isIterable({
|
||||||
|
[Symbol.iterator]() {
|
||||||
|
return {
|
||||||
|
next() {
|
||||||
|
return {
|
||||||
|
value: 'endless iterable',
|
||||||
|
done: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
assert.isTrue(
|
||||||
|
isIterable(
|
||||||
|
(function* generators() {
|
||||||
|
yield 123;
|
||||||
|
})()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
21
ts/util/assert.ts
Normal file
21
ts/util/assert.ts
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import { getEnvironment, Environment } from '../environment';
|
||||||
|
import * as log from '../logging/log';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* In production, logs an error and continues. In all other environments, throws an error.
|
||||||
|
*/
|
||||||
|
export function assert(condition: unknown, message: string): asserts condition {
|
||||||
|
if (!condition) {
|
||||||
|
const err = new Error(message);
|
||||||
|
if (getEnvironment() !== Environment.Production) {
|
||||||
|
if (getEnvironment() === Environment.Development) {
|
||||||
|
debugger; // eslint-disable-line no-debugger
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
log.error(err);
|
||||||
|
}
|
||||||
|
}
|
9
ts/util/isIterable.ts
Normal file
9
ts/util/isIterable.ts
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
export function isIterable(value: unknown): value is Iterable<unknown> {
|
||||||
|
return (
|
||||||
|
(typeof value === 'object' && value !== null && Symbol.iterator in value) ||
|
||||||
|
typeof value === 'string'
|
||||||
|
);
|
||||||
|
}
|
3
ts/window.d.ts
vendored
3
ts/window.d.ts
vendored
|
@ -30,6 +30,7 @@ import * as Groups from './groups';
|
||||||
import * as Crypto from './Crypto';
|
import * as Crypto from './Crypto';
|
||||||
import * as RemoteConfig from './RemoteConfig';
|
import * as RemoteConfig from './RemoteConfig';
|
||||||
import * as OS from './OS';
|
import * as OS from './OS';
|
||||||
|
import { getEnvironment } from './environment';
|
||||||
import * as zkgroup from './util/zkgroup';
|
import * as zkgroup from './util/zkgroup';
|
||||||
import { LocalizerType, BodyRangesType, BodyRangeType } from './types/Util';
|
import { LocalizerType, BodyRangesType, BodyRangeType } from './types/Util';
|
||||||
import * as Attachment from './types/Attachment';
|
import * as Attachment from './types/Attachment';
|
||||||
|
@ -141,7 +142,7 @@ declare global {
|
||||||
getCallSystemNotification: () => Promise<boolean>;
|
getCallSystemNotification: () => Promise<boolean>;
|
||||||
getConversations: () => ConversationModelCollectionType;
|
getConversations: () => ConversationModelCollectionType;
|
||||||
getCountMutedConversations: () => Promise<boolean>;
|
getCountMutedConversations: () => Promise<boolean>;
|
||||||
getEnvironment: () => string;
|
getEnvironment: typeof getEnvironment;
|
||||||
getExpiration: () => string;
|
getExpiration: () => string;
|
||||||
getGuid: () => string;
|
getGuid: () => string;
|
||||||
getInboxCollection: () => ConversationModelCollectionType;
|
getInboxCollection: () => ConversationModelCollectionType;
|
||||||
|
|
Loading…
Reference in a new issue