Skip to content

feat(canvas): opt-in local cache for plugin Canvases (#473) - #479

Merged
InstaZDLL merged 6 commits into
mainfrom
feat/473-canvas-local-cache
Aug 2, 2026
Merged

feat(canvas): opt-in local cache for plugin Canvases (#473)#479
InstaZDLL merged 6 commits into
mainfrom
feat/473-canvas-local-cache

Conversation

@InstaZDLL

@InstaZDLL InstaZDLL commented Aug 2, 2026

Copy link
Copy Markdown
Owner

Follow-up to #478 (the waveflow:canvas/v1 world). Adds an opt-in local cache for plugin-sourced Canvases — mirroring the motion-artwork cache — so a Canvas can play offline and without re-streaming the CDN mp4 on every replay.

By default a plugin Canvas is a remote URL the webview streams; nothing is written to disk. With the cache on, fetch_track_canvas downloads the resolved mp4 into an app-wide LRU and returns the on-disk path instead.

Backend

  • paths.rs: canvas_cache_dir (<app-data>/waveflow/canvas_cache/) — a separate dir from motion_cache so the two caches size/evict/clear independently.
  • commands/canvas.rs: fetch_track_canvas reads app_setting['canvas.cache_enabled'] (default OFF) and, when on, downloads the safe URL via motion_cache::cache_mp4 (fallback to the remote URL if the download fails). Adds get_canvas_cache_info / set_canvas_cache_enabled / clear_canvas_cache, reusing the shared motion_cache primitives (download + LRU eviction + SSRF guard) — no new cache engine.
  • tauri.conf.json: asset-protocol scope canvas_cache/** so convertFileSrc resolves the local path.

Frontend

  • lib/tauri/canvas.ts: CanvasCacheInfo + getCanvasCacheInfo / setCanvasCacheEnabled / clearCanvasCache.
  • lib/tauri/plugins.ts: isCanvasPlugin predicate (mirrors isMetadataPlugin).
  • PluginOptions.tsx: extracted a shared LocalCacheOption component (toggle + footprint + clear) and reused it for both the motion-artwork cache and the new Canvas cache — no duplicated 130-line component. Rendered for canvas-world plugins.
  • PluginsCard.tsx: the ⚙️ gear now shows for canvas-world plugins too.
  • i18n: settings.canvasCache.* across all 17 locales (reuses each locale's existing clear/clearConfirm).

CanvasStage already tells a local path from a remote URL (the isRemoteCanvasUrl helper from #478), so no renderer change was needed.

Docs

CLAUDE.md (canvas clause), docs/features/plugins.md + docs/features/ui.md (opt-in cache paragraphs).

Validation

  • cargo check -p waveflow + cargo clippy -p waveflow --all-targets — clean
  • bun run typecheck / bun run lint — clean
  • i18n verified against the real files (UTF-8 accents/scripts intact, clean 7-line-add diff per locale, no CRLF drift)

Summary by CodeRabbit

  • Nouvelles fonctionnalités

    • Ajout d’une mise en cache locale optionnelle des vidéos Canvas, désactivée par défaut.
    • Lecture hors ligne des vidéos mises en cache.
    • Affichage de l’état, de l’espace utilisé et du nombre de fichiers.
    • Activation, désactivation et suppression du cache depuis les options Canvas.
  • Améliorations

    • Renforcement de la sécurité et de la fiabilité des téléchargements Canvas.
  • Documentation

    • Documentation et traductions mises à jour dans les langues prises en charge.

@InstaZDLL InstaZDLL added scope: frontend React/Vite frontend (src/) scope: backend Rust/Tauri backend (src-tauri/) scope: i18n Translations (src/i18n/) scope: docs Docs, README, assets scope: plugins Plugin runtime, SDK, store, and bundled plugins type: feat New feature size: l 200-500 lines labels Aug 2, 2026
@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Le changement ajoute un cache local optionnel pour les vidéos Canvas fournies par les plugins. Le backend expose sa gestion via Tauri. Le frontend ajoute les réglages, les traductions et la détection des plugins Canvas. La validation des URL et des redirections est renforcée.

Changes

Cache local des Canvas

Layer / File(s) Summary
Validation des téléchargements Canvas
src-tauri/crates/core/src/artwork/motion_cache.rs, src-tauri/crates/app/src/commands/motion_artwork.rs
cache_mp4 valide l’URL initiale et chaque redirection HTTPS. Il refuse les cibles dangereuses, limite les redirections à dix étapes et expurge les URL des erreurs ordinaires.
Résolution et gestion backend du cache
src-tauri/crates/app/src/commands/canvas.rs, src-tauri/crates/app/src/paths.rs, src-tauri/crates/app/src/lib.rs, src-tauri/crates/app/tauri.conf.json
Le backend utilise un répertoire Canvas dédié. Il télécharge les MP4 lorsque le cache est actif et conserve l’URL distante après une erreur ordinaire. Tauri expose les commandes de consultation, d’activation et de purge.
API frontend et contrôles de cache
src/lib/tauri/canvas.ts, src/lib/tauri/plugins.ts, src/components/views/settings/PluginOptions.tsx, src/components/views/settings/PluginsCard.tsx, src/i18n/locales/*
L’API frontend expose l’état, l’activation et la purge du cache. LocalCacheOption est partagé avec le cache motion-artwork. Les plugins Canvas disposent d’un bouton d’options et de traductions dédiées.
Documentation du cache Canvas
CLAUDE.md, docs/features/plugins.md, docs/features/ui.md
La documentation décrit le cache local, le répertoire dédié, les contrôles de cache et l’intégration dans l’architecture des plugins.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Application
  participant PluginCanvas
  participant motion_cache
  participant InterfaceReglages
  Application->>PluginCanvas: Résout le Canvas du morceau
  PluginCanvas-->>Application: Retourne une URL MP4 distante
  Application->>motion_cache: Valide et télécharge le MP4 si le cache est actif
  motion_cache-->>Application: Retourne le chemin local ou l’URL distante
  InterfaceReglages->>Application: Consulte ou modifie l’état du cache
  Application-->>InterfaceReglages: Retourne l’activation et l’empreinte du cache
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed Le titre suit Conventional Commits et décrit clairement l’ajout du cache local optionnel pour les Canvas de plugins.
Description check ✅ Passed La description détaille les changements, la validation et la portée, mais omet les sections formelles du modèle.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/473-canvas-local-cache

Comment @coderabbitai help to get the list of available commands.

@InstaZDLL InstaZDLL self-assigned this Aug 2, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src-tauri/crates/app/src/commands/canvas.rs`:
- Around line 160-165: Persist a durable track/plugin-to-cached-file association
when the local cache stores a resolved MP4, then consult that association in the
canvas command before the offline early return around the offline check. In
offline mode, return the cached local path when a matching association exists;
otherwise preserve the existing None result, and keep the short-circuit before
any plugin invocation or HTTP download.
- Around line 225-245: Update the motion_cache::cache_mp4 download flow used by
the canvas URL handling to prevent unsafe redirects. Configure reqwest with a
custom redirect policy that validates every redirect destination via
is_safe_motion_url, or disable automatic redirects and handle each redirect
explicitly while applying the same validation; preserve the existing
cache-failure fallback to canvas.url.

In `@src/lib/tauri/plugins.ts`:
- Around line 617-623: Update the documentation for the PluginInfo.world field
to include the waveflow:canvas/v1 world, or accurately document the
waveflow:canvas prefix accepted by isCanvasPlugin. Keep the documented world
values aligned with the plugin worlds recognized by the interface.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 4ddd5313-e7ab-44c9-8851-a3a10bba2ee7

📥 Commits

Reviewing files that changed from the base of the PR and between de01b35 and 8ce04be.

📒 Files selected for processing (28)
  • CLAUDE.md
  • docs/features/plugins.md
  • docs/features/ui.md
  • src-tauri/crates/app/src/commands/canvas.rs
  • src-tauri/crates/app/src/lib.rs
  • src-tauri/crates/app/src/paths.rs
  • src-tauri/crates/app/tauri.conf.json
  • src/components/views/settings/PluginOptions.tsx
  • src/components/views/settings/PluginsCard.tsx
  • src/i18n/locales/ar.json
  • src/i18n/locales/de.json
  • src/i18n/locales/en.json
  • src/i18n/locales/es.json
  • src/i18n/locales/fr.json
  • src/i18n/locales/hi.json
  • src/i18n/locales/id.json
  • src/i18n/locales/it.json
  • src/i18n/locales/ja.json
  • src/i18n/locales/ko.json
  • src/i18n/locales/nl.json
  • src/i18n/locales/pt-BR.json
  • src/i18n/locales/pt.json
  • src/i18n/locales/ru.json
  • src/i18n/locales/tr.json
  • src/i18n/locales/zh-CN.json
  • src/i18n/locales/zh-TW.json
  • src/lib/tauri/canvas.ts
  • src/lib/tauri/plugins.ts

Comment thread src-tauri/crates/app/src/commands/canvas.rs
Comment thread src-tauri/crates/app/src/commands/canvas.rs
Comment thread src/lib/tauri/plugins.ts
@InstaZDLL

Copy link
Copy Markdown
Owner Author

@coderabbitai verified each finding. Applied 2, skipped 1 — pushed in 10f904b.

Applied

  • motion_cache::cache_mp4 — redirect SSRF: real hole. cache_mp4 built the client with no redirect policy, so reqwest's default chased up to 10 redirects while the caller only validated the initial url — a redirect to an internal / loopback / non-https target would slip past. Fixed with a redirect::Policy::custom that re-runs is_safe_motion_url on every hop (and caps at 10), erroring out otherwise. The existing cache-failure fallback to canvas.url is preserved (a rejected redirect surfaces as a download error → fallback). This hardens both the Canvas cache and the pre-existing motion-artwork cache, since they share this function.
  • plugins.tsPluginInfo.world doc: added waveflow:canvas/v1 to the documented world labels, aligning it with isCanvasPlugin.

Skipped

  • Offline → serve the cached local path via a durable track→file association: intentional consistency, not a regression. The Canvas cache deliberately mirrors the motion-artwork cache, which also returns None in offline mode (motion_artwork.rs short-circuits on offline::is_offline() before consulting the cache). Adding a persistent track/plugin→cached-file association for offline replay is a new feature that should land for both caches together (they're the same pattern), so it's out of scope for this follow-up. The cache key is blake3(url), and resolving a track to its url is itself a network op, so there's no existing association to consult offline without that new layer. Filing it as a future enhancement makes more sense than diverging the two caches here.

Validation: cargo check/clippy -p waveflow-core + bun run typecheck clean.

@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

git show --stat --oneline 10f904b2
git show --format=fuller --find-renames --find-copies -- \
  10f904b2 -- \
  src-tauri/crates/app/src/commands/motion_cache.rs \
  src/lib/tauri/plugins.ts

motion_cache_file="$(fd -t f '^motion_cache\.rs$' src-tauri)"
motion_artwork_file="$(fd -t f '^motion_artwork\.rs$' src-tauri)"
plugins_file="src/lib/tauri/plugins.ts"

printf '\n== motion_cache outline ==\n'
ast-grep outline "$motion_cache_file" --items all --type function --match 'cache_mp4|is_safe_motion_url' || true
printf '\n== redirect and safe-url implementation ==\n'
rg -n -C 18 'fn is_safe_motion_url|async fn cache_mp4|redirect::Policy|attempt\.url|previous\(\)' "$motion_cache_file"

printf '\n== offline behavior ==\n'
rg -n -C 12 'offline::is_offline|fetch_track_motion_artwork|cache_mp4' "$motion_artwork_file"

printf '\n== PluginInfo world documentation ==\n'
rg -n -C 6 'interface PluginInfo|world:|waveflow:canvas|isCanvasPlugin' "$plugins_file"

Length of output: 12569


@InstaZDLL Confirmé après vérification de 10f904b2.

  • motion_cache::cache_mp4 valide maintenant chaque cible de redirection avec is_safe_motion_url. Le correctif protège le cache Canvas et le cache motion-artwork partagé.
  • La documentation de PluginInfo.world inclut maintenant waveflow:canvas/v1.
  • Je retire le constat sur la lecture hors ligne. Le comportement est cohérent avec fetch_album_motion_artwork, qui retourne None avant toute résolution de plugin lorsque offline::is_offline() est actif. Une association durable entre piste, plugin et fichier cache est nécessaire pour traiter ce cas correctement dans les deux caches.

Les validations signalées sont suffisantes pour ces changements. Je n’ai pas exécuté les commandes de compilation dans cet environnement.

Souhaitez-vous que je crée une issue GitHub de suivi pour l’association durable nécessaire à la lecture hors ligne des caches Canvas et motion-artwork ?


✏️ Learnings added
Learnt from: InstaZDLL
URL: https://github.com/InstaZDLL/WaveFlow/pull/479

Timestamp: 2026-08-02T13:32:50.555Z
Learning: Dans `src-tauri/crates/app/src/commands/motion_artwork.rs`, `fetch_album_motion_artwork` retourne `Ok(None)` lorsque `offline::is_offline()` est actif, avant toute résolution de plugin et tout accès au cache. Les caches Canvas et motion-artwork partagent le modèle `motion_cache::cache_mp4`, dont la clé est `blake3(url)`. La lecture hors ligne nécessite donc une association durable entre la piste, le plugin et le fichier mis en cache, à implémenter conjointement pour les deux caches.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

You are interacting with an AI system.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src-tauri/crates/core/src/artwork/motion_cache.rs`:
- Around line 126-129: Update the redirect validation around is_safe_motion_url
in motion-cache handling to normalize hostnames by removing trailing dots before
applying safety checks, and reject targets whose resolved addresses are loopback
or otherwise internal at every redirect hop. Ensure the fallback to the remote
URL cannot expose an unsafe target, and add coverage for trailing-dot localhost
and 127.0.0.1 variants.
- Around line 123-126: In the redirect policy closure, update the
previous-attempt threshold in the visible redirect handling logic from >= 10 to
> 10 so exactly ten redirects are permitted while larger counts remain rejected.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: a1f739db-539f-4321-879f-1aa85d19c355

📥 Commits

Reviewing files that changed from the base of the PR and between 8ce04be and 10f904b.

📒 Files selected for processing (2)
  • src-tauri/crates/core/src/artwork/motion_cache.rs
  • src/lib/tauri/plugins.ts

Comment thread src-tauri/crates/core/src/artwork/motion_cache.rs Outdated
Comment thread src-tauri/crates/core/src/artwork/motion_cache.rs Outdated
@InstaZDLL

Copy link
Copy Markdown
Owner Author

@coderabbitai both applied — pushed.

  • Trailing-dot SSRF bypass: real gap. is_safe_motion_url now strips a trailing FQDN dot before the localhost + IP-literal checks ("localhost." != "localhost", and 127.0.0.1. fails to parse as an IP so it read as a harmless hostname). Added coverage for https://localhost./… and https://127.0.0.1./… (both now rejected). This hardens every caller of the shared guard (initial URL + each redirect hop).
  • Redirect count off-by-one: changed previous().len() >= 10> 10, so exactly 10 redirects are permitted (matching reqwest's default) and the 11th is rejected.

Skipped the "resolve each hop's address and reject internal resolutions" part: that's DNS resolution (DNS-rebinding), which the guard's doc explicitly calls out as an accepted residual ("a hostname that resolves to an internal IP at connect time … is a residual we accept — plugins are curated and user-installed"). Adding per-hop DNS resolution is a larger change beyond this PR's scope and a deliberate existing trade-off; the literal + scheme + trailing-dot checks cover the practical case.

Validation: cargo test -p waveflow-core --lib motion_cache (incl. the new trailing-dot cases) + cargo clippy clean.

@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

@InstaZDLL Confirmé.

  • La normalisation du point final bloque les contournements localhost. et 127.0.0.1. avant les contrôles SSRF.
  • La politique de redirection autorise maintenant exactement 10 redirections et rejette la 11e.
  • Les tests ciblés et cargo clippy couvrent correctement ces modifications.

Je n’insiste pas sur la résolution DNS par saut. Le risque de DNS rebinding reste documenté et accepté pour les plugins sélectionnés ou installés par l’utilisateur.


✏️ Learnings added
Learnt from: InstaZDLL
URL: https://github.com/InstaZDLL/WaveFlow/pull/479

Timestamp: 2026-08-02T14:01:52.097Z
Learning: Dans `src-tauri/crates/core/src/artwork/motion_cache.rs`, `is_safe_motion_url` doit retirer un point final de FQDN avant les contrôles `localhost` et littéral IP, car `localhost.` et `127.0.0.1.` peuvent contourner ces contrôles. La protection SSRF accepte explicitement comme risque résiduel la résolution DNS d’un nom d’hôte vers une adresse interne au moment de la connexion, y compris le DNS rebinding, pour les plugins sélectionnés ou installés par l’utilisateur.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

You are interacting with an AI system.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src-tauri/crates/core/src/artwork/motion_cache.rs (2)

122-138: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Bloquez les requêtes HTTP en mode hors ligne.

cache_mp4 peut appeler send() sans vérifier offline::is_offline(). Conservez le retour du fichier déjà en cache, puis quittez avant de créer le client HTTP lorsqu’aucun cache n’est disponible.

Correction proposée
     if path.exists() {
         // ...
         return Ok(path);
     }

+    if offline::is_offline() {
+        return Err("offline".to_owned());
+    }
+
     // Follow redirects, but re-validate EVERY hop against the SSRF guard

As per coding guidelines, chaque chemin HTTP sortant dans src-tauri/crates/core/src/**/*.rs doit vérifier offline::is_offline() avant la requête et retourner un résultat en cache ou vide.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src-tauri/crates/core/src/artwork/motion_cache.rs` around lines 122 - 138,
Update cache_mp4 to check offline::is_offline() after attempting to retrieve the
cached file and before constructing the HTTP client; return the cached file when
available, otherwise return the existing empty result without calling send().

Source: Coding guidelines


122-138: 🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Validez l’URL initiale dans cache_mp4.

Les deux appelants actuels effectuent ce contrôle, mais cache_mp4 est publique. Ajoutez is_safe_motion_url(url) avant toute requête réseau, puis conservez la validation de chaque redirection.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src-tauri/crates/core/src/artwork/motion_cache.rs` around lines 122 - 138,
Validate the initial URL at the start of the public cache_mp4 flow by calling
is_safe_motion_url(url) before constructing or using the HTTP client, and reject
unsafe URLs without any network request. Preserve the existing per-hop
validation in the redirect policy.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src-tauri/crates/core/src/artwork/motion_cache.rs`:
- Around line 122-138: Update cache_mp4 to check offline::is_offline() after
attempting to retrieve the cached file and before constructing the HTTP client;
return the cached file when available, otherwise return the existing empty
result without calling send().
- Around line 122-138: Validate the initial URL at the start of the public
cache_mp4 flow by calling is_safe_motion_url(url) before constructing or using
the HTTP client, and reject unsafe URLs without any network request. Preserve
the existing per-hop validation in the redirect policy.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 2c730bf3-d1f1-4cd2-9252-de7f03de802b

📥 Commits

Reviewing files that changed from the base of the PR and between 10f904b and fff142d.

📒 Files selected for processing (1)
  • src-tauri/crates/core/src/artwork/motion_cache.rs

@InstaZDLL

Copy link
Copy Markdown
Owner Author

@coderabbitai one applied, one skipped.

  • Validate the initial URL inside cache_mp4 — applied. Added a is_safe_motion_url(url) guard at the very top of cache_mp4, before any filesystem or network work, so the function is self-defending instead of trusting the caller's discipline (the redirect policy only covers hops 2+; this covers hop 1). Added cache_mp4_refuses_unsafe_url_before_network (loopback + non-https → Err, no network).
  • Add offline::is_offline() to cache_mp4 — skipped. cache_mp4 lives in waveflow-core, which has no offline module (that's app-side, crate::offline). The offline gate already runs in the callers (fetch_album_motion_artwork / fetch_track_canvas) which short-circuit before ever calling cache_mp4, so it's unreachable offline. Also cache_mp4 returns Result<PathBuf, String> (a cached path or an error), not an "empty result", and its first action is already the path.exists() cache-hit short-circuit — there's no valid change to make here. Serving the cache offline is the separate "durable association" feature discussed earlier, out of scope.

Validation: cargo test -p waveflow-core --lib motion_cache (incl. the new case) + cargo clippy clean.

@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

@InstaZDLL Confirmé.

cache_mp4 valide maintenant l’URL initiale avant tout accès au cache ou au réseau. Le test couvre les URL loopback et non HTTPS.

Je retire la demande de contrôle offline::is_offline() dans cache_mp4. waveflow-core ne doit pas dépendre du module offline de l’application. Les appelants effectuent déjà ce contrôle avant l’appel à cache_mp4.

Les validations ciblées et cargo clippy sont suffisantes pour ces changements.


✏️ Learnings added
Learnt from: InstaZDLL
URL: https://github.com/InstaZDLL/WaveFlow/pull/479

Timestamp: 2026-08-02T15:44:12.401Z
Learning: Dans `src-tauri/crates/core/src/artwork/motion_cache.rs`, `cache_mp4` appartient à `waveflow-core` et ne doit pas dépendre de `crate::offline`, qui appartient au crate applicatif. Les appelants app-side, notamment `fetch_album_motion_artwork` et `fetch_track_canvas`, doivent appliquer le contrôle `offline::is_offline()` avant d’appeler `cache_mp4`. La fonction `cache_mp4` renvoie `Result<PathBuf, String>` et doit valider elle-même l’URL initiale avec `is_safe_motion_url` avant tout accès au système de fichiers ou au réseau.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

You are interacting with an AI system.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src-tauri/crates/core/src/artwork/motion_cache.rs (1)

129-145: 🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

Ne repliez pas vers l’URL distante après un rejet de redirection.

cache_mp4 valide les redirections, mais fetch_track_canvas renvoie canvas.url si le cache est désactivé ou si le téléchargement échoue. CanvasStage transmet alors cette URL à <video>, qui suit ses redirections. Une URL HTTPS publique peut ainsi atteindre 127.0.0.1, un réseau privé ou HTTP.

Séparez le rejet de sécurité des erreurs ordinaires. Dans ce cas, ignorez le plugin sans renvoyer l’URL distante. Lorsque le cache est désactivé, utilisez un proxy contrôlé ou un autre chemin qui valide chaque redirection. Ajoutez un test public → loopback.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src-tauri/crates/core/src/artwork/motion_cache.rs` around lines 129 - 145,
Update fetch_track_canvas and its cache_mp4 failure handling so a rejected or
unsafe redirect is treated as a security failure: skip the plugin and never
return canvas.url to CanvasStage. Keep ordinary download failures distinct, and
when caching is disabled route the media through a controlled path that
validates every redirect instead of handing the remote URL to <video>. Add a
public-URL-to-loopback redirect test covering this behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src-tauri/crates/core/src/artwork/motion_cache.rs`:
- Around line 342-360: Make cache_mp4_refuses_unsafe_url_before_network assert
that both returned errors begin with “refusing unsafe url” rather than only
checking is_err(), proving the in-function guard rejected each URL. Also verify
the temporary cache directory remains free of created files after both calls.
- Around line 109-115: Update the unsafe-URL error returned by the motion-cache
validation in cache_mp4 so it does not interpolate the full url or expose query,
fragment, or userinfo; return a generic safe message instead. Audit the other
errors in the same URL-fetching flow that interpolate url and apply the same
redaction rule, while preserving the existing rejection behavior.

---

Outside diff comments:
In `@src-tauri/crates/core/src/artwork/motion_cache.rs`:
- Around line 129-145: Update fetch_track_canvas and its cache_mp4 failure
handling so a rejected or unsafe redirect is treated as a security failure: skip
the plugin and never return canvas.url to CanvasStage. Keep ordinary download
failures distinct, and when caching is disabled route the media through a
controlled path that validates every redirect instead of handing the remote URL
to <video>. Add a public-URL-to-loopback redirect test covering this behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 536092c0-b6fc-4bf1-87fa-cd6aeeb20705

📥 Commits

Reviewing files that changed from the base of the PR and between fff142d and 198ba47.

📒 Files selected for processing (1)
  • src-tauri/crates/core/src/artwork/motion_cache.rs

Comment thread src-tauri/crates/core/src/artwork/motion_cache.rs
Comment thread src-tauri/crates/core/src/artwork/motion_cache.rs
CodeRabbit round on #479. Three hardening fixes to the shared motion/canvas
mp4 cache:

- `cache_mp4` now returns a typed `CacheError { UnsafeUrl, Other }` instead of
  a bare String. An `UnsafeUrl` (initial url or a redirect hop failing the SSRF
  guard, or the redirect chain aborting) is a security rejection; `Other` is an
  ordinary failure (network / HTTP / oversize / disk). Previously both callers
  fell back to streaming the raw remote url on ANY cache error — so an unsafe
  redirect the per-hop guard refused was handed straight to the webview
  `<video>`, which would follow it unchecked, fully defeating the guard. Both
  `fetch_track_canvas` and the motion-cover fanout now skip the plugin on an
  `UnsafeUrl` and only degrade to streaming on an `Other` failure.

- Redact plugin-supplied urls in logged error strings (drop userinfo / query /
  fragment via `redact_url`) so a signed-token CDN url can't leak into logs;
  the `UnsafeUrl` message interpolates no url at all.

- Strengthen `cache_mp4_refuses_unsafe_url_before_network` to assert the typed
  `UnsafeUrl` variant and that no file is staged before the rejection; add a
  `redact_url` unit test.

Cache-off still streams the (initial-validated) remote url to `<video>`: a
redirect after that point is the same documented accepted residual as DNS
rebinding (initial url validated, `<video>` can't read cross-origin bodies,
plugins are curated/user-installed).
@InstaZDLL InstaZDLL added size: xl > 500 lines and removed size: l 200-500 lines labels Aug 2, 2026
@InstaZDLL

Copy link
Copy Markdown
Owner Author

@coderabbitai Traité dans 8d87e8c2.

Finding — unsafe redirect must be a security failure (skip, never stream the raw url) [FIXED]
You were right that this was a real hole: with the cache ON, cache_mp4 refusing an unsafe redirect made both callers fall back to streaming the same raw url to the webview <video>, which follows it unchecked — fully defeating the per-hop redirect guard. cache_mp4 now returns a typed CacheError { UnsafeUrl, Other }; UnsafeUrl covers the initial-url guard, an unsafe redirect hop, and an aborted redirect chain (reqwest::Error::is_redirect()). fetch_track_canvas and the motion-cover fanout now continue (skip the plugin) on UnsafeUrl and only degrade to streaming on an Other (network/HTTP/disk) failure.

Finding — redact url in error strings [FIXED]
Added redact_url (drops userinfo/query/fragment, keeps scheme+host+path); applied to the download/HTTP-status errors. The UnsafeUrl variant interpolates no url at all. Unit-tested.

Finding — strengthen the guard test [FIXED]
cache_mp4_refuses_unsafe_url_before_network now asserts the typed UnsafeUrl variant and that no file is staged in the temp dir before the rejection.

Declined — cache-OFF hands the remote url to <video> + a public→loopback redirect integration test
Cache-off streams the (initial-validated) remote url to the webview; a redirect after that point is the same class of accepted residual as DNS rebinding, already documented for the motion cover: the initial url is SSRF-validated, <video> can't read cross-origin response bodies, and plugins are curated/user-installed. Closing it would require a validating proxy/custom-protocol — disproportionate here.
The live redirect-to-loopback test isn't feasible in this crate without refactoring cache_mp4's internal client for resolver injection (reqwest's redirect::Attempt has no public constructor, and a "public" host can't be pointed at loopback otherwise). The redirect policy is a one-line closure over is_safe_motion_url, which is exhaustively unit-tested for every loopback/private/link-local form; the security-relevant caller behaviour (skip on UnsafeUrl) is now the thing under test.

@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

git show --stat --oneline 8d87e8c2
git show --format=fuller --find-renames --find-copies 8d87e8c2 -- \
  src-tauri/crates/core/src/artwork/motion_cache.rs \
  src-tauri/crates/app/src/commands/canvas.rs \
  src-tauri/crates/app/src/commands/motion_artwork.rs

cache_file="src-tauri/crates/core/src/artwork/motion_cache.rs"
canvas_file="src-tauri/crates/app/src/commands/canvas.rs"
motion_file="src-tauri/crates/app/src/commands/motion_artwork.rs"

printf '\n== cache module outline ==\n'
ast-grep outline "$cache_file" --items all --type function,struct,enum --match 'CacheError|cache_mp4|redact_url|is_safe_motion_url' || true

printf '\n== cache module implementation and tests ==\n'
rg -n -C 18 'enum CacheError|fn redact_url|async fn cache_mp4|is_safe_motion_url|is_redirect|cache_mp4_refuses_unsafe_url_before_network|redact' "$cache_file"

printf '\n== Canvas cache-error handling ==\n'
rg -n -C 16 'cache_mp4|CacheError|UnsafeUrl|fetch_track_canvas|canvas\.url' "$canvas_file"

printf '\n== motion-artwork cache-error handling ==\n'
rg -n -C 16 'cache_mp4|CacheError|UnsafeUrl|fetch_album_motion_artwork|motion' "$motion_file"

Length of output: 54223


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '== resolved reqwest dependency ==\n'
rg -n -C 2 '^name = "reqwest"$|^version = ' Cargo.lock src-tauri/Cargo.lock 2>/dev/null || true
rg -n -C 3 'reqwest' src-tauri/Cargo.toml src-tauri/crates/**/Cargo.toml 2>/dev/null || true

printf '\n== locally available reqwest Error display implementation ==\n'
for root in "${CARGO_HOME:-$HOME/.cargo}/registry/src" /usr/local/cargo/registry/src; do
  if [ -d "$root" ]; then
    fd -t f '^error\.rs$' "$root" -p | while IFS= read -r f; do
      if rg -q 'pub struct Error|impl.*Display.*Error' "$f"; then
        printf '\n-- %s --\n' "$f"
        rg -n -C 12 'impl.*Display.*Error|fmt\(&self|with_url|url:' "$f" || true
      fi
    done
  fi
done

Length of output: 6244


🤖 web_search executed:

In reqwest 0.12 source code, does impl Display for reqwest::Error include the request URL, query string, userinfo, or fragment? Find the error.rs implementation and cite the relevant code.

Length of output: 20848


@InstaZDLL Le traitement de CacheError::UnsafeUrl est correct.

  • cache_mp4 retourne UnsafeUrl pour une URL initiale non sûre et pour un abandon de redirection.
  • fetch_track_canvas et fetch_album_motion_artwork ignorent alors le plugin.
  • Le chemin cache activé ne transmet plus une redirection refusée au composant <video>.

Je maintiens toutefois le constat sur la redaction des URL.

Dans cache_mp4, le chemin d’échec réseau crée ce message :

format!("download {}: {e}", redact_url(url))

redact_url(url) redige bien le préfixe. Mais {e} formate directement reqwest::Error. Avec reqwest 0.12, ce type peut inclure son URL associée complète dans Display, y compris une query string. La documentation de reqwest indique explicitement ce risque et fournit Error::without_url() pour ce cas : https://docs.rs/reqwest/latest/reqwest/struct.Error.html

Il faut retirer l’URL attachée de l’erreur avant son formatage, puis conserver seulement la version produite par redact_url. Ajouter un test qui vérifie qu’un échec de requête ne journalise pas ?sig=... compléterait le correctif.

Le comportement cache désactivé reste le risque résiduel documenté. Je ne maintiens pas de constat supplémentaire sur ce point.

You are interacting with an AI system.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src-tauri/crates/core/src/artwork/motion_cache.rs (1)

384-448: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Ajoutez un test de régression pour la limite de dix redirections.

Les nouveaux tests couvrent bien les hôtes avec point final, le rejet pré-réseau typé, et redact_url. Ils ne couvrent pas la limite exacte de dix redirections déjà corrigée deux fois par le passé (>= 10 puis > 10). Un test avec un serveur HTTP local simulé (par exemple wiremock ou mockito) qui enchaîne dix redirections puis vérifie le rejet de la onzième éviterait une régression silencieuse sur cette limite.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src-tauri/crates/core/src/artwork/motion_cache.rs` around lines 384 - 448,
Ajoutez un test de régression autour de la logique de redirection de cache_mp4,
en utilisant un serveur HTTP local simulé pour enchaîner exactement dix
redirections puis vérifier que la onzième est rejetée. Le test doit confirmer
que la limite autorise dix redirections mais refuse toute redirection
supplémentaire, sans effectuer de requête réseau externe.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src-tauri/crates/core/src/artwork/motion_cache.rs`:
- Around line 384-448: Ajoutez un test de régression autour de la logique de
redirection de cache_mp4, en utilisant un serveur HTTP local simulé pour
enchaîner exactement dix redirections puis vérifier que la onzième est rejetée.
Le test doit confirmer que la limite autorise dix redirections mais refuse toute
redirection supplémentaire, sans effectuer de requête réseau externe.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 4e07e270-f038-4f4f-b254-17c89daa6473

📥 Commits

Reviewing files that changed from the base of the PR and between 198ba47 and 8d87e8c.

📒 Files selected for processing (3)
  • src-tauri/crates/app/src/commands/canvas.rs
  • src-tauri/crates/app/src/commands/motion_artwork.rs
  • src-tauri/crates/core/src/artwork/motion_cache.rs

CodeRabbit round on #479. Extract the redirect-policy decision into a pure
`redirect_decision(previous_hops, url)` -> `RedirectDecision` and unit-test the
"10 hops allowed, 11th rejected" boundary + per-hop SSRF re-validation.

A live mock-server test is infeasible here: `is_safe_motion_url` refuses
loopback, so every hop to a 127.0.0.1 mock is rejected as unsafe before the
count matters (and the initial request to it is refused up front). Splitting the
decision out makes the limit deterministically testable with no network.
@InstaZDLL

Copy link
Copy Markdown
Owner Author

@coderabbitai Traité dans b5beee66.

Finding — regression-test the redirect hop limit [FIXED, adapted]
Un vrai serveur mock local est infaisable contre ce code : is_safe_motion_url refuse loopback, donc chaque hop vers un mock 127.0.0.1 est rejeté comme unsafe redirect target avant que la branche du compteur soit atteinte — et la requête initiale vers le mock est elle-même refusée par la garde d'entrée. Pointer un host « public » vers loopback demanderait d'injecter un resolver dans le client interne de cache_mp4 (reqwest n'expose pas de constructeur pour redirect::Attempt), refactor disproportionné.

À la place j'ai extrait la décision de la closure en fonction pure redirect_decision(previous_hops, url) -> RedirectDecision { Follow, TooMany, Unsafe }, et je teste exactement la borne demandée sans réseau : 0..=10 hops sur cible sûre → Follow, 11e (et 50e) → TooMany, cible interne → Unsafe, et la limite prime sur la garde SSRF (fail-closed dans les deux cas). La closure appelle désormais cette fonction, donc le comportement prod et le test partagent la même logique. 5 passed sur motion_cache.

@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

git show --stat --oneline b5beee66
git show --format=fuller --find-renames --find-copies b5beee66 -- \
  src-tauri/crates/core/src/artwork/motion_cache.rs

cache_file="src-tauri/crates/core/src/artwork/motion_cache.rs"

printf '\n== symbol outline ==\n'
ast-grep outline "$cache_file" --items all --type function,enum --match 'redirect_decision|RedirectDecision|cache_mp4|is_safe_motion_url' || true

printf '\n== redirect production path and tests ==\n'
rg -n -C 18 'RedirectDecision|redirect_decision|redirect::Policy|previous\(\)|TooMany|Unsafe|Follow|redirect.*hop' "$cache_file"

Length of output: 17629


@InstaZDLL Confirmé après vérification de b5beee66.

redirect_decision est appelée par la politique reqwest::redirect::Policy::custom. Le test couvre donc la même logique que le chemin de production.

Le test confirme les limites requises :

  • 0..=10 sauts précédents avec une cible sûre retournent Follow.
  • Le 11e saut, ainsi que les suivants, retournent TooMany.
  • Une cible loopback retourne Unsafe tant que la limite n’est pas atteinte.
  • À partir du 11e saut, TooMany a priorité. Le comportement reste fail-closed.

La solution sans serveur mock est adaptée. Le constat est traité.

You are interacting with an AI system.

@InstaZDLL
InstaZDLL merged commit 70cd206 into main Aug 2, 2026
14 checks passed
@InstaZDLL
InstaZDLL deleted the feat/473-canvas-local-cache branch August 2, 2026 16:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

scope: backend Rust/Tauri backend (src-tauri/) scope: docs Docs, README, assets scope: frontend React/Vite frontend (src/) scope: i18n Translations (src/i18n/) scope: plugins Plugin runtime, SDK, store, and bundled plugins size: xl > 500 lines type: feat New feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant