Add schema utils
This commit is contained in:
parent
c8a729f8be
commit
b26466e59d
45 changed files with 674 additions and 151 deletions
|
@ -9,6 +9,7 @@ import LRU from 'lru-cache';
|
|||
|
||||
import type { OptionalResourceService } from './OptionalResourceService';
|
||||
import { SignalService as Proto } from '../ts/protobuf';
|
||||
import { parseUnknown } from '../ts/util/schemas';
|
||||
|
||||
const MANIFEST_PATH = join(__dirname, '..', 'build', 'jumbomoji.json');
|
||||
|
||||
|
@ -64,8 +65,9 @@ export class EmojiService {
|
|||
public static async create(
|
||||
resourceService: OptionalResourceService
|
||||
): Promise<EmojiService> {
|
||||
const json = await readFile(MANIFEST_PATH, 'utf8');
|
||||
const manifest = manifestSchema.parse(JSON.parse(json));
|
||||
const contents = await readFile(MANIFEST_PATH, 'utf8');
|
||||
const json: unknown = JSON.parse(contents);
|
||||
const manifest = parseUnknown(manifestSchema, json);
|
||||
return new EmojiService(resourceService, manifest);
|
||||
}
|
||||
|
||||
|
|
|
@ -17,6 +17,7 @@ import { OptionalResourcesDictSchema } from '../ts/types/OptionalResource';
|
|||
import * as log from '../ts/logging/log';
|
||||
import { getGotOptions } from '../ts/updater/got';
|
||||
import { drop } from '../ts/util/drop';
|
||||
import { parseUnknown } from '../ts/util/schemas';
|
||||
|
||||
const RESOURCES_DICT_PATH = join(
|
||||
__dirname,
|
||||
|
@ -106,8 +107,10 @@ export class OptionalResourceService {
|
|||
return;
|
||||
}
|
||||
|
||||
const json = JSON.parse(await readFile(RESOURCES_DICT_PATH, 'utf8'));
|
||||
this.maybeDeclaration = OptionalResourcesDictSchema.parse(json);
|
||||
const json: unknown = JSON.parse(
|
||||
await readFile(RESOURCES_DICT_PATH, 'utf8')
|
||||
);
|
||||
this.maybeDeclaration = parseUnknown(OptionalResourcesDictSchema, json);
|
||||
|
||||
// Clean unknown resources
|
||||
let subPaths: Array<string>;
|
||||
|
|
|
@ -56,6 +56,7 @@ import {
|
|||
isVideoTypeSupported,
|
||||
} from '../ts/util/GoogleChrome';
|
||||
import { decryptAttachmentV2ToSink } from '../ts/AttachmentCrypto';
|
||||
import { parseLoose } from '../ts/util/schemas';
|
||||
|
||||
let initialized = false;
|
||||
|
||||
|
@ -471,7 +472,7 @@ export async function handleAttachmentRequest(req: Request): Promise<Response> {
|
|||
let disposition: z.infer<typeof dispositionSchema> = 'attachment';
|
||||
const dispositionParam = url.searchParams.get('disposition');
|
||||
if (dispositionParam != null) {
|
||||
disposition = dispositionSchema.parse(dispositionParam);
|
||||
disposition = parseLoose(dispositionSchema, dispositionParam);
|
||||
}
|
||||
|
||||
strictAssert(attachmentsDir != null, 'not initialized');
|
||||
|
|
|
@ -12,6 +12,7 @@ import * as Errors from '../ts/types/errors';
|
|||
import { isProduction } from '../ts/util/version';
|
||||
import { isNotNil } from '../ts/util/isNotNil';
|
||||
import OS from '../ts/util/os/osMain';
|
||||
import { parseUnknown } from '../ts/util/schemas';
|
||||
|
||||
// See https://github.com/rust-minidump/rust-minidump/blob/main/minidump-processor/json-schema.md
|
||||
const dumpString = z.string().or(z.null()).optional();
|
||||
|
@ -120,9 +121,8 @@ export function setup(
|
|||
pendingDumps.map(async fullPath => {
|
||||
const content = await readFile(fullPath);
|
||||
try {
|
||||
const dump = dumpSchema.parse(
|
||||
JSON.parse(dumpToJSONString(content))
|
||||
);
|
||||
const json: unknown = JSON.parse(dumpToJSONString(content));
|
||||
const dump = parseUnknown(dumpSchema, json);
|
||||
if (dump.crash_info?.type !== 'Simulated Exception') {
|
||||
return fullPath;
|
||||
}
|
||||
|
@ -173,7 +173,8 @@ export function setup(
|
|||
const content = await readFile(fullPath);
|
||||
const { mtime } = await stat(fullPath);
|
||||
|
||||
const dump = dumpSchema.parse(JSON.parse(dumpToJSONString(content)));
|
||||
const json: unknown = JSON.parse(dumpToJSONString(content));
|
||||
const dump = parseUnknown(dumpSchema, json);
|
||||
|
||||
if (dump.crash_info?.type === 'Simulated Exception') {
|
||||
return undefined;
|
||||
|
|
|
@ -5,6 +5,7 @@ import { join } from 'path';
|
|||
import { readFile } from 'fs/promises';
|
||||
import { DNSFallbackSchema } from '../ts/types/DNSFallback';
|
||||
import type { DNSFallbackType } from '../ts/types/DNSFallback';
|
||||
import { parseUnknown } from '../ts/util/schemas';
|
||||
|
||||
let cached: DNSFallbackType | undefined;
|
||||
|
||||
|
@ -25,9 +26,9 @@ export async function getDNSFallback(): Promise<DNSFallbackType> {
|
|||
return cached;
|
||||
}
|
||||
|
||||
const json = JSON.parse(str);
|
||||
const json: unknown = JSON.parse(str);
|
||||
|
||||
const result = DNSFallbackSchema.parse(json);
|
||||
const result = parseUnknown(DNSFallbackSchema, json);
|
||||
cached = result;
|
||||
return result;
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ import type { LoggerType } from '../ts/types/Logging';
|
|||
import type { HourCyclePreference, LocaleMessagesType } from '../ts/types/I18N';
|
||||
import type { LocalizerType } from '../ts/types/Util';
|
||||
import * as Errors from '../ts/types/errors';
|
||||
import { parseUnknown } from '../ts/util/schemas';
|
||||
|
||||
const TextInfoSchema = z.object({
|
||||
direction: z.enum(['ltr', 'rtl']),
|
||||
|
@ -70,16 +71,18 @@ function getLocaleDirection(
|
|||
try {
|
||||
// @ts-expect-error -- TS doesn't know about this method
|
||||
if (typeof locale.getTextInfo === 'function') {
|
||||
return TextInfoSchema.parse(
|
||||
return parseUnknown(
|
||||
TextInfoSchema,
|
||||
// @ts-expect-error -- TS doesn't know about this method
|
||||
locale.getTextInfo()
|
||||
locale.getTextInfo() as unknown
|
||||
).direction;
|
||||
}
|
||||
// @ts-expect-error -- TS doesn't know about this property
|
||||
if (typeof locale.textInfo === 'object') {
|
||||
return TextInfoSchema.parse(
|
||||
return parseUnknown(
|
||||
TextInfoSchema,
|
||||
// @ts-expect-error -- TS doesn't know about this property
|
||||
locale.textInfo
|
||||
locale.textInfo as unknown
|
||||
).direction;
|
||||
}
|
||||
} catch (error) {
|
||||
|
|
|
@ -123,6 +123,7 @@ import { ZoomFactorService } from '../ts/services/ZoomFactorService';
|
|||
import { SafeStorageBackendChangeError } from '../ts/types/SafeStorageBackendChangeError';
|
||||
import { LINUX_PASSWORD_STORE_FLAGS } from '../ts/util/linuxPasswordStoreFlags';
|
||||
import { getOwn } from '../ts/util/getOwn';
|
||||
import { safeParseLoose, safeParseUnknown } from '../ts/util/schemas';
|
||||
|
||||
const animationSettings = systemPreferences.getAnimationSettings();
|
||||
|
||||
|
@ -436,7 +437,8 @@ export const windowConfigSchema = z.object({
|
|||
type WindowConfigType = z.infer<typeof windowConfigSchema>;
|
||||
|
||||
let windowConfig: WindowConfigType | undefined;
|
||||
const windowConfigParsed = windowConfigSchema.safeParse(
|
||||
const windowConfigParsed = safeParseUnknown(
|
||||
windowConfigSchema,
|
||||
windowFromEphemeral || windowFromUserConfig
|
||||
);
|
||||
if (windowConfigParsed.success) {
|
||||
|
@ -2692,7 +2694,7 @@ ipc.on('delete-all-data', () => {
|
|||
ipc.on('get-config', async event => {
|
||||
const theme = await getResolvedThemeSetting();
|
||||
|
||||
const directoryConfig = directoryConfigSchema.safeParse({
|
||||
const directoryConfig = safeParseLoose(directoryConfigSchema, {
|
||||
directoryUrl: config.get<string | null>('directoryUrl') || undefined,
|
||||
directoryMRENCLAVE:
|
||||
config.get<string | null>('directoryMRENCLAVE') || undefined,
|
||||
|
@ -2705,7 +2707,7 @@ ipc.on('get-config', async event => {
|
|||
);
|
||||
}
|
||||
|
||||
const parsed = rendererConfigSchema.safeParse({
|
||||
const parsed = safeParseLoose(rendererConfigSchema, {
|
||||
name: packageJson.productName,
|
||||
availableLocales: getResolvedMessagesLocale().availableLocales,
|
||||
resolvedTranslationsLocale: getResolvedMessagesLocale().name,
|
||||
|
|
14
patches/zod+3.22.3.patch
Normal file
14
patches/zod+3.22.3.patch
Normal file
|
@ -0,0 +1,14 @@
|
|||
diff --git a/node_modules/zod/lib/types.d.ts b/node_modules/zod/lib/types.d.ts
|
||||
index 0ece6e8..57bbe86 100644
|
||||
--- a/node_modules/zod/lib/types.d.ts
|
||||
+++ b/node_modules/zod/lib/types.d.ts
|
||||
@@ -56,7 +56,9 @@ export declare abstract class ZodType<Output = any, Def extends ZodTypeDef = Zod
|
||||
};
|
||||
_parseSync(input: ParseInput): SyncParseReturnType<Output>;
|
||||
_parseAsync(input: ParseInput): AsyncParseReturnType<Output>;
|
||||
+ /** @deprecated (Signal Desktop: Use ts/util/schema.ts instead) */
|
||||
parse(data: unknown, params?: Partial<ParseParams>): Output;
|
||||
+ /** @deprecated (Signal Desktop: Use ts/util/schema.ts instead) */
|
||||
safeParse(data: unknown, params?: Partial<ParseParams>): SafeParseReturnType<Input, Output>;
|
||||
parseAsync(data: unknown, params?: Partial<ParseParams>): Promise<Output>;
|
||||
safeParseAsync(data: unknown, params?: Partial<ParseParams>): Promise<SafeParseReturnType<Input, Output>>;
|
|
@ -67,6 +67,7 @@ import {
|
|||
SIGNED_PRE_KEY_ID_KEY,
|
||||
} from './textsecure/AccountManager';
|
||||
import { formatGroups, groupWhile } from './util/groupWhile';
|
||||
import { parseUnknown } from './util/schemas';
|
||||
|
||||
const TIMESTAMP_THRESHOLD = 5 * 1000; // 5 seconds
|
||||
const LOW_KEYS_THRESHOLD = 25;
|
||||
|
@ -99,7 +100,7 @@ const identityKeySchema = z.object({
|
|||
|
||||
function validateIdentityKey(attrs: unknown): attrs is IdentityKeyType {
|
||||
// We'll throw if this doesn't match
|
||||
identityKeySchema.parse(attrs);
|
||||
parseUnknown(identityKeySchema, attrs);
|
||||
return true;
|
||||
}
|
||||
/*
|
||||
|
|
|
@ -9,6 +9,7 @@ import * as log from '../logging/log';
|
|||
import type { BadgeType, BadgeImageType } from './types';
|
||||
import { parseBadgeCategory } from './BadgeCategory';
|
||||
import { BadgeImageTheme, parseBadgeImageTheme } from './BadgeImageTheme';
|
||||
import { safeParseUnknown } from '../util/schemas';
|
||||
|
||||
const MAX_BADGES = 1000;
|
||||
|
||||
|
@ -40,7 +41,7 @@ export function parseBoostBadgeListFromServer(
|
|||
): Record<string, BadgeType> {
|
||||
const result: Record<string, BadgeType> = {};
|
||||
|
||||
const parseResult = boostBadgesFromServerSchema.safeParse(value);
|
||||
const parseResult = safeParseUnknown(boostBadgesFromServerSchema, value);
|
||||
if (!parseResult.success) {
|
||||
log.warn(
|
||||
'parseBoostBadgeListFromServer: server response was invalid:',
|
||||
|
@ -73,7 +74,7 @@ export function parseBadgeFromServer(
|
|||
value: unknown,
|
||||
updatesUrl: string
|
||||
): BadgeType | undefined {
|
||||
const parseResult = badgeFromServerSchema.safeParse(value);
|
||||
const parseResult = safeParseUnknown(badgeFromServerSchema, value);
|
||||
if (!parseResult.success) {
|
||||
log.warn(
|
||||
'parseBadgeFromServer: badge was invalid:',
|
||||
|
|
|
@ -15,6 +15,7 @@ import { Input } from './Input';
|
|||
import { AutoSizeTextArea } from './AutoSizeTextArea';
|
||||
import { Button, ButtonVariant } from './Button';
|
||||
import { strictAssert } from '../util/assert';
|
||||
import { safeParsePartial } from '../util/schemas';
|
||||
|
||||
const formSchema = z.object({
|
||||
nickname: z
|
||||
|
@ -67,7 +68,7 @@ export function EditNicknameAndNoteModal({
|
|||
const familyNameValue = toOptionalStringValue(familyName);
|
||||
const noteValue = toOptionalStringValue(note);
|
||||
const hasEitherName = givenNameValue != null || familyNameValue != null;
|
||||
return formSchema.safeParse({
|
||||
return safeParsePartial(formSchema, {
|
||||
nickname: hasEitherName
|
||||
? { givenName: givenNameValue, familyName: familyNameValue }
|
||||
: null,
|
||||
|
|
|
@ -45,6 +45,7 @@ import type { MIMEType } from '../types/MIME';
|
|||
import { AttachmentDownloadSource } from '../sql/Interface';
|
||||
import { drop } from '../util/drop';
|
||||
import { getAttachmentCiphertextLength } from '../AttachmentCrypto';
|
||||
import { safeParsePartial } from '../util/schemas';
|
||||
|
||||
export enum AttachmentDownloadUrgency {
|
||||
IMMEDIATE = 'immediate',
|
||||
|
@ -175,7 +176,7 @@ export class AttachmentDownloadManager extends JobManager<CoreAttachmentDownload
|
|||
source,
|
||||
urgency = AttachmentDownloadUrgency.STANDARD,
|
||||
} = newJobData;
|
||||
const parseResult = coreAttachmentDownloadJobSchema.safeParse({
|
||||
const parseResult = safeParsePartial(coreAttachmentDownloadJobSchema, {
|
||||
messageId,
|
||||
receivedAt,
|
||||
sentAt,
|
||||
|
|
|
@ -16,6 +16,7 @@ import { DataReader, DataWriter } from '../sql/Client';
|
|||
import type { CallLinkType } from '../types/CallLink';
|
||||
import { calling } from '../services/calling';
|
||||
import { sleeper } from '../util/sleeper';
|
||||
import { parseUnknown } from '../util/schemas';
|
||||
|
||||
const MAX_RETRY_TIME = DAY;
|
||||
const MAX_PARALLEL_JOBS = 5;
|
||||
|
@ -46,7 +47,7 @@ export class CallLinkRefreshJobQueue extends JobQueue<CallLinkRefreshJobData> {
|
|||
}
|
||||
|
||||
protected parseData(data: unknown): CallLinkRefreshJobData {
|
||||
return callLinkRefreshJobDataSchema.parse(data);
|
||||
return parseUnknown(callLinkRefreshJobDataSchema, data);
|
||||
}
|
||||
|
||||
protected async run(
|
||||
|
|
|
@ -50,6 +50,7 @@ import { drop } from '../util/drop';
|
|||
import { isInPast } from '../util/timestamp';
|
||||
import { clearTimeoutIfNecessary } from '../util/clearTimeoutIfNecessary';
|
||||
import { FIBONACCI } from '../util/BackOff';
|
||||
import { parseUnknown } from '../util/schemas';
|
||||
|
||||
// Note: generally, we only want to add to this list. If you do need to change one of
|
||||
// these values, you'll likely need to write a database migration.
|
||||
|
@ -415,7 +416,7 @@ export class ConversationJobQueue extends JobQueue<ConversationQueueJobData> {
|
|||
}
|
||||
|
||||
protected parseData(data: unknown): ConversationQueueJobData {
|
||||
return conversationQueueJobDataSchema.parse(data);
|
||||
return parseUnknown(conversationQueueJobDataSchema, data);
|
||||
}
|
||||
|
||||
protected override getInMemoryQueue({
|
||||
|
|
|
@ -10,6 +10,7 @@ import { DataWriter } from '../sql/Client';
|
|||
import type { JOB_STATUS } from './JobQueue';
|
||||
import { JobQueue } from './JobQueue';
|
||||
import { jobQueueDatabaseStore } from './JobQueueDatabaseStore';
|
||||
import { parseUnknown } from '../util/schemas';
|
||||
|
||||
const groupAvatarJobDataSchema = z.object({
|
||||
conversationId: z.string(),
|
||||
|
@ -20,7 +21,7 @@ export type GroupAvatarJobData = z.infer<typeof groupAvatarJobDataSchema>;
|
|||
|
||||
export class GroupAvatarJobQueue extends JobQueue<GroupAvatarJobData> {
|
||||
protected parseData(data: unknown): GroupAvatarJobData {
|
||||
return groupAvatarJobDataSchema.parse(data);
|
||||
return parseUnknown(groupAvatarJobDataSchema, data);
|
||||
}
|
||||
|
||||
protected async run(
|
||||
|
|
|
@ -7,6 +7,7 @@ import type { JOB_STATUS } from './JobQueue';
|
|||
import { JobQueue } from './JobQueue';
|
||||
|
||||
import { jobQueueDatabaseStore } from './JobQueueDatabaseStore';
|
||||
import { parseUnknown } from '../util/schemas';
|
||||
|
||||
const removeStorageKeyJobDataSchema = z.object({
|
||||
key: z.enum([
|
||||
|
@ -22,7 +23,7 @@ type RemoveStorageKeyJobData = z.infer<typeof removeStorageKeyJobDataSchema>;
|
|||
|
||||
export class RemoveStorageKeyJobQueue extends JobQueue<RemoveStorageKeyJobData> {
|
||||
protected parseData(data: unknown): RemoveStorageKeyJobData {
|
||||
return removeStorageKeyJobDataSchema.parse(data);
|
||||
return parseUnknown(removeStorageKeyJobDataSchema, data);
|
||||
}
|
||||
|
||||
protected async run({
|
||||
|
|
|
@ -17,6 +17,7 @@ import { parseIntWithFallback } from '../util/parseIntWithFallback';
|
|||
import type { WebAPIType } from '../textsecure/WebAPI';
|
||||
import { HTTPError } from '../textsecure/Errors';
|
||||
import { sleeper } from '../util/sleeper';
|
||||
import { parseUnknown } from '../util/schemas';
|
||||
|
||||
const RETRY_WAIT_TIME = durations.MINUTE;
|
||||
const RETRYABLE_4XX_FAILURE_STATUSES = new Set([
|
||||
|
@ -44,7 +45,7 @@ export class ReportSpamJobQueue extends JobQueue<ReportSpamJobData> {
|
|||
}
|
||||
|
||||
protected parseData(data: unknown): ReportSpamJobData {
|
||||
return reportSpamJobDataSchema.parse(data);
|
||||
return parseUnknown(reportSpamJobDataSchema, data);
|
||||
}
|
||||
|
||||
protected async run(
|
||||
|
|
|
@ -24,6 +24,7 @@ import {
|
|||
} from './helpers/handleMultipleSendErrors';
|
||||
import { isConversationUnregistered } from '../util/isConversationUnregistered';
|
||||
import { isConversationAccepted } from '../util/isConversationAccepted';
|
||||
import { parseUnknown } from '../util/schemas';
|
||||
|
||||
const MAX_RETRY_TIME = DAY;
|
||||
const MAX_PARALLEL_JOBS = 5;
|
||||
|
@ -43,7 +44,7 @@ export class SingleProtoJobQueue extends JobQueue<SingleProtoJobData> {
|
|||
}
|
||||
|
||||
protected parseData(data: unknown): SingleProtoJobData {
|
||||
return singleProtoJobDataSchema.parse(data);
|
||||
return parseUnknown(singleProtoJobDataSchema, data);
|
||||
}
|
||||
|
||||
protected async run(
|
||||
|
|
|
@ -11,6 +11,7 @@ import { getUserAgent } from '../util/getUserAgent';
|
|||
import { maybeParseUrl } from '../util/url';
|
||||
import * as durations from '../util/durations';
|
||||
import type { LoggerType } from '../types/Logging';
|
||||
import { parseUnknown } from '../util/schemas';
|
||||
|
||||
const BASE_URL = 'https://debuglogs.org';
|
||||
|
||||
|
@ -26,7 +27,7 @@ const tokenBodySchema = z
|
|||
const parseTokenBody = (
|
||||
rawBody: unknown
|
||||
): { fields: Record<string, unknown>; url: string } => {
|
||||
const body = tokenBodySchema.parse(rawBody);
|
||||
const body = parseUnknown(tokenBodySchema, rawBody);
|
||||
|
||||
const parsedUrl = maybeParseUrl(body.url);
|
||||
if (!parsedUrl) {
|
||||
|
|
|
@ -5,6 +5,7 @@ import { parse } from 'csv-parse';
|
|||
import fs from 'fs/promises';
|
||||
import { z } from 'zod';
|
||||
import { _getAvailableLocales } from '../../app/locale';
|
||||
import { parseUnknown } from '../util/schemas';
|
||||
|
||||
const type = process.argv[2];
|
||||
if (type !== 'countries' && type !== 'locales') {
|
||||
|
@ -119,7 +120,7 @@ function assertValuesForAllCountries(result: LocaleDisplayNamesResult) {
|
|||
async function main() {
|
||||
const contents = await fs.readFile(localeDisplayNamesDataPath, 'utf-8');
|
||||
const records = await parseCsv(contents);
|
||||
const data = LocaleDisplayNames.parse(records);
|
||||
const data = parseUnknown(LocaleDisplayNames, records as unknown);
|
||||
const result = convertData(data);
|
||||
if (type === 'locales') {
|
||||
assertValuesForAllLocales(result);
|
||||
|
|
|
@ -9,6 +9,7 @@ import prettier from 'prettier';
|
|||
|
||||
import type { OptionalResourceType } from '../types/OptionalResource';
|
||||
import { OptionalResourcesDictSchema } from '../types/OptionalResource';
|
||||
import { parseUnknown } from '../util/schemas';
|
||||
|
||||
const MANIFEST_URL =
|
||||
'https://updates.signal.org/dynamic/android/emoji/search/manifest.json';
|
||||
|
@ -29,7 +30,7 @@ async function fetchJSON(url: string): Promise<unknown> {
|
|||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const manifest = ManifestSchema.parse(await fetchJSON(MANIFEST_URL));
|
||||
const manifest = parseUnknown(ManifestSchema, await fetchJSON(MANIFEST_URL));
|
||||
|
||||
// eslint-disable-next-line dot-notation
|
||||
manifest.languageToSmartlingLocale['zh_TW'] = 'zh-Hant';
|
||||
|
@ -75,8 +76,9 @@ async function main(): Promise<void> {
|
|||
'build',
|
||||
'optional-resources.json'
|
||||
);
|
||||
const resources = OptionalResourcesDictSchema.parse(
|
||||
JSON.parse(await readFile(resourcesPath, 'utf8'))
|
||||
const resources = parseUnknown(
|
||||
OptionalResourcesDictSchema,
|
||||
JSON.parse(await readFile(resourcesPath, 'utf8')) as unknown
|
||||
);
|
||||
|
||||
for (const [locale, resource] of extraResources) {
|
||||
|
|
|
@ -9,6 +9,7 @@ import prettier from 'prettier';
|
|||
|
||||
import type { OptionalResourceType } from '../types/OptionalResource';
|
||||
import { OptionalResourcesDictSchema } from '../types/OptionalResource';
|
||||
import { parseUnknown } from '../util/schemas';
|
||||
|
||||
const VERSION = 10;
|
||||
|
||||
|
@ -28,7 +29,10 @@ async function fetchJSON(url: string): Promise<unknown> {
|
|||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const { jumbomoji } = ManifestSchema.parse(await fetchJSON(MANIFEST_URL));
|
||||
const { jumbomoji } = parseUnknown(
|
||||
ManifestSchema,
|
||||
await fetchJSON(MANIFEST_URL)
|
||||
);
|
||||
|
||||
const extraResources = new Map<string, OptionalResourceType>();
|
||||
|
||||
|
@ -68,8 +72,9 @@ async function main(): Promise<void> {
|
|||
'build',
|
||||
'optional-resources.json'
|
||||
);
|
||||
const resources = OptionalResourcesDictSchema.parse(
|
||||
JSON.parse(await readFile(resourcesPath, 'utf8'))
|
||||
const resources = parseUnknown(
|
||||
OptionalResourcesDictSchema,
|
||||
JSON.parse(await readFile(resourcesPath, 'utf8')) as unknown
|
||||
);
|
||||
|
||||
for (const [sheet, resource] of extraResources) {
|
||||
|
|
|
@ -13,6 +13,7 @@ import logSymbols from 'log-symbols';
|
|||
import { explodePromise } from '../util/explodePromise';
|
||||
import { missingCaseError } from '../util/missingCaseError';
|
||||
import { SECOND } from '../util/durations';
|
||||
import { parseUnknown } from '../util/schemas';
|
||||
|
||||
const ROOT_DIR = join(__dirname, '..', '..');
|
||||
|
||||
|
@ -137,7 +138,10 @@ async function launchElectron(
|
|||
return;
|
||||
}
|
||||
|
||||
const event = eventSchema.parse(JSON.parse(match[1]));
|
||||
const event = parseUnknown(
|
||||
eventSchema,
|
||||
JSON.parse(match[1]) as unknown
|
||||
);
|
||||
if (event.type === 'pass') {
|
||||
pass += 1;
|
||||
|
||||
|
|
|
@ -17,9 +17,10 @@ import type { WebAPIType } from '../textsecure/WebAPI';
|
|||
import { SignalService as Proto } from '../protobuf';
|
||||
|
||||
import SenderCertificate = Proto.SenderCertificate;
|
||||
import { safeParseUnknown } from '../util/schemas';
|
||||
|
||||
function isWellFormed(data: unknown): data is SerializedCertificateType {
|
||||
return serializedCertificateSchema.safeParse(data).success;
|
||||
return safeParseUnknown(serializedCertificateSchema, data).success;
|
||||
}
|
||||
|
||||
// In case your clock is different from the server's, we "fake" expire certificates early.
|
||||
|
|
|
@ -207,6 +207,7 @@ import {
|
|||
} from '../types/AttachmentBackup';
|
||||
import { redactGenericText } from '../util/privacy';
|
||||
import { getAttachmentCiphertextLength } from '../AttachmentCrypto';
|
||||
import { parseStrict, parseUnknown, safeParseUnknown } from '../util/schemas';
|
||||
|
||||
type ConversationRow = Readonly<{
|
||||
json: string;
|
||||
|
@ -3664,7 +3665,7 @@ function getCallHistory(
|
|||
return;
|
||||
}
|
||||
|
||||
return callHistoryDetailsSchema.parse(row);
|
||||
return parseUnknown(callHistoryDetailsSchema, row as unknown);
|
||||
}
|
||||
|
||||
const SEEN_STATUS_UNSEEN = sqlConstant(SeenStatus.Unseen);
|
||||
|
@ -3746,7 +3747,7 @@ function getCallHistoryForCallLogEventTarget(
|
|||
return null;
|
||||
}
|
||||
|
||||
return callHistoryDetailsSchema.parse(row);
|
||||
return parseUnknown(callHistoryDetailsSchema, row as unknown);
|
||||
}
|
||||
|
||||
function getConversationIdForCallHistory(
|
||||
|
@ -4110,7 +4111,7 @@ function getCallHistoryGroupsCount(
|
|||
return 0;
|
||||
}
|
||||
|
||||
return countSchema.parse(result);
|
||||
return parseUnknown(countSchema, result as unknown);
|
||||
}
|
||||
|
||||
const groupsDataSchema = z.array(
|
||||
|
@ -4135,7 +4136,8 @@ function getCallHistoryGroups(
|
|||
// getCallHistoryGroupData creates a temporary table and thus requires
|
||||
// write access.
|
||||
const writable = toUnsafeWritableDB(db, 'only temp table use');
|
||||
const groupsData = groupsDataSchema.parse(
|
||||
const groupsData = parseUnknown(
|
||||
groupsDataSchema,
|
||||
getCallHistoryGroupData(writable, false, filter, pagination)
|
||||
);
|
||||
|
||||
|
@ -4145,8 +4147,9 @@ function getCallHistoryGroups(
|
|||
.map(groupData => {
|
||||
return {
|
||||
...groupData,
|
||||
possibleChildren: possibleChildrenSchema.parse(
|
||||
JSON.parse(groupData.possibleChildren)
|
||||
possibleChildren: parseUnknown(
|
||||
possibleChildrenSchema,
|
||||
JSON.parse(groupData.possibleChildren) as unknown
|
||||
),
|
||||
inPeriod: new Set(groupData.inPeriod.split(',')),
|
||||
};
|
||||
|
@ -4167,7 +4170,7 @@ function getCallHistoryGroups(
|
|||
}
|
||||
}
|
||||
|
||||
return callHistoryGroupSchema.parse({ ...rest, type, children });
|
||||
return parseStrict(callHistoryGroupSchema, { ...rest, type, children });
|
||||
})
|
||||
.reverse();
|
||||
}
|
||||
|
@ -4788,14 +4791,14 @@ function getAttachmentDownloadJob(
|
|||
|
||||
function removeAllBackupAttachmentDownloadJobs(db: WritableDB): void {
|
||||
const [query, params] = sql`
|
||||
DELETE FROM attachment_downloads
|
||||
DELETE FROM attachment_downloads
|
||||
WHERE source = ${AttachmentDownloadSource.BACKUP_IMPORT};`;
|
||||
db.prepare(query).run(params);
|
||||
}
|
||||
|
||||
function getSizeOfPendingBackupAttachmentDownloadJobs(db: ReadableDB): number {
|
||||
const [query, params] = sql`
|
||||
SELECT SUM(ciphertextSize) FROM attachment_downloads
|
||||
SELECT SUM(ciphertextSize) FROM attachment_downloads
|
||||
WHERE source = ${AttachmentDownloadSource.BACKUP_IMPORT};`;
|
||||
return db.prepare(query).pluck().get(params);
|
||||
}
|
||||
|
@ -4842,7 +4845,7 @@ function getNextAttachmentDownloadJobs(
|
|||
})
|
||||
AND
|
||||
messageId IN (${sqlJoin(prioritizeMessageIds)})
|
||||
AND
|
||||
AND
|
||||
${sourceWhereFragment}
|
||||
-- for priority messages, let's load them oldest first; this helps, e.g. for stories where we
|
||||
-- want the oldest one first
|
||||
|
@ -4862,7 +4865,7 @@ function getNextAttachmentDownloadJobs(
|
|||
active = 0
|
||||
AND
|
||||
(retryAfter is NULL OR retryAfter <= ${timestamp})
|
||||
AND
|
||||
AND
|
||||
${sourceWhereFragment}
|
||||
ORDER BY receivedAt DESC
|
||||
LIMIT ${numJobsRemaining}
|
||||
|
@ -4876,14 +4879,14 @@ function getNextAttachmentDownloadJobs(
|
|||
try {
|
||||
return allJobs.map(row => {
|
||||
try {
|
||||
return attachmentDownloadJobSchema.parse({
|
||||
return parseUnknown(attachmentDownloadJobSchema, {
|
||||
...row,
|
||||
active: Boolean(row.active),
|
||||
attachment: jsonToObject(row.attachmentJson),
|
||||
ciphertextSize:
|
||||
row.ciphertextSize ||
|
||||
getAttachmentCiphertextLength(row.attachment.size),
|
||||
});
|
||||
} as unknown);
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`getNextAttachmentDownloadJobs: Error with job for message ${row.messageId}, deleting.`
|
||||
|
@ -5040,11 +5043,11 @@ function getNextAttachmentBackupJobs(
|
|||
const rows = db.prepare(query).all(params);
|
||||
return rows
|
||||
.map(row => {
|
||||
const parseResult = attachmentBackupJobSchema.safeParse({
|
||||
const parseResult = safeParseUnknown(attachmentBackupJobSchema, {
|
||||
...row,
|
||||
active: Boolean(row.active),
|
||||
data: jsonToObject(row.data),
|
||||
});
|
||||
} as unknown);
|
||||
if (!parseResult.success) {
|
||||
const redactedMediaName = redactGenericText(row.mediaName);
|
||||
logger.error(
|
||||
|
|
|
@ -12,6 +12,7 @@ import {
|
|||
import type { AttachmentType } from '../../types/Attachment';
|
||||
import { jsonToObject, objectToJSON, sql } from '../util';
|
||||
import { AttachmentDownloadSource } from '../Interface';
|
||||
import { parsePartial } from '../../util/schemas';
|
||||
|
||||
export const version = 1040;
|
||||
|
||||
|
@ -68,7 +69,7 @@ export function updateToSchemaVersion1040(
|
|||
attempts INTEGER NOT NULL,
|
||||
retryAfter INTEGER,
|
||||
lastAttemptTimestamp INTEGER,
|
||||
|
||||
|
||||
PRIMARY KEY (messageId, attachmentType, digest)
|
||||
) STRICT;
|
||||
`);
|
||||
|
@ -84,7 +85,7 @@ export function updateToSchemaVersion1040(
|
|||
// 5. Add new index on active & receivedAt. For most queries when there are lots of
|
||||
// jobs (like during backup restore), many jobs will match the the WHERE clause, so
|
||||
// the ORDER BY on receivedAt is probably the most expensive part.
|
||||
db.exec(`
|
||||
db.exec(`
|
||||
CREATE INDEX attachment_downloads_active_receivedAt
|
||||
ON attachment_downloads (
|
||||
active, receivedAt
|
||||
|
@ -94,7 +95,7 @@ export function updateToSchemaVersion1040(
|
|||
// 6. Add new index on active & messageId. In order to prioritize visible messages,
|
||||
// we'll also query for rows with a matching messageId. For these, the messageId
|
||||
// matching is likely going to be the most expensive part.
|
||||
db.exec(`
|
||||
db.exec(`
|
||||
CREATE INDEX attachment_downloads_active_messageId
|
||||
ON attachment_downloads (
|
||||
active, messageId
|
||||
|
@ -103,7 +104,7 @@ export function updateToSchemaVersion1040(
|
|||
|
||||
// 7. Add new index just on messageId, for the ON DELETE CASCADE foreign key
|
||||
// constraint
|
||||
db.exec(`
|
||||
db.exec(`
|
||||
CREATE INDEX attachment_downloads_messageId
|
||||
ON attachment_downloads (
|
||||
messageId
|
||||
|
@ -139,7 +140,7 @@ export function updateToSchemaVersion1040(
|
|||
ciphertextSize: 0,
|
||||
};
|
||||
|
||||
const parsed = attachmentDownloadJobSchema.parse(updatedJob);
|
||||
const parsed = parsePartial(attachmentDownloadJobSchema, updatedJob);
|
||||
|
||||
rowsToTransfer.push(parsed as AttachmentDownloadJobType);
|
||||
} catch {
|
||||
|
@ -160,13 +161,13 @@ export function updateToSchemaVersion1040(
|
|||
(
|
||||
messageId,
|
||||
attachmentType,
|
||||
receivedAt,
|
||||
receivedAt,
|
||||
sentAt,
|
||||
digest,
|
||||
contentType,
|
||||
size,
|
||||
attachmentJson,
|
||||
active,
|
||||
active,
|
||||
attempts,
|
||||
retryAfter,
|
||||
lastAttemptTimestamp
|
||||
|
@ -181,7 +182,7 @@ export function updateToSchemaVersion1040(
|
|||
${row.contentType},
|
||||
${row.size},
|
||||
${objectToJSON(row.attachment)},
|
||||
${row.active ? 1 : 0},
|
||||
${row.active ? 1 : 0},
|
||||
${row.attempts},
|
||||
${row.retryAfter},
|
||||
${row.lastAttemptTimestamp}
|
||||
|
|
|
@ -23,6 +23,7 @@ import type { WritableDB, MessageType, ConversationType } from '../Interface';
|
|||
import { strictAssert } from '../../util/assert';
|
||||
import { missingCaseError } from '../../util/missingCaseError';
|
||||
import { isAciString } from '../../util/isAciString';
|
||||
import { safeParseStrict } from '../../util/schemas';
|
||||
|
||||
// Legacy type for calls that never had a call id
|
||||
type DirectCallHistoryDetailsType = {
|
||||
|
@ -177,7 +178,7 @@ function convertLegacyCallDetails(
|
|||
endedTimestamp: null,
|
||||
};
|
||||
|
||||
const result = callHistoryDetailsSchema.safeParse(callHistory);
|
||||
const result = safeParseStrict(callHistoryDetailsSchema, callHistory);
|
||||
if (result.success) {
|
||||
return result.data;
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ import { prepare } from '../Server';
|
|||
import { sql } from '../util';
|
||||
import { strictAssert } from '../../util/assert';
|
||||
import { CallStatusValue } from '../../types/CallDisposition';
|
||||
import { parseStrict, parseUnknown } from '../../util/schemas';
|
||||
|
||||
export function callLinkExists(db: ReadableDB, roomId: string): boolean {
|
||||
const [query, params] = sql`
|
||||
|
@ -58,7 +59,7 @@ export function getCallLinkRecordByRoomId(
|
|||
return undefined;
|
||||
}
|
||||
|
||||
return callLinkRecordSchema.parse(row);
|
||||
return parseUnknown(callLinkRecordSchema, row as unknown);
|
||||
}
|
||||
|
||||
export function getAllCallLinks(db: ReadableDB): ReadonlyArray<CallLinkType> {
|
||||
|
@ -68,7 +69,9 @@ export function getAllCallLinks(db: ReadableDB): ReadonlyArray<CallLinkType> {
|
|||
return db
|
||||
.prepare(query)
|
||||
.all()
|
||||
.map(item => callLinkFromRecord(callLinkRecordSchema.parse(item)));
|
||||
.map((item: unknown) =>
|
||||
callLinkFromRecord(parseUnknown(callLinkRecordSchema, item))
|
||||
);
|
||||
}
|
||||
|
||||
function _insertCallLink(db: WritableDB, callLink: CallLinkType): void {
|
||||
|
@ -142,7 +145,10 @@ export function updateCallLinkState(
|
|||
callLinkState: CallLinkStateType
|
||||
): CallLinkType {
|
||||
const { name, restrictions, expiration, revoked } = callLinkState;
|
||||
const restrictionsValue = callLinkRestrictionsSchema.parse(restrictions);
|
||||
const restrictionsValue = parseStrict(
|
||||
callLinkRestrictionsSchema,
|
||||
restrictions
|
||||
);
|
||||
const [query, params] = sql`
|
||||
UPDATE callLinks
|
||||
SET
|
||||
|
@ -153,9 +159,9 @@ export function updateCallLinkState(
|
|||
WHERE roomId = ${roomId}
|
||||
RETURNING *;
|
||||
`;
|
||||
const row = db.prepare(query).get(params);
|
||||
const row: unknown = db.prepare(query).get(params);
|
||||
strictAssert(row, 'Expected row to be returned');
|
||||
return callLinkFromRecord(callLinkRecordSchema.parse(row));
|
||||
return callLinkFromRecord(parseUnknown(callLinkRecordSchema, row));
|
||||
}
|
||||
|
||||
export function updateCallLinkAdminKeyByRoomId(
|
||||
|
@ -302,7 +308,7 @@ export function getAllCallLinkRecordsWithAdminKey(
|
|||
return db
|
||||
.prepare(query)
|
||||
.all()
|
||||
.map(item => callLinkRecordSchema.parse(item));
|
||||
.map((item: unknown) => parseUnknown(callLinkRecordSchema, item));
|
||||
}
|
||||
|
||||
export function getAllMarkedDeletedCallLinkRoomIds(
|
||||
|
|
|
@ -8,7 +8,6 @@ import type {
|
|||
} from '../../types/GroupSendEndorsements';
|
||||
import {
|
||||
groupSendEndorsementExpirationSchema,
|
||||
groupSendCombinedEndorsementSchema,
|
||||
groupSendMemberEndorsementSchema,
|
||||
groupSendEndorsementsDataSchema,
|
||||
} from '../../types/GroupSendEndorsements';
|
||||
|
@ -17,6 +16,7 @@ import type { ReadableDB, WritableDB } from '../Interface';
|
|||
import { sql } from '../util';
|
||||
import type { AciString } from '../../types/ServiceId';
|
||||
import { strictAssert } from '../../util/assert';
|
||||
import { parseLoose, parseUnknown } from '../../util/schemas';
|
||||
|
||||
/**
|
||||
* We don't need to store more than one endorsement per group or per member.
|
||||
|
@ -110,7 +110,7 @@ export function getGroupSendCombinedEndorsementExpiration(
|
|||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
return groupSendEndorsementExpirationSchema.parse(value);
|
||||
return parseUnknown(groupSendEndorsementExpirationSchema, value as unknown);
|
||||
}
|
||||
|
||||
export function getGroupSendEndorsementsData(
|
||||
|
@ -128,24 +128,21 @@ export function getGroupSendEndorsementsData(
|
|||
WHERE groupId IS ${groupId}
|
||||
`;
|
||||
|
||||
const combinedEndorsement = groupSendCombinedEndorsementSchema
|
||||
.optional()
|
||||
.parse(
|
||||
prepare<Array<unknown>>(db, selectCombinedEndorsement).get(
|
||||
selectCombinedEndorsementParams
|
||||
)
|
||||
);
|
||||
const combinedEndorsement: unknown = prepare<Array<unknown>>(
|
||||
db,
|
||||
selectCombinedEndorsement
|
||||
).get(selectCombinedEndorsementParams);
|
||||
|
||||
if (combinedEndorsement == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const memberEndorsements = prepare<Array<unknown>>(
|
||||
const memberEndorsements: Array<unknown> = prepare<Array<unknown>>(
|
||||
db,
|
||||
selectMemberEndorsements
|
||||
).all(selectMemberEndorsementsParams);
|
||||
|
||||
return groupSendEndorsementsDataSchema.parse({
|
||||
return parseLoose(groupSendEndorsementsDataSchema, {
|
||||
combinedEndorsement,
|
||||
memberEndorsements,
|
||||
});
|
||||
|
@ -168,5 +165,5 @@ export function getGroupSendMemberEndorsement(
|
|||
if (row == null) {
|
||||
return null;
|
||||
}
|
||||
return groupSendMemberEndorsementSchema.parse(row);
|
||||
return parseUnknown(groupSendMemberEndorsementSchema, row as unknown);
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@ import type { JOB_STATUS } from '../../jobs/JobQueue';
|
|||
import { JobQueue } from '../../jobs/JobQueue';
|
||||
import type { ParsedJob, StoredJob, JobQueueStore } from '../../jobs/types';
|
||||
import { sleep } from '../../util/sleep';
|
||||
import { parseUnknown } from '../../util/schemas';
|
||||
|
||||
describe('JobQueue', () => {
|
||||
describe('end-to-end tests', () => {
|
||||
|
@ -36,7 +37,7 @@ describe('JobQueue', () => {
|
|||
|
||||
class Queue extends JobQueue<TestJobData> {
|
||||
parseData(data: unknown): TestJobData {
|
||||
return testJobSchema.parse(data);
|
||||
return parseUnknown(testJobSchema, data);
|
||||
}
|
||||
|
||||
async run({
|
||||
|
@ -86,7 +87,7 @@ describe('JobQueue', () => {
|
|||
|
||||
class Queue extends JobQueue<number> {
|
||||
parseData(data: unknown): number {
|
||||
return z.number().parse(data);
|
||||
return parseUnknown(z.number(), data);
|
||||
}
|
||||
|
||||
async run(): Promise<typeof JOB_STATUS.NEEDS_RETRY | undefined> {
|
||||
|
@ -137,7 +138,7 @@ describe('JobQueue', () => {
|
|||
|
||||
class Queue extends JobQueue<number> {
|
||||
parseData(data: unknown): number {
|
||||
return z.number().parse(data);
|
||||
return parseUnknown(z.number(), data);
|
||||
}
|
||||
|
||||
protected override getInMemoryQueue(
|
||||
|
@ -180,7 +181,7 @@ describe('JobQueue', () => {
|
|||
|
||||
class TestQueue extends JobQueue<string> {
|
||||
parseData(data: unknown): string {
|
||||
return z.string().parse(data);
|
||||
return parseUnknown(z.string(), data);
|
||||
}
|
||||
|
||||
async run(): Promise<typeof JOB_STATUS.NEEDS_RETRY | undefined> {
|
||||
|
@ -248,7 +249,7 @@ describe('JobQueue', () => {
|
|||
|
||||
class TestQueue extends JobQueue<string> {
|
||||
parseData(data: unknown): string {
|
||||
return z.string().parse(data);
|
||||
return parseUnknown(z.string(), data);
|
||||
}
|
||||
|
||||
async run(): Promise<typeof JOB_STATUS.NEEDS_RETRY | undefined> {
|
||||
|
@ -353,7 +354,6 @@ describe('JobQueue', () => {
|
|||
// Chai's `assert.instanceOf` doesn't tell TypeScript anything, so we do it here.
|
||||
if (!(booErr instanceof JobError)) {
|
||||
assert.fail('Expected error to be a JobError');
|
||||
return;
|
||||
}
|
||||
assert.include(booErr.message, 'bar job always fails in this test');
|
||||
|
||||
|
@ -367,7 +367,7 @@ describe('JobQueue', () => {
|
|||
|
||||
class TestQueue extends JobQueue<string> {
|
||||
parseData(data: unknown): string {
|
||||
return z.string().parse(data);
|
||||
return parseUnknown(z.string(), data);
|
||||
}
|
||||
|
||||
async run(
|
||||
|
@ -412,7 +412,7 @@ describe('JobQueue', () => {
|
|||
|
||||
class TestQueue extends JobQueue<number> {
|
||||
parseData(data: unknown): number {
|
||||
return z.number().parse(data);
|
||||
return parseUnknown(z.number(), data);
|
||||
}
|
||||
|
||||
async run(
|
||||
|
@ -490,7 +490,6 @@ describe('JobQueue', () => {
|
|||
// Chai's `assert.instanceOf` doesn't tell TypeScript anything, so we do it here.
|
||||
if (!(jobError instanceof JobError)) {
|
||||
assert.fail('Expected error to be a JobError');
|
||||
return;
|
||||
}
|
||||
assert.include(
|
||||
jobError.message,
|
||||
|
@ -740,7 +739,7 @@ describe('JobQueue', () => {
|
|||
while (true) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const [job] = await once(this.eventEmitter, 'drip');
|
||||
yield storedJobSchema.parse(job);
|
||||
yield parseUnknown(storedJobSchema, job as unknown);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -766,7 +765,7 @@ describe('JobQueue', () => {
|
|||
|
||||
class TestQueue extends JobQueue<number> {
|
||||
parseData(data: unknown): number {
|
||||
return z.number().parse(data);
|
||||
return parseUnknown(z.number(), data);
|
||||
}
|
||||
|
||||
async run({
|
||||
|
|
|
@ -22,6 +22,7 @@ import { getCallIdFromEra } from '../../util/callDisposition';
|
|||
import { isValidUuid } from '../../util/isValidUuid';
|
||||
import { createDB, updateToVersion } from './helpers';
|
||||
import type { WritableDB, MessageType } from '../../sql/Interface';
|
||||
import { parsePartial } from '../../util/schemas';
|
||||
|
||||
describe('SQL/updateToSchemaVersion89', () => {
|
||||
let db: WritableDB;
|
||||
|
@ -152,8 +153,8 @@ describe('SQL/updateToSchemaVersion89', () => {
|
|||
return db
|
||||
.prepare(selectHistoryQuery)
|
||||
.all()
|
||||
.map(row => {
|
||||
return callHistoryDetailsSchema.parse({
|
||||
.map((row: object) => {
|
||||
return parsePartial(callHistoryDetailsSchema, {
|
||||
...row,
|
||||
|
||||
// Not present at the time of migration, but required by zod
|
||||
|
|
242
ts/test-node/util/schemas_test.ts
Normal file
242
ts/test-node/util/schemas_test.ts
Normal file
|
@ -0,0 +1,242 @@
|
|||
// Copyright 2024 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { z } from 'zod';
|
||||
import assert from 'node:assert/strict';
|
||||
import {
|
||||
parseLoose,
|
||||
parsePartial,
|
||||
parseStrict,
|
||||
parseUnknown,
|
||||
SchemaParseError,
|
||||
} from '../../util/schemas';
|
||||
|
||||
describe('schemas', () => {
|
||||
const schema = z.object({ prop: z.literal('value') });
|
||||
|
||||
it('rejects invalid inputs', () => {
|
||||
function assertThrows(fn: () => void) {
|
||||
assert.throws(fn, SchemaParseError);
|
||||
}
|
||||
|
||||
const input = { prop: 42 };
|
||||
// @ts-expect-error: not unknown
|
||||
assertThrows(() => parseUnknown(schema, input));
|
||||
// @ts-expect-error: invalid type
|
||||
assertThrows(() => parseStrict(schema, input));
|
||||
assertThrows(() => parseLoose(schema, input));
|
||||
// @ts-expect-error: invalid type
|
||||
assertThrows(() => parsePartial(schema, input));
|
||||
});
|
||||
|
||||
it('accepts valid inputs', () => {
|
||||
const valid = { prop: 'value' };
|
||||
|
||||
function assertShape(value: { prop: 'value' }) {
|
||||
assert.deepEqual(value, valid);
|
||||
}
|
||||
|
||||
// unknown
|
||||
{
|
||||
const input = valid as unknown;
|
||||
assertShape(parseUnknown(schema, input));
|
||||
// @ts-expect-error: not strict
|
||||
assertShape(parseStrict(schema, input));
|
||||
// @ts-expect-error: not loose
|
||||
assertShape(parseLoose(schema, input));
|
||||
// @ts-expect-error: not partial
|
||||
assertShape(parsePartial(schema, input));
|
||||
}
|
||||
// any
|
||||
{
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const input = valid as unknown as any;
|
||||
// @ts-expect-error: not unknown
|
||||
assertShape(parseUnknown(schema, input));
|
||||
// @ts-expect-error: not strict
|
||||
assertShape(parseStrict(schema, input));
|
||||
// @ts-expect-error: not loose
|
||||
assertShape(parseLoose(schema, input));
|
||||
// @ts-expect-error: not partial
|
||||
assertShape(parsePartial(schema, input));
|
||||
}
|
||||
// {}
|
||||
{
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
const input = valid as unknown as {};
|
||||
// @ts-expect-error: not unknown
|
||||
assertShape(parseUnknown(schema, input));
|
||||
// @ts-expect-error: not strict
|
||||
assertShape(parseStrict(schema, input));
|
||||
// @ts-expect-error: not loose
|
||||
assertShape(parseLoose(schema, input));
|
||||
// @ts-expect-error: not partial
|
||||
assertShape(parsePartial(schema, input));
|
||||
}
|
||||
// never
|
||||
{
|
||||
const input = valid as unknown as never;
|
||||
// @ts-expect-error: not unknown
|
||||
assertShape(parseUnknown(schema, input));
|
||||
// @ts-expect-error: not strict
|
||||
assertShape(parseStrict(schema, input));
|
||||
// @ts-expect-error: not loose
|
||||
assertShape(parseLoose(schema, input));
|
||||
// @ts-expect-error: not partial
|
||||
assertShape(parsePartial(schema, input));
|
||||
}
|
||||
// { prop: "value" }
|
||||
{
|
||||
const input = valid as { prop: 'value' };
|
||||
// @ts-expect-error: not unknown
|
||||
assertShape(parseUnknown(schema, input));
|
||||
assertShape(parseStrict(schema, input));
|
||||
assertShape(parseLoose(schema, input));
|
||||
assertShape(parsePartial(schema, input));
|
||||
}
|
||||
// { prop?: "value" }
|
||||
{
|
||||
const input = valid as { prop?: 'value' };
|
||||
// @ts-expect-error: not unknown
|
||||
assertShape(parseUnknown(schema, input));
|
||||
// @ts-expect-error: not strict
|
||||
assertShape(parseStrict(schema, input));
|
||||
// @ts-expect-error: not loose
|
||||
assertShape(parseLoose(schema, input));
|
||||
assertShape(parsePartial(schema, input));
|
||||
}
|
||||
// { prop: "value" | void }
|
||||
{
|
||||
const input = valid as { prop: 'value' | void };
|
||||
// @ts-expect-error: not unknown
|
||||
assertShape(parseUnknown(schema, input));
|
||||
// @ts-expect-error: not strict
|
||||
assertShape(parseStrict(schema, input));
|
||||
assertShape(parseLoose(schema, input));
|
||||
assertShape(parsePartial(schema, input));
|
||||
}
|
||||
// { prop: "value" | undefined }
|
||||
{
|
||||
const input = valid as { prop: 'value' | undefined };
|
||||
// @ts-expect-error: not unknown
|
||||
assertShape(parseUnknown(schema, input));
|
||||
// @ts-expect-error: not strict
|
||||
assertShape(parseStrict(schema, input));
|
||||
assertShape(parseLoose(schema, input));
|
||||
assertShape(parsePartial(schema, input));
|
||||
}
|
||||
// { prop: "value" | null }
|
||||
{
|
||||
const input = valid as { prop: 'value' | null };
|
||||
// @ts-expect-error: not unknown
|
||||
assertShape(parseUnknown(schema, input));
|
||||
// @ts-expect-error: not strict
|
||||
assertShape(parseStrict(schema, input));
|
||||
assertShape(parseLoose(schema, input));
|
||||
assertShape(parsePartial(schema, input));
|
||||
}
|
||||
|
||||
// { prop: string }
|
||||
{
|
||||
const input = valid as { prop: string };
|
||||
// @ts-expect-error: not unknown
|
||||
assertShape(parseUnknown(schema, input));
|
||||
// @ts-expect-error: not strict
|
||||
assertShape(parseStrict(schema, input));
|
||||
assertShape(parseLoose(schema, input));
|
||||
// @ts-expect-error: not partial
|
||||
assertShape(parsePartial(schema, input));
|
||||
}
|
||||
// { prop?: string }
|
||||
{
|
||||
const input = valid as { prop?: string };
|
||||
// @ts-expect-error: not unknown
|
||||
assertShape(parseUnknown(schema, input));
|
||||
// @ts-expect-error: not strict
|
||||
assertShape(parseStrict(schema, input));
|
||||
// @ts-expect-error: not loose
|
||||
assertShape(parseLoose(schema, input));
|
||||
// @ts-expect-error: not partial
|
||||
assertShape(parsePartial(schema, input));
|
||||
}
|
||||
// { prop: void }
|
||||
{
|
||||
const input = valid as unknown as { prop: void };
|
||||
// @ts-expect-error: not unknown
|
||||
assertShape(parseUnknown(schema, input));
|
||||
// @ts-expect-error: not strict
|
||||
assertShape(parseStrict(schema, input));
|
||||
assertShape(parseLoose(schema, input));
|
||||
assertShape(parsePartial(schema, input));
|
||||
}
|
||||
// { prop: undefined }
|
||||
{
|
||||
const input = valid as unknown as { prop: undefined };
|
||||
// @ts-expect-error: not unknown
|
||||
assertShape(parseUnknown(schema, input));
|
||||
// @ts-expect-error: not strict
|
||||
assertShape(parseStrict(schema, input));
|
||||
assertShape(parseLoose(schema, input));
|
||||
assertShape(parsePartial(schema, input));
|
||||
}
|
||||
// { prop: null }
|
||||
{
|
||||
const input = valid as unknown as { prop: null };
|
||||
// @ts-expect-error: not unknown
|
||||
assertShape(parseUnknown(schema, input));
|
||||
// @ts-expect-error: not strict
|
||||
assertShape(parseStrict(schema, input));
|
||||
assertShape(parseLoose(schema, input));
|
||||
assertShape(parsePartial(schema, input));
|
||||
}
|
||||
// { prop: unknown }
|
||||
{
|
||||
const input = valid as { prop: unknown };
|
||||
// @ts-expect-error: not unknown
|
||||
assertShape(parseUnknown(schema, input));
|
||||
// @ts-expect-error: not strict
|
||||
assertShape(parseStrict(schema, input));
|
||||
assertShape(parseLoose(schema, input));
|
||||
// @ts-expect-error: not partial
|
||||
assertShape(parsePartial(schema, input));
|
||||
}
|
||||
// { prop: any }
|
||||
{
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const input = valid as { prop: any };
|
||||
// @ts-expect-error: not unknown
|
||||
assertShape(parseUnknown(schema, input));
|
||||
// (ideally not allowed)
|
||||
assertShape(parseStrict(schema, input));
|
||||
// (ideally not allowed)
|
||||
assertShape(parseLoose(schema, input));
|
||||
// (ideally not allowed)
|
||||
assertShape(parsePartial(schema, input));
|
||||
}
|
||||
// { prop: {} }
|
||||
{
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
const input = valid as { prop: {} };
|
||||
// @ts-expect-error: not unknown
|
||||
assertShape(parseUnknown(schema, input));
|
||||
// @ts-expect-error: not strict
|
||||
assertShape(parseStrict(schema, input));
|
||||
// (ideally not allowed)
|
||||
assertShape(parseLoose(schema, input));
|
||||
// @ts-expect-error: not partial
|
||||
assertShape(parsePartial(schema, input));
|
||||
}
|
||||
// { prop: never }
|
||||
{
|
||||
const input = valid as { prop: never };
|
||||
// @ts-expect-error: not unknown
|
||||
assertShape(parseUnknown(schema, input));
|
||||
// (ideally not allowed)
|
||||
assertShape(parseStrict(schema, input));
|
||||
// (ideally not allowed)
|
||||
assertShape(parseLoose(schema, input));
|
||||
// (ideally not allowed)
|
||||
assertShape(parsePartial(schema, input));
|
||||
}
|
||||
});
|
||||
});
|
|
@ -73,6 +73,7 @@ import { safeParseNumber } from '../util/numbers';
|
|||
import { isStagingServer } from '../util/isStagingServer';
|
||||
import type { IWebSocketResource } from './WebsocketResources';
|
||||
import type { GroupSendToken } from '../types/GroupSendEndorsements';
|
||||
import { parseUnknown, safeParseUnknown } from '../util/schemas';
|
||||
|
||||
// Note: this will break some code that expects to be able to use err.response when a
|
||||
// web request fails, because it will force it to text. But it is very useful for
|
||||
|
@ -1948,7 +1949,7 @@ export function initialize({
|
|||
responseType: 'json',
|
||||
});
|
||||
|
||||
return whoamiResultZod.parse(response);
|
||||
return parseUnknown(whoamiResultZod, response);
|
||||
}
|
||||
|
||||
async function sendChallengeResponse(challengeResponse: ChallengeType) {
|
||||
|
@ -2027,7 +2028,7 @@ export function initialize({
|
|||
httpType: 'GET',
|
||||
responseType: 'json',
|
||||
});
|
||||
const res = remoteConfigResponseZod.parse(rawRes);
|
||||
const res = parseUnknown(remoteConfigResponseZod, rawRes);
|
||||
|
||||
return {
|
||||
...res,
|
||||
|
@ -2157,7 +2158,7 @@ export function initialize({
|
|||
responseType: 'json',
|
||||
});
|
||||
|
||||
const result = verifyServiceIdResponse.safeParse(res);
|
||||
const result = safeParseUnknown(verifyServiceIdResponse, res);
|
||||
|
||||
if (result.success) {
|
||||
return result.data;
|
||||
|
@ -2223,7 +2224,8 @@ export function initialize({
|
|||
hash,
|
||||
}: GetAccountForUsernameOptionsType) {
|
||||
const hashBase64 = toWebSafeBase64(Bytes.toBase64(hash));
|
||||
return getAccountForUsernameResultZod.parse(
|
||||
return parseUnknown(
|
||||
getAccountForUsernameResultZod,
|
||||
await _ajax({
|
||||
call: 'username',
|
||||
httpType: 'GET',
|
||||
|
@ -2251,7 +2253,7 @@ export function initialize({
|
|||
return;
|
||||
}
|
||||
|
||||
return uploadAvatarHeadersZod.parse(res);
|
||||
return parseUnknown(uploadAvatarHeadersZod, res as unknown);
|
||||
}
|
||||
|
||||
async function getProfileUnauth(
|
||||
|
@ -2389,7 +2391,7 @@ export function initialize({
|
|||
abortSignal,
|
||||
});
|
||||
|
||||
return reserveUsernameResultZod.parse(response);
|
||||
return parseUnknown(reserveUsernameResultZod, response);
|
||||
}
|
||||
async function confirmUsername({
|
||||
hash,
|
||||
|
@ -2408,14 +2410,15 @@ export function initialize({
|
|||
responseType: 'json',
|
||||
abortSignal,
|
||||
});
|
||||
return confirmUsernameResultZod.parse(response);
|
||||
return parseUnknown(confirmUsernameResultZod, response);
|
||||
}
|
||||
|
||||
async function replaceUsernameLink({
|
||||
encryptedUsername,
|
||||
keepLinkHandle,
|
||||
}: ReplaceUsernameLinkOptionsType): Promise<ReplaceUsernameLinkResultType> {
|
||||
return replaceUsernameLinkResultZod.parse(
|
||||
return parseUnknown(
|
||||
replaceUsernameLinkResultZod,
|
||||
await _ajax({
|
||||
call: 'usernameLink',
|
||||
httpType: 'PUT',
|
||||
|
@ -2440,7 +2443,8 @@ export function initialize({
|
|||
async function resolveUsernameLink(
|
||||
serverId: string
|
||||
): Promise<ResolveUsernameLinkResultType> {
|
||||
return resolveUsernameLinkResultZod.parse(
|
||||
return parseUnknown(
|
||||
resolveUsernameLinkResultZod,
|
||||
await _ajax({
|
||||
httpType: 'GET',
|
||||
call: 'usernameLink',
|
||||
|
@ -2475,7 +2479,8 @@ export function initialize({
|
|||
transport: VerificationTransport
|
||||
) {
|
||||
// Create a new blank session using just a E164
|
||||
let session = verificationSessionZod.parse(
|
||||
let session = parseUnknown(
|
||||
verificationSessionZod,
|
||||
await _ajax({
|
||||
call: 'verificationSession',
|
||||
httpType: 'POST',
|
||||
|
@ -2490,7 +2495,8 @@ export function initialize({
|
|||
);
|
||||
|
||||
// Submit a captcha solution to the session
|
||||
session = verificationSessionZod.parse(
|
||||
session = parseUnknown(
|
||||
verificationSessionZod,
|
||||
await _ajax({
|
||||
call: 'verificationSession',
|
||||
httpType: 'PATCH',
|
||||
|
@ -2511,7 +2517,8 @@ export function initialize({
|
|||
}
|
||||
|
||||
// Request an SMS or Voice confirmation
|
||||
session = verificationSessionZod.parse(
|
||||
session = parseUnknown(
|
||||
verificationSessionZod,
|
||||
await _ajax({
|
||||
call: 'verificationSession',
|
||||
httpType: 'POST',
|
||||
|
@ -2618,7 +2625,8 @@ export function initialize({
|
|||
aciPqLastResortPreKey,
|
||||
pniPqLastResortPreKey,
|
||||
}: CreateAccountOptionsType) {
|
||||
const session = verificationSessionZod.parse(
|
||||
const session = parseUnknown(
|
||||
verificationSessionZod,
|
||||
await _ajax({
|
||||
isRegistration: true,
|
||||
call: 'verificationSession',
|
||||
|
@ -2676,7 +2684,7 @@ export function initialize({
|
|||
jsonData,
|
||||
});
|
||||
|
||||
return createAccountResultZod.parse(responseJson);
|
||||
return parseUnknown(createAccountResultZod, responseJson);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
@ -2726,7 +2734,7 @@ export function initialize({
|
|||
jsonData,
|
||||
});
|
||||
|
||||
return linkDeviceResultZod.parse(responseJson);
|
||||
return parseUnknown(linkDeviceResultZod, responseJson);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
@ -2842,7 +2850,7 @@ export function initialize({
|
|||
responseType: 'json',
|
||||
});
|
||||
|
||||
return getBackupInfoResponseSchema.parse(res);
|
||||
return parseUnknown(getBackupInfoResponseSchema, res);
|
||||
}
|
||||
|
||||
async function getBackupStream({
|
||||
|
@ -2880,7 +2888,7 @@ export function initialize({
|
|||
responseType: 'json',
|
||||
});
|
||||
|
||||
return attachmentUploadFormResponse.parse(res);
|
||||
return parseUnknown(attachmentUploadFormResponse, res);
|
||||
}
|
||||
|
||||
function createFetchForAttachmentUpload({
|
||||
|
@ -2932,7 +2940,7 @@ export function initialize({
|
|||
responseType: 'json',
|
||||
});
|
||||
|
||||
return attachmentUploadFormResponse.parse(res);
|
||||
return parseUnknown(attachmentUploadFormResponse, res);
|
||||
}
|
||||
|
||||
async function refreshBackup(headers: BackupPresentationHeadersType) {
|
||||
|
@ -2961,7 +2969,7 @@ export function initialize({
|
|||
responseType: 'json',
|
||||
});
|
||||
|
||||
return getBackupCredentialsResponseSchema.parse(res);
|
||||
return parseUnknown(getBackupCredentialsResponseSchema, res);
|
||||
}
|
||||
|
||||
async function getBackupCDNCredentials({
|
||||
|
@ -2979,7 +2987,7 @@ export function initialize({
|
|||
responseType: 'json',
|
||||
});
|
||||
|
||||
return getBackupCDNCredentialsResponseSchema.parse(res);
|
||||
return parseUnknown(getBackupCDNCredentialsResponseSchema, res);
|
||||
}
|
||||
|
||||
async function setBackupId({
|
||||
|
@ -3051,7 +3059,7 @@ export function initialize({
|
|||
},
|
||||
});
|
||||
|
||||
return backupMediaBatchResponseSchema.parse(res);
|
||||
return parseUnknown(backupMediaBatchResponseSchema, res);
|
||||
}
|
||||
|
||||
async function backupDeleteMedia({
|
||||
|
@ -3099,7 +3107,7 @@ export function initialize({
|
|||
urlParameters: `?${params.join('&')}`,
|
||||
});
|
||||
|
||||
return backupListMediaResponseSchema.parse(res);
|
||||
return parseUnknown(backupListMediaResponseSchema, res);
|
||||
}
|
||||
|
||||
async function callLinkCreateAuth(
|
||||
|
@ -3111,7 +3119,7 @@ export function initialize({
|
|||
responseType: 'json',
|
||||
jsonData: { createCallLinkCredentialRequest: requestBase64 },
|
||||
});
|
||||
return callLinkCreateAuthResponseSchema.parse(response);
|
||||
return parseUnknown(callLinkCreateAuthResponseSchema, response);
|
||||
}
|
||||
|
||||
async function setPhoneNumberDiscoverability(newValue: boolean) {
|
||||
|
@ -3354,7 +3362,10 @@ export function initialize({
|
|||
accessKey: accessKeys != null ? Bytes.toBase64(accessKeys) : undefined,
|
||||
groupSendToken,
|
||||
});
|
||||
const parseResult = multiRecipient200ResponseSchema.safeParse(response);
|
||||
const parseResult = safeParseUnknown(
|
||||
multiRecipient200ResponseSchema,
|
||||
response
|
||||
);
|
||||
if (parseResult.success) {
|
||||
return parseResult.data;
|
||||
}
|
||||
|
@ -3490,8 +3501,10 @@ export function initialize({
|
|||
urlParameters: `/${encryptedStickers.length}`,
|
||||
});
|
||||
|
||||
const { packId, manifest, stickers } =
|
||||
StickerPackUploadFormSchema.parse(formJson);
|
||||
const { packId, manifest, stickers } = parseUnknown(
|
||||
StickerPackUploadFormSchema,
|
||||
formJson
|
||||
);
|
||||
|
||||
// Upload manifest
|
||||
const manifestParams = makePutParams(manifest, encryptedManifest);
|
||||
|
@ -3718,7 +3731,8 @@ export function initialize({
|
|||
}
|
||||
|
||||
async function getAttachmentUploadForm() {
|
||||
return attachmentUploadFormResponse.parse(
|
||||
return parseUnknown(
|
||||
attachmentUploadFormResponse,
|
||||
await _ajax({
|
||||
call: 'attachmentUploadForm',
|
||||
httpType: 'GET',
|
||||
|
|
|
@ -62,6 +62,7 @@ import { ToastType } from '../types/Toast';
|
|||
import { AbortableProcess } from '../util/AbortableProcess';
|
||||
import type { WebAPICredentials } from './Types';
|
||||
import { NORMAL_DISCONNECT_CODE } from './SocketManager';
|
||||
import { parseUnknown } from '../util/schemas';
|
||||
|
||||
const THIRTY_SECONDS = 30 * durations.SECOND;
|
||||
|
||||
|
@ -107,7 +108,7 @@ export namespace AggregatedStats {
|
|||
try {
|
||||
const json = localStorage.getItem(key);
|
||||
return json != null
|
||||
? AggregatedStatsSchema.parse(JSON.parse(json))
|
||||
? parseUnknown(AggregatedStatsSchema, JSON.parse(json) as unknown)
|
||||
: createEmpty();
|
||||
} catch (error) {
|
||||
log.warn(
|
||||
|
|
|
@ -6,6 +6,7 @@ import type { ConversationType } from '../state/ducks/conversations';
|
|||
import { safeParseInteger } from '../util/numbers';
|
||||
import { byteLength } from '../Bytes';
|
||||
import type { StorageServiceFieldsType } from '../sql/Interface';
|
||||
import { parsePartial } from '../util/schemas';
|
||||
|
||||
export enum CallLinkUpdateSyncType {
|
||||
Update = 'Update',
|
||||
|
@ -44,7 +45,10 @@ export const callLinkRestrictionsSchema = z.nativeEnum(CallLinkRestrictions);
|
|||
export function toCallLinkRestrictions(
|
||||
restrictions: number | string
|
||||
): CallLinkRestrictions {
|
||||
return callLinkRestrictionsSchema.parse(safeParseInteger(restrictions));
|
||||
return parsePartial(
|
||||
callLinkRestrictionsSchema,
|
||||
safeParseInteger(restrictions)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
import { z } from 'zod';
|
||||
import { aciSchema, type AciString } from './ServiceId';
|
||||
import * as Bytes from '../Bytes';
|
||||
import { parseStrict } from '../util/schemas';
|
||||
|
||||
const GROUPV2_ID_LENGTH = 32; // 32 bytes
|
||||
|
||||
|
@ -94,5 +95,5 @@ export const groupSendTokenSchema = z
|
|||
export type GroupSendToken = z.infer<typeof groupSendTokenSchema>;
|
||||
|
||||
export function toGroupSendToken(token: Uint8Array): GroupSendToken {
|
||||
return groupSendTokenSchema.parse(token);
|
||||
return parseStrict(groupSendTokenSchema, token);
|
||||
}
|
||||
|
|
|
@ -68,6 +68,7 @@ import { drop } from './drop';
|
|||
import { sendCallLinkUpdateSync } from './sendCallLinkUpdateSync';
|
||||
import { storageServiceUploadJob } from '../services/storage';
|
||||
import { CallLinkDeleteManager } from '../jobs/CallLinkDeleteManager';
|
||||
import { parsePartial, parseStrict } from './schemas';
|
||||
|
||||
// utils
|
||||
// -----
|
||||
|
@ -200,7 +201,7 @@ export function getCallEventForProto(
|
|||
callEventProto: Proto.SyncMessage.ICallEvent,
|
||||
eventSource: string
|
||||
): CallEventDetails {
|
||||
const callEvent = callEventNormalizeSchema.parse(callEventProto);
|
||||
const callEvent = parsePartial(callEventNormalizeSchema, callEventProto);
|
||||
const { callId, peerId, timestamp } = callEvent;
|
||||
|
||||
let type: CallType;
|
||||
|
@ -251,7 +252,7 @@ export function getCallEventForProto(
|
|||
throw new TypeError(`Unknown call event ${callEvent.event}`);
|
||||
}
|
||||
|
||||
return callEventDetailsSchema.parse({
|
||||
return parseStrict(callEventDetailsSchema, {
|
||||
callId,
|
||||
peerId,
|
||||
ringerId: null,
|
||||
|
@ -279,7 +280,10 @@ const callLogEventFromProto: Partial<
|
|||
export function getCallLogEventForProto(
|
||||
callLogEventProto: Proto.SyncMessage.ICallLogEvent
|
||||
): CallLogEventDetails {
|
||||
const callLogEvent = callLogEventNormalizeSchema.parse(callLogEventProto);
|
||||
const callLogEvent = parsePartial(
|
||||
callLogEventNormalizeSchema,
|
||||
callLogEventProto
|
||||
);
|
||||
|
||||
const type = callLogEventFromProto[callLogEvent.type];
|
||||
if (type == null) {
|
||||
|
@ -496,7 +500,7 @@ export function getCallDetailsFromDirectCall(
|
|||
call: Call
|
||||
): CallDetails {
|
||||
const ringerId = call.isIncoming ? call.remoteUserId : null;
|
||||
return callDetailsSchema.parse({
|
||||
return parseStrict(callDetailsSchema, {
|
||||
callId: Long.fromValue(call.callId).toString(),
|
||||
peerId,
|
||||
ringerId,
|
||||
|
@ -518,7 +522,7 @@ export function getCallDetailsFromEndedDirectCall(
|
|||
wasVideoCall: boolean,
|
||||
timestamp: number
|
||||
): CallDetails {
|
||||
return callDetailsSchema.parse({
|
||||
return parseStrict(callDetailsSchema, {
|
||||
callId,
|
||||
peerId,
|
||||
ringerId,
|
||||
|
@ -535,7 +539,7 @@ export function getCallDetailsFromGroupCallMeta(
|
|||
peerId: AciString | string,
|
||||
groupCallMeta: GroupCallMeta
|
||||
): CallDetails {
|
||||
return callDetailsSchema.parse({
|
||||
return parseStrict(callDetailsSchema, {
|
||||
callId: groupCallMeta.callId,
|
||||
peerId,
|
||||
ringerId: groupCallMeta.ringerId,
|
||||
|
@ -552,7 +556,7 @@ export function getCallDetailsForAdhocCall(
|
|||
peerId: AciString | string,
|
||||
callId: string
|
||||
): CallDetails {
|
||||
return callDetailsSchema.parse({
|
||||
return parseStrict(callDetailsSchema, {
|
||||
callId,
|
||||
peerId,
|
||||
ringerId: null,
|
||||
|
@ -575,7 +579,11 @@ export function getCallEventDetails(
|
|||
event: LocalCallEvent,
|
||||
eventSource: string
|
||||
): CallEventDetails {
|
||||
return callEventDetailsSchema.parse({ ...callDetails, event, eventSource });
|
||||
return parseStrict(callEventDetailsSchema, {
|
||||
...callDetails,
|
||||
event,
|
||||
eventSource,
|
||||
});
|
||||
}
|
||||
|
||||
// transitions
|
||||
|
@ -646,7 +654,7 @@ export function transitionCallHistory(
|
|||
`transitionCallHistory: Transitioned call history timestamp (before: ${callHistory?.timestamp}, after: ${timestamp})`
|
||||
);
|
||||
|
||||
return callHistoryDetailsSchema.parse({
|
||||
return parseStrict(callHistoryDetailsSchema, {
|
||||
callId,
|
||||
peerId,
|
||||
ringerId,
|
||||
|
|
|
@ -33,6 +33,7 @@ import {
|
|||
getKeyFromCallLink,
|
||||
toAdminKeyBytes,
|
||||
} from './callLinks';
|
||||
import { parseStrict } from './schemas';
|
||||
|
||||
/**
|
||||
* RingRTC conversions
|
||||
|
@ -56,7 +57,7 @@ const RingRTCCallLinkRestrictionsSchema = z.nativeEnum(
|
|||
export function callLinkRestrictionsToRingRTC(
|
||||
restrictions: CallLinkRestrictions
|
||||
): RingRTCCallLinkRestrictions {
|
||||
return RingRTCCallLinkRestrictionsSchema.parse(restrictions);
|
||||
return parseStrict(RingRTCCallLinkRestrictionsSchema, restrictions);
|
||||
}
|
||||
|
||||
export function getRoomIdFromRootKey(rootKey: CallLinkRootKey): string {
|
||||
|
@ -152,7 +153,7 @@ export function callLinkToRecord(callLink: CallLinkType): CallLinkRecord {
|
|||
const adminKey = callLink.adminKey
|
||||
? toAdminKeyBytes(callLink.adminKey)
|
||||
: null;
|
||||
return callLinkRecordSchema.parse({
|
||||
return parseStrict(callLinkRecordSchema, {
|
||||
roomId: callLink.roomId,
|
||||
rootKey,
|
||||
adminKey,
|
||||
|
|
|
@ -27,6 +27,7 @@ import { ToastType } from '../types/Toast';
|
|||
import * as Errors from '../types/errors';
|
||||
import { isTestOrMockEnvironment } from '../environment';
|
||||
import { isAlpha } from './version';
|
||||
import { parseStrict } from './schemas';
|
||||
|
||||
export function decodeGroupSendEndorsementResponse({
|
||||
groupId,
|
||||
|
@ -91,7 +92,7 @@ export function decodeGroupSendEndorsementResponse({
|
|||
`decodeGroupSendEndorsementResponse: Received endorsements (group: ${idForLogging}, expiration: ${expiration}, members: ${groupMembers.length})`
|
||||
);
|
||||
|
||||
const groupEndorsementsData: GroupSendEndorsementsData = {
|
||||
return parseStrict(groupSendEndorsementsDataSchema, {
|
||||
combinedEndorsement: {
|
||||
groupId,
|
||||
expiration,
|
||||
|
@ -110,9 +111,7 @@ export function decodeGroupSendEndorsementResponse({
|
|||
endorsement: endorsement.getContents(),
|
||||
};
|
||||
}),
|
||||
};
|
||||
|
||||
return groupSendEndorsementsDataSchema.parse(groupEndorsementsData);
|
||||
});
|
||||
}
|
||||
|
||||
const TWO_DAYS = DurationInSeconds.fromDays(2);
|
||||
|
|
|
@ -5,6 +5,7 @@ import { z } from 'zod';
|
|||
import { groupBy } from 'lodash';
|
||||
import * as log from '../logging/log';
|
||||
import { aciSchema } from '../types/ServiceId';
|
||||
import { safeParseStrict } from './schemas';
|
||||
|
||||
const retryItemSchema = z
|
||||
.object({
|
||||
|
@ -53,7 +54,8 @@ export class RetryPlaceholders {
|
|||
);
|
||||
}
|
||||
|
||||
const parsed = retryItemListSchema.safeParse(
|
||||
const parsed = safeParseStrict(
|
||||
retryItemListSchema,
|
||||
window.storage.get(STORAGE_KEY, new Array<RetryItemType>())
|
||||
);
|
||||
if (!parsed.success) {
|
||||
|
@ -104,7 +106,7 @@ export class RetryPlaceholders {
|
|||
// Basic data management
|
||||
|
||||
async add(item: RetryItemType): Promise<void> {
|
||||
const parsed = retryItemSchema.safeParse(item);
|
||||
const parsed = safeParseStrict(retryItemSchema, item);
|
||||
if (!parsed.success) {
|
||||
throw new Error(
|
||||
`RetryPlaceholders.add: Item did not match schema ${JSON.stringify(
|
||||
|
|
179
ts/util/schemas.ts
Normal file
179
ts/util/schemas.ts
Normal file
|
@ -0,0 +1,179 @@
|
|||
// Copyright 2024 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import type {
|
||||
IfAny,
|
||||
IfEmptyObject,
|
||||
IfNever,
|
||||
IfUnknown,
|
||||
IsLiteral,
|
||||
LiteralToPrimitive,
|
||||
Primitive,
|
||||
} from 'type-fest';
|
||||
import type { SafeParseReturnType, ZodError, ZodType, ZodTypeDef } from 'zod';
|
||||
|
||||
type Schema<Input, Output> = ZodType<Output, ZodTypeDef, Input>;
|
||||
type SafeResult<Output> = SafeParseReturnType<unknown, Output>;
|
||||
|
||||
type LooseInput<T> =
|
||||
IsLiteral<T> extends true ? LiteralToPrimitive<T> : Record<keyof T, unknown>;
|
||||
|
||||
type PartialInput<T> = T extends Primitive
|
||||
? T | null | void
|
||||
: { [Key in keyof T]?: T[Key] | null | void };
|
||||
|
||||
export class SchemaParseError extends TypeError {
|
||||
constructor(schema: Schema<unknown, unknown>, error: ZodError<unknown>) {
|
||||
let message = 'zod: issues found when parsing with schema';
|
||||
if (schema.description) {
|
||||
message += ` (${schema.description})`;
|
||||
}
|
||||
message += ':';
|
||||
for (const issue of error.issues) {
|
||||
message += `\n - ${issue.path.join('.')}: ${issue.message}`;
|
||||
}
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
function parse<Output>(
|
||||
schema: Schema<unknown, Output>,
|
||||
input: unknown
|
||||
): Output {
|
||||
const result = schema.safeParse(input);
|
||||
if (result.success) {
|
||||
return result.data;
|
||||
}
|
||||
throw new SchemaParseError(schema, result.error);
|
||||
}
|
||||
|
||||
function safeParse<Output>(
|
||||
schema: Schema<unknown, Output>,
|
||||
input: unknown
|
||||
): SafeResult<Output> {
|
||||
return schema.safeParse(input);
|
||||
}
|
||||
|
||||
/**
|
||||
* This uses type-fest to validate that the data being passed into parse() and
|
||||
* safeParse() is not types like `any`, `{}`, `never`, or an unexpected `unknown`.
|
||||
*
|
||||
* `never` is hard to prevent from being passed in, so instead we make the function
|
||||
* arguments themselves not constructable using an intersection with a warning.
|
||||
*/
|
||||
|
||||
// Must be exactly `unknown`
|
||||
type UnknownArgs<Data> =
|
||||
IfAny<Data> extends true
|
||||
? [data: Data] & 'Unexpected input `any` must be `unknown`'
|
||||
: IfNever<Data> extends true
|
||||
? [data: Data] & 'Unexpected input `never` must be `unknown`'
|
||||
: IfEmptyObject<Data> extends true
|
||||
? [data: Data] & 'Unexpected input `{}` must be `unknown`'
|
||||
: IfUnknown<Data> extends true
|
||||
? [data: Data]
|
||||
: [data: Data] & 'Unexpected input type must be `unknown`';
|
||||
|
||||
type TypedArgs<Data> =
|
||||
IfAny<Data> extends true
|
||||
? [data: Data] & 'Unexpected input `any` must be typed'
|
||||
: IfNever<Data> extends true
|
||||
? [data: Data] & 'Unexpected input `never` must be typed'
|
||||
: IfEmptyObject<Data> extends true
|
||||
? [data: Data] & 'Unexpected input `{}` must be typed'
|
||||
: IfUnknown<Data> extends true
|
||||
? [data: Data] & 'Unexpected input `unknown` must be typed'
|
||||
: [data: Data];
|
||||
|
||||
// prettier-ignore
|
||||
type ParseUnknown = <Input, Output, Data>(schema: Schema<Input, Output>, ...args: UnknownArgs<Data>) => Output;
|
||||
// prettier-ignore
|
||||
type SafeParseUnknown = <Input, Output, Data>(schema: Schema<Input, Output>, ...args: UnknownArgs<Data>) => SafeResult<Output>;
|
||||
// prettier-ignore
|
||||
type ParseStrict = <Input, Output, Data extends Input>(schema: Schema<Input, Output>, ...args: TypedArgs<Data>) => Output;
|
||||
// prettier-ignore
|
||||
type SafeParseStrict = <Input, Output, Data extends Input>(schema: Schema<Input, Output>, ...args: TypedArgs<Data>) => SafeResult<Output>;
|
||||
// prettier-ignore
|
||||
type ParseLoose = <Input, Output, Data extends LooseInput<Input>>(schema: Schema<Input, Output>, ...args: TypedArgs<Data>) => Output;
|
||||
// prettier-ignore
|
||||
type SafeParseLoose = <Input, Output, Data extends LooseInput<Input>>(schema: Schema<Input, Output>, ...args: TypedArgs<Data>) => SafeResult<Output>;
|
||||
// prettier-ignore
|
||||
type ParsePartial = <Input, Output, Data extends PartialInput<Input>>(schema: Schema<Input, Output>, ...args: TypedArgs<Data>) => Output;
|
||||
// prettier-ignore
|
||||
type SafeParsePartial = <Input, Output, Data extends PartialInput<Input>>(schema: Schema<Input, Output>, ...args: TypedArgs<Data>) => SafeResult<Output>;
|
||||
|
||||
/**
|
||||
* Parse an *unknown* value with a zod schema.
|
||||
* ```ts
|
||||
* type Input = unknown // unknown
|
||||
* type Output = { prop: string }
|
||||
* ```
|
||||
* @throws {SchemaParseError}
|
||||
*/
|
||||
export const parseUnknown: ParseUnknown = parse;
|
||||
|
||||
/**
|
||||
* Safely parse an *unknown* value with a zod schema.
|
||||
* ```ts
|
||||
* type Input = unknown // unknown
|
||||
* type Output = { success: true, error: null, data: { prop: string } }
|
||||
* ```
|
||||
*/
|
||||
export const safeParseUnknown: SafeParseUnknown = safeParse;
|
||||
|
||||
/**
|
||||
* Parse a *strict* value with a zod schema.
|
||||
* ```ts
|
||||
* type Input = { prop: string } // strict
|
||||
* type Output = { prop: string }
|
||||
* ```
|
||||
* @throws {SchemaParseError}
|
||||
*/
|
||||
export const parseStrict: ParseStrict = parse;
|
||||
|
||||
/**
|
||||
* Safely parse a *strict* value with a zod schema.
|
||||
* ```ts
|
||||
* type Input = { prop: string } // strict
|
||||
* type Output = { success: true, error: null, data: { prop: string } }
|
||||
* ```
|
||||
*/
|
||||
export const safeParseStrict: SafeParseStrict = safeParse;
|
||||
|
||||
/**
|
||||
* Parse a *loose* value with a zod schema.
|
||||
* ```ts
|
||||
* type Input = { prop: unknown } // loose
|
||||
* type Output = { prop: string }
|
||||
* ```
|
||||
* @throws {SchemaParseError}
|
||||
*/
|
||||
export const parseLoose: ParseLoose = parse;
|
||||
|
||||
/**
|
||||
* Safely parse a *loose* value with a zod schema.
|
||||
* ```ts
|
||||
* type Input = { prop: unknown } // loose
|
||||
* type Output = { success: true, error: null, data: { prop: string } }
|
||||
* ```
|
||||
*/
|
||||
export const safeParseLoose: SafeParseLoose = safeParse;
|
||||
|
||||
/**
|
||||
* Parse a *partial* value with a zod schema.
|
||||
* ```ts
|
||||
* type Input = { prop?: string | null | undefined } // partial
|
||||
* type Output = { prop: string }
|
||||
* ```
|
||||
* @throws {SchemaParseError}
|
||||
*/
|
||||
export const parsePartial: ParsePartial = parse;
|
||||
|
||||
/**
|
||||
* Safely parse a *partial* value with a zod schema.
|
||||
* ```ts
|
||||
* type Input = { prop?: string | null | undefined } // partial
|
||||
* type Output = { success: true, error: null, data: { prop: string } }
|
||||
* ```
|
||||
*/
|
||||
export const safeParsePartial: SafeParsePartial = safeParse;
|
|
@ -73,6 +73,7 @@ import {
|
|||
import { maybeUpdateGroup } from '../groups';
|
||||
import type { GroupSendToken } from '../types/GroupSendEndorsements';
|
||||
import { isAciString } from './isAciString';
|
||||
import { safeParseStrict, safeParseUnknown } from './schemas';
|
||||
|
||||
const UNKNOWN_RECIPIENT = 404;
|
||||
const INCORRECT_AUTH_KEY = 401;
|
||||
|
@ -603,7 +604,7 @@ export async function sendToGroupViaSenderKey(
|
|||
{ online, story, urgent }
|
||||
);
|
||||
|
||||
const parsed = multiRecipient200ResponseSchema.safeParse(result);
|
||||
const parsed = safeParseStrict(multiRecipient200ResponseSchema, result);
|
||||
if (parsed.success) {
|
||||
const { uuids404 } = parsed.data;
|
||||
if (uuids404 && uuids404.length > 0) {
|
||||
|
@ -1022,7 +1023,10 @@ async function handle409Response(
|
|||
error: HTTPError
|
||||
) {
|
||||
const logId = sendTarget.idForLogging();
|
||||
const parsed = multiRecipient409ResponseSchema.safeParse(error.response);
|
||||
const parsed = safeParseUnknown(
|
||||
multiRecipient409ResponseSchema,
|
||||
error.response
|
||||
);
|
||||
if (parsed.success) {
|
||||
await waitForAll({
|
||||
tasks: parsed.data.map(item => async () => {
|
||||
|
@ -1068,7 +1072,10 @@ async function handle410Response(
|
|||
) {
|
||||
const logId = sendTarget.idForLogging();
|
||||
|
||||
const parsed = multiRecipient410ResponseSchema.safeParse(error.response);
|
||||
const parsed = safeParseUnknown(
|
||||
multiRecipient410ResponseSchema,
|
||||
error.response
|
||||
);
|
||||
if (parsed.success) {
|
||||
await waitForAll({
|
||||
tasks: parsed.data.map(item => async () => {
|
||||
|
|
|
@ -8,6 +8,7 @@ import { z } from 'zod';
|
|||
import { strictAssert } from './assert';
|
||||
import * as log from '../logging/log';
|
||||
import * as Errors from '../types/errors';
|
||||
import { parsePartial, parseUnknown, safeParseUnknown } from './schemas';
|
||||
|
||||
function toUrl(input: URL | string): URL | null {
|
||||
if (input instanceof URL) {
|
||||
|
@ -164,7 +165,10 @@ function _route<Key extends string, Args extends object>(
|
|||
);
|
||||
return null;
|
||||
}
|
||||
const parseResult = config.schema.safeParse(rawArgs);
|
||||
const parseResult = safeParseUnknown(
|
||||
config.schema,
|
||||
rawArgs as unknown
|
||||
);
|
||||
if (parseResult.success) {
|
||||
const args = parseResult.data;
|
||||
return {
|
||||
|
@ -183,13 +187,13 @@ function _route<Key extends string, Args extends object>(
|
|||
},
|
||||
toWebUrl(args) {
|
||||
if (config.toWebUrl) {
|
||||
return config.toWebUrl(config.schema.parse(args));
|
||||
return config.toWebUrl(parseUnknown(config.schema, args as unknown));
|
||||
}
|
||||
throw new Error('Route does not support web URLs');
|
||||
},
|
||||
toAppUrl(args) {
|
||||
if (config.toAppUrl) {
|
||||
return config.toAppUrl(config.schema.parse(args));
|
||||
return config.toAppUrl(parseUnknown(config.schema, args as unknown));
|
||||
}
|
||||
throw new Error('Route does not support app URLs');
|
||||
},
|
||||
|
@ -219,7 +223,7 @@ export const contactByPhoneNumberRoute = _route('contactByPhoneNumber', {
|
|||
}),
|
||||
parse(result) {
|
||||
return {
|
||||
phoneNumber: paramSchema.parse(result.hash.groups.phoneNumber),
|
||||
phoneNumber: parsePartial(paramSchema, result.hash.groups.phoneNumber),
|
||||
};
|
||||
},
|
||||
toWebUrl(args) {
|
||||
|
|
|
@ -30,6 +30,7 @@ import {
|
|||
onSync as onViewSync,
|
||||
viewSyncTaskSchema,
|
||||
} from '../messageModifiers/ViewSyncs';
|
||||
import { safeParseUnknown } from './schemas';
|
||||
|
||||
const syncTaskDataSchema = z.union([
|
||||
deleteMessageSchema,
|
||||
|
@ -86,7 +87,7 @@ export async function queueSyncTasks(
|
|||
await removeSyncTaskById(id);
|
||||
return;
|
||||
}
|
||||
const parseResult = syncTaskDataSchema.safeParse(data);
|
||||
const parseResult = safeParseUnknown(syncTaskDataSchema, data);
|
||||
if (!parseResult.success) {
|
||||
log.error(
|
||||
`${innerLogId}: Failed to parse. Deleting. Error: ${parseResult.error}`
|
||||
|
|
|
@ -20,6 +20,7 @@ import {
|
|||
} from '../context/localeMessages';
|
||||
import { waitForSettingsChange } from '../context/waitForSettingsChange';
|
||||
import { isTestOrMockEnvironment } from '../environment';
|
||||
import { parseUnknown } from '../util/schemas';
|
||||
|
||||
const emojiListCache = new Map<string, LocaleEmojiListType>();
|
||||
|
||||
|
@ -55,8 +56,8 @@ export const MinimalSignalContext: MinimalSignalContextType = {
|
|||
'OptionalResourceService:getData',
|
||||
`emoji-index-${locale}.json`
|
||||
);
|
||||
const json = JSON.parse(Buffer.from(buf).toString());
|
||||
const result = LocaleEmojiListSchema.parse(json);
|
||||
const json: unknown = JSON.parse(Buffer.from(buf).toString());
|
||||
const result = parseUnknown(LocaleEmojiListSchema, json);
|
||||
emojiListCache.set(locale, result);
|
||||
return result;
|
||||
},
|
||||
|
|
Loading…
Reference in a new issue