Skip to content

feat(score): score PDF storage + IPC (clearfolio viewer contract, PR 1/3)#581

Open
seonghobae wants to merge 1 commit into
developfrom
feat/score-pdf-storage
Open

feat(score): score PDF storage + IPC (clearfolio viewer contract, PR 1/3)#581
seonghobae wants to merge 1 commit into
developfrom
feat/score-pdf-storage

Conversation

@seonghobae

Copy link
Copy Markdown
Collaborator

What

PR 1 of 3 for the 악보 (Score PDF) feature: Rust storage + IPC only. The React viewer ships in a later PR.

New Tauri commands in apps/desktop/src-tauri/src/main.rs:

  • attach_score_pdf(project_id, song_id) — opens an OS file dialog filtered to .pdf, validates the selection (%PDF- magic bytes, ≤ 25MB, real non-symlinked file), copies it into the app-owned <app_local_data_dir>/<project_id>/scores/<uuid>.pdf, and returns {scoreId, fileName, fileSizeBytes}.
  • read_score_pdf(project_id, score_id) — validates both ids against strict allowlist shapes, rebuilds the path locally, canonicalizes and enforces the scores-root prefix, returns the file bytes.
  • remove_score_pdf(project_id, score_id) — same guards; idempotent delete returning bool.

All three are registered in the invoke handler, the build.rs app manifest, and capabilities/main.json.

Why

Adopts the clearfolio viewer IPC contract (scoreId/fileName/fileSizeBytes payload plus attach/read/remove verbs) so the later viewer PR can drop in against a stable backend surface.

License blocker note: clearfolio's contract shape is adopted as a design reference only — no clearfolio code is vendored or copied into this repository, keeping BandScope clear of clearfolio's license terms. Everything here is an independent implementation following BandScope's existing command patterns (normalize_local_audio_source, youtube_source_from_metadata).

Drive-by fix

import_youtube_url, save_project, and load_project were registered in the invoke handler but missing from the build.rs app manifest and capabilities/main.json, so their permissions were never autogenerated or granted to the main window. They are now listed in both, and the autogenerated permission TOMLs are committed.

Security Notes

  • Attack surface / trust boundaries: file import via OS dialog (User Input Boundary), three new IPC commands (IPC Boundary), app-owned score storage (Storage Boundary).
  • Realistic threats: path traversal via crafted project_id/score_id, symlink escapes out of the app data dir, mislabeled or oversized non-PDF files attached as scores, JS-supplied arbitrary paths.
  • Mitigations:
    • The source path comes exclusively from the native file dialog — JS can never pass a filesystem path to any of the three commands.
    • Magic-byte (%PDF-) + 25MB size + extension + non-symlink + regular-file validation before the copy.
    • The file is copied into the app-owned scores directory and renamed to a locally minted UUID v4; no external path is referenced after attach returns.
    • project_id must match the exact shape minted by next_project_id (project-<digits>-<digits>); score_id must be an exact lowercase hyphenated UUID. Both are allowlist checks with no separators or .. possible.
    • Reads/deletes rebuild the path locally, refuse symlinks, then canonicalize and enforce a scores-root prefix guard.
    • Errors are generic; no untrusted paths are echoed back or logged.
  • Remaining risk: the copied PDF is still untrusted content — the later viewer PR must render it in a sandboxed PDF renderer, never as trusted HTML. Retention/cleanup of orphaned score files follows the existing project-workspace cleanup story.
  • Test points: unit tests cover the project-id guard (traversal payloads rejected, generated ids accepted), the UUID guard (uppercase/braced/traversal rejected), magic-byte/size/extension/symlink rejection, and canonicalize+prefix traversal/symlink-escape rejection.

Tests

  • cargo fmt --check, cargo clippy -- -D warnings, cargo test (12 passed, incl. 4 new score tests)
  • npm run lint, npm run typecheck, npm run build, npm run check:rust — green
  • Pre-existing local flakiness in apps/desktop/src/App.test.tsx reproduces identically on a clean develop checkout and is unrelated (no TS/JS source touched here).

🤖 Generated with Claude Code

…1/3)

Add three Tauri commands backing the upcoming score viewer:

- attach_score_pdf(project_id, song_id): OS file dialog (.pdf only),
  validates %PDF- magic bytes and a 25MB size cap, copies the file
  into the app-owned <app_local_data_dir>/<project_id>/scores/<uuid>.pdf
  and returns {scoreId, fileName, fileSizeBytes}.
- read_score_pdf(project_id, score_id): strict lowercase-UUID allowlist,
  path rebuilt locally, canonicalize + prefix guard against traversal,
  returns file bytes.
- remove_score_pdf(project_id, score_id): same guards, idempotent delete.

Project ids are validated against the exact shape minted by
next_project_id (project-<nanos>-<counter>) before any path join.
Pure helpers are separated from the command wrappers and covered by
unit tests (uuid guard, traversal/symlink rejection, magic-byte and
size checks).

Drive-by fix: import_youtube_url, save_project, and load_project were
registered in the invoke handler but missing from the build.rs app
manifest and capabilities/main.json, so their permissions were never
generated or granted. Added them alongside the new score commands.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RjGVapDZ3k7V7zKYk16P4C
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant