Include document rendering kind in thumbnail cache identity #510

Open
opened 2026-09-30 10:16:08 +00:00 by kayg · 3 comments
Owner

Static review of the document thumbnail worker found a cache identity gap. The worker can choose PDF or TextCard from the file's MIME type and suffix. The thumbnail cache and terminal failure marker use only the content hash (and thumbnail size for successful output).

Evidence: crates/plugins/files/src/thumbnails.rs selects TextCard from is_text_document before sniff_media, consults thumbnail_failed before that choice, and marks failed when format eligibility fails. crates/calternal-fs/src/thumbnails.rs uses {hash}-{size}.webp and {hash}.failed keys. Thus the cache identity omits a value that changes the rendering decision. A file rename or MIME change can keep the bytes and hash while changing that decision.

No original-file damage or authorization bypass was reproduced. This is a derived-preview consistency finding. Review whether successful and failed cache states need a rendering-kind discriminator, and how the stable thumbnail route should express it. The merge audit did not change the shared cache contract; it fixed unverified early size rejections so they do not mark a stale Index hash as failed.

Static review of the document thumbnail worker found a cache identity gap. The worker can choose PDF or TextCard from the file's MIME type and suffix. The thumbnail cache and terminal failure marker use only the content hash (and thumbnail size for successful output). Evidence: crates/plugins/files/src/thumbnails.rs selects TextCard from is_text_document before sniff_media, consults thumbnail_failed before that choice, and marks failed when format eligibility fails. crates/calternal-fs/src/thumbnails.rs uses {hash}-{size}.webp and {hash}.failed keys. Thus the cache identity omits a value that changes the rendering decision. A file rename or MIME change can keep the bytes and hash while changing that decision. No original-file damage or authorization bypass was reproduced. This is a derived-preview consistency finding. Review whether successful and failed cache states need a rendering-kind discriminator, and how the stable thumbnail route should express it. The merge audit did not change the shared cache contract; it fixed unverified early size rejections so they do not mark a stale Index hash as failed.
Author
Owner

Starting #510 on branch job/fix-510 at 558457cf32e1d429da3834a05ff2720d11284302; local origin/dev is 15e17aeafc8ea160c109e62fba57f6961c39d21f. I am syncing the job branch with origin/dev before tracing the cache contract.

Starting #510 on branch `job/fix-510` at `558457cf32e1d429da3834a05ff2720d11284302`; local `origin/dev` is `15e17aeafc8ea160c109e62fba57f6961c39d21f`. I am syncing the job branch with `origin/dev` before tracing the cache contract.
Author
Owner

Finding from origin/dev at 15e17aeafc8ea160c109e62fba57f6961c39d21f: .system/cache/thumbs keyed successful previews as {hash}-{size}.webp and terminal failures as {hash}.failed. files::thumbnails::generate_inner can select TextCard from path/MIME before sniffing and PDF after sniffing, while the lookup/marker identity omitted that choice. The stable Files thumbnail route accepted only size. I added typed cache entries and failure markers for media, pdf, and text-card; document_rendering_kinds_have_independent_success_and_failure_entries now proves the same hash can hold and retrieve separate outputs, and each success clears only its matching failure. cargo clippy -p calternal-fs --all-targets -- -D warnings and cargo test -p calternal-fs passed.

Finding from `origin/dev` at `15e17aeafc8ea160c109e62fba57f6961c39d21f`: `.system/cache/thumbs` keyed successful previews as `{hash}-{size}.webp` and terminal failures as `{hash}.failed`. `files::thumbnails::generate_inner` can select `TextCard` from path/MIME before sniffing and PDF after sniffing, while the lookup/marker identity omitted that choice. The stable Files thumbnail route accepted only size. I added typed cache entries and failure markers for `media`, `pdf`, and `text-card`; `document_rendering_kinds_have_independent_success_and_failure_entries` now proves the same hash can hold and retrieve separate outputs, and each success clears only its matching failure. `cargo clippy -p calternal-fs --all-targets -- -D warnings` and `cargo test -p calternal-fs` passed.
Author
Owner

#510 — final report

Head: 873f06febd5bd651ec9c8d3b7612c2fadaa1eab7

Built

Thumbnail success and failure cache entries now include the fixed renderer kind (media, pdf, or text-card). Files workers, typed thumbnail routes, Calendar projections, and preview URLs use the same indexed renderer selection. The OpenAPI schema and generated client expose the query kind.

