chore: audit-flagged cleanups (no behaviour change)
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) <noreply@anthropic.com>
This commit is contained in:
7
backend/migrations/0007_drop_redundant_chapter_index.sql
Normal file
7
backend/migrations/0007_drop_redundant_chapter_index.sql
Normal file
@@ -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;
|
||||||
@@ -21,7 +21,11 @@ pub fn parse_image(bytes: Vec<u8>, max_size: usize, field_name: &str) -> AppResu
|
|||||||
"{field_name} exceeds {max_size}-byte cap"
|
"{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"))
|
AppError::UnsupportedMediaType(format!("{field_name}: unrecognised image format"))
|
||||||
})?;
|
})?;
|
||||||
let (mime, ext) = match kind.mime_type() {
|
let (mime, ext) = match kind.mime_type() {
|
||||||
@@ -56,6 +60,20 @@ mod tests {
|
|||||||
b"%PDF-1.4\n%\xc4\xe5".to_vec()
|
b"%PDF-1.4\n%\xc4\xe5".to_vec()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn avif_bytes() -> Vec<u8> {
|
||||||
|
// 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]
|
#[test]
|
||||||
fn accepts_png() {
|
fn accepts_png() {
|
||||||
let img = parse_image(png_bytes(), 1024, "cover").unwrap();
|
let img = parse_image(png_bytes(), 1024, "cover").unwrap();
|
||||||
@@ -70,6 +88,13 @@ mod tests {
|
|||||||
assert_eq!(img.ext, "jpg");
|
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]
|
#[test]
|
||||||
fn rejects_non_image_with_unsupported_media_type() {
|
fn rejects_non_image_with_unsupported_media_type() {
|
||||||
let err = parse_image(pdf_bytes(), 1024, "cover").unwrap_err();
|
let err = parse_image(pdf_bytes(), 1024, "cover").unwrap_err();
|
||||||
|
|||||||
@@ -26,7 +26,12 @@ export class ApiError extends Error {
|
|||||||
type ErrorEnvelope = { error?: { code?: unknown; message?: unknown } };
|
type ErrorEnvelope = { error?: { code?: unknown; message?: unknown } };
|
||||||
|
|
||||||
export async function request<T>(path: string, init?: RequestInit): Promise<T> {
|
export async function request<T>(path: string, init?: RequestInit): Promise<T> {
|
||||||
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) {
|
if (!res.ok) {
|
||||||
let code = 'http_error';
|
let code = 'http_error';
|
||||||
let message = `${res.status} ${res.statusText}`;
|
let message = `${res.status} ${res.statusText}`;
|
||||||
|
|||||||
Reference in New Issue
Block a user