diff --git a/desktop/src-tauri/Cargo.lock b/desktop/src-tauri/Cargo.lock index 68b702431a..92126818ba 100644 --- a/desktop/src-tauri/Cargo.lock +++ b/desktop/src-tauri/Cargo.lock @@ -1110,6 +1110,7 @@ dependencies = [ "getrandom 0.2.17", "hex", "image", + "include_dir", "infer", "iroh", "keyring", @@ -4249,6 +4250,25 @@ version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09e54e57b4c48b40f7aec75635392b12b3421fa26fe8b4332e63138ed278459c" +[[package]] +name = "include_dir" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "923d117408f1e49d914f1a379a309cffe4f18c05cf4e3d12e613a15fc81bd0dd" +dependencies = [ + "include_dir_macros", +] + +[[package]] +name = "include_dir_macros" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cab85a7ed0bd5f0e76d93846e0147172bed2e2d3f859bcc33a8d9699cad1a75" +dependencies = [ + "proc-macro2", + "quote", +] + [[package]] name = "indexmap" version = "1.9.3" diff --git a/desktop/src-tauri/Cargo.toml b/desktop/src-tauri/Cargo.toml index f41fa2d6e3..598c4fbc46 100644 --- a/desktop/src-tauri/Cargo.toml +++ b/desktop/src-tauri/Cargo.toml @@ -81,6 +81,11 @@ tauri-plugin-updater = "2" tauri-plugin-process = "2" infer = "0.19" hex = "0.4" +# Compile-time embedding of the Project Canvas seed template. Keep the +# `cargo:rerun-if-changed` line in build.rs alongside it: `include_dir!` expands +# to `include_bytes!` per file, so rustc notices edits to existing files but not +# files being added or removed. +include_dir = "0.7" ed25519-dalek = "=3.0.0-rc.0" tokio = { version = "1", features = ["fs", "sync", "rt", "macros", "time", "net", "io-util"] } tokio-tungstenite = { version = "0.29", features = ["rustls-tls-webpki-roots"] } diff --git a/desktop/src-tauri/build.rs b/desktop/src-tauri/build.rs index 8b0e63f12b..fed9c774c3 100644 --- a/desktop/src-tauri/build.rs +++ b/desktop/src-tauri/build.rs @@ -20,6 +20,11 @@ fn main() { println!("cargo:rerun-if-env-changed=BUZZ_BUILD_AUTO_CONNECT_DEFAULT_RELAY"); println!("cargo:rerun-if-env-changed=BUZZ_BUILD_DEMO_SLUG"); println!("cargo:rustc-check-cfg=cfg(buzz_updater_enabled)"); + // `include_dir!` embeds the Project Canvas seed template through one + // `include_bytes!` per file, so rustc already tracks edits to existing + // files — but not a file being added or removed. Without this the binary + // can silently ship a stale file list. + println!("cargo:rerun-if-changed=resources/project-canvas-template"); if let Ok(slug) = std::env::var("BUZZ_BUILD_DEMO_SLUG") { let valid = !slug.is_empty() diff --git a/desktop/src-tauri/src/project_canvas_package/mod.rs b/desktop/src-tauri/src/project_canvas_package/mod.rs index 8217953cc3..19b781c946 100644 --- a/desktop/src-tauri/src/project_canvas_package/mod.rs +++ b/desktop/src-tauri/src/project_canvas_package/mod.rs @@ -3,6 +3,7 @@ mod manifest; mod path_security; mod protocol; mod storage; +mod template; #[cfg(test)] mod tests; @@ -22,7 +23,9 @@ use storage::{ active_snapshot, clear_committed_updates, commit_snapshot, pending_updates, prepare_snapshot, project_source_location, prune_revisions, record_pending_update, record_source_binding, snapshot_for_revision, validate_widget_id, ProjectBinding, ProjectCanvasSourceLocation, + ValidatedPackage, }; +use template::bundled_template; const MAX_ACTIVE_LOADS: usize = 64; @@ -65,7 +68,7 @@ impl ProjectCanvasRuntime { fn get_or_activate( &self, request: ProjectCanvasPackageRequest, - template: &std::path::Path, + template: Option<&ValidatedPackage>, ) -> Result { let binding = ProjectBinding::parse(request)?; ensure_supported_platform()?; @@ -77,7 +80,7 @@ impl ProjectCanvasRuntime { let root = self.root()?; let snapshot = match active_snapshot(&root, &binding)? { Some(snapshot) => snapshot, - None => prepare_snapshot(&root, &binding, Some(template))?, + None => prepare_snapshot(&root, &binding, template)?, }; let mut retained = self.referenced_revisions(&binding)?; retained.insert(snapshot.revision.clone()); @@ -91,7 +94,7 @@ impl ProjectCanvasRuntime { fn activate( &self, request: ProjectCanvasPackageRequest, - template: &std::path::Path, + template: Option<&ValidatedPackage>, ) -> Result { let binding = ProjectBinding::parse(request)?; ensure_supported_platform()?; @@ -100,7 +103,7 @@ impl ProjectCanvasRuntime { .lock() .map_err(|_| "project canvas activation lock is unavailable".to_string())?; let root = self.root()?; - let snapshot = prepare_snapshot(&root, &binding, Some(template))?; + let snapshot = prepare_snapshot(&root, &binding, template)?; let mut retained = self.referenced_revisions(&binding)?; retained.insert(snapshot.revision.clone()); prune_revisions(&root, &binding, &retained)?; @@ -395,12 +398,14 @@ struct ProjectCanvasPendingPresentation { #[tauri::command] pub(crate) async fn get_project_canvas_package( request: ProjectCanvasPackageRequest, - app: AppHandle, runtime: State<'_, ProjectCanvasRuntime>, ) -> Result { - let template = template_path(&app)?; let runtime = runtime.inner().clone(); - run_blocking(move || runtime.get_or_activate(request, &template)).await + run_blocking(move || { + let template = bundled_template()?; + runtime.get_or_activate(request, Some(&template)) + }) + .await } #[tauri::command] @@ -415,12 +420,14 @@ pub(crate) async fn get_project_canvas_updates( #[tauri::command] pub(crate) async fn activate_project_canvas_package( request: ProjectCanvasPackageRequest, - app: AppHandle, runtime: State<'_, ProjectCanvasRuntime>, ) -> Result { - let template = template_path(&app)?; let runtime = runtime.inner().clone(); - run_blocking(move || runtime.activate(request, &template)).await + run_blocking(move || { + let template = bundled_template()?; + runtime.activate(request, Some(&template)) + }) + .await } #[tauri::command] @@ -474,38 +481,6 @@ pub(crate) fn start_agent_update_listener(app: AppHandle) -> Result<(), String> ipc::start(app) } -fn template_path(app: &AppHandle) -> Result { - let resource_root = app.path().resource_dir().map_err(|error| error.to_string()); - let dev_exe_dir = if cfg!(debug_assertions) { - std::env::current_exe() - .ok() - .and_then(|exe| exe.parent().map(std::path::Path::to_path_buf)) - } else { - None - }; - template_path_from(resource_root, dev_exe_dir) -} - -/// Joins the bundled `project-canvas-template` resource against the app's -/// resource root. -/// -/// Dev builds run the bare executable out of the cargo target directory, -/// where `tauri-build` copies `bundle.resources` next to the binary — but -/// Tauri only recognizes that layout when the directory is literally named -/// `target`, so a redirected CARGO_TARGET_DIR makes `resource_dir()` fail -/// with "unknown path" even though the resources are present. Fall back to -/// the executable's directory there; release builds pass `dev_exe_dir: None` -/// and keep failing loudly. -fn template_path_from( - resource_root: Result, - dev_exe_dir: Option, -) -> Result { - resource_root - .or_else(|error| dev_exe_dir.ok_or(error)) - .map(|root| root.join("resources").join("project-canvas-template")) - .map_err(|error| format!("resolve project canvas template: {error}")) -} - async fn run_blocking(task: F) -> Result where T: Send + 'static, diff --git a/desktop/src-tauri/src/project_canvas_package/storage.rs b/desktop/src-tauri/src/project_canvas_package/storage.rs index f01a14f1d5..ca1f1a1c5a 100644 --- a/desktop/src-tauri/src/project_canvas_package/storage.rs +++ b/desktop/src-tauri/src/project_canvas_package/storage.rs @@ -1,7 +1,7 @@ use std::{ collections::{BTreeMap, BTreeSet}, fs::{self, OpenOptions}, - io::Write, + io::{ErrorKind, Write}, path::{Path, PathBuf}, sync::Arc, }; @@ -129,7 +129,7 @@ pub(super) struct ValidatedSnapshot { pub(super) data: serde_json::Value, } -struct ValidatedPackage { +pub(super) struct ValidatedPackage { files: BTreeMap>, revision: String, manifest: ValidatedManifest, @@ -262,7 +262,7 @@ pub(super) fn snapshot_for_revision( pub(super) fn prepare_snapshot( canvas_root: &Path, binding: &ProjectBinding, - template: Option<&Path>, + template: Option<&ValidatedPackage>, ) -> Result { let canvas_root = canonical_canvas_root(canvas_root, true)? .ok_or_else(|| "project canvas root was not created".to_string())?; @@ -597,7 +597,7 @@ fn validate_index(canvas_root: &Path, index: &CanvasIndex) -> Result<(), String> fn seed_if_missing( canvas_root: &Path, project_root: &Path, - template: Option<&Path>, + template: Option<&ValidatedPackage>, ) -> Result<(), String> { if project_root.join("manifest.json").is_file() { ensure_secure_descendant(canvas_root, project_root, false)?; @@ -619,8 +619,7 @@ fn seed_if_missing( .map_err(|error| format!("remove empty project canvas directory: {error}"))?; } - let template = template.ok_or_else(|| "project canvas template is unavailable".to_string())?; - let package = scan_package(template, template)?; + let package = template.ok_or_else(|| "project canvas template is unavailable".to_string())?; let parent = project_root .parent() .ok_or_else(|| "project canvas directory has no parent".to_string())?; @@ -642,14 +641,53 @@ fn seed_if_missing( } fn scan_package(trusted_root: &Path, root: &Path) -> Result { - ensure_no_symlink(root)?; - if !root.is_dir() { + ensure_package_dir(root)?; + validate_package_files(read_package_tree(trusted_root, root)?) +} + +#[cfg(test)] +pub(super) fn scan_package_for_test(trusted_root: &Path, root: &Path) -> Result<(), String> { + scan_package(trusted_root, root).map(|_| ()) +} + +/// One non-following inspection covering existence, symlink, and directory. +/// +/// `symlink_metadata` names the path it failed on, where the previous +/// `ensure_no_symlink` + `Path::is_dir()` pair died inside the metadata call +/// with an unnamed `os error 2`. It is also strictly stronger: the directory +/// test now reads off the non-following metadata rather than `Path::is_dir()`, +/// which follows links. +fn ensure_package_dir(root: &Path) -> Result<(), String> { + let metadata = match fs::symlink_metadata(root) { + Ok(metadata) => metadata, + Err(error) if error.kind() == ErrorKind::NotFound => { + return Err(format!( + "project canvas package directory does not exist: {}", + root.display() + )) + } + Err(error) => return Err(format!("inspect project canvas path: {error}")), + }; + if metadata.file_type().is_symlink() { return Err(format!( - "project canvas package directory does not exist: {}", + "project canvas paths cannot be symlinks: {}", root.display() )); } - let files = read_package_tree(trusted_root, root)?; + if !metadata.is_dir() { + return Err(format!( + "project canvas package is not a directory: {}", + root.display() + )); + } + Ok(()) +} + +/// The single validation gate for every canvas package, read off disk or +/// embedded in the binary. There is no second, weaker path. +pub(super) fn validate_package_files( + files: BTreeMap>, +) -> Result { if files.len() > MAX_PACKAGE_FILES { return Err(format!( "project canvas package exceeds {MAX_PACKAGE_FILES} files" diff --git a/desktop/src-tauri/src/project_canvas_package/template.rs b/desktop/src-tauri/src/project_canvas_package/template.rs new file mode 100644 index 0000000000..801b376b89 --- /dev/null +++ b/desktop/src-tauri/src/project_canvas_package/template.rs @@ -0,0 +1,73 @@ +//! The seed package a project's first canvas is created from, embedded in the +//! binary at compile time. +//! +//! Seeding used to read `resources/project-canvas-template` from disk, resolved +//! against the running executable. In dev builds that directory lives inside +//! the cargo target directory — shared across worktrees, rewritten by sibling +//! builds, and removed wholesale by toolchain cleanups — so when it disappeared +//! under a running app the first-ever activation of a project failed with an +//! unnamed `os error 2` and could not self-heal. There is no runtime lookup +//! left to go stale: the bytes are part of the binary, and the on-disk copy is +//! no longer bundled. + +use std::{ + collections::BTreeMap, + sync::{Arc, OnceLock}, +}; + +use include_dir::{include_dir, Dir, DirEntry}; + +use super::{ + manifest::validate_relative_path, + storage::{validate_package_files, ValidatedPackage}, +}; + +static TEMPLATE: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/resources/project-canvas-template"); + +/// The embedded template, validated through the same gate every on-disk canvas +/// package passes. +/// +/// Validation hashes 2.7 MB, so the result is computed once per process and +/// shared. A template that fails validation fails every seed identically — +/// `bundled_template_seeds_a_valid_snapshot` keeps that from reaching a build. +pub(super) fn bundled_template() -> Result, String> { + static VALIDATED: OnceLock, String>> = OnceLock::new(); + VALIDATED + .get_or_init(|| validate_package_files(template_files()?).map(Arc::new)) + .clone() +} + +/// The embedded file tree, keyed by package-relative path. +/// +/// `include_dir` normalizes to `/` separators on every host, and each key is +/// still run through `validate_relative_path` so the embedded template gets no +/// weaker a path gate than a package read off disk. +pub(super) fn template_files() -> Result>, String> { + let mut files = BTreeMap::new(); + collect_entries(TEMPLATE.entries(), &mut files)?; + Ok(files) +} + +fn collect_entries( + entries: &[DirEntry<'_>], + files: &mut BTreeMap>, +) -> Result<(), String> { + for entry in entries { + match entry { + DirEntry::Dir(directory) => collect_entries(directory.entries(), files)?, + DirEntry::File(file) => { + let path = file + .path() + .to_str() + .ok_or_else(|| "project canvas package paths must be UTF-8".to_string())?; + // Matches the on-disk scan, which ignores Finder metadata rather + // than failing the whole package on it. + if path.rsplit('/').next() == Some(".DS_Store") { + continue; + } + files.insert(validate_relative_path(path)?, file.contents().to_vec()); + } + } + } + Ok(()) +} diff --git a/desktop/src-tauri/src/project_canvas_package/tests.rs b/desktop/src-tauri/src/project_canvas_package/tests.rs index 9867b6e21e..a9fbc1f1a2 100644 --- a/desktop/src-tauri/src/project_canvas_package/tests.rs +++ b/desktop/src-tauri/src/project_canvas_package/tests.rs @@ -1,14 +1,19 @@ -use std::{collections::BTreeSet, fs, path::Path}; +use std::{ + collections::{BTreeMap, BTreeSet}, + fs, + path::Path, +}; use tempfile::TempDir; use super::{ - manifest::{MAX_DATA_BYTES, MAX_FILE_BYTES, MAX_PACKAGE_FILES}, + manifest::{validate_relative_path, MAX_DATA_BYTES, MAX_FILE_BYTES, MAX_PACKAGE_FILES}, protocol, storage::{ active_snapshot, commit_snapshot, prepare_snapshot, prune_revisions, record_source_binding, - ProjectBinding, + scan_package_for_test, validate_package_files, ProjectBinding, ValidatedPackage, }, + template::{bundled_template, template_files}, ProjectCanvasAgentUpdateRequest, ProjectCanvasPackageRequest, ProjectCanvasRuntime, ProjectCanvasUpdateChange, }; @@ -22,56 +27,69 @@ fn request() -> ProjectCanvasPackageRequest { } } -fn write_package(root: &Path, marker: &str) { - fs::create_dir_all(root.join("widgets")).unwrap(); - fs::create_dir_all(root.join("styles")).unwrap(); - fs::create_dir_all(root.join("data")).unwrap(); - fs::create_dir_all(root.join("assets")).unwrap(); - fs::write( - root.join("manifest.json"), - serde_json::to_vec_pretty(&serde_json::json!({ - "format": "buzz-project-canvas", - "protocolVersion": 1, - "scripts": ["widgets/chore-board.js", "canvas.js"], - "styles": ["styles/canvas.css"], - "data": "data/dashboards.json", - "capabilities": [ - "project.metadata.read", - "project.channels.read", - "project.reviews.read" - ] - })) - .unwrap(), - ) - .unwrap(); - fs::write( - root.join("widgets/chore-board.js"), - "globalThis.renderChores = () => {};", - ) - .unwrap(); - fs::write( - root.join("canvas.js"), - format!("globalThis.canvasMarker = {marker:?};"), - ) - .unwrap(); - fs::write(root.join("styles/canvas.css"), "body { margin: 0; }").unwrap(); - fs::write( - root.join("data/dashboards.json"), - serde_json::to_vec(&serde_json::json!({ - "marker": marker, - "dashboards": { - "test": { - "widgets": [{ - "id": "chore-board", - "data": { "marker": marker } - }] +fn package_files(marker: &str) -> BTreeMap> { + BTreeMap::from([ + ( + "manifest.json".to_string(), + serde_json::to_vec_pretty(&serde_json::json!({ + "format": "buzz-project-canvas", + "protocolVersion": 1, + "scripts": ["widgets/chore-board.js", "canvas.js"], + "styles": ["styles/canvas.css"], + "data": "data/dashboards.json", + "capabilities": [ + "project.metadata.read", + "project.channels.read", + "project.reviews.read" + ] + })) + .unwrap(), + ), + ( + "widgets/chore-board.js".to_string(), + b"globalThis.renderChores = () => {};".to_vec(), + ), + ( + "canvas.js".to_string(), + format!("globalThis.canvasMarker = {marker:?};").into_bytes(), + ), + ( + "styles/canvas.css".to_string(), + b"body { margin: 0; }".to_vec(), + ), + ( + "data/dashboards.json".to_string(), + serde_json::to_vec(&serde_json::json!({ + "marker": marker, + "dashboards": { + "test": { + "widgets": [{ + "id": "chore-board", + "data": { "marker": marker } + }] + } } - } - })) - .unwrap(), - ) - .unwrap(); - fs::write(root.join("assets/pixel.png"), [137, 80, 78, 71]).unwrap(); + })) + .unwrap(), + ), + ("assets/pixel.png".to_string(), vec![137, 80, 78, 71]), + ]) +} + +fn write_package(root: &Path, marker: &str) { + write_package_files(root, &package_files(marker)); +} + +fn write_package_files(root: &Path, files: &BTreeMap>) { + for (relative, bytes) in files { + let destination = root.join(relative); + fs::create_dir_all(destination.parent().unwrap()).unwrap(); + fs::write(destination, bytes).unwrap(); + } +} + +fn template_package(marker: &str) -> ValidatedPackage { + validate_package_files(package_files(marker)).unwrap() } fn source_root(temp: &TempDir, binding: &ProjectBinding) -> std::path::PathBuf { @@ -254,8 +272,7 @@ fn revision_retention_keeps_active_live_and_recent_snapshots() { #[test] fn first_activation_seeds_the_validated_template() { let temp = TempDir::new().unwrap(); - let template = temp.path().join("template"); - write_package(&template, "seeded"); + let template = template_package("seeded"); let binding = ProjectBinding::parse(request()).unwrap(); let snapshot = @@ -271,15 +288,12 @@ fn first_activation_seeds_the_validated_template() { .any(|entry| entry.file_name().to_string_lossy().starts_with(".seed-"))); } -// Binds the shipped resource to the loader: these are the exact files -// `tauri-build` copies next to the executable, so a template edit that fails +// Binds the embedded seed bytes to the loader, so a template edit that fails // manifest validation surfaces here instead of on the first canvas load. #[test] fn bundled_template_seeds_a_valid_snapshot() { let temp = TempDir::new().unwrap(); - let template = Path::new(env!("CARGO_MANIFEST_DIR")) - .join("resources") - .join("project-canvas-template"); + let template = bundled_template().unwrap(); let binding = ProjectBinding::parse(request()).unwrap(); let snapshot = @@ -288,6 +302,95 @@ fn bundled_template_seeds_a_valid_snapshot() { assert!(snapshot.data["dashboards"].is_object()); } +// `include_dir!` expands to one `include_bytes!` per file, so a file added to +// or removed from the template only reaches the binary if the build script's +// `rerun-if-changed` fired. Compare the embedded key set against the tree on +// disk to catch a stale or partial embed. +#[test] +fn bundled_template_contains_the_expected_tree() { + let root = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("resources") + .join("project-canvas-template"); + let mut expected = BTreeSet::new(); + collect_relative_paths(&root, &root, &mut expected); + + let embedded = template_files() + .unwrap() + .into_keys() + .collect::>(); + + assert_eq!(embedded, expected); +} + +fn collect_relative_paths(root: &Path, directory: &Path, paths: &mut BTreeSet) { + for entry in fs::read_dir(directory).unwrap() { + let path = entry.unwrap().path(); + if path.is_dir() { + collect_relative_paths(root, &path, paths); + continue; + } + if path.file_name() == Some(std::ffi::OsStr::new(".DS_Store")) { + continue; + } + paths.insert( + path.strip_prefix(root) + .unwrap() + .to_str() + .unwrap() + .replace(std::path::MAIN_SEPARATOR, "/"), + ); + } +} + +// The embedded template must clear the same path gate as any package read off +// disk — it is seeded through the identical validation, not a weaker one. +#[test] +fn bundled_template_paths_are_package_safe() { + for path in template_files().unwrap().keys() { + assert_eq!( + validate_relative_path(path).as_deref(), + Ok(path.as_str()), + "{path}" + ); + assert!(!Path::new(path).is_absolute(), "{path}"); + assert!(!path.contains('\\'), "{path}"); + assert!(!path.split('/').any(|segment| segment == ".."), "{path}"); + } +} + +// The missing-directory arm has to run before the symlink inspection, or an +// absent package dies inside `symlink_metadata` with an unnamed `os error 2` — +// exactly the failure that made a vanished template unreadable to debug. +#[test] +fn missing_package_directory_names_the_path() { + let temp = TempDir::new().unwrap(); + let absent = temp.path().join("absent-revision"); + + let error = scan_package_for_test(temp.path(), &absent).unwrap_err(); + + assert!(error.contains("does not exist"), "{error}"); + assert!(error.contains("absent-revision"), "{error}"); +} + +// A symlinked package root is refused even when it points at a valid package: +// the existence check reads non-following metadata, so it cannot mask the +// symlink arm behind a `Path::is_dir()` that follows links. +#[cfg(unix)] +#[test] +fn package_root_symlink_is_rejected() { + use std::os::unix::fs::symlink; + + let temp = TempDir::new().unwrap(); + let real = temp.path().join("real-package"); + write_package(&real, "root-symlink"); + let linked = temp.path().join("linked-package"); + symlink(&real, &linked).unwrap(); + + assert!(scan_package_for_test(temp.path(), &real).is_ok()); + let error = scan_package_for_test(temp.path(), &linked).unwrap_err(); + assert!(error.contains("cannot be symlinks"), "{error}"); +} + #[test] fn source_index_is_machine_readable_sorted_and_path_derived() { let temp = TempDir::new().unwrap(); @@ -337,7 +440,7 @@ fn malformed_source_index_does_not_block_a_valid_canvas_load() { fs::write(root.join("index.json"), b"not json").unwrap(); let runtime = ProjectCanvasRuntime::with_root(root); - let descriptor = runtime.get_or_activate(request(), temp.path()).unwrap(); + let descriptor = runtime.get_or_activate(request(), None).unwrap(); assert_eq!(descriptor.data["marker"], "load-with-corrupt-index"); assert!(runtime.source_location(request()).is_ok()); @@ -571,9 +674,10 @@ fn host_sdk_routes_serve_the_bundled_sources() { #[test] fn manifest_accepts_the_full_capability_set_and_rejects_unknown_ones() { let temp = TempDir::new().unwrap(); - let template = temp.path().join("template"); - write_package(&template, "capabilities"); - let manifest_path = template.join("manifest.json"); + let binding = ProjectBinding::parse(request()).unwrap(); + let source = source_root(&temp, &binding); + write_package(&source, "capabilities"); + let manifest_path = source.join("manifest.json"); let mut manifest: serde_json::Value = serde_json::from_slice(&fs::read(&manifest_path).unwrap()).unwrap(); // Must accept every capability the desktop protocol schema recognizes. @@ -587,27 +691,23 @@ fn manifest_accepts_the_full_capability_set_and_rejects_unknown_ones() { "app.open" ]); fs::write(&manifest_path, serde_json::to_vec(&manifest).unwrap()).unwrap(); - let binding = ProjectBinding::parse(request()).unwrap(); - assert!(prepare_snapshot(&temp.path().join("CANVASES"), &binding, Some(&template)).is_ok()); + assert!(prepare_snapshot(&temp.path().join("CANVASES"), &binding, None).is_ok()); manifest["capabilities"] = serde_json::json!(["network"]); fs::write(&manifest_path, serde_json::to_vec(&manifest).unwrap()).unwrap(); - // Fresh root: prepare_snapshot only seeds from the template on first use. - let error = - prepare_snapshot(&temp.path().join("CANVASES2"), &binding, Some(&template)).unwrap_err(); + let error = prepare_snapshot(&temp.path().join("CANVASES"), &binding, None).unwrap_err(); assert!(error.contains("unsupported project canvas capability")); } #[test] fn invalid_or_undeclared_package_files_fail_closed() { let temp = TempDir::new().unwrap(); - let template = temp.path().join("template"); - write_package(&template, "bad"); - fs::write(template.join("index.html"), "").unwrap(); let binding = ProjectBinding::parse(request()).unwrap(); + let source = source_root(&temp, &binding); + write_package(&source, "bad"); + fs::write(source.join("index.html"), "").unwrap(); - let error = - prepare_snapshot(&temp.path().join("CANVASES"), &binding, Some(&template)).unwrap_err(); + let error = prepare_snapshot(&temp.path().join("CANVASES"), &binding, None).unwrap_err(); assert!(error.contains("unsupported project canvas file type")); } @@ -625,8 +725,9 @@ fn finder_metadata_does_not_break_package_reload() { #[test] fn manifest_paths_cannot_traverse_the_package() { let temp = TempDir::new().unwrap(); - let template = temp.path().join("template"); - write_package(&template, "bad-path"); + let binding = ProjectBinding::parse(request()).unwrap(); + let source = source_root(&temp, &binding); + write_package(&source, "bad-path"); let manifest = serde_json::json!({ "format": "buzz-project-canvas", "protocolVersion": 1, @@ -636,13 +737,12 @@ fn manifest_paths_cannot_traverse_the_package() { "capabilities": [] }); fs::write( - template.join("manifest.json"), + source.join("manifest.json"), serde_json::to_vec(&manifest).unwrap(), ) .unwrap(); - let binding = ProjectBinding::parse(request()).unwrap(); - assert!(prepare_snapshot(&temp.path().join("CANVASES"), &binding, Some(&template)).is_err()); + assert!(prepare_snapshot(&temp.path().join("CANVASES"), &binding, None).is_err()); } #[cfg(unix)] @@ -818,43 +918,3 @@ fn agent_update_socket_serves_when_started_outside_the_async_runtime() { wait.recv_timeout(Duration::from_secs(5)) .expect("socket bound before the runtime handoff should still accept connections"); } - -#[test] -fn template_resolution_prefers_the_resource_dir() { - let resolved = super::template_path_from( - Ok(std::path::PathBuf::from("/bundle")), - Some(std::path::PathBuf::from("/exe")), - ); - assert_eq!( - resolved, - Ok(std::path::PathBuf::from( - "/bundle/resources/project-canvas-template" - )) - ); -} - -// A redirected CARGO_TARGET_DIR defeats Tauri's `target/` directory heuristic, -// so dev builds must resolve resources next to the executable instead of -// failing the canvas load with "unknown path". -#[test] -fn template_resolution_falls_back_to_the_dev_executable_directory() { - let resolved = super::template_path_from( - Err("unknown path".to_string()), - Some(std::path::PathBuf::from("/exe")), - ); - assert_eq!( - resolved, - Ok(std::path::PathBuf::from( - "/exe/resources/project-canvas-template" - )) - ); -} - -#[test] -fn template_resolution_without_a_dev_fallback_propagates_the_error() { - let resolved = super::template_path_from(Err("unknown path".to_string()), None); - assert_eq!( - resolved, - Err("resolve project canvas template: unknown path".to_string()) - ); -} diff --git a/desktop/src-tauri/tauri.conf.json b/desktop/src-tauri/tauri.conf.json index c7e0ee90ce..995297963a 100644 --- a/desktop/src-tauri/tauri.conf.json +++ b/desktop/src-tauri/tauri.conf.json @@ -52,7 +52,6 @@ "bundle": { "active": true, "targets": "all", - "resources": ["resources/project-canvas-template/**/*"], "externalBin": [ "binaries/buzz-acp", "binaries/buzz-agent",