Before, keys were {hash}-{size}.webp and {hash}.failed, so the same bytes could collide across renderers. After, they are {hash}-{kind}-{size}.webp and {hash}-{kind}.failed. Regression tests cover independent same-hash PDF/TextCard entries and route responses. The real-server adversarial probe uploaded identical PDF and text bytes and confirmed their URLs returned distinct thumbnails.

Gates and probes

  • cargo fmt --check: passed, no output.
  • cargo clippy -p calternal-fs --all-targets -- -D warnings: passed, no warnings.
  • cargo test -p calternal-fs: 43 unit tests and 42 storage tests passed; doc tests passed.
  • cargo clippy -p calternal-plugin-files --all-targets -- -D warnings: passed, no warnings.
  • cargo test -p calternal-plugin-files: test result: ok. 139 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 121.40s
  • cargo clippy -p calternal-plugin-calendar --all-targets -- -D warnings: passed, no warnings.
  • cargo test -p calternal-plugin-calendar: 80 unit tests, 1 cache test, and 3 protocol tests passed.
  • cargo clippy -p calternal-server --all-targets -- -D warnings: passed, no warnings.
  • cargo test -p calternal-server: test result: ok. 93 passed; 0 failed; 3 ignored; 0 measured; 0 filtered out; finished in 18.88s
  • Web: svelte-check found 0 errors and 0 warnings; Test Files 140 passed (140); Tests 917 passed (917); Duration 77.24s.
  • Real-server adversarial probe passed, including Cross-User classification gate: 328 operations classified, document sandbox checks, and HEIF/AVIF/PDF upload probe passed.
  • git diff --check: passed.

Performance

The release server built successfully; output: I started the local 10,000-PDF profile, but stopped the incomplete run under the time-cap rule after the job had exceeded four hours. The last checked database snapshot showed 547 indexed rows. The full route measurements were not produced, and docs/perf/baseline.json was not changed. This is the remaining gap.

Finished `release` profile [optimized] target(s) in 50m 53s

Decisions and compatibility

  • A missing kind query defaults to media to keep existing media thumbnail URLs working.
  • An invalid kind returns 400. A hash that does not resolve to an authorized indexed file of the requested renderer returns 403.
  • The stable kind is selected from the indexed MIME type and supported suffix. DESIGN §39 specifies the kind names and typed cache keys; it did not specify the legacy default or these error statuses.

Production-build screenshots

All full-page captures cover 390, 820, and 1440 px in light and dark for Calendar grid, Agenda, Files grid, and Search. The two extra 3× Calendar grid captures are linked below.

Surface 390 px (light / dark) 820 px (light / dark) 1440 px (light / dark)
Calendar grid light / dark light / dark light / dark
Agenda light / dark light / dark light / dark
Files grid light / dark light / dark light / dark
Search light / dark light / dark light / dark

Calendar grid 3×: light, dark.

