From d81aca42a08eae02065e261a0bf2a035fcb3a93d Mon Sep 17 00:00:00 2001 From: MechaCat02 Date: Sat, 16 May 2026 23:32:02 +0200 Subject: [PATCH] chore: audit-flagged cleanups (no behaviour change) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four small follow-ups from the 0.9.0 audit, none of them user-visible: - Migration 0007 drops `chapters_manga_idx`. The 0001 schema declared both `UNIQUE (manga_id, number)` and `CREATE INDEX chapters_manga_idx ON (manga_id, number)`, but Postgres maintains an identical index for the unique constraint automatically — the explicit one was just paying for a second per-write update. Query plans are unchanged because the planner already preferred the constraint's index. - `upload::parse_image` sniffs from the first 64 bytes instead of the full image buffer. `infer` only looks at magic bytes anyway, so scanning 20 MiB is wasted work. Functionally identical; cheaper in the hot path. - AVIF was on the whitelist but had no test fixture. New `avif_bytes` helper produces a minimal `ftyp avif` header that `infer` recognises, and a new `accepts_avif` unit test covers the path end-to-end. - Frontend `request()` sets `credentials: 'include'`. Same-origin callers see no change (default was already `'same-origin'`), but the first user who configures `CORS_ALLOWED_ORIGINS` for a cross-origin deployment gets working cookies without having to chase a runtime ApiError trail. No version bump. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../0007_drop_redundant_chapter_index.sql | 7 +++++ backend/src/upload/mod.rs | 27 ++++++++++++++++++- frontend/src/lib/api/client.ts | 7 ++++- 3 files changed, 39 insertions(+), 2 deletions(-) create mode 100644 backend/migrations/0007_drop_redundant_chapter_index.sql diff --git a/backend/migrations/0007_drop_redundant_chapter_index.sql b/backend/migrations/0007_drop_redundant_chapter_index.sql new file mode 100644 index 0000000..0cc1e42 --- /dev/null +++ b/backend/migrations/0007_drop_redundant_chapter_index.sql @@ -0,0 +1,7 @@ +-- `chapters_manga_idx` over (manga_id, number) duplicates the implicit +-- index Postgres maintains for the `UNIQUE (manga_id, number)` +-- constraint from 0001 — same leading columns, same ordering, same +-- selectivity. Dropping the explicit one saves a per-write index +-- update without changing query plans. + +DROP INDEX IF EXISTS chapters_manga_idx; diff --git a/backend/src/upload/mod.rs b/backend/src/upload/mod.rs index 7463dd7..cdeb5d1 100644 --- a/backend/src/upload/mod.rs +++ b/backend/src/upload/mod.rs @@ -21,7 +21,11 @@ pub fn parse_image(bytes: Vec, max_size: usize, field_name: &str) -> AppResu "{field_name} exceeds {max_size}-byte cap" ))); } - let kind = infer::get(&bytes).ok_or_else(|| { + // `infer` only looks at magic bytes in the first dozen-or-so bytes; + // hand it a small head slice rather than walking the whole 20-MiB + // image buffer when sniffing. + let head = &bytes[..bytes.len().min(64)]; + let kind = infer::get(head).ok_or_else(|| { AppError::UnsupportedMediaType(format!("{field_name}: unrecognised image format")) })?; let (mime, ext) = match kind.mime_type() { @@ -56,6 +60,20 @@ mod tests { b"%PDF-1.4\n%\xc4\xe5".to_vec() } + fn avif_bytes() -> Vec { + // Minimal `ftyp avif` header that `infer` recognises. A real + // AVIF would continue with `mdat`/`meta` boxes; the magic bytes + // alone are enough for sniffing. + vec![ + 0x00, 0x00, 0x00, 0x18, // box size = 24 + b'f', b't', b'y', b'p', // box type + b'a', b'v', b'i', b'f', // major brand + 0x00, 0x00, 0x00, 0x00, // minor version + b'm', b'i', b'f', b'1', // compatible brand + b'a', b'v', b'i', b'f', // compatible brand + ] + } + #[test] fn accepts_png() { let img = parse_image(png_bytes(), 1024, "cover").unwrap(); @@ -70,6 +88,13 @@ mod tests { assert_eq!(img.ext, "jpg"); } + #[test] + fn accepts_avif() { + let img = parse_image(avif_bytes(), 1024, "cover").unwrap(); + assert_eq!(img.mime, "image/avif"); + assert_eq!(img.ext, "avif"); + } + #[test] fn rejects_non_image_with_unsupported_media_type() { let err = parse_image(pdf_bytes(), 1024, "cover").unwrap_err(); diff --git a/frontend/src/lib/api/client.ts b/frontend/src/lib/api/client.ts index 96b70e2..bc418f9 100644 --- a/frontend/src/lib/api/client.ts +++ b/frontend/src/lib/api/client.ts @@ -26,7 +26,12 @@ export class ApiError extends Error { type ErrorEnvelope = { error?: { code?: unknown; message?: unknown } }; export async function request(path: string, init?: RequestInit): Promise { - const res = await fetch(`${BASE}${path}`, init); + // Forward credentials (session cookie) explicitly so cross-origin + // deployments — those configured via CORS_ALLOWED_ORIGINS — keep + // working. For same-origin requests this is a no-op compared to the + // default 'same-origin', so the same-origin happy path is + // unchanged. + const res = await fetch(`${BASE}${path}`, { credentials: 'include', ...init }); if (!res.ok) { let code = 'http_error'; let message = `${res.status} ${res.statusText}`;