use std::path::{Path, PathBuf}; use anyhow::Context; use serde::Deserialize; #[derive(Debug, Deserialize)] struct Manifest { package: PackageMeta, runtime: RuntimeMeta, ui: UiMeta, } #[derive(Debug, Deserialize)] struct PackageMeta { id: String, name: String, version: String, description: Option, author: Option, capabilities: Option>, } #[derive(Debug, Deserialize)] struct RuntimeMeta { module: String, } #[derive(Debug, Deserialize)] struct UiMeta { entry: String, } fn main() -> anyhow::Result<()> { let args: Vec = std::env::args().collect(); match args.get(1).map(String::as_str) { Some("check") => { let dir = args.get(2).context("usage: weft-pack check ")?; let result = check_package(Path::new(dir))?; println!("{result}"); } Some("info") => { let dir = args.get(2).context("usage: weft-pack info ")?; let manifest = load_manifest(Path::new(dir))?; print_info(&manifest); } Some("install") => { let dir = args.get(2).context("usage: weft-pack install ")?; install_package(Path::new(dir))?; } Some("uninstall") => { let app_id = args.get(2).context("usage: weft-pack uninstall ")?; uninstall_package(app_id)?; } Some("list") => { list_installed(); } Some("build-image") => { let dir = args.get(2).context("usage: weft-pack build-image ")?; let out = args .windows(2) .find(|w| w[0] == "--out") .map(|w| w[1].as_str()); build_image(Path::new(dir), out.map(Path::new))?; } Some("build-verity") => { let img = args.get(2).context("usage: weft-pack build-verity ")?; let out = args .windows(2) .find(|w| w[0] == "--out") .map(|w| w[1].as_str()); build_verity(Path::new(img), out.map(Path::new))?; } Some("bundle") => { let dir = args.get(2).context("usage: weft-pack bundle ")?; let out = args .windows(2) .find(|w| w[0] == "--out") .map(|w| w[1].as_str()); bundle_package(Path::new(dir), out.map(Path::new))?; } Some("unbundle") => { let archive = args.get(2).context("usage: weft-pack unbundle ")?; let out = args .windows(2) .find(|w| w[0] == "--out") .map(|w| w[1].as_str()) .unwrap_or("."); unbundle_package(Path::new(archive), Path::new(out))?; } Some("generate-key") => { let out = args.get(2).map(String::as_str).unwrap_or("."); generate_key(Path::new(out))?; } Some("sign") => { let dir = args .get(2) .context("usage: weft-pack sign --key ")?; let key = args .windows(2) .find(|w| w[0] == "--key") .map(|w| &w[1]) .context("missing --key ")?; sign_package(Path::new(dir), Path::new(key))?; } Some("verify") => { let dir = args .get(2) .context("usage: weft-pack verify --key ")?; let key = args .windows(2) .find(|w| w[0] == "--key") .map(|w| &w[1]) .context("missing --key ")?; let ok = verify_package(Path::new(dir), Path::new(key))?; if ok { println!("OK"); } else { eprintln!("INVALID signature"); std::process::exit(1); } } _ => { eprintln!("usage:"); eprintln!(" weft-pack check validate a package directory"); eprintln!(" weft-pack info print package metadata"); eprintln!(" weft-pack install install package to app store"); eprintln!(" weft-pack uninstall remove installed package"); eprintln!(" weft-pack list list installed packages"); eprintln!( " weft-pack build-image [--out ] create EROFS image with mkfs.erofs" ); eprintln!(" weft-pack build-verity [--out ] add dm-verity hash tree"); eprintln!(" weft-pack bundle [--out ] create .app.tar.zst archive"); eprintln!(" weft-pack unbundle [--out ] extract .app.tar.zst"); eprintln!(" weft-pack generate-key [] generate Ed25519 keypair"); eprintln!(" weft-pack sign --key sign package with private key"); eprintln!(" weft-pack verify --key verify package signature"); std::process::exit(1); } } Ok(()) } fn check_package(dir: &Path) -> anyhow::Result { let mut errors: Vec = Vec::new(); let manifest = match load_manifest(dir) { Ok(m) => Some(m), Err(e) => { errors.push(format!("wapp.toml: {e}")); None } }; if let Some(ref m) = manifest { if !is_valid_app_id(&m.package.id) { errors.push(format!( "package.id '{}' does not match required pattern", m.package.id )); } if m.package.name.is_empty() { errors.push("package.name is empty".into()); } if m.package.name.len() > 64 { errors.push(format!( "package.name exceeds 64 characters ({})", m.package.name.len() )); } let wasm_path = dir.join(&m.runtime.module); if !wasm_path.exists() { errors.push(format!( "runtime.module '{}' not found", wasm_path.display() )); } else if !is_wasm_module(&wasm_path) { errors.push(format!( "runtime.module '{}' is not a valid Wasm module (bad magic bytes)", wasm_path.display() )); } let ui_path = dir.join(&m.ui.entry); if !ui_path.exists() { errors.push(format!("ui.entry '{}' not found", ui_path.display())); } const KNOWN_CAPS: &[&str] = &[ "fs:rw:app-data", "fs:read:app-data", "fs:rw:xdg-documents", "fs:read:xdg-documents", ]; for cap in m.package.capabilities.iter().flatten() { if !KNOWN_CAPS.contains(&cap.as_str()) { errors.push(format!("unknown capability '{cap}'")); } } } if errors.is_empty() { Ok("OK".into()) } else { Err(anyhow::anyhow!("{}", errors.join("\n"))) } } fn load_manifest(dir: &Path) -> anyhow::Result { let manifest_path = dir.join("wapp.toml"); let text = std::fs::read_to_string(&manifest_path) .with_context(|| format!("read {}", manifest_path.display()))?; toml::from_str(&text).with_context(|| format!("parse {}", manifest_path.display())) } fn print_info(m: &Manifest) { println!("id: {}", m.package.id); println!("name: {}", m.package.name); println!("version: {}", m.package.version); if let Some(ref d) = m.package.description { println!("desc: {d}"); } if let Some(ref a) = m.package.author { println!("author: {a}"); } println!("module: {}", m.runtime.module); println!("ui: {}", m.ui.entry); if let Some(ref caps) = m.package.capabilities { for cap in caps { println!("cap: {cap}"); } } } fn is_valid_app_id(id: &str) -> bool { let parts: Vec<&str> = id.split('.').collect(); if parts.len() < 3 { return false; } parts.iter().all(|p| { !p.is_empty() && p.chars() .next() .map(|c| c.is_ascii_lowercase()) .unwrap_or(false) && p.chars() .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit()) }) } fn is_wasm_module(path: &Path) -> bool { const MAGIC: [u8; 4] = [0x00, 0x61, 0x73, 0x6D]; let mut buf = [0u8; 4]; match std::fs::File::open(path) { Ok(mut f) => { use std::io::Read; f.read_exact(&mut buf) .map(|_| buf == MAGIC) .unwrap_or(false) } Err(_) => false, } } fn resolve_install_root() -> anyhow::Result { if let Ok(explicit) = std::env::var("WEFT_APP_STORE") { return Ok(PathBuf::from(explicit)); } if let Ok(home) = std::env::var("HOME") { return Ok(PathBuf::from(home) .join(".local") .join("share") .join("weft") .join("apps")); } anyhow::bail!("cannot determine install root: HOME and WEFT_APP_STORE are both unset") } fn install_package(path: &Path) -> anyhow::Result<()> { let root = resolve_install_root()?; install_package_to(path, &root) } fn install_package_to(path: &Path, store_root: &Path) -> anyhow::Result<()> { let dir_buf; let dir: &Path = if path.extension().is_some_and(|e| e == "zst" || e == "tar") || path.to_string_lossy().ends_with(".app.tar.zst") { let tmp = std::env::temp_dir().join(format!( "weft-install-{}", std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap_or_default() .subsec_nanos() )); std::fs::create_dir_all(&tmp) .with_context(|| format!("create temp dir {}", tmp.display()))?; unbundle_package(path, &tmp)?; let name = path .file_name() .and_then(|n| n.to_str()) .and_then(|n| n.strip_suffix(".app.tar.zst")) .context("cannot determine app_id from archive filename")?; dir_buf = tmp.join(name); &dir_buf } else { path }; check_package(dir)?; let manifest = load_manifest(dir)?; let app_id = &manifest.package.id; let dest = store_root.join(app_id); if dest.exists() { anyhow::bail!( "package '{}' is already installed at {}; remove it first", app_id, dest.display() ); } copy_dir(dir, &dest) .with_context(|| format!("copy {} -> {}", dir.display(), dest.display()))?; println!("installed {} -> {}", app_id, dest.display()); Ok(()) } fn uninstall_package(app_id: &str) -> anyhow::Result<()> { let root = resolve_install_root()?; uninstall_package_from(app_id, &root) } fn uninstall_package_from(app_id: &str, store_root: &Path) -> anyhow::Result<()> { if !is_valid_app_id(app_id) { anyhow::bail!("'{}' is not a valid app ID", app_id); } let target = store_root.join(app_id); if !target.exists() { anyhow::bail!( "package '{}' is not installed at {}", app_id, target.display() ); } std::fs::remove_dir_all(&target).with_context(|| format!("remove {}", target.display()))?; println!("uninstalled {}", app_id); Ok(()) } fn list_installed_roots() -> Vec { if let Ok(explicit) = std::env::var("WEFT_APP_STORE") { return vec![PathBuf::from(explicit)]; } let mut roots = Vec::new(); if let Ok(home) = std::env::var("HOME") { roots.push( PathBuf::from(home) .join(".local") .join("share") .join("weft") .join("apps"), ); } roots.push(PathBuf::from("/usr/share/weft/apps")); roots } fn list_installed() { let mut seen = std::collections::HashSet::new(); let mut count = 0usize; for root in list_installed_roots() { let Ok(entries) = std::fs::read_dir(&root) else { continue; }; let mut pkgs: Vec<(String, String, String)> = Vec::new(); for entry in entries.flatten() { let manifest_path = entry.path().join("wapp.toml"); let Ok(contents) = std::fs::read_to_string(&manifest_path) else { continue; }; let Ok(m) = toml::from_str::(&contents) else { continue; }; if seen.insert(m.package.id.clone()) { pkgs.push((m.package.id, m.package.name, m.package.version)); } } pkgs.sort_by(|a, b| a.0.cmp(&b.0)); for (id, name, version) in pkgs { println!("{id} {name} {version}"); count += 1; } } if count == 0 { println!("no packages installed"); } } fn copy_dir(src: &Path, dst: &Path) -> anyhow::Result<()> { std::fs::create_dir_all(dst)?; for entry in std::fs::read_dir(src)? { let entry = entry?; let src_path = entry.path(); let dst_path = dst.join(entry.file_name()); if src_path.is_dir() { copy_dir(&src_path, &dst_path)?; } else { std::fs::copy(&src_path, &dst_path) .with_context(|| format!("copy {}", src_path.display()))?; } } Ok(()) } fn build_image(dir: &Path, out_path: Option<&Path>) -> anyhow::Result<()> { let manifest = load_manifest(dir)?; let app_id = &manifest.package.id; let default_name = format!("{app_id}.app.img"); let output = match out_path { Some(p) => p.to_path_buf(), None => PathBuf::from(&default_name), }; if output.exists() { anyhow::bail!("{} already exists", output.display()); } let status = std::process::Command::new("mkfs.erofs") .arg(&output) .arg(dir) .status() .context("spawn mkfs.erofs; ensure erofs-utils is installed")?; if !status.success() { anyhow::bail!("mkfs.erofs failed with status {status}"); } println!("image: {}", output.display()); Ok(()) } fn build_verity(img: &Path, hash_out: Option<&Path>) -> anyhow::Result<()> { let stem = img.file_stem().context("no file stem")?.to_string_lossy(); let default_hash = img.with_file_name(format!("{stem}.hash")); let hash_path = hash_out.map(|p| p.to_path_buf()).unwrap_or(default_hash); if hash_path.exists() { anyhow::bail!("{} already exists", hash_path.display()); } let output = std::process::Command::new("veritysetup") .args([ "format", &img.to_string_lossy(), &hash_path.to_string_lossy(), ]) .output() .context("spawn veritysetup; ensure cryptsetup-bin is installed")?; if !output.status.success() { anyhow::bail!( "veritysetup failed: {}", String::from_utf8_lossy(&output.stderr) ); } let stdout = String::from_utf8_lossy(&output.stdout); let root_hash = stdout .lines() .find(|l| l.starts_with("Root hash:")) .context("root hash not found in veritysetup output")?; let hash_value = root_hash.trim_start_matches("Root hash:").trim(); let roothash_path = img.with_extension("roothash"); std::fs::write(&roothash_path, hash_value) .with_context(|| format!("write {}", roothash_path.display()))?; println!("hash image: {}", hash_path.display()); println!("root hash: {hash_value}"); println!("roothash: {}", roothash_path.display()); Ok(()) } fn bundle_package(dir: &Path, out_dir: Option<&Path>) -> anyhow::Result<()> { let manifest = load_manifest(dir)?; let app_id = &manifest.package.id; let archive_name = format!("{app_id}.app.tar.zst"); let dest_dir = out_dir.unwrap_or_else(|| Path::new(".")); let archive_path = dest_dir.join(&archive_name); if archive_path.exists() { anyhow::bail!("{} already exists", archive_path.display()); } let file = std::fs::File::create(&archive_path) .with_context(|| format!("create {}", archive_path.display()))?; let encoder = zstd::Encoder::new(file, 0) .context("create zstd encoder")? .auto_finish(); let mut tar = tar::Builder::new(encoder); tar.follow_symlinks(false); tar.append_dir_all(app_id, dir) .with_context(|| format!("append {} to archive", dir.display()))?; tar.finish().context("finish tar archive")?; println!("bundled: {}", archive_path.display()); Ok(()) } fn unbundle_package(archive: &Path, out_dir: &Path) -> anyhow::Result<()> { let file = std::fs::File::open(archive).with_context(|| format!("open {}", archive.display()))?; let decoder = zstd::Decoder::new(file).context("create zstd decoder")?; let mut tar = tar::Archive::new(decoder); tar.unpack(out_dir) .with_context(|| format!("unpack to {}", out_dir.display()))?; println!("unbundled: {} -> {}", archive.display(), out_dir.display()); Ok(()) } fn collect_bundle_files( base: &Path, dir: &Path, entries: &mut Vec<(String, [u8; 32])>, ) -> anyhow::Result<()> { use sha2::{Digest, Sha256}; for entry in std::fs::read_dir(dir).with_context(|| format!("read dir {}", dir.display()))? { let entry = entry?; let path = entry.path(); let rel = path .strip_prefix(base) .context("strip prefix")? .to_string_lossy() .into_owned(); if rel == "signature.sig" { continue; } if path.is_dir() { collect_bundle_files(base, &path, entries)?; } else { let contents = std::fs::read(&path).with_context(|| format!("read {}", path.display()))?; let hash: [u8; 32] = Sha256::digest(&contents).into(); entries.push((rel, hash)); } } Ok(()) } fn canonical_bundle_hash(dir: &Path) -> anyhow::Result<[u8; 32]> { use sha2::{Digest, Sha256}; let mut entries: Vec<(String, [u8; 32])> = Vec::new(); collect_bundle_files(dir, dir, &mut entries)?; entries.sort_by(|a, b| a.0.cmp(&b.0)); let mut canonical = String::new(); for (path, hash) in &entries { canonical.push_str(&format!("{path}\t{}\n", hex::encode(hash))); } Ok(Sha256::digest(canonical.as_bytes()).into()) } fn generate_key(output_dir: &Path) -> anyhow::Result<()> { use ed25519_dalek::SigningKey; use rand::rngs::OsRng; let key_path = output_dir.join("weft-sign.key"); let pub_path = output_dir.join("weft-sign.pub"); if key_path.exists() || pub_path.exists() { anyhow::bail!( "key files already exist in {}; remove them first", output_dir.display() ); } let signing_key = SigningKey::generate(&mut OsRng); let verifying_key = signing_key.verifying_key(); std::fs::create_dir_all(output_dir) .with_context(|| format!("create {}", output_dir.display()))?; std::fs::write(&key_path, hex::encode(signing_key.to_bytes())) .with_context(|| format!("write {}", key_path.display()))?; std::fs::write(&pub_path, hex::encode(verifying_key.to_bytes())) .with_context(|| format!("write {}", pub_path.display()))?; println!("private key: {}", key_path.display()); println!("public key: {}", pub_path.display()); Ok(()) } fn sign_package(dir: &Path, key_file: &Path) -> anyhow::Result<()> { use ed25519_dalek::{Signer, SigningKey}; let key_hex = std::fs::read_to_string(key_file) .with_context(|| format!("read {}", key_file.display()))?; let key_bytes: [u8; 32] = hex::decode(key_hex.trim()) .context("decode signing key: expected 64 hex chars")? .try_into() .map_err(|_| anyhow::anyhow!("signing key must be 32 bytes"))?; let signing_key = SigningKey::from_bytes(&key_bytes); let hash = canonical_bundle_hash(dir)?; let signature = signing_key.sign(&hash); let sig_path = dir.join("signature.sig"); std::fs::write(&sig_path, hex::encode(signature.to_bytes())) .with_context(|| format!("write {}", sig_path.display()))?; println!("signed: {}", sig_path.display()); Ok(()) } fn verify_package(dir: &Path, pub_key_file: &Path) -> anyhow::Result { use ed25519_dalek::{Signature, Verifier, VerifyingKey}; let pub_hex = std::fs::read_to_string(pub_key_file) .with_context(|| format!("read {}", pub_key_file.display()))?; let pub_bytes: [u8; 32] = hex::decode(pub_hex.trim()) .context("decode public key: expected 64 hex chars")? .try_into() .map_err(|_| anyhow::anyhow!("public key must be 32 bytes"))?; let verifying_key = VerifyingKey::from_bytes(&pub_bytes).context("invalid public key bytes")?; let sig_path = dir.join("signature.sig"); let sig_hex = std::fs::read_to_string(&sig_path) .with_context(|| format!("read {}", sig_path.display()))?; let sig_bytes: [u8; 64] = hex::decode(sig_hex.trim()) .context("decode signature: expected 128 hex chars")? .try_into() .map_err(|_| anyhow::anyhow!("signature must be 64 bytes"))?; let signature = Signature::from_bytes(&sig_bytes); let hash = canonical_bundle_hash(dir)?; Ok(verifying_key.verify(&hash, &signature).is_ok()) } #[cfg(test)] mod tests { use super::*; #[test] fn app_id_valid() { assert!(is_valid_app_id("com.example.notes")); assert!(is_valid_app_id("org.weft.calculator")); assert!(is_valid_app_id("io.github.username.app")); } #[test] fn app_id_invalid() { assert!(!is_valid_app_id("com.example")); assert!(!is_valid_app_id("Com.example.notes")); assert!(!is_valid_app_id("com.example.notes-app")); assert!(!is_valid_app_id("com..example.notes")); assert!(!is_valid_app_id("")); assert!(!is_valid_app_id("com.Example.notes")); } #[test] fn check_package_missing_manifest() { let tmp = std::env::temp_dir().join("weft_pack_test_empty"); let _ = std::fs::create_dir_all(&tmp); let result = check_package(&tmp); assert!(result.is_err()); let _ = std::fs::remove_dir_all(&tmp); } #[test] fn check_package_missing_wasm() { use std::fs; let tmp = std::env::temp_dir().join("weft_pack_test_no_wasm"); let ui_dir = tmp.join("ui"); let _ = fs::create_dir_all(&ui_dir); fs::write(ui_dir.join("index.html"), b"").unwrap(); fs::write( tmp.join("wapp.toml"), r#" [package] id = "com.example.nowasm" name = "No Wasm" version = "0.1.0" [runtime] module = "app.wasm" [ui] entry = "ui/index.html" "#, ) .unwrap(); let result = check_package(&tmp); assert!(result.is_err()); let _ = fs::remove_dir_all(&tmp); } #[test] fn check_package_missing_ui_entry() { use std::fs; let tmp = std::env::temp_dir().join("weft_pack_test_no_ui"); let _ = fs::create_dir_all(&tmp); fs::write(tmp.join("app.wasm"), b"\0asm\x01\0\0\0").unwrap(); fs::write( tmp.join("wapp.toml"), r#" [package] id = "com.example.noui" name = "No UI" version = "0.1.0" [runtime] module = "app.wasm" [ui] entry = "ui/index.html" "#, ) .unwrap(); let result = check_package(&tmp); assert!(result.is_err()); let _ = fs::remove_dir_all(&tmp); } #[test] fn check_package_valid() { use std::fs; let tmp = std::env::temp_dir().join("weft_pack_test_valid"); let ui_dir = tmp.join("ui"); let _ = fs::create_dir_all(&ui_dir); fs::write(tmp.join("app.wasm"), b"\0asm\x01\0\0\0").unwrap(); fs::write(ui_dir.join("index.html"), b"").unwrap(); fs::write( tmp.join("wapp.toml"), r#" [package] id = "com.example.test" name = "Test App" version = "1.0.0" [runtime] module = "app.wasm" [ui] entry = "ui/index.html" "#, ) .unwrap(); let result = check_package(&tmp); assert!(result.is_ok(), "{result:?}"); assert_eq!(result.unwrap(), "OK"); let _ = fs::remove_dir_all(&tmp); } #[test] fn install_package_copies_to_store() { use std::fs; let id = format!("weft.pack.install{}", std::process::id()); let src = std::env::temp_dir().join(format!("weft_pack_install_src_{}", id)); let store = std::env::temp_dir().join(format!("weft_pack_install_store_{}", id)); let ui_dir = src.join("ui"); let _ = fs::create_dir_all(&ui_dir); fs::write(src.join("app.wasm"), b"\0asm").unwrap(); fs::write(ui_dir.join("index.html"), b"").unwrap(); let app_id = format!("com.example.t{}", std::process::id()); fs::write( src.join("wapp.toml"), format!( "[package]\nid = \"{app_id}\"\nname = \"Test\"\nversion = \"1.0.0\"\n\n\ [runtime]\nmodule = \"app.wasm\"\n\n[ui]\nentry = \"ui/index.html\"\n" ), ) .unwrap(); let result = install_package_to(&src, &store); assert!(result.is_ok(), "{result:?}"); assert!(store.join(&app_id).join("app.wasm").exists()); assert!(store.join(&app_id).join("wapp.toml").exists()); assert!(store.join(&app_id).join("ui").join("index.html").exists()); assert!(install_package_to(&src, &store).is_err()); let _ = fs::remove_dir_all(&src); let _ = fs::remove_dir_all(&store); } #[test] fn uninstall_package_removes_directory() { use std::fs; let id = format!("weft.pack.uninstall{}", std::process::id()); let src = std::env::temp_dir().join(format!("weft_pack_uninstall_src_{}", id)); let store = std::env::temp_dir().join(format!("weft_pack_uninstall_store_{}", id)); let ui_dir = src.join("ui"); let _ = fs::create_dir_all(&ui_dir); fs::write(src.join("app.wasm"), b"\0asm").unwrap(); fs::write(ui_dir.join("index.html"), b"").unwrap(); let app_id = format!("com.example.u{}", std::process::id()); fs::write( src.join("wapp.toml"), format!( "[package]\nid = \"{app_id}\"\nname = \"U\"\nversion = \"1.0.0\"\n\n\ [runtime]\nmodule = \"app.wasm\"\n\n[ui]\nentry = \"ui/index.html\"\n" ), ) .unwrap(); install_package_to(&src, &store).unwrap(); assert!(store.join(&app_id).exists()); let result = uninstall_package_from(&app_id, &store); assert!(result.is_ok(), "{result:?}"); assert!(!store.join(&app_id).exists()); assert!(uninstall_package_from(&app_id, &store).is_err()); let _ = fs::remove_dir_all(&src); let _ = fs::remove_dir_all(&store); } #[test] fn check_package_bad_wasm_magic() { use std::fs; let tmp = std::env::temp_dir().join("weft_pack_test_bad_wasm"); let ui_dir = tmp.join("ui"); let _ = fs::create_dir_all(&ui_dir); fs::write(tmp.join("app.wasm"), b"NOT_WASM").unwrap(); fs::write(ui_dir.join("index.html"), b"").unwrap(); fs::write( tmp.join("wapp.toml"), r#" [package] id = "com.example.badwasm" name = "Bad Wasm" version = "0.1.0" [runtime] module = "app.wasm" [ui] entry = "ui/index.html" "#, ) .unwrap(); let result = check_package(&tmp); assert!(result.is_err()); let msg = result.unwrap_err().to_string(); assert!(msg.contains("bad magic bytes"), "got: {msg}"); let _ = fs::remove_dir_all(&tmp); } #[test] fn check_package_invalid_app_id() { use std::fs; let tmp = std::env::temp_dir().join("weft_pack_test_invalid_id"); let ui_dir = tmp.join("ui"); let _ = fs::create_dir_all(&ui_dir); fs::write(tmp.join("app.wasm"), b"\0asm").unwrap(); fs::write(ui_dir.join("index.html"), b"").unwrap(); fs::write( tmp.join("wapp.toml"), r#" [package] id = "bad-id" name = "Bad" version = "0.1.0" [runtime] module = "app.wasm" [ui] entry = "ui/index.html" "#, ) .unwrap(); let result = check_package(&tmp); assert!(result.is_err()); let _ = fs::remove_dir_all(&tmp); } #[test] fn install_package_from_archive() { use std::fs; let id = format!("instarch_{}", std::process::id()); let src = std::env::temp_dir().join(&id); let ui = src.join("ui"); let _ = fs::create_dir_all(&ui); fs::write(src.join("app.wasm"), b"\0asm\x01\0\0\0").unwrap(); fs::write(ui.join("index.html"), b"").unwrap(); let app_id = format!("com.example.ia{}", std::process::id()); fs::write( src.join("wapp.toml"), format!( "[package]\nid = \"{app_id}\"\nname = \"IA\"\nversion = \"1.0.0\"\n\n\ [runtime]\nmodule = \"app.wasm\"\n\n[ui]\nentry = \"ui/index.html\"\n" ), ) .unwrap(); let bundle_dir = std::env::temp_dir().join(format!("{id}_bnd")); let _ = fs::create_dir_all(&bundle_dir); bundle_package(&src, Some(&bundle_dir)).unwrap(); let archive = bundle_dir.join(format!("{app_id}.app.tar.zst")); let store = std::env::temp_dir().join(format!("{id}_store")); let _ = fs::create_dir_all(&store); install_package_to(&archive, &store).unwrap(); assert!(store.join(&app_id).join("app.wasm").exists()); let _ = fs::remove_dir_all(&src); let _ = fs::remove_dir_all(&bundle_dir); let _ = fs::remove_dir_all(&store); } #[test] fn bundle_and_unbundle_roundtrip() { use std::fs; let id = format!("bundle_{}", std::process::id()); let src = std::env::temp_dir().join(&id); let out = std::env::temp_dir().join(format!("{id}_out")); let ui = src.join("ui"); let _ = fs::create_dir_all(&ui); fs::write(src.join("app.wasm"), b"\0asm\x01\0\0\0").unwrap(); fs::write(ui.join("index.html"), b"").unwrap(); let app_id = format!("com.example.b{}", std::process::id()); fs::write( src.join("wapp.toml"), format!( "[package]\nid = \"{app_id}\"\nname = \"B\"\nversion = \"1.0.0\"\n\n\ [runtime]\nmodule = \"app.wasm\"\n\n[ui]\nentry = \"ui/index.html\"\n" ), ) .unwrap(); let _ = fs::create_dir_all(&out); bundle_package(&src, Some(&out)).unwrap(); let archive = out.join(format!("{app_id}.app.tar.zst")); assert!(archive.exists()); let unpack = std::env::temp_dir().join(format!("{id}_unpack")); let _ = fs::create_dir_all(&unpack); unbundle_package(&archive, &unpack).unwrap(); assert!(unpack.join(&app_id).join("app.wasm").exists()); assert!(unpack.join(&app_id).join("ui").join("index.html").exists()); let _ = fs::remove_dir_all(&src); let _ = fs::remove_dir_all(&out); let _ = fs::remove_dir_all(&unpack); } #[test] fn sign_and_verify_roundtrip() { use std::fs; let id = format!("sign_verify_{}", std::process::id()); let dir = std::env::temp_dir().join(&id); let key_dir = std::env::temp_dir().join(format!("{id}_keys")); let ui = dir.join("ui"); let _ = fs::create_dir_all(&ui); fs::write(dir.join("app.wasm"), b"\0asm\x01\0\0\0").unwrap(); fs::write(ui.join("index.html"), b"").unwrap(); fs::write( dir.join("wapp.toml"), "[package]\nid = \"com.example.signed\"\nname = \"S\"\nversion = \"1.0.0\"\n\n\ [runtime]\nmodule = \"app.wasm\"\n\n[ui]\nentry = \"ui/index.html\"\n", ) .unwrap(); generate_key(&key_dir).unwrap(); let key_file = key_dir.join("weft-sign.key"); let pub_file = key_dir.join("weft-sign.pub"); sign_package(&dir, &key_file).unwrap(); assert!(dir.join("signature.sig").exists()); let ok = verify_package(&dir, &pub_file).unwrap(); assert!(ok, "signature should verify"); let _ = fs::remove_dir_all(&dir); let _ = fs::remove_dir_all(&key_dir); } #[test] fn verify_rejects_tampered_bundle() { use std::fs; let id = format!("tamper_{}", std::process::id()); let dir = std::env::temp_dir().join(&id); let key_dir = std::env::temp_dir().join(format!("{id}_keys")); let ui = dir.join("ui"); let _ = fs::create_dir_all(&ui); fs::write(dir.join("app.wasm"), b"\0asm\x01\0\0\0").unwrap(); fs::write(ui.join("index.html"), b"").unwrap(); fs::write( dir.join("wapp.toml"), "[package]\nid = \"com.example.tamper\"\nname = \"T\"\nversion = \"1.0.0\"\n\n\ [runtime]\nmodule = \"app.wasm\"\n\n[ui]\nentry = \"ui/index.html\"\n", ) .unwrap(); generate_key(&key_dir).unwrap(); sign_package(&dir, &key_dir.join("weft-sign.key")).unwrap(); fs::write(dir.join("app.wasm"), b"\0asm\x01\0\0\x01").unwrap(); let ok = verify_package(&dir, &key_dir.join("weft-sign.pub")).unwrap(); assert!(!ok, "tampered bundle should not verify"); let _ = fs::remove_dir_all(&dir); let _ = fs::remove_dir_all(&key_dir); } #[test] fn list_installed_roots_uses_weft_app_store_when_set() { let prior = std::env::var("WEFT_APP_STORE").ok(); unsafe { std::env::set_var("WEFT_APP_STORE", "/custom/store") }; let roots = list_installed_roots(); unsafe { match prior { Some(v) => std::env::set_var("WEFT_APP_STORE", v), None => std::env::remove_var("WEFT_APP_STORE"), } } assert_eq!(roots, vec![PathBuf::from("/custom/store")]); } #[test] fn list_installed_roots_includes_system_path() { let prior = std::env::var("WEFT_APP_STORE").ok(); unsafe { std::env::remove_var("WEFT_APP_STORE") }; let roots = list_installed_roots(); unsafe { if let Some(v) = prior { std::env::set_var("WEFT_APP_STORE", v); } } assert!( roots .iter() .any(|p| p == &PathBuf::from("/usr/share/weft/apps")) ); } }