# #510 — final report Head: `873f06febd5bd651ec9c8d3b7612c2fadaa1eab7` ## Built Thumbnail success and failure cache entries now include the fixed renderer kind (`media`, `pdf`, or `text-card`). Files workers, typed thumbnail routes, Calendar projections, and preview URLs use the same indexed renderer selection. The OpenAPI schema and generated client expose the query kind. Before, keys were `{hash}-{size}.webp` and `{hash}.failed`, so the same bytes could collide across renderers. After, they are `{hash}-{kind}-{size}.webp` and `{hash}-{kind}.failed`. Regression tests cover independent same-hash PDF/TextCard entries and route responses. The real-server adversarial probe uploaded identical PDF and text bytes and confirmed their URLs returned distinct thumbnails. ## Gates and probes - `cargo fmt --check`: passed, no output. - `cargo clippy -p calternal-fs --all-targets -- -D warnings`: passed, no warnings. - `cargo test -p calternal-fs`: 43 unit tests and 42 storage tests passed; doc tests passed. - `cargo clippy -p calternal-plugin-files --all-targets -- -D warnings`: passed, no warnings. - `cargo test -p calternal-plugin-files`: `test result: ok. 139 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 121.40s` - `cargo clippy -p calternal-plugin-calendar --all-targets -- -D warnings`: passed, no warnings. - `cargo test -p calternal-plugin-calendar`: 80 unit tests, 1 cache test, and 3 protocol tests passed. - `cargo clippy -p calternal-server --all-targets -- -D warnings`: passed, no warnings. - `cargo test -p calternal-server`: `test result: ok. 93 passed; 0 failed; 3 ignored; 0 measured; 0 filtered out; finished in 18.88s` - Web: `svelte-check found 0 errors and 0 warnings`; `Test Files 140 passed (140)`; `Tests 917 passed (917)`; `Duration 77.24s`. - Real-server adversarial probe passed, including `Cross-User classification gate: 328 operations classified`, document sandbox checks, and `HEIF/AVIF/PDF upload probe passed`. - `git diff --check`: passed. ## Performance The release server built successfully; output: I started the local 10,000-PDF profile, but stopped the incomplete run under the time-cap rule after the job had exceeded four hours. The last checked database snapshot showed 547 indexed rows. The full route measurements were not produced, and `docs/perf/baseline.json` was not changed. This is the remaining gap. ```text Finished `release` profile [optimized] target(s) in 50m 53s ``` ## Decisions and compatibility - A missing `kind` query defaults to `media` to keep existing media thumbnail URLs working. - An invalid kind returns 400. A hash that does not resolve to an authorized indexed file of the requested renderer returns 403. - The stable kind is selected from the indexed MIME type and supported suffix. DESIGN §39 specifies the kind names and typed cache keys; it did not specify the legacy default or these error statuses. ## Production-build screenshots All full-page captures cover 390, 820, and 1440 px in light and dark for Calendar grid, Agenda, Files grid, and Search. The two extra 3× Calendar grid captures are linked below. | Surface | 390 px (light / dark) | 820 px (light / dark) | 1440 px (light / dark) | | --- | --- | --- | --- | | Calendar grid | [light](https://git.kayg.org/attachments/d99abb1c-45c6-424e-8d53-41a76b981526) / [dark](https://git.kayg.org/attachments/3b60bd05-fa69-4f39-919b-451e9cf3baeb) | [light](https://git.kayg.org/attachments/713e97f6-40df-4364-b1b0-b8efab6a08bb) / [dark](https://git.kayg.org/attachments/696ff49f-bffa-4439-803d-67c656ec7e64) | [light](https://git.kayg.org/attachments/0471be4a-8a11-4e95-ad90-4480909c8f77) / [dark](https://git.kayg.org/attachments/4e80b37e-be56-4738-adbb-c3abb2d6b434) | | Agenda | [light](https://git.kayg.org/attachments/711c3680-247f-49a1-a78c-6a13dafbaf59) / [dark](https://git.kayg.org/attachments/fec546b8-53a8-4d17-a045-5315163bb667) | [light](https://git.kayg.org/attachments/cb981363-a93f-45e3-9283-b1684ddebadd) / [dark](https://git.kayg.org/attachments/680c0126-11a6-4b76-8394-e220f485a85b) | [light](https://git.kayg.org/attachments/4af71331-6830-406a-9ca4-2b9fedd9ec70) / [dark](https://git.kayg.org/attachments/79499641-eab2-40a2-851e-a48a4339e133) | | Files grid | [light](https://git.kayg.org/attachments/f40648b0-6b99-4ac9-8961-000828d7fc7b) / [dark](https://git.kayg.org/attachments/1ddf9900-22b2-4a1a-a2db-6b9c8d0159f5) | [light](https://git.kayg.org/attachments/1c4df1b9-455e-4c2e-8976-5127801c6531) / [dark](https://git.kayg.org/attachments/c88d97d8-fc1d-47aa-972f-0398c00a0ed5) | [light](https://git.kayg.org/attachments/7bd4bf98-bc42-4483-b1d1-faec57feb2d8) / [dark](https://git.kayg.org/attachments/8c9cee67-8937-4c4a-91bb-cf75aebad661) | | Search | [light](https://git.kayg.org/attachments/d2bb6f62-79e8-4e48-bb63-3e77a34d1f5b) / [dark](https://git.kayg.org/attachments/8e9754a1-5eac-4676-9372-f65af81a7ff8) | [light](https://git.kayg.org/attachments/e17017e9-1768-4e23-aaf8-6982c368442e) / [dark](https://git.kayg.org/attachments/d7ad51f2-96dc-4dc5-8d74-4aa54f5b4071) | [light](https://git.kayg.org/attachments/585a99a2-199f-45dc-8b18-1ae9bbdf4562) / [dark](https://git.kayg.org/attachments/def67fa0-8a0e-4a40-9057-7b077d1ede8c) | Calendar grid 3×: [light](https://git.kayg.org/attachments/1022150a-8c91-429f-8781-bfe3b5529c3b), [dark](https://git.kayg.org/attachments/22318a29-6e26-42ae-990f-33927f4b377e).
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
kayg/calternal#510
No description provided.