feat(audit): Batch 24 — one validate-and-coerce schema layer for settings (CC9)
zod field schemas in settingsSchema.ts replace setSettings' bespoke per-key switch: parse → skip-on-fail → store, side effects (secret encryption, launch-at-startup sync) kept in settings.ts. sanitizeOptions extracted to settingsOptions.ts to break the import cycle; backup import flows through the same schema. Field-by-field parity pinned in test/settingsSchema.test.ts (15 tests); full suite unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
import {
|
||||
AUDIO_FORMATS,
|
||||
VIDEO_CONTAINERS,
|
||||
VIDEO_CODECS,
|
||||
SPONSORBLOCK_CATEGORIES,
|
||||
DEFAULT_DOWNLOAD_OPTIONS,
|
||||
type DownloadOptions,
|
||||
type SponsorBlockCategory,
|
||||
type AudioFormat,
|
||||
type VideoContainer,
|
||||
type VideoCodecPref
|
||||
} from '@shared/ipc'
|
||||
|
||||
// Coerce an untrusted partial into a valid DownloadOptions, falling back to the
|
||||
// defaults for any missing/invalid field. Used both to migrate older settings
|
||||
// files (which predate downloadOptions) and to validate renderer writes. Lives
|
||||
// in its own module (not settings.ts) so the settings field schemas (CC9) can
|
||||
// reuse it without an import cycle.
|
||||
export function sanitizeOptions(input: unknown): DownloadOptions {
|
||||
const o = (input && typeof input === 'object' ? input : {}) as Partial<DownloadOptions>
|
||||
const d = DEFAULT_DOWNLOAD_OPTIONS
|
||||
const bool = (v: unknown, fallback: boolean): boolean => (typeof v === 'boolean' ? v : fallback)
|
||||
const cats = Array.isArray(o.sponsorBlockCategories)
|
||||
? (o.sponsorBlockCategories.filter((c) =>
|
||||
(SPONSORBLOCK_CATEGORIES as readonly string[]).includes(c)
|
||||
) as SponsorBlockCategory[])
|
||||
: d.sponsorBlockCategories
|
||||
return {
|
||||
audioFormat: (AUDIO_FORMATS as readonly string[]).includes(o.audioFormat as string)
|
||||
? (o.audioFormat as AudioFormat)
|
||||
: d.audioFormat,
|
||||
videoContainer: (VIDEO_CONTAINERS as readonly string[]).includes(o.videoContainer as string)
|
||||
? (o.videoContainer as VideoContainer)
|
||||
: d.videoContainer,
|
||||
preferredVideoCodec: (VIDEO_CODECS as readonly string[]).includes(
|
||||
o.preferredVideoCodec as string
|
||||
)
|
||||
? (o.preferredVideoCodec as VideoCodecPref)
|
||||
: d.preferredVideoCodec,
|
||||
formatSort: typeof o.formatSort === 'string' ? o.formatSort.trim() : d.formatSort,
|
||||
embedSubtitles: bool(o.embedSubtitles, d.embedSubtitles),
|
||||
subtitleLanguages:
|
||||
typeof o.subtitleLanguages === 'string' && o.subtitleLanguages.trim()
|
||||
? o.subtitleLanguages.trim()
|
||||
: d.subtitleLanguages,
|
||||
autoSubtitles: bool(o.autoSubtitles, d.autoSubtitles),
|
||||
sponsorBlock: bool(o.sponsorBlock, d.sponsorBlock),
|
||||
sponsorBlockMode: o.sponsorBlockMode === 'mark' ? 'mark' : 'remove',
|
||||
sponsorBlockCategories: cats,
|
||||
embedChapters: bool(o.embedChapters, d.embedChapters),
|
||||
splitChapters: bool(o.splitChapters, d.splitChapters),
|
||||
embedMetadata: bool(o.embedMetadata, d.embedMetadata),
|
||||
metadataTitle: typeof o.metadataTitle === 'string' ? o.metadataTitle : undefined,
|
||||
metadataArtist: typeof o.metadataArtist === 'string' ? o.metadataArtist : undefined,
|
||||
metadataAlbum: typeof o.metadataAlbum === 'string' ? o.metadataAlbum : undefined,
|
||||
embedThumbnail: bool(o.embedThumbnail, d.embedThumbnail),
|
||||
cropThumbnail: bool(o.cropThumbnail, d.cropThumbnail),
|
||||
writeInfoJson: bool(o.writeInfoJson, d.writeInfoJson),
|
||||
writeThumbnailFile: bool(o.writeThumbnailFile, d.writeThumbnailFile),
|
||||
writeDescription: bool(o.writeDescription, d.writeDescription)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user