From aaf4568d89a28a50bcdbb9b278476bb4554621fb Mon Sep 17 00:00:00 2001 From: 4gray Date: Sun, 2 Aug 2026 11:13:52 +0200 Subject: [PATCH] fix(downloads): fail closed on invalid stored coordinates --- .../download-request-identity.spec.ts | 30 +++++++++++++--- .../database/download-request-identity.ts | 35 +++++++++++-------- 2 files changed, 47 insertions(+), 18 deletions(-) diff --git a/apps/electron-backend/src/app/events/database/download-request-identity.spec.ts b/apps/electron-backend/src/app/events/database/download-request-identity.spec.ts index 9688c3c39..3e46ca836 100644 --- a/apps/electron-backend/src/app/events/database/download-request-identity.spec.ts +++ b/apps/electron-backend/src/app/events/database/download-request-identity.spec.ts @@ -1,6 +1,9 @@ import * as schema from '../../database/schema'; import type { DownloadsDatabase } from './download-task'; -import { resolveExistingDownloadIdentity } from './download-request-identity'; +import { + type ExistingDownloadIdentityResolution, + resolveExistingDownloadIdentity, +} from './download-request-identity'; type DownloadRow = typeof schema.downloads.$inferSelect; @@ -66,9 +69,10 @@ describe('resolveExistingDownloadIdentity', () => { const row = createDownloadRow(); const harness = createQueryHarness([row], []); - await expect( - resolveExistingDownloadIdentity(harness.db, episodeRequest) - ).resolves.toEqual({ + const resolution: ExistingDownloadIdentityResolution = + await resolveExistingDownloadIdentity(harness.db, episodeRequest); + + expect(resolution).toEqual({ item: row, kind: 'match', migrateCanonicalId: false, @@ -112,6 +116,24 @@ describe('resolveExistingDownloadIdentity', () => { ).resolves.toEqual({ kind: 'conflict' }); }); + it.each([ + ['series id', { seriesXtreamId: Number.MAX_SAFE_INTEGER + 1 }], + ['season number', { seasonNumber: 2.5 }], + ['episode number', { episodeNumber: Number.NaN }], + ])( + 'fails closed when the canonical row has a present but unsafe %s', + async (_label, override) => { + const harness = createQueryHarness( + [createDownloadRow(override)], + [] + ); + + await expect( + resolveExistingDownloadIdentity(harness.db, episodeRequest) + ).resolves.toEqual({ kind: 'conflict' }); + } + ); + it('fails closed when multiple rows share the legacy coordinates', async () => { const harness = createQueryHarness([], [ createDownloadRow({ id: 42, xtreamId: 77 }), diff --git a/apps/electron-backend/src/app/events/database/download-request-identity.ts b/apps/electron-backend/src/app/events/database/download-request-identity.ts index e594bc5cd..314427424 100644 --- a/apps/electron-backend/src/app/events/database/download-request-identity.ts +++ b/apps/electron-backend/src/app/events/database/download-request-identity.ts @@ -11,20 +11,20 @@ export const DOWNLOAD_IDENTITY_KIND = { } as const; interface DownloadIdentityNone { - kind: typeof DOWNLOAD_IDENTITY_KIND.NONE; + readonly kind: typeof DOWNLOAD_IDENTITY_KIND.NONE; } interface DownloadIdentityConflict { - kind: typeof DOWNLOAD_IDENTITY_KIND.CONFLICT; + readonly kind: typeof DOWNLOAD_IDENTITY_KIND.CONFLICT; } interface DownloadIdentityMatch { - item: DownloadRow; - kind: typeof DOWNLOAD_IDENTITY_KIND.MATCH; - migrateCanonicalId: boolean; + readonly item: DownloadRow; + readonly kind: typeof DOWNLOAD_IDENTITY_KIND.MATCH; + readonly migrateCanonicalId: boolean; } -export type DownloadIdentityResolution = +export type ExistingDownloadIdentityResolution = | DownloadIdentityNone | DownloadIdentityConflict | DownloadIdentityMatch; @@ -38,7 +38,7 @@ export interface DownloadIdentityRequest { xtreamId: number; } -function isSafeInteger(value: number | undefined): value is number { +function isSafeInteger(value: number | null | undefined): value is number { return Number.isSafeInteger(value); } @@ -51,25 +51,32 @@ function rowHasConflictingCoordinates( > > ): boolean { + const { episodeNumber, seasonNumber, seriesXtreamId } = row; if ( - !isSafeInteger(row.seriesXtreamId ?? undefined) || - !isSafeInteger(row.seasonNumber ?? undefined) || - !isSafeInteger(row.episodeNumber ?? undefined) + seriesXtreamId === null || + seriesXtreamId === undefined || + seasonNumber === null || + seasonNumber === undefined || + episodeNumber === null || + episodeNumber === undefined ) { return false; } return ( - row.seriesXtreamId !== request.seriesXtreamId || - row.seasonNumber !== request.seasonNumber || - row.episodeNumber !== request.episodeNumber + !isSafeInteger(seriesXtreamId) || + !isSafeInteger(seasonNumber) || + !isSafeInteger(episodeNumber) || + seriesXtreamId !== request.seriesXtreamId || + seasonNumber !== request.seasonNumber || + episodeNumber !== request.episodeNumber ); } export async function resolveExistingDownloadIdentity( db: DownloadsDatabase, request: DownloadIdentityRequest -): Promise { +): Promise { const canonicalRows = await db .select() .from(schema.downloads)