//! Declares the Tauri `AppManifest`, so every app command is ACL-gated per window, and refuses //! to build unless every registered command is granted in exactly one capability file — the //! file whose `windows` the command's name says it belongs to. Without an app manifest, tauri //! 2.11 skips the ACL for app commands entirely (`webview/mod.rs:1794`), so any local window //! could call any command. //! //! The parser and the rules live in `src/command_census.rs`, which `cargo test` also compiles, //! so they have unit tests. Spec: `docs/superpowers/specs/2026-09-22-app-manifest-lockdown-design.md`. #[path = "src/command_census.rs"] mod command_census; use std::path::Path; fn fail(problems: &[String]) -> ! { eprintln!(); eprintln!( "capabilities do not match generate_handler! ({} problem{}):", problems.len(), if problems.len() == 1 { "" } else { "s" } ); for p in problems { eprintln!(" - {p}"); } eprintln!(); eprintln!( "Every app command needs exactly one bare `allow-` grant: \ `viewer_*` commands in capabilities/file-viewer.json, everything else in \ capabilities/default.json. See src/command_census.rs." ); eprintln!(); std::process::exit(1); } fn main() { // tauri-build already emits rerun-if-changed for `capabilities` and `permissions`. println!("cargo:rerun-if-changed=src/lib.rs"); println!("cargo:rerun-if-changed=src/command_census.rs"); let lib_rs = std::fs::read_to_string("src/lib.rs") .expect("build.rs runs with CWD = src-tauri, so src/lib.rs must be readable"); let Some(commands) = command_census::registered_commands(&lib_rs) else { fail(&["src/lib.rs has no `generate_handler![ … ])` block to derive the AppManifest from" .to_string()]); }; let mut files = Vec::new(); for entry in std::fs::read_dir("capabilities").expect("capabilities/ must exist") { let path = entry.expect("readable entry in capabilities/").path(); if path.extension().is_some_and(|e| e == "json") { let name = path .file_name() .expect("a file name") .to_string_lossy() .into_owned(); let json = std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("{name}: {e}")); match command_census::capability_file(&name, &json) { Ok(file) => files.push(file), Err(problem) => fail(&[problem]), } } } files.sort_by(|a, b| a.name.cmp(&b.name)); let problems = command_census::check(&commands, &files); if !problems.is_empty() { fail(&problems); } prune_permissions(&commands); // `AppManifest::commands` takes `&'static [&'static str]` and the struct is `Copy`, so // there is no owned form; leaking is fine in a process that exits right after. let leaked: Vec<&'static str> = commands .into_iter() .map(|c| &*Box::leak(c.into_boxed_str())) .collect(); let leaked: &'static [&'static str] = Box::leak(leaked.into_boxed_slice()); let attributes = tauri_build::Attributes::new() .app_manifest(tauri_build::AppManifest::new().commands(leaked)); if let Err(error) = tauri_build::try_build(attributes) { // Same shape as `tauri_build::build()`: message on stdout, then exit 1. println!("{error:#}"); std::process::exit(1); } } /// tauri-build writes `permissions/autogenerated/.toml` for every manifest command /// and never deletes one, so a command removed from `lib.rs` would leave a permission a /// capability could still reference (and the build would pass). Delete only the stale files: /// tauri-build also emits `rerun-if-changed=permissions`, so regenerating everything would /// touch every mtime and re-run this script — and recompile the crate — on every cargo /// invocation. Anything else under `permissions/` is a hand-written grant the census cannot /// see, so it is refused. fn prune_permissions(commands: &[String]) { let root = Path::new("permissions"); let Ok(entries) = std::fs::read_dir(root) else { return; }; for entry in entries { let path = entry.expect("readable entry in permissions/").path(); if path.file_name().is_some_and(|n| n == "autogenerated") && path.is_dir() { for file in std::fs::read_dir(&path).expect("readable permissions/autogenerated") { let file = file.expect("readable entry").path(); let stem = file.file_stem().and_then(|s| s.to_str()).unwrap_or(""); let live = file.extension().is_some_and(|e| e == "toml") && commands.iter().any(|c| c == stem); if !live { std::fs::remove_file(&file) .unwrap_or_else(|e| panic!("cannot delete stale {}: {e}", file.display())); } } } else { fail(&[format!( "{} is not generated by build.rs; hand-written permissions are not allowed \ (every grant is a bare allow-* string in a capability file)", path.display() )]); } } }