Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions apps/desktop/src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion apps/desktop/src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ bandscope-desktop-core = { path = "../core" }
rfd = "0.17.2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tauri = { version = "2.11.1", default-features = false, features = ["wry"] }
tauri = { version = "2.11.1", default-features = false, features = ["protocol-asset", "wry"] }
time = { version = "0.3", features = ["formatting", "macros"] }
tokio = { version = "1.50.0", features = ["time"] }
url = "2.5.8"
Expand Down
16 changes: 16 additions & 0 deletions apps/desktop/src-tauri/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,20 @@ fn lookup_bootstrap_source(
.ok_or_else(|| "Analysis job source was not found. Choose local audio again.".to_string())
}

/// Allow only the already-normalized source file to be served by the asset protocol.
///
/// Security Notes: the path is produced by the native file dialog or by the
/// validated, app-owned YouTube cache path. The protocol starts with an empty
/// scope, so this does not expose a directory or accept a path from JavaScript.
fn allow_audio_source_for_playback<R: Runtime>(
app: &tauri::AppHandle<R>,
source: &LocalAudioSourcePayload,
) -> Result<(), String> {
app.asset_protocol_scope()
.allow_file(&source.source_path)
.map_err(|_| "Could not prepare the selected audio for playback.".to_string())
}

fn drain_analysis_status_updates(
state: &AppState,
app: &tauri::AppHandle<impl Runtime>,
Expand Down Expand Up @@ -643,6 +657,7 @@ fn select_local_audio_source(
.pick_file()
.ok_or_else(|| "Choose a WAV, MP3, FLAC, or M4A file to start analysis.".to_string())?;
let source = normalize_local_audio_source(&path)?;
allow_audio_source_for_playback(&app, &source)?;
let project_id = next_project_id(&state);
let project_root = app_owned_root(&app, "projects", &project_id)?;
let cache_root = app_owned_root(&app, "cache", &project_id)?;
Expand Down Expand Up @@ -712,6 +727,7 @@ async fn import_youtube_url(
if parsed.get("ok").and_then(|v| v.as_bool()) == Some(true) {
if let Some(metadata) = parsed.get("metadata") {
let source = youtube_source_from_metadata(metadata, &cache_root)?;
allow_audio_source_for_playback(&app, &source)?;

let summary = ProjectBootstrapSummaryPayload {
project_id,
Expand Down
6 changes: 5 additions & 1 deletion apps/desktop/src-tauri/tauri.conf.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,11 @@
}
],
"security": {
"csp": "default-src 'self'; img-src 'self' asset: data: blob:; style-src 'self'; script-src 'self'; connect-src 'self' ipc: http://ipc.localhost; media-src 'self' asset: data: blob:; font-src 'self' data:",
"csp": "default-src 'self'; img-src 'self' asset: data: blob:; style-src 'self'; script-src 'self'; connect-src 'self' ipc: http://ipc.localhost; media-src 'self' asset: http://asset.localhost data: blob:; font-src 'self' data:",
Comment thread
seonghobae marked this conversation as resolved.
"assetProtocol": {
"enable": true,
"scope": []
},
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
"capabilities": ["main-capability"]
}
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { render, screen } from "@testing-library/react";
import { createDemoRehearsalSong } from "@bandscope/shared-types";
import { afterEach, describe, expect, it, vi } from "vitest";
import { RehearsalPlayer } from "./RehearsalPlayer";

const originalTauriInternals = Object.getOwnPropertyDescriptor(
window,
"__TAURI_INTERNALS__",
);

describe("RehearsalPlayer audio authority", () => {
afterEach(() => {
vi.restoreAllMocks();
if (originalTauriInternals) {
Object.defineProperty(window, "__TAURI_INTERNALS__", originalTauriInternals);
} else {
delete (window as Window & { __TAURI_INTERNALS__?: unknown })
.__TAURI_INTERNALS__;
}
});

it("refuses to start when local-audio metadata has no playable asset URL", () => {
const song = createDemoRehearsalSong();

render(
<RehearsalPlayer
song={song}
hasLocalAudio={true}
audioSourcePath="browser://selected-audio"
startNonce={1}
/>,
);

expect(
screen.getByRole("button", { name: /start the count-in/i }),
).toBeDisabled();
expect(
screen.getByTestId("rehearsal-loop-next-action").textContent,
).not.toMatch(/count in 4 beats/i);
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
});

it("reports native asset conversion failures instead of presenting missing-audio copy", () => {
Object.defineProperty(window, "__TAURI_INTERNALS__", {
configurable: true,
value: {
convertFileSrc: () => {
throw new Error("asset conversion failed");
},
},
});
const song = createDemoRehearsalSong();

render(
<RehearsalPlayer
song={song}
hasLocalAudio={true}
audioSourcePath="/Users/test/Music/late-night-set.wav"
/>,
);

expect(
screen.getByRole("button", { name: /start the count-in/i }),
).toBeDisabled();
expect(screen.getByRole("alert").textContent).toMatch(
/could not play this local audio/i,
);
});
});
Loading