Add schema utils

This commit is contained in:
Jamie Kyle 2024-10-02 12:03:10 -07:00 committed by GitHub
parent c8a729f8be
commit b26466e59d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
45 changed files with 674 additions and 151 deletions

View file

@ -9,6 +9,7 @@ import LRU from 'lru-cache';
import type { OptionalResourceService } from './OptionalResourceService'; import type { OptionalResourceService } from './OptionalResourceService';
import { SignalService as Proto } from '../ts/protobuf'; import { SignalService as Proto } from '../ts/protobuf';
import { parseUnknown } from '../ts/util/schemas';
const MANIFEST_PATH = join(__dirname, '..', 'build', 'jumbomoji.json'); const MANIFEST_PATH = join(__dirname, '..', 'build', 'jumbomoji.json');
@ -64,8 +65,9 @@ export class EmojiService {
public static async create( public static async create(
resourceService: OptionalResourceService resourceService: OptionalResourceService
): Promise<EmojiService> { ): Promise<EmojiService> {
const json = await readFile(MANIFEST_PATH, 'utf8'); const contents = await readFile(MANIFEST_PATH, 'utf8');
const manifest = manifestSchema.parse(JSON.parse(json)); const json: unknown = JSON.parse(contents);
const manifest = parseUnknown(manifestSchema, json);
return new EmojiService(resourceService, manifest); return new EmojiService(resourceService, manifest);
} }

View file

@ -17,6 +17,7 @@ import { OptionalResourcesDictSchema } from '../ts/types/OptionalResource';
import * as log from '../ts/logging/log'; import * as log from '../ts/logging/log';
import { getGotOptions } from '../ts/updater/got'; import { getGotOptions } from '../ts/updater/got';
import { drop } from '../ts/util/drop'; import { drop } from '../ts/util/drop';
import { parseUnknown } from '../ts/util/schemas';
const RESOURCES_DICT_PATH = join( const RESOURCES_DICT_PATH = join(
__dirname, __dirname,
@ -106,8 +107,10 @@ export class OptionalResourceService {
return; return;
} }
const json = JSON.parse(await readFile(RESOURCES_DICT_PATH, 'utf8')); const json: unknown = JSON.parse(
this.maybeDeclaration = OptionalResourcesDictSchema.parse(json); await readFile(RESOURCES_DICT_PATH, 'utf8')
);
this.maybeDeclaration = parseUnknown(OptionalResourcesDictSchema, json);
// Clean unknown resources // Clean unknown resources
let subPaths: Array<string>; let subPaths: Array<string>;

View file

@ -56,6 +56,7 @@ import {
isVideoTypeSupported, isVideoTypeSupported,
} from '../ts/util/GoogleChrome'; } from '../ts/util/GoogleChrome';
import { decryptAttachmentV2ToSink } from '../ts/AttachmentCrypto'; import { decryptAttachmentV2ToSink } from '../ts/AttachmentCrypto';
import { parseLoose } from '../ts/util/schemas';
let initialized = false; let initialized = false;
@ -471,7 +472,7 @@ export async function handleAttachmentRequest(req: Request): Promise<Response> {
let disposition: z.infer<typeof dispositionSchema> = 'attachment'; let disposition: z.infer<typeof dispositionSchema> = 'attachment';
const dispositionParam = url.searchParams.get('disposition'); const dispositionParam = url.searchParams.get('disposition');
if (dispositionParam != null) { if (dispositionParam != null) {
disposition = dispositionSchema.parse(dispositionParam); disposition = parseLoose(dispositionSchema, dispositionParam);
} }
strictAssert(attachmentsDir != null, 'not initialized'); strictAssert(attachmentsDir != null, 'not initialized');

View file

@ -12,6 +12,7 @@ import * as Errors from '../ts/types/errors';
import { isProduction } from '../ts/util/version'; import { isProduction } from '../ts/util/version';
import { isNotNil } from '../ts/util/isNotNil'; import { isNotNil } from '../ts/util/isNotNil';
import OS from '../ts/util/os/osMain'; 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 // See https://github.com/rust-minidump/rust-minidump/blob/main/minidump-processor/json-schema.md
const dumpString = z.string().or(z.null()).optional(); const dumpString = z.string().or(z.null()).optional();
@ -120,9 +121,8 @@ export function setup(
pendingDumps.map(async fullPath => { pendingDumps.map(async fullPath => {
const content = await readFile(fullPath); const content = await readFile(fullPath);
try { try {
const dump = dumpSchema.parse( const json: unknown = JSON.parse(dumpToJSONString(content));
JSON.parse(dumpToJSONString(content)) const dump = parseUnknown(dumpSchema, json);
);
if (dump.crash_info?.type !== 'Simulated Exception') { if (dump.crash_info?.type !== 'Simulated Exception') {
return fullPath; return fullPath;
} }
@ -173,7 +173,8 @@ export function setup(
const content = await readFile(fullPath); const content = await readFile(fullPath);
const { mtime } = await stat(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') { if (dump.crash_info?.type === 'Simulated Exception') {
return undefined; return undefined;

View file

@ -5,6 +5,7 @@ import { join } from 'path';
import { readFile } from 'fs/promises'; import { readFile } from 'fs/promises';
import { DNSFallbackSchema } from '../ts/types/DNSFallback'; import { DNSFallbackSchema } from '../ts/types/DNSFallback';
import type { DNSFallbackType } from '../ts/types/DNSFallback'; import type { DNSFallbackType } from '../ts/types/DNSFallback';
import { parseUnknown } from '../ts/util/schemas';
let cached: DNSFallbackType | undefined; let cached: DNSFallbackType | undefined;
@ -25,9 +26,9 @@ export async function getDNSFallback(): Promise<DNSFallbackType> {
return cached; 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; cached = result;
return result; return result;
} }

View file

@ -13,6 +13,7 @@ import type { LoggerType } from '../ts/types/Logging';
import type { HourCyclePreference, LocaleMessagesType } from '../ts/types/I18N'; import type { HourCyclePreference, LocaleMessagesType } from '../ts/types/I18N';
import type { LocalizerType } from '../ts/types/Util'; import type { LocalizerType } from '../ts/types/Util';
import * as Errors from '../ts/types/errors'; import * as Errors from '../ts/types/errors';
import { parseUnknown } from '../ts/util/schemas';
const TextInfoSchema = z.object({ const TextInfoSchema = z.object({
direction: z.enum(['ltr', 'rtl']), direction: z.enum(['ltr', 'rtl']),
@ -70,16 +71,18 @@ function getLocaleDirection(
try { try {
// @ts-expect-error -- TS doesn't know about this method // @ts-expect-error -- TS doesn't know about this method
if (typeof locale.getTextInfo === 'function') { if (typeof locale.getTextInfo === 'function') {
return TextInfoSchema.parse( return parseUnknown(
TextInfoSchema,
// @ts-expect-error -- TS doesn't know about this method // @ts-expect-error -- TS doesn't know about this method
locale.getTextInfo() locale.getTextInfo() as unknown
).direction; ).direction;
} }
// @ts-expect-error -- TS doesn't know about this property // @ts-expect-error -- TS doesn't know about this property
if (typeof locale.textInfo === 'object') { if (typeof locale.textInfo === 'object') {
return TextInfoSchema.parse( return parseUnknown(
TextInfoSchema,
// @ts-expect-error -- TS doesn't know about this property // @ts-expect-error -- TS doesn't know about this property
locale.textInfo locale.textInfo as unknown
).direction; ).direction;
} }
} catch (error) { } catch (error) {

View file

@ -123,6 +123,7 @@ import { ZoomFactorService } from '../ts/services/ZoomFactorService';
import { SafeStorageBackendChangeError } from '../ts/types/SafeStorageBackendChangeError'; import { SafeStorageBackendChangeError } from '../ts/types/SafeStorageBackendChangeError';
import { LINUX_PASSWORD_STORE_FLAGS } from '../ts/util/linuxPasswordStoreFlags'; import { LINUX_PASSWORD_STORE_FLAGS } from '../ts/util/linuxPasswordStoreFlags';
import { getOwn } from '../ts/util/getOwn'; import { getOwn } from '../ts/util/getOwn';
import { safeParseLoose, safeParseUnknown } from '../ts/util/schemas';
const animationSettings = systemPreferences.getAnimationSettings(); const animationSettings = systemPreferences.getAnimationSettings();
@ -436,7 +437,8 @@ export const windowConfigSchema = z.object({
type WindowConfigType = z.infer<typeof windowConfigSchema>; type WindowConfigType = z.infer<typeof windowConfigSchema>;
let windowConfig: WindowConfigType | undefined; let windowConfig: WindowConfigType | undefined;
const windowConfigParsed = windowConfigSchema.safeParse( const windowConfigParsed = safeParseUnknown(
windowConfigSchema,
windowFromEphemeral || windowFromUserConfig windowFromEphemeral || windowFromUserConfig
); );
if (windowConfigParsed.success) { if (windowConfigParsed.success) {
@ -2692,7 +2694,7 @@ ipc.on('delete-all-data', () => {
ipc.on('get-config', async event => { ipc.on('get-config', async event => {
const theme = await getResolvedThemeSetting(); const theme = await getResolvedThemeSetting();
const directoryConfig = directoryConfigSchema.safeParse({ const directoryConfig = safeParseLoose(directoryConfigSchema, {
directoryUrl: config.get<string | null>('directoryUrl') || undefined, directoryUrl: config.get<string | null>('directoryUrl') || undefined,
directoryMRENCLAVE: directoryMRENCLAVE:
config.get<string | null>('directoryMRENCLAVE') || undefined, 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, name: packageJson.productName,
availableLocales: getResolvedMessagesLocale().availableLocales, availableLocales: getResolvedMessagesLocale().availableLocales,
resolvedTranslationsLocale: getResolvedMessagesLocale().name, resolvedTranslationsLocale: getResolvedMessagesLocale().name,

14
patches/zod+3.22.3.patch Normal file
View 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>>;

View file

@ -67,6 +67,7 @@ import {
SIGNED_PRE_KEY_ID_KEY, SIGNED_PRE_KEY_ID_KEY,
} from './textsecure/AccountManager'; } from './textsecure/AccountManager';
import { formatGroups, groupWhile } from './util/groupWhile'; import { formatGroups, groupWhile } from './util/groupWhile';
import { parseUnknown } from './util/schemas';
const TIMESTAMP_THRESHOLD = 5 * 1000; // 5 seconds const TIMESTAMP_THRESHOLD = 5 * 1000; // 5 seconds
const LOW_KEYS_THRESHOLD = 25; const LOW_KEYS_THRESHOLD = 25;
@ -99,7 +100,7 @@ const identityKeySchema = z.object({
function validateIdentityKey(attrs: unknown): attrs is IdentityKeyType { function validateIdentityKey(attrs: unknown): attrs is IdentityKeyType {
// We'll throw if this doesn't match // We'll throw if this doesn't match
identityKeySchema.parse(attrs); parseUnknown(identityKeySchema, attrs);
return true; return true;
} }
/* /*

View file

@ -9,6 +9,7 @@ import * as log from '../logging/log';
import type { BadgeType, BadgeImageType } from './types'; import type { BadgeType, BadgeImageType } from './types';
import { parseBadgeCategory } from './BadgeCategory'; import { parseBadgeCategory } from './BadgeCategory';
import { BadgeImageTheme, parseBadgeImageTheme } from './BadgeImageTheme'; import { BadgeImageTheme, parseBadgeImageTheme } from './BadgeImageTheme';
import { safeParseUnknown } from '../util/schemas';
const MAX_BADGES = 1000; const MAX_BADGES = 1000;
@ -40,7 +41,7 @@ export function parseBoostBadgeListFromServer(
): Record<string, BadgeType> { ): Record<string, BadgeType> {
const result: Record<string, BadgeType> = {}; const result: Record<string, BadgeType> = {};
const parseResult = boostBadgesFromServerSchema.safeParse(value); const parseResult = safeParseUnknown(boostBadgesFromServerSchema, value);
if (!parseResult.success) { if (!parseResult.success) {
log.warn( log.warn(
'parseBoostBadgeListFromServer: server response was invalid:', 'parseBoostBadgeListFromServer: server response was invalid:',
@ -73,7 +74,7 @@ export function parseBadgeFromServer(
value: unknown, value: unknown,
updatesUrl: string updatesUrl: string
): BadgeType | undefined { ): BadgeType | undefined {
const parseResult = badgeFromServerSchema.safeParse(value); const parseResult = safeParseUnknown(badgeFromServerSchema, value);
if (!parseResult.success) { if (!parseResult.success) {
log.warn( log.warn(
'parseBadgeFromServer: badge was invalid:', 'parseBadgeFromServer: badge was invalid:',

View file

@ -15,6 +15,7 @@ import { Input } from './Input';
import { AutoSizeTextArea } from './AutoSizeTextArea'; import { AutoSizeTextArea } from './AutoSizeTextArea';
import { Button, ButtonVariant } from './Button'; import { Button, ButtonVariant } from './Button';
import { strictAssert } from '../util/assert'; import { strictAssert } from '../util/assert';
import { safeParsePartial } from '../util/schemas';
const formSchema = z.object({ const formSchema = z.object({
nickname: z nickname: z
@ -67,7 +68,7 @@ export function EditNicknameAndNoteModal({
const familyNameValue = toOptionalStringValue(familyName); const familyNameValue = toOptionalStringValue(familyName);
const noteValue = toOptionalStringValue(note); const noteValue = toOptionalStringValue(note);
const hasEitherName = givenNameValue != null || familyNameValue != null; const hasEitherName = givenNameValue != null || familyNameValue != null;
return formSchema.safeParse({ return safeParsePartial(formSchema, {
nickname: hasEitherName nickname: hasEitherName
? { givenName: givenNameValue, familyName: familyNameValue } ? { givenName: givenNameValue, familyName: familyNameValue }
: null, : null,

View file

@ -45,6 +45,7 @@ import type { MIMEType } from '../types/MIME';
import { AttachmentDownloadSource } from '../sql/Interface'; import { AttachmentDownloadSource } from '../sql/Interface';
import { drop } from '../util/drop'; import { drop } from '../util/drop';
import { getAttachmentCiphertextLength } from '../AttachmentCrypto'; import { getAttachmentCiphertextLength } from '../AttachmentCrypto';
import { safeParsePartial } from '../util/schemas';
export enum AttachmentDownloadUrgency { export enum AttachmentDownloadUrgency {
IMMEDIATE = 'immediate', IMMEDIATE = 'immediate',
@ -175,7 +176,7 @@ export class AttachmentDownloadManager extends JobManager<CoreAttachmentDownload
source, source,
urgency = AttachmentDownloadUrgency.STANDARD, urgency = AttachmentDownloadUrgency.STANDARD,
} = newJobData; } = newJobData;
const parseResult = coreAttachmentDownloadJobSchema.safeParse({ const parseResult = safeParsePartial(coreAttachmentDownloadJobSchema, {
messageId, messageId,
receivedAt, receivedAt,
sentAt, sentAt,

View file

@ -16,6 +16,7 @@ import { DataReader, DataWriter } from '../sql/Client';
import type { CallLinkType } from '../types/CallLink'; import type { CallLinkType } from '../types/CallLink';
import { calling } from '../services/calling'; import { calling } from '../services/calling';
import { sleeper } from '../util/sleeper'; import { sleeper } from '../util/sleeper';
import { parseUnknown } from '../util/schemas';
const MAX_RETRY_TIME = DAY; const MAX_RETRY_TIME = DAY;
const MAX_PARALLEL_JOBS = 5; const MAX_PARALLEL_JOBS = 5;
@ -46,7 +47,7 @@ export class CallLinkRefreshJobQueue extends JobQueue<CallLinkRefreshJobData> {
} }
protected parseData(data: unknown): CallLinkRefreshJobData { protected parseData(data: unknown): CallLinkRefreshJobData {
return callLinkRefreshJobDataSchema.parse(data); return parseUnknown(callLinkRefreshJobDataSchema, data);
} }
protected async run( protected async run(

View file

@ -50,6 +50,7 @@ import { drop } from '../util/drop';
import { isInPast } from '../util/timestamp'; import { isInPast } from '../util/timestamp';
import { clearTimeoutIfNecessary } from '../util/clearTimeoutIfNecessary'; import { clearTimeoutIfNecessary } from '../util/clearTimeoutIfNecessary';
import { FIBONACCI } from '../util/BackOff'; 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 // 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. // 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 { protected parseData(data: unknown): ConversationQueueJobData {
return conversationQueueJobDataSchema.parse(data); return parseUnknown(conversationQueueJobDataSchema, data);
} }
protected override getInMemoryQueue({ protected override getInMemoryQueue({

View file

@ -10,6 +10,7 @@ import { DataWriter } from '../sql/Client';
import type { JOB_STATUS } from './JobQueue'; import type { JOB_STATUS } from './JobQueue';
import { JobQueue } from './JobQueue'; import { JobQueue } from './JobQueue';
import { jobQueueDatabaseStore } from './JobQueueDatabaseStore'; import { jobQueueDatabaseStore } from './JobQueueDatabaseStore';
import { parseUnknown } from '../util/schemas';
const groupAvatarJobDataSchema = z.object({ const groupAvatarJobDataSchema = z.object({
conversationId: z.string(), conversationId: z.string(),
@ -20,7 +21,7 @@ export type GroupAvatarJobData = z.infer<typeof groupAvatarJobDataSchema>;
export class GroupAvatarJobQueue extends JobQueue<GroupAvatarJobData> { export class GroupAvatarJobQueue extends JobQueue<GroupAvatarJobData> {
protected parseData(data: unknown): GroupAvatarJobData { protected parseData(data: unknown): GroupAvatarJobData {
return groupAvatarJobDataSchema.parse(data); return parseUnknown(groupAvatarJobDataSchema, data);
} }
protected async run( protected async run(

View file

@ -7,6 +7,7 @@ import type { JOB_STATUS } from './JobQueue';
import { JobQueue } from './JobQueue'; import { JobQueue } from './JobQueue';
import { jobQueueDatabaseStore } from './JobQueueDatabaseStore'; import { jobQueueDatabaseStore } from './JobQueueDatabaseStore';
import { parseUnknown } from '../util/schemas';
const removeStorageKeyJobDataSchema = z.object({ const removeStorageKeyJobDataSchema = z.object({
key: z.enum([ key: z.enum([
@ -22,7 +23,7 @@ type RemoveStorageKeyJobData = z.infer<typeof removeStorageKeyJobDataSchema>;
export class RemoveStorageKeyJobQueue extends JobQueue<RemoveStorageKeyJobData> { export class RemoveStorageKeyJobQueue extends JobQueue<RemoveStorageKeyJobData> {
protected parseData(data: unknown): RemoveStorageKeyJobData { protected parseData(data: unknown): RemoveStorageKeyJobData {
return removeStorageKeyJobDataSchema.parse(data); return parseUnknown(removeStorageKeyJobDataSchema, data);
} }
protected async run({ protected async run({

View file

@ -17,6 +17,7 @@ import { parseIntWithFallback } from '../util/parseIntWithFallback';
import type { WebAPIType } from '../textsecure/WebAPI'; import type { WebAPIType } from '../textsecure/WebAPI';
import { HTTPError } from '../textsecure/Errors'; import { HTTPError } from '../textsecure/Errors';
import { sleeper } from '../util/sleeper'; import { sleeper } from '../util/sleeper';
import { parseUnknown } from '../util/schemas';
const RETRY_WAIT_TIME = durations.MINUTE; const RETRY_WAIT_TIME = durations.MINUTE;
const RETRYABLE_4XX_FAILURE_STATUSES = new Set([ const RETRYABLE_4XX_FAILURE_STATUSES = new Set([
@ -44,7 +45,7 @@ export class ReportSpamJobQueue extends JobQueue<ReportSpamJobData> {
} }
protected parseData(data: unknown): ReportSpamJobData { protected parseData(data: unknown): ReportSpamJobData {
return reportSpamJobDataSchema.parse(data); return parseUnknown(reportSpamJobDataSchema, data);
} }
protected async run( protected async run(

View file

@ -24,6 +24,7 @@ import {
} from './helpers/handleMultipleSendErrors'; } from './helpers/handleMultipleSendErrors';
import { isConversationUnregistered } from '../util/isConversationUnregistered'; import { isConversationUnregistered } from '../util/isConversationUnregistered';
import { isConversationAccepted } from '../util/isConversationAccepted'; import { isConversationAccepted } from '../util/isConversationAccepted';
import { parseUnknown } from '../util/schemas';
const MAX_RETRY_TIME = DAY; const MAX_RETRY_TIME = DAY;
const MAX_PARALLEL_JOBS = 5; const MAX_PARALLEL_JOBS = 5;
@ -43,7 +44,7 @@ export class SingleProtoJobQueue extends JobQueue<SingleProtoJobData> {
} }
protected parseData(data: unknown): SingleProtoJobData { protected parseData(data: unknown): SingleProtoJobData {
return singleProtoJobDataSchema.parse(data); return parseUnknown(singleProtoJobDataSchema, data);
} }
protected async run( protected async run(

View file

@ -11,6 +11,7 @@ import { getUserAgent } from '../util/getUserAgent';
import { maybeParseUrl } from '../util/url'; import { maybeParseUrl } from '../util/url';
import * as durations from '../util/durations'; import * as durations from '../util/durations';
import type { LoggerType } from '../types/Logging'; import type { LoggerType } from '../types/Logging';
import { parseUnknown } from '../util/schemas';
const BASE_URL = 'https://debuglogs.org'; const BASE_URL = 'https://debuglogs.org';
@ -26,7 +27,7 @@ const tokenBodySchema = z
const parseTokenBody = ( const parseTokenBody = (
rawBody: unknown rawBody: unknown
): { fields: Record<string, unknown>; url: string } => { ): { fields: Record<string, unknown>; url: string } => {
const body = tokenBodySchema.parse(rawBody); const body = parseUnknown(tokenBodySchema, rawBody);
const parsedUrl = maybeParseUrl(body.url); const parsedUrl = maybeParseUrl(body.url);
if (!parsedUrl) { if (!parsedUrl) {

View file

@ -5,6 +5,7 @@ import { parse } from 'csv-parse';
import fs from 'fs/promises'; import fs from 'fs/promises';
import { z } from 'zod'; import { z } from 'zod';
import { _getAvailableLocales } from '../../app/locale'; import { _getAvailableLocales } from '../../app/locale';
import { parseUnknown } from '../util/schemas';
const type = process.argv[2]; const type = process.argv[2];
if (type !== 'countries' && type !== 'locales') { if (type !== 'countries' && type !== 'locales') {
@ -119,7 +120,7 @@ function assertValuesForAllCountries(result: LocaleDisplayNamesResult) {
async function main() { async function main() {
const contents = await fs.readFile(localeDisplayNamesDataPath, 'utf-8'); const contents = await fs.readFile(localeDisplayNamesDataPath, 'utf-8');
const records = await parseCsv(contents); const records = await parseCsv(contents);
const data = LocaleDisplayNames.parse(records); const data = parseUnknown(LocaleDisplayNames, records as unknown);
const result = convertData(data); const result = convertData(data);
if (type === 'locales') { if (type === 'locales') {
assertValuesForAllLocales(result); assertValuesForAllLocales(result);

View file

@ -9,6 +9,7 @@ import prettier from 'prettier';
import type { OptionalResourceType } from '../types/OptionalResource'; import type { OptionalResourceType } from '../types/OptionalResource';
import { OptionalResourcesDictSchema } from '../types/OptionalResource'; import { OptionalResourcesDictSchema } from '../types/OptionalResource';
import { parseUnknown } from '../util/schemas';
const MANIFEST_URL = const MANIFEST_URL =
'https://updates.signal.org/dynamic/android/emoji/search/manifest.json'; '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> { 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 // eslint-disable-next-line dot-notation
manifest.languageToSmartlingLocale['zh_TW'] = 'zh-Hant'; manifest.languageToSmartlingLocale['zh_TW'] = 'zh-Hant';
@ -75,8 +76,9 @@ async function main(): Promise<void> {
'build', 'build',
'optional-resources.json' 'optional-resources.json'
); );
const resources = OptionalResourcesDictSchema.parse( const resources = parseUnknown(
JSON.parse(await readFile(resourcesPath, 'utf8')) OptionalResourcesDictSchema,
JSON.parse(await readFile(resourcesPath, 'utf8')) as unknown
); );
for (const [locale, resource] of extraResources) { for (const [locale, resource] of extraResources) {

View file

@ -9,6 +9,7 @@ import prettier from 'prettier';
import type { OptionalResourceType } from '../types/OptionalResource'; import type { OptionalResourceType } from '../types/OptionalResource';
import { OptionalResourcesDictSchema } from '../types/OptionalResource'; import { OptionalResourcesDictSchema } from '../types/OptionalResource';
import { parseUnknown } from '../util/schemas';
const VERSION = 10; const VERSION = 10;
@ -28,7 +29,10 @@ async function fetchJSON(url: string): Promise<unknown> {
} }
async function main(): Promise<void> { 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>(); const extraResources = new Map<string, OptionalResourceType>();
@ -68,8 +72,9 @@ async function main(): Promise<void> {
'build', 'build',
'optional-resources.json' 'optional-resources.json'
); );
const resources = OptionalResourcesDictSchema.parse( const resources = parseUnknown(
JSON.parse(await readFile(resourcesPath, 'utf8')) OptionalResourcesDictSchema,
JSON.parse(await readFile(resourcesPath, 'utf8')) as unknown
); );
for (const [sheet, resource] of extraResources) { for (const [sheet, resource] of extraResources) {

View file

@ -13,6 +13,7 @@ import logSymbols from 'log-symbols';
import { explodePromise } from '../util/explodePromise'; import { explodePromise } from '../util/explodePromise';
import { missingCaseError } from '../util/missingCaseError'; import { missingCaseError } from '../util/missingCaseError';
import { SECOND } from '../util/durations'; import { SECOND } from '../util/durations';
import { parseUnknown } from '../util/schemas';
const ROOT_DIR = join(__dirname, '..', '..'); const ROOT_DIR = join(__dirname, '..', '..');
@ -137,7 +138,10 @@ async function launchElectron(
return; return;
} }
const event = eventSchema.parse(JSON.parse(match[1])); const event = parseUnknown(
eventSchema,
JSON.parse(match[1]) as unknown
);
if (event.type === 'pass') { if (event.type === 'pass') {
pass += 1; pass += 1;

View file

@ -17,9 +17,10 @@ import type { WebAPIType } from '../textsecure/WebAPI';
import { SignalService as Proto } from '../protobuf'; import { SignalService as Proto } from '../protobuf';
import SenderCertificate = Proto.SenderCertificate; import SenderCertificate = Proto.SenderCertificate;
import { safeParseUnknown } from '../util/schemas';
function isWellFormed(data: unknown): data is SerializedCertificateType { 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. // In case your clock is different from the server's, we "fake" expire certificates early.

View file

@ -207,6 +207,7 @@ import {
} from '../types/AttachmentBackup'; } from '../types/AttachmentBackup';
import { redactGenericText } from '../util/privacy'; import { redactGenericText } from '../util/privacy';
import { getAttachmentCiphertextLength } from '../AttachmentCrypto'; import { getAttachmentCiphertextLength } from '../AttachmentCrypto';
import { parseStrict, parseUnknown, safeParseUnknown } from '../util/schemas';
type ConversationRow = Readonly<{ type ConversationRow = Readonly<{
json: string; json: string;
@ -3664,7 +3665,7 @@ function getCallHistory(
return; return;
} }
return callHistoryDetailsSchema.parse(row); return parseUnknown(callHistoryDetailsSchema, row as unknown);
} }
const SEEN_STATUS_UNSEEN = sqlConstant(SeenStatus.Unseen); const SEEN_STATUS_UNSEEN = sqlConstant(SeenStatus.Unseen);
@ -3746,7 +3747,7 @@ function getCallHistoryForCallLogEventTarget(
return null; return null;
} }
return callHistoryDetailsSchema.parse(row); return parseUnknown(callHistoryDetailsSchema, row as unknown);
} }
function getConversationIdForCallHistory( function getConversationIdForCallHistory(
@ -4110,7 +4111,7 @@ function getCallHistoryGroupsCount(
return 0; return 0;
} }
return countSchema.parse(result); return parseUnknown(countSchema, result as unknown);
} }
const groupsDataSchema = z.array( const groupsDataSchema = z.array(
@ -4135,7 +4136,8 @@ function getCallHistoryGroups(
// getCallHistoryGroupData creates a temporary table and thus requires // getCallHistoryGroupData creates a temporary table and thus requires
// write access. // write access.
const writable = toUnsafeWritableDB(db, 'only temp table use'); const writable = toUnsafeWritableDB(db, 'only temp table use');
const groupsData = groupsDataSchema.parse( const groupsData = parseUnknown(
groupsDataSchema,
getCallHistoryGroupData(writable, false, filter, pagination) getCallHistoryGroupData(writable, false, filter, pagination)
); );
@ -4145,8 +4147,9 @@ function getCallHistoryGroups(
.map(groupData => { .map(groupData => {
return { return {
...groupData, ...groupData,
possibleChildren: possibleChildrenSchema.parse( possibleChildren: parseUnknown(
JSON.parse(groupData.possibleChildren) possibleChildrenSchema,
JSON.parse(groupData.possibleChildren) as unknown
), ),
inPeriod: new Set(groupData.inPeriod.split(',')), 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(); .reverse();
} }
@ -4788,14 +4791,14 @@ function getAttachmentDownloadJob(
function removeAllBackupAttachmentDownloadJobs(db: WritableDB): void { function removeAllBackupAttachmentDownloadJobs(db: WritableDB): void {
const [query, params] = sql` const [query, params] = sql`
DELETE FROM attachment_downloads DELETE FROM attachment_downloads
WHERE source = ${AttachmentDownloadSource.BACKUP_IMPORT};`; WHERE source = ${AttachmentDownloadSource.BACKUP_IMPORT};`;
db.prepare(query).run(params); db.prepare(query).run(params);
} }
function getSizeOfPendingBackupAttachmentDownloadJobs(db: ReadableDB): number { function getSizeOfPendingBackupAttachmentDownloadJobs(db: ReadableDB): number {
const [query, params] = sql` const [query, params] = sql`
SELECT SUM(ciphertextSize) FROM attachment_downloads SELECT SUM(ciphertextSize) FROM attachment_downloads
WHERE source = ${AttachmentDownloadSource.BACKUP_IMPORT};`; WHERE source = ${AttachmentDownloadSource.BACKUP_IMPORT};`;
return db.prepare(query).pluck().get(params); return db.prepare(query).pluck().get(params);
} }
@ -4842,7 +4845,7 @@ function getNextAttachmentDownloadJobs(
}) })
AND AND
messageId IN (${sqlJoin(prioritizeMessageIds)}) messageId IN (${sqlJoin(prioritizeMessageIds)})
AND AND
${sourceWhereFragment} ${sourceWhereFragment}
-- for priority messages, let's load them oldest first; this helps, e.g. for stories where we -- for priority messages, let's load them oldest first; this helps, e.g. for stories where we
-- want the oldest one first -- want the oldest one first
@ -4862,7 +4865,7 @@ function getNextAttachmentDownloadJobs(
active = 0 active = 0
AND AND
(retryAfter is NULL OR retryAfter <= ${timestamp}) (retryAfter is NULL OR retryAfter <= ${timestamp})
AND AND
${sourceWhereFragment} ${sourceWhereFragment}
ORDER BY receivedAt DESC ORDER BY receivedAt DESC
LIMIT ${numJobsRemaining} LIMIT ${numJobsRemaining}
@ -4876,14 +4879,14 @@ function getNextAttachmentDownloadJobs(
try { try {
return allJobs.map(row => { return allJobs.map(row => {
try { try {
return attachmentDownloadJobSchema.parse({ return parseUnknown(attachmentDownloadJobSchema, {
...row, ...row,
active: Boolean(row.active), active: Boolean(row.active),
attachment: jsonToObject(row.attachmentJson), attachment: jsonToObject(row.attachmentJson),
ciphertextSize: ciphertextSize:
row.ciphertextSize || row.ciphertextSize ||
getAttachmentCiphertextLength(row.attachment.size), getAttachmentCiphertextLength(row.attachment.size),
}); } as unknown);
} catch (error) { } catch (error) {
logger.error( logger.error(
`getNextAttachmentDownloadJobs: Error with job for message ${row.messageId}, deleting.` `getNextAttachmentDownloadJobs: Error with job for message ${row.messageId}, deleting.`
@ -5040,11 +5043,11 @@ function getNextAttachmentBackupJobs(
const rows = db.prepare(query).all(params); const rows = db.prepare(query).all(params);
return rows return rows
.map(row => { .map(row => {
const parseResult = attachmentBackupJobSchema.safeParse({ const parseResult = safeParseUnknown(attachmentBackupJobSchema, {
...row, ...row,
active: Boolean(row.active), active: Boolean(row.active),
data: jsonToObject(row.data), data: jsonToObject(row.data),
}); } as unknown);
if (!parseResult.success) { if (!parseResult.success) {
const redactedMediaName = redactGenericText(row.mediaName); const redactedMediaName = redactGenericText(row.mediaName);
logger.error( logger.error(

View file

@ -12,6 +12,7 @@ import {
import type { AttachmentType } from '../../types/Attachment'; import type { AttachmentType } from '../../types/Attachment';
import { jsonToObject, objectToJSON, sql } from '../util'; import { jsonToObject, objectToJSON, sql } from '../util';
import { AttachmentDownloadSource } from '../Interface'; import { AttachmentDownloadSource } from '../Interface';
import { parsePartial } from '../../util/schemas';
export const version = 1040; export const version = 1040;
@ -68,7 +69,7 @@ export function updateToSchemaVersion1040(
attempts INTEGER NOT NULL, attempts INTEGER NOT NULL,
retryAfter INTEGER, retryAfter INTEGER,
lastAttemptTimestamp INTEGER, lastAttemptTimestamp INTEGER,
PRIMARY KEY (messageId, attachmentType, digest) PRIMARY KEY (messageId, attachmentType, digest)
) STRICT; ) STRICT;
`); `);
@ -84,7 +85,7 @@ export function updateToSchemaVersion1040(
// 5. Add new index on active & receivedAt. For most queries when there are lots of // 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 // 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. // the ORDER BY on receivedAt is probably the most expensive part.
db.exec(` db.exec(`
CREATE INDEX attachment_downloads_active_receivedAt CREATE INDEX attachment_downloads_active_receivedAt
ON attachment_downloads ( ON attachment_downloads (
active, receivedAt active, receivedAt
@ -94,7 +95,7 @@ export function updateToSchemaVersion1040(
// 6. Add new index on active & messageId. In order to prioritize visible messages, // 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 // we'll also query for rows with a matching messageId. For these, the messageId
// matching is likely going to be the most expensive part. // matching is likely going to be the most expensive part.
db.exec(` db.exec(`
CREATE INDEX attachment_downloads_active_messageId CREATE INDEX attachment_downloads_active_messageId
ON attachment_downloads ( ON attachment_downloads (
active, messageId active, messageId
@ -103,7 +104,7 @@ export function updateToSchemaVersion1040(
// 7. Add new index just on messageId, for the ON DELETE CASCADE foreign key // 7. Add new index just on messageId, for the ON DELETE CASCADE foreign key
// constraint // constraint
db.exec(` db.exec(`
CREATE INDEX attachment_downloads_messageId CREATE INDEX attachment_downloads_messageId
ON attachment_downloads ( ON attachment_downloads (
messageId messageId
@ -139,7 +140,7 @@ export function updateToSchemaVersion1040(
ciphertextSize: 0, ciphertextSize: 0,
}; };
const parsed = attachmentDownloadJobSchema.parse(updatedJob); const parsed = parsePartial(attachmentDownloadJobSchema, updatedJob);
rowsToTransfer.push(parsed as AttachmentDownloadJobType); rowsToTransfer.push(parsed as AttachmentDownloadJobType);
} catch { } catch {
@ -160,13 +161,13 @@ export function updateToSchemaVersion1040(
( (
messageId, messageId,
attachmentType, attachmentType,
receivedAt, receivedAt,
sentAt, sentAt,
digest, digest,
contentType, contentType,
size, size,
attachmentJson, attachmentJson,
active, active,
attempts, attempts,
retryAfter, retryAfter,
lastAttemptTimestamp lastAttemptTimestamp
@ -181,7 +182,7 @@ export function updateToSchemaVersion1040(
${row.contentType}, ${row.contentType},
${row.size}, ${row.size},
${objectToJSON(row.attachment)}, ${objectToJSON(row.attachment)},
${row.active ? 1 : 0}, ${row.active ? 1 : 0},
${row.attempts}, ${row.attempts},
${row.retryAfter}, ${row.retryAfter},
${row.lastAttemptTimestamp} ${row.lastAttemptTimestamp}

View file

@ -23,6 +23,7 @@ import type { WritableDB, MessageType, ConversationType } from '../Interface';
import { strictAssert } from '../../util/assert'; import { strictAssert } from '../../util/assert';
import { missingCaseError } from '../../util/missingCaseError'; import { missingCaseError } from '../../util/missingCaseError';
import { isAciString } from '../../util/isAciString'; import { isAciString } from '../../util/isAciString';
import { safeParseStrict } from '../../util/schemas';
// Legacy type for calls that never had a call id // Legacy type for calls that never had a call id
type DirectCallHistoryDetailsType = { type DirectCallHistoryDetailsType = {
@ -177,7 +178,7 @@ function convertLegacyCallDetails(
endedTimestamp: null, endedTimestamp: null,
}; };
const result = callHistoryDetailsSchema.safeParse(callHistory); const result = safeParseStrict(callHistoryDetailsSchema, callHistory);
if (result.success) { if (result.success) {
return result.data; return result.data;
} }

View file

@ -21,6 +21,7 @@ import { prepare } from '../Server';
import { sql } from '../util'; import { sql } from '../util';
import { strictAssert } from '../../util/assert'; import { strictAssert } from '../../util/assert';
import { CallStatusValue } from '../../types/CallDisposition'; import { CallStatusValue } from '../../types/CallDisposition';
import { parseStrict, parseUnknown } from '../../util/schemas';
export function callLinkExists(db: ReadableDB, roomId: string): boolean { export function callLinkExists(db: ReadableDB, roomId: string): boolean {
const [query, params] = sql` const [query, params] = sql`
@ -58,7 +59,7 @@ export function getCallLinkRecordByRoomId(
return undefined; return undefined;
} }
return callLinkRecordSchema.parse(row); return parseUnknown(callLinkRecordSchema, row as unknown);
} }
export function getAllCallLinks(db: ReadableDB): ReadonlyArray<CallLinkType> { export function getAllCallLinks(db: ReadableDB): ReadonlyArray<CallLinkType> {
@ -68,7 +69,9 @@ export function getAllCallLinks(db: ReadableDB): ReadonlyArray<CallLinkType> {
return db return db
.prepare(query) .prepare(query)
.all() .all()
.map(item => callLinkFromRecord(callLinkRecordSchema.parse(item))); .map((item: unknown) =>
callLinkFromRecord(parseUnknown(callLinkRecordSchema, item))
);
} }
function _insertCallLink(db: WritableDB, callLink: CallLinkType): void { function _insertCallLink(db: WritableDB, callLink: CallLinkType): void {
@ -142,7 +145,10 @@ export function updateCallLinkState(
callLinkState: CallLinkStateType callLinkState: CallLinkStateType
): CallLinkType { ): CallLinkType {
const { name, restrictions, expiration, revoked } = callLinkState; const { name, restrictions, expiration, revoked } = callLinkState;
const restrictionsValue = callLinkRestrictionsSchema.parse(restrictions); const restrictionsValue = parseStrict(
callLinkRestrictionsSchema,
restrictions
);
const [query, params] = sql` const [query, params] = sql`
UPDATE callLinks UPDATE callLinks
SET SET
@ -153,9 +159,9 @@ export function updateCallLinkState(
WHERE roomId = ${roomId} WHERE roomId = ${roomId}
RETURNING *; RETURNING *;
`; `;
const row = db.prepare(query).get(params); const row: unknown = db.prepare(query).get(params);
strictAssert(row, 'Expected row to be returned'); strictAssert(row, 'Expected row to be returned');
return callLinkFromRecord(callLinkRecordSchema.parse(row)); return callLinkFromRecord(parseUnknown(callLinkRecordSchema, row));
} }
export function updateCallLinkAdminKeyByRoomId( export function updateCallLinkAdminKeyByRoomId(
@ -302,7 +308,7 @@ export function getAllCallLinkRecordsWithAdminKey(
return db return db
.prepare(query) .prepare(query)
.all() .all()
.map(item => callLinkRecordSchema.parse(item)); .map((item: unknown) => parseUnknown(callLinkRecordSchema, item));
} }
export function getAllMarkedDeletedCallLinkRoomIds( export function getAllMarkedDeletedCallLinkRoomIds(

View file

@ -8,7 +8,6 @@ import type {
} from '../../types/GroupSendEndorsements'; } from '../../types/GroupSendEndorsements';
import { import {
groupSendEndorsementExpirationSchema, groupSendEndorsementExpirationSchema,
groupSendCombinedEndorsementSchema,
groupSendMemberEndorsementSchema, groupSendMemberEndorsementSchema,
groupSendEndorsementsDataSchema, groupSendEndorsementsDataSchema,
} from '../../types/GroupSendEndorsements'; } from '../../types/GroupSendEndorsements';
@ -17,6 +16,7 @@ import type { ReadableDB, WritableDB } from '../Interface';
import { sql } from '../util'; import { sql } from '../util';
import type { AciString } from '../../types/ServiceId'; import type { AciString } from '../../types/ServiceId';
import { strictAssert } from '../../util/assert'; 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. * 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) { if (value == null) {
return null; return null;
} }
return groupSendEndorsementExpirationSchema.parse(value); return parseUnknown(groupSendEndorsementExpirationSchema, value as unknown);
} }
export function getGroupSendEndorsementsData( export function getGroupSendEndorsementsData(
@ -128,24 +128,21 @@ export function getGroupSendEndorsementsData(
WHERE groupId IS ${groupId} WHERE groupId IS ${groupId}
`; `;
const combinedEndorsement = groupSendCombinedEndorsementSchema const combinedEndorsement: unknown = prepare<Array<unknown>>(
.optional() db,
.parse( selectCombinedEndorsement
prepare<Array<unknown>>(db, selectCombinedEndorsement).get( ).get(selectCombinedEndorsementParams);
selectCombinedEndorsementParams
)
);
if (combinedEndorsement == null) { if (combinedEndorsement == null) {
return null; return null;
} }
const memberEndorsements = prepare<Array<unknown>>( const memberEndorsements: Array<unknown> = prepare<Array<unknown>>(
db, db,
selectMemberEndorsements selectMemberEndorsements
).all(selectMemberEndorsementsParams); ).all(selectMemberEndorsementsParams);
return groupSendEndorsementsDataSchema.parse({ return parseLoose(groupSendEndorsementsDataSchema, {
combinedEndorsement, combinedEndorsement,
memberEndorsements, memberEndorsements,
}); });
@ -168,5 +165,5 @@ export function getGroupSendMemberEndorsement(
if (row == null) { if (row == null) {
return null; return null;
} }
return groupSendMemberEndorsementSchema.parse(row); return parseUnknown(groupSendMemberEndorsementSchema, row as unknown);
} }

View file

@ -20,6 +20,7 @@ import type { JOB_STATUS } from '../../jobs/JobQueue';
import { JobQueue } from '../../jobs/JobQueue'; import { JobQueue } from '../../jobs/JobQueue';
import type { ParsedJob, StoredJob, JobQueueStore } from '../../jobs/types'; import type { ParsedJob, StoredJob, JobQueueStore } from '../../jobs/types';
import { sleep } from '../../util/sleep'; import { sleep } from '../../util/sleep';
import { parseUnknown } from '../../util/schemas';
describe('JobQueue', () => { describe('JobQueue', () => {
describe('end-to-end tests', () => { describe('end-to-end tests', () => {
@ -36,7 +37,7 @@ describe('JobQueue', () => {
class Queue extends JobQueue<TestJobData> { class Queue extends JobQueue<TestJobData> {
parseData(data: unknown): TestJobData { parseData(data: unknown): TestJobData {
return testJobSchema.parse(data); return parseUnknown(testJobSchema, data);
} }
async run({ async run({
@ -86,7 +87,7 @@ describe('JobQueue', () => {
class Queue extends JobQueue<number> { class Queue extends JobQueue<number> {
parseData(data: unknown): number { parseData(data: unknown): number {
return z.number().parse(data); return parseUnknown(z.number(), data);
} }
async run(): Promise<typeof JOB_STATUS.NEEDS_RETRY | undefined> { async run(): Promise<typeof JOB_STATUS.NEEDS_RETRY | undefined> {
@ -137,7 +138,7 @@ describe('JobQueue', () => {
class Queue extends JobQueue<number> { class Queue extends JobQueue<number> {
parseData(data: unknown): number { parseData(data: unknown): number {
return z.number().parse(data); return parseUnknown(z.number(), data);
} }
protected override getInMemoryQueue( protected override getInMemoryQueue(
@ -180,7 +181,7 @@ describe('JobQueue', () => {
class TestQueue extends JobQueue<string> { class TestQueue extends JobQueue<string> {
parseData(data: unknown): string { parseData(data: unknown): string {
return z.string().parse(data); return parseUnknown(z.string(), data);
} }
async run(): Promise<typeof JOB_STATUS.NEEDS_RETRY | undefined> { async run(): Promise<typeof JOB_STATUS.NEEDS_RETRY | undefined> {
@ -248,7 +249,7 @@ describe('JobQueue', () => {
class TestQueue extends JobQueue<string> { class TestQueue extends JobQueue<string> {
parseData(data: unknown): string { parseData(data: unknown): string {
return z.string().parse(data); return parseUnknown(z.string(), data);
} }
async run(): Promise<typeof JOB_STATUS.NEEDS_RETRY | undefined> { 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. // Chai's `assert.instanceOf` doesn't tell TypeScript anything, so we do it here.
if (!(booErr instanceof JobError)) { if (!(booErr instanceof JobError)) {
assert.fail('Expected error to be a JobError'); assert.fail('Expected error to be a JobError');
return;
} }
assert.include(booErr.message, 'bar job always fails in this test'); assert.include(booErr.message, 'bar job always fails in this test');
@ -367,7 +367,7 @@ describe('JobQueue', () => {
class TestQueue extends JobQueue<string> { class TestQueue extends JobQueue<string> {
parseData(data: unknown): string { parseData(data: unknown): string {
return z.string().parse(data); return parseUnknown(z.string(), data);
} }
async run( async run(
@ -412,7 +412,7 @@ describe('JobQueue', () => {
class TestQueue extends JobQueue<number> { class TestQueue extends JobQueue<number> {
parseData(data: unknown): number { parseData(data: unknown): number {
return z.number().parse(data); return parseUnknown(z.number(), data);
} }
async run( async run(
@ -490,7 +490,6 @@ describe('JobQueue', () => {
// Chai's `assert.instanceOf` doesn't tell TypeScript anything, so we do it here. // Chai's `assert.instanceOf` doesn't tell TypeScript anything, so we do it here.
if (!(jobError instanceof JobError)) { if (!(jobError instanceof JobError)) {
assert.fail('Expected error to be a JobError'); assert.fail('Expected error to be a JobError');
return;
} }
assert.include( assert.include(
jobError.message, jobError.message,
@ -740,7 +739,7 @@ describe('JobQueue', () => {
while (true) { while (true) {
// eslint-disable-next-line no-await-in-loop // eslint-disable-next-line no-await-in-loop
const [job] = await once(this.eventEmitter, 'drip'); 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> { class TestQueue extends JobQueue<number> {
parseData(data: unknown): number { parseData(data: unknown): number {
return z.number().parse(data); return parseUnknown(z.number(), data);
} }
async run({ async run({

View file

@ -22,6 +22,7 @@ import { getCallIdFromEra } from '../../util/callDisposition';
import { isValidUuid } from '../../util/isValidUuid'; import { isValidUuid } from '../../util/isValidUuid';
import { createDB, updateToVersion } from './helpers'; import { createDB, updateToVersion } from './helpers';
import type { WritableDB, MessageType } from '../../sql/Interface'; import type { WritableDB, MessageType } from '../../sql/Interface';
import { parsePartial } from '../../util/schemas';
describe('SQL/updateToSchemaVersion89', () => { describe('SQL/updateToSchemaVersion89', () => {
let db: WritableDB; let db: WritableDB;
@ -152,8 +153,8 @@ describe('SQL/updateToSchemaVersion89', () => {
return db return db
.prepare(selectHistoryQuery) .prepare(selectHistoryQuery)
.all() .all()
.map(row => { .map((row: object) => {
return callHistoryDetailsSchema.parse({ return parsePartial(callHistoryDetailsSchema, {
...row, ...row,
// Not present at the time of migration, but required by zod // Not present at the time of migration, but required by zod

View 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));
}
});
});

View file

@ -73,6 +73,7 @@ import { safeParseNumber } from '../util/numbers';
import { isStagingServer } from '../util/isStagingServer'; import { isStagingServer } from '../util/isStagingServer';
import type { IWebSocketResource } from './WebsocketResources'; import type { IWebSocketResource } from './WebsocketResources';
import type { GroupSendToken } from '../types/GroupSendEndorsements'; 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 // 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 // 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', responseType: 'json',
}); });
return whoamiResultZod.parse(response); return parseUnknown(whoamiResultZod, response);
} }
async function sendChallengeResponse(challengeResponse: ChallengeType) { async function sendChallengeResponse(challengeResponse: ChallengeType) {
@ -2027,7 +2028,7 @@ export function initialize({
httpType: 'GET', httpType: 'GET',
responseType: 'json', responseType: 'json',
}); });
const res = remoteConfigResponseZod.parse(rawRes); const res = parseUnknown(remoteConfigResponseZod, rawRes);
return { return {
...res, ...res,
@ -2157,7 +2158,7 @@ export function initialize({
responseType: 'json', responseType: 'json',
}); });
const result = verifyServiceIdResponse.safeParse(res); const result = safeParseUnknown(verifyServiceIdResponse, res);
if (result.success) { if (result.success) {
return result.data; return result.data;
@ -2223,7 +2224,8 @@ export function initialize({
hash, hash,
}: GetAccountForUsernameOptionsType) { }: GetAccountForUsernameOptionsType) {
const hashBase64 = toWebSafeBase64(Bytes.toBase64(hash)); const hashBase64 = toWebSafeBase64(Bytes.toBase64(hash));
return getAccountForUsernameResultZod.parse( return parseUnknown(
getAccountForUsernameResultZod,
await _ajax({ await _ajax({
call: 'username', call: 'username',
httpType: 'GET', httpType: 'GET',
@ -2251,7 +2253,7 @@ export function initialize({
return; return;
} }
return uploadAvatarHeadersZod.parse(res); return parseUnknown(uploadAvatarHeadersZod, res as unknown);
} }
async function getProfileUnauth( async function getProfileUnauth(
@ -2389,7 +2391,7 @@ export function initialize({
abortSignal, abortSignal,
}); });
return reserveUsernameResultZod.parse(response); return parseUnknown(reserveUsernameResultZod, response);
} }
async function confirmUsername({ async function confirmUsername({
hash, hash,
@ -2408,14 +2410,15 @@ export function initialize({
responseType: 'json', responseType: 'json',
abortSignal, abortSignal,
}); });
return confirmUsernameResultZod.parse(response); return parseUnknown(confirmUsernameResultZod, response);
} }
async function replaceUsernameLink({ async function replaceUsernameLink({
encryptedUsername, encryptedUsername,
keepLinkHandle, keepLinkHandle,
}: ReplaceUsernameLinkOptionsType): Promise<ReplaceUsernameLinkResultType> { }: ReplaceUsernameLinkOptionsType): Promise<ReplaceUsernameLinkResultType> {
return replaceUsernameLinkResultZod.parse( return parseUnknown(
replaceUsernameLinkResultZod,
await _ajax({ await _ajax({
call: 'usernameLink', call: 'usernameLink',
httpType: 'PUT', httpType: 'PUT',
@ -2440,7 +2443,8 @@ export function initialize({
async function resolveUsernameLink( async function resolveUsernameLink(
serverId: string serverId: string
): Promise<ResolveUsernameLinkResultType> { ): Promise<ResolveUsernameLinkResultType> {
return resolveUsernameLinkResultZod.parse( return parseUnknown(
resolveUsernameLinkResultZod,
await _ajax({ await _ajax({
httpType: 'GET', httpType: 'GET',
call: 'usernameLink', call: 'usernameLink',
@ -2475,7 +2479,8 @@ export function initialize({
transport: VerificationTransport transport: VerificationTransport
) { ) {
// Create a new blank session using just a E164 // Create a new blank session using just a E164
let session = verificationSessionZod.parse( let session = parseUnknown(
verificationSessionZod,
await _ajax({ await _ajax({
call: 'verificationSession', call: 'verificationSession',
httpType: 'POST', httpType: 'POST',
@ -2490,7 +2495,8 @@ export function initialize({
); );
// Submit a captcha solution to the session // Submit a captcha solution to the session
session = verificationSessionZod.parse( session = parseUnknown(
verificationSessionZod,
await _ajax({ await _ajax({
call: 'verificationSession', call: 'verificationSession',
httpType: 'PATCH', httpType: 'PATCH',
@ -2511,7 +2517,8 @@ export function initialize({
} }
// Request an SMS or Voice confirmation // Request an SMS or Voice confirmation
session = verificationSessionZod.parse( session = parseUnknown(
verificationSessionZod,
await _ajax({ await _ajax({
call: 'verificationSession', call: 'verificationSession',
httpType: 'POST', httpType: 'POST',
@ -2618,7 +2625,8 @@ export function initialize({
aciPqLastResortPreKey, aciPqLastResortPreKey,
pniPqLastResortPreKey, pniPqLastResortPreKey,
}: CreateAccountOptionsType) { }: CreateAccountOptionsType) {
const session = verificationSessionZod.parse( const session = parseUnknown(
verificationSessionZod,
await _ajax({ await _ajax({
isRegistration: true, isRegistration: true,
call: 'verificationSession', call: 'verificationSession',
@ -2676,7 +2684,7 @@ export function initialize({
jsonData, jsonData,
}); });
return createAccountResultZod.parse(responseJson); return parseUnknown(createAccountResultZod, responseJson);
} }
); );
} }
@ -2726,7 +2734,7 @@ export function initialize({
jsonData, jsonData,
}); });
return linkDeviceResultZod.parse(responseJson); return parseUnknown(linkDeviceResultZod, responseJson);
} }
); );
} }
@ -2842,7 +2850,7 @@ export function initialize({
responseType: 'json', responseType: 'json',
}); });
return getBackupInfoResponseSchema.parse(res); return parseUnknown(getBackupInfoResponseSchema, res);
} }
async function getBackupStream({ async function getBackupStream({
@ -2880,7 +2888,7 @@ export function initialize({
responseType: 'json', responseType: 'json',
}); });
return attachmentUploadFormResponse.parse(res); return parseUnknown(attachmentUploadFormResponse, res);
} }
function createFetchForAttachmentUpload({ function createFetchForAttachmentUpload({
@ -2932,7 +2940,7 @@ export function initialize({
responseType: 'json', responseType: 'json',
}); });
return attachmentUploadFormResponse.parse(res); return parseUnknown(attachmentUploadFormResponse, res);
} }
async function refreshBackup(headers: BackupPresentationHeadersType) { async function refreshBackup(headers: BackupPresentationHeadersType) {
@ -2961,7 +2969,7 @@ export function initialize({
responseType: 'json', responseType: 'json',
}); });
return getBackupCredentialsResponseSchema.parse(res); return parseUnknown(getBackupCredentialsResponseSchema, res);
} }
async function getBackupCDNCredentials({ async function getBackupCDNCredentials({
@ -2979,7 +2987,7 @@ export function initialize({
responseType: 'json', responseType: 'json',
}); });
return getBackupCDNCredentialsResponseSchema.parse(res); return parseUnknown(getBackupCDNCredentialsResponseSchema, res);
} }
async function setBackupId({ async function setBackupId({
@ -3051,7 +3059,7 @@ export function initialize({
}, },
}); });
return backupMediaBatchResponseSchema.parse(res); return parseUnknown(backupMediaBatchResponseSchema, res);
} }
async function backupDeleteMedia({ async function backupDeleteMedia({
@ -3099,7 +3107,7 @@ export function initialize({
urlParameters: `?${params.join('&')}`, urlParameters: `?${params.join('&')}`,
}); });
return backupListMediaResponseSchema.parse(res); return parseUnknown(backupListMediaResponseSchema, res);
} }
async function callLinkCreateAuth( async function callLinkCreateAuth(
@ -3111,7 +3119,7 @@ export function initialize({
responseType: 'json', responseType: 'json',
jsonData: { createCallLinkCredentialRequest: requestBase64 }, jsonData: { createCallLinkCredentialRequest: requestBase64 },
}); });
return callLinkCreateAuthResponseSchema.parse(response); return parseUnknown(callLinkCreateAuthResponseSchema, response);
} }
async function setPhoneNumberDiscoverability(newValue: boolean) { async function setPhoneNumberDiscoverability(newValue: boolean) {
@ -3354,7 +3362,10 @@ export function initialize({
accessKey: accessKeys != null ? Bytes.toBase64(accessKeys) : undefined, accessKey: accessKeys != null ? Bytes.toBase64(accessKeys) : undefined,
groupSendToken, groupSendToken,
}); });
const parseResult = multiRecipient200ResponseSchema.safeParse(response); const parseResult = safeParseUnknown(
multiRecipient200ResponseSchema,
response
);
if (parseResult.success) { if (parseResult.success) {
return parseResult.data; return parseResult.data;
} }
@ -3490,8 +3501,10 @@ export function initialize({
urlParameters: `/${encryptedStickers.length}`, urlParameters: `/${encryptedStickers.length}`,
}); });
const { packId, manifest, stickers } = const { packId, manifest, stickers } = parseUnknown(
StickerPackUploadFormSchema.parse(formJson); StickerPackUploadFormSchema,
formJson
);
// Upload manifest // Upload manifest
const manifestParams = makePutParams(manifest, encryptedManifest); const manifestParams = makePutParams(manifest, encryptedManifest);
@ -3718,7 +3731,8 @@ export function initialize({
} }
async function getAttachmentUploadForm() { async function getAttachmentUploadForm() {
return attachmentUploadFormResponse.parse( return parseUnknown(
attachmentUploadFormResponse,
await _ajax({ await _ajax({
call: 'attachmentUploadForm', call: 'attachmentUploadForm',
httpType: 'GET', httpType: 'GET',

View file

@ -62,6 +62,7 @@ import { ToastType } from '../types/Toast';
import { AbortableProcess } from '../util/AbortableProcess'; import { AbortableProcess } from '../util/AbortableProcess';
import type { WebAPICredentials } from './Types'; import type { WebAPICredentials } from './Types';
import { NORMAL_DISCONNECT_CODE } from './SocketManager'; import { NORMAL_DISCONNECT_CODE } from './SocketManager';
import { parseUnknown } from '../util/schemas';
const THIRTY_SECONDS = 30 * durations.SECOND; const THIRTY_SECONDS = 30 * durations.SECOND;
@ -107,7 +108,7 @@ export namespace AggregatedStats {
try { try {
const json = localStorage.getItem(key); const json = localStorage.getItem(key);
return json != null return json != null
? AggregatedStatsSchema.parse(JSON.parse(json)) ? parseUnknown(AggregatedStatsSchema, JSON.parse(json) as unknown)
: createEmpty(); : createEmpty();
} catch (error) { } catch (error) {
log.warn( log.warn(

View file

@ -6,6 +6,7 @@ import type { ConversationType } from '../state/ducks/conversations';
import { safeParseInteger } from '../util/numbers'; import { safeParseInteger } from '../util/numbers';
import { byteLength } from '../Bytes'; import { byteLength } from '../Bytes';
import type { StorageServiceFieldsType } from '../sql/Interface'; import type { StorageServiceFieldsType } from '../sql/Interface';
import { parsePartial } from '../util/schemas';
export enum CallLinkUpdateSyncType { export enum CallLinkUpdateSyncType {
Update = 'Update', Update = 'Update',
@ -44,7 +45,10 @@ export const callLinkRestrictionsSchema = z.nativeEnum(CallLinkRestrictions);
export function toCallLinkRestrictions( export function toCallLinkRestrictions(
restrictions: number | string restrictions: number | string
): CallLinkRestrictions { ): CallLinkRestrictions {
return callLinkRestrictionsSchema.parse(safeParseInteger(restrictions)); return parsePartial(
callLinkRestrictionsSchema,
safeParseInteger(restrictions)
);
} }
/** /**

View file

@ -3,6 +3,7 @@
import { z } from 'zod'; import { z } from 'zod';
import { aciSchema, type AciString } from './ServiceId'; import { aciSchema, type AciString } from './ServiceId';
import * as Bytes from '../Bytes'; import * as Bytes from '../Bytes';
import { parseStrict } from '../util/schemas';
const GROUPV2_ID_LENGTH = 32; // 32 bytes const GROUPV2_ID_LENGTH = 32; // 32 bytes
@ -94,5 +95,5 @@ export const groupSendTokenSchema = z
export type GroupSendToken = z.infer<typeof groupSendTokenSchema>; export type GroupSendToken = z.infer<typeof groupSendTokenSchema>;
export function toGroupSendToken(token: Uint8Array): GroupSendToken { export function toGroupSendToken(token: Uint8Array): GroupSendToken {
return groupSendTokenSchema.parse(token); return parseStrict(groupSendTokenSchema, token);
} }

View file

@ -68,6 +68,7 @@ import { drop } from './drop';
import { sendCallLinkUpdateSync } from './sendCallLinkUpdateSync'; import { sendCallLinkUpdateSync } from './sendCallLinkUpdateSync';
import { storageServiceUploadJob } from '../services/storage'; import { storageServiceUploadJob } from '../services/storage';
import { CallLinkDeleteManager } from '../jobs/CallLinkDeleteManager'; import { CallLinkDeleteManager } from '../jobs/CallLinkDeleteManager';
import { parsePartial, parseStrict } from './schemas';
// utils // utils
// ----- // -----
@ -200,7 +201,7 @@ export function getCallEventForProto(
callEventProto: Proto.SyncMessage.ICallEvent, callEventProto: Proto.SyncMessage.ICallEvent,
eventSource: string eventSource: string
): CallEventDetails { ): CallEventDetails {
const callEvent = callEventNormalizeSchema.parse(callEventProto); const callEvent = parsePartial(callEventNormalizeSchema, callEventProto);
const { callId, peerId, timestamp } = callEvent; const { callId, peerId, timestamp } = callEvent;
let type: CallType; let type: CallType;
@ -251,7 +252,7 @@ export function getCallEventForProto(
throw new TypeError(`Unknown call event ${callEvent.event}`); throw new TypeError(`Unknown call event ${callEvent.event}`);
} }
return callEventDetailsSchema.parse({ return parseStrict(callEventDetailsSchema, {
callId, callId,
peerId, peerId,
ringerId: null, ringerId: null,
@ -279,7 +280,10 @@ const callLogEventFromProto: Partial<
export function getCallLogEventForProto( export function getCallLogEventForProto(
callLogEventProto: Proto.SyncMessage.ICallLogEvent callLogEventProto: Proto.SyncMessage.ICallLogEvent
): CallLogEventDetails { ): CallLogEventDetails {
const callLogEvent = callLogEventNormalizeSchema.parse(callLogEventProto); const callLogEvent = parsePartial(
callLogEventNormalizeSchema,
callLogEventProto
);
const type = callLogEventFromProto[callLogEvent.type]; const type = callLogEventFromProto[callLogEvent.type];
if (type == null) { if (type == null) {
@ -496,7 +500,7 @@ export function getCallDetailsFromDirectCall(
call: Call call: Call
): CallDetails { ): CallDetails {
const ringerId = call.isIncoming ? call.remoteUserId : null; const ringerId = call.isIncoming ? call.remoteUserId : null;
return callDetailsSchema.parse({ return parseStrict(callDetailsSchema, {
callId: Long.fromValue(call.callId).toString(), callId: Long.fromValue(call.callId).toString(),
peerId, peerId,
ringerId, ringerId,
@ -518,7 +522,7 @@ export function getCallDetailsFromEndedDirectCall(
wasVideoCall: boolean, wasVideoCall: boolean,
timestamp: number timestamp: number
): CallDetails { ): CallDetails {
return callDetailsSchema.parse({ return parseStrict(callDetailsSchema, {
callId, callId,
peerId, peerId,
ringerId, ringerId,
@ -535,7 +539,7 @@ export function getCallDetailsFromGroupCallMeta(
peerId: AciString | string, peerId: AciString | string,
groupCallMeta: GroupCallMeta groupCallMeta: GroupCallMeta
): CallDetails { ): CallDetails {
return callDetailsSchema.parse({ return parseStrict(callDetailsSchema, {
callId: groupCallMeta.callId, callId: groupCallMeta.callId,
peerId, peerId,
ringerId: groupCallMeta.ringerId, ringerId: groupCallMeta.ringerId,
@ -552,7 +556,7 @@ export function getCallDetailsForAdhocCall(
peerId: AciString | string, peerId: AciString | string,
callId: string callId: string
): CallDetails { ): CallDetails {
return callDetailsSchema.parse({ return parseStrict(callDetailsSchema, {
callId, callId,
peerId, peerId,
ringerId: null, ringerId: null,
@ -575,7 +579,11 @@ export function getCallEventDetails(
event: LocalCallEvent, event: LocalCallEvent,
eventSource: string eventSource: string
): CallEventDetails { ): CallEventDetails {
return callEventDetailsSchema.parse({ ...callDetails, event, eventSource }); return parseStrict(callEventDetailsSchema, {
...callDetails,
event,
eventSource,
});
} }
// transitions // transitions
@ -646,7 +654,7 @@ export function transitionCallHistory(
`transitionCallHistory: Transitioned call history timestamp (before: ${callHistory?.timestamp}, after: ${timestamp})` `transitionCallHistory: Transitioned call history timestamp (before: ${callHistory?.timestamp}, after: ${timestamp})`
); );
return callHistoryDetailsSchema.parse({ return parseStrict(callHistoryDetailsSchema, {
callId, callId,
peerId, peerId,
ringerId, ringerId,

View file

@ -33,6 +33,7 @@ import {
getKeyFromCallLink, getKeyFromCallLink,
toAdminKeyBytes, toAdminKeyBytes,
} from './callLinks'; } from './callLinks';
import { parseStrict } from './schemas';
/** /**
* RingRTC conversions * RingRTC conversions
@ -56,7 +57,7 @@ const RingRTCCallLinkRestrictionsSchema = z.nativeEnum(
export function callLinkRestrictionsToRingRTC( export function callLinkRestrictionsToRingRTC(
restrictions: CallLinkRestrictions restrictions: CallLinkRestrictions
): RingRTCCallLinkRestrictions { ): RingRTCCallLinkRestrictions {
return RingRTCCallLinkRestrictionsSchema.parse(restrictions); return parseStrict(RingRTCCallLinkRestrictionsSchema, restrictions);
} }
export function getRoomIdFromRootKey(rootKey: CallLinkRootKey): string { export function getRoomIdFromRootKey(rootKey: CallLinkRootKey): string {
@ -152,7 +153,7 @@ export function callLinkToRecord(callLink: CallLinkType): CallLinkRecord {
const adminKey = callLink.adminKey const adminKey = callLink.adminKey
? toAdminKeyBytes(callLink.adminKey) ? toAdminKeyBytes(callLink.adminKey)
: null; : null;
return callLinkRecordSchema.parse({ return parseStrict(callLinkRecordSchema, {
roomId: callLink.roomId, roomId: callLink.roomId,
rootKey, rootKey,
adminKey, adminKey,

View file

@ -27,6 +27,7 @@ import { ToastType } from '../types/Toast';
import * as Errors from '../types/errors'; import * as Errors from '../types/errors';
import { isTestOrMockEnvironment } from '../environment'; import { isTestOrMockEnvironment } from '../environment';
import { isAlpha } from './version'; import { isAlpha } from './version';
import { parseStrict } from './schemas';
export function decodeGroupSendEndorsementResponse({ export function decodeGroupSendEndorsementResponse({
groupId, groupId,
@ -91,7 +92,7 @@ export function decodeGroupSendEndorsementResponse({
`decodeGroupSendEndorsementResponse: Received endorsements (group: ${idForLogging}, expiration: ${expiration}, members: ${groupMembers.length})` `decodeGroupSendEndorsementResponse: Received endorsements (group: ${idForLogging}, expiration: ${expiration}, members: ${groupMembers.length})`
); );
const groupEndorsementsData: GroupSendEndorsementsData = { return parseStrict(groupSendEndorsementsDataSchema, {
combinedEndorsement: { combinedEndorsement: {
groupId, groupId,
expiration, expiration,
@ -110,9 +111,7 @@ export function decodeGroupSendEndorsementResponse({
endorsement: endorsement.getContents(), endorsement: endorsement.getContents(),
}; };
}), }),
}; });
return groupSendEndorsementsDataSchema.parse(groupEndorsementsData);
} }
const TWO_DAYS = DurationInSeconds.fromDays(2); const TWO_DAYS = DurationInSeconds.fromDays(2);

View file

@ -5,6 +5,7 @@ import { z } from 'zod';
import { groupBy } from 'lodash'; import { groupBy } from 'lodash';
import * as log from '../logging/log'; import * as log from '../logging/log';
import { aciSchema } from '../types/ServiceId'; import { aciSchema } from '../types/ServiceId';
import { safeParseStrict } from './schemas';
const retryItemSchema = z const retryItemSchema = z
.object({ .object({
@ -53,7 +54,8 @@ export class RetryPlaceholders {
); );
} }
const parsed = retryItemListSchema.safeParse( const parsed = safeParseStrict(
retryItemListSchema,
window.storage.get(STORAGE_KEY, new Array<RetryItemType>()) window.storage.get(STORAGE_KEY, new Array<RetryItemType>())
); );
if (!parsed.success) { if (!parsed.success) {
@ -104,7 +106,7 @@ export class RetryPlaceholders {
// Basic data management // Basic data management
async add(item: RetryItemType): Promise<void> { async add(item: RetryItemType): Promise<void> {
const parsed = retryItemSchema.safeParse(item); const parsed = safeParseStrict(retryItemSchema, item);
if (!parsed.success) { if (!parsed.success) {
throw new Error( throw new Error(
`RetryPlaceholders.add: Item did not match schema ${JSON.stringify( `RetryPlaceholders.add: Item did not match schema ${JSON.stringify(

179
ts/util/schemas.ts Normal file
View 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;

View file

@ -73,6 +73,7 @@ import {
import { maybeUpdateGroup } from '../groups'; import { maybeUpdateGroup } from '../groups';
import type { GroupSendToken } from '../types/GroupSendEndorsements'; import type { GroupSendToken } from '../types/GroupSendEndorsements';
import { isAciString } from './isAciString'; import { isAciString } from './isAciString';
import { safeParseStrict, safeParseUnknown } from './schemas';
const UNKNOWN_RECIPIENT = 404; const UNKNOWN_RECIPIENT = 404;
const INCORRECT_AUTH_KEY = 401; const INCORRECT_AUTH_KEY = 401;
@ -603,7 +604,7 @@ export async function sendToGroupViaSenderKey(
{ online, story, urgent } { online, story, urgent }
); );
const parsed = multiRecipient200ResponseSchema.safeParse(result); const parsed = safeParseStrict(multiRecipient200ResponseSchema, result);
if (parsed.success) { if (parsed.success) {
const { uuids404 } = parsed.data; const { uuids404 } = parsed.data;
if (uuids404 && uuids404.length > 0) { if (uuids404 && uuids404.length > 0) {
@ -1022,7 +1023,10 @@ async function handle409Response(
error: HTTPError error: HTTPError
) { ) {
const logId = sendTarget.idForLogging(); const logId = sendTarget.idForLogging();
const parsed = multiRecipient409ResponseSchema.safeParse(error.response); const parsed = safeParseUnknown(
multiRecipient409ResponseSchema,
error.response
);
if (parsed.success) { if (parsed.success) {
await waitForAll({ await waitForAll({
tasks: parsed.data.map(item => async () => { tasks: parsed.data.map(item => async () => {
@ -1068,7 +1072,10 @@ async function handle410Response(
) { ) {
const logId = sendTarget.idForLogging(); const logId = sendTarget.idForLogging();
const parsed = multiRecipient410ResponseSchema.safeParse(error.response); const parsed = safeParseUnknown(
multiRecipient410ResponseSchema,
error.response
);
if (parsed.success) { if (parsed.success) {
await waitForAll({ await waitForAll({
tasks: parsed.data.map(item => async () => { tasks: parsed.data.map(item => async () => {

View file

@ -8,6 +8,7 @@ import { z } from 'zod';
import { strictAssert } from './assert'; import { strictAssert } from './assert';
import * as log from '../logging/log'; import * as log from '../logging/log';
import * as Errors from '../types/errors'; import * as Errors from '../types/errors';
import { parsePartial, parseUnknown, safeParseUnknown } from './schemas';
function toUrl(input: URL | string): URL | null { function toUrl(input: URL | string): URL | null {
if (input instanceof URL) { if (input instanceof URL) {
@ -164,7 +165,10 @@ function _route<Key extends string, Args extends object>(
); );
return null; return null;
} }
const parseResult = config.schema.safeParse(rawArgs); const parseResult = safeParseUnknown(
config.schema,
rawArgs as unknown
);
if (parseResult.success) { if (parseResult.success) {
const args = parseResult.data; const args = parseResult.data;
return { return {
@ -183,13 +187,13 @@ function _route<Key extends string, Args extends object>(
}, },
toWebUrl(args) { toWebUrl(args) {
if (config.toWebUrl) { 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'); throw new Error('Route does not support web URLs');
}, },
toAppUrl(args) { toAppUrl(args) {
if (config.toAppUrl) { 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'); throw new Error('Route does not support app URLs');
}, },
@ -219,7 +223,7 @@ export const contactByPhoneNumberRoute = _route('contactByPhoneNumber', {
}), }),
parse(result) { parse(result) {
return { return {
phoneNumber: paramSchema.parse(result.hash.groups.phoneNumber), phoneNumber: parsePartial(paramSchema, result.hash.groups.phoneNumber),
}; };
}, },
toWebUrl(args) { toWebUrl(args) {

View file

@ -30,6 +30,7 @@ import {
onSync as onViewSync, onSync as onViewSync,
viewSyncTaskSchema, viewSyncTaskSchema,
} from '../messageModifiers/ViewSyncs'; } from '../messageModifiers/ViewSyncs';
import { safeParseUnknown } from './schemas';
const syncTaskDataSchema = z.union([ const syncTaskDataSchema = z.union([
deleteMessageSchema, deleteMessageSchema,
@ -86,7 +87,7 @@ export async function queueSyncTasks(
await removeSyncTaskById(id); await removeSyncTaskById(id);
return; return;
} }
const parseResult = syncTaskDataSchema.safeParse(data); const parseResult = safeParseUnknown(syncTaskDataSchema, data);
if (!parseResult.success) { if (!parseResult.success) {
log.error( log.error(
`${innerLogId}: Failed to parse. Deleting. Error: ${parseResult.error}` `${innerLogId}: Failed to parse. Deleting. Error: ${parseResult.error}`

View file

@ -20,6 +20,7 @@ import {
} from '../context/localeMessages'; } from '../context/localeMessages';
import { waitForSettingsChange } from '../context/waitForSettingsChange'; import { waitForSettingsChange } from '../context/waitForSettingsChange';
import { isTestOrMockEnvironment } from '../environment'; import { isTestOrMockEnvironment } from '../environment';
import { parseUnknown } from '../util/schemas';
const emojiListCache = new Map<string, LocaleEmojiListType>(); const emojiListCache = new Map<string, LocaleEmojiListType>();
@ -55,8 +56,8 @@ export const MinimalSignalContext: MinimalSignalContextType = {
'OptionalResourceService:getData', 'OptionalResourceService:getData',
`emoji-index-${locale}.json` `emoji-index-${locale}.json`
); );
const json = JSON.parse(Buffer.from(buf).toString()); const json: unknown = JSON.parse(Buffer.from(buf).toString());
const result = LocaleEmojiListSchema.parse(json); const result = parseUnknown(LocaleEmojiListSchema, json);
emojiListCache.set(locale, result); emojiListCache.set(locale, result);
return result; return result;
}, },