WEFT_OS/crates/weft-pack/src/main.rs

940 lines
32 KiB
Rust
Raw Normal View History

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<String>,
author: Option<String>,
capabilities: Option<Vec<String>>,
}
#[derive(Debug, Deserialize)]
struct RuntimeMeta {
module: String,
}
#[derive(Debug, Deserialize)]
struct UiMeta {
entry: String,
}
fn main() -> anyhow::Result<()> {
let args: Vec<String> = std::env::args().collect();
match args.get(1).map(String::as_str) {
Some("check") => {
let dir = args.get(2).context("usage: weft-pack check <dir>")?;
let result = check_package(Path::new(dir))?;
println!("{result}");
}
Some("info") => {
let dir = args.get(2).context("usage: weft-pack info <dir>")?;
let manifest = load_manifest(Path::new(dir))?;
print_info(&manifest);
}
Some("install") => {
let dir = args.get(2).context("usage: weft-pack install <dir>")?;
install_package(Path::new(dir))?;
}
Some("uninstall") => {
let app_id = args.get(2).context("usage: weft-pack uninstall <app_id>")?;
uninstall_package(app_id)?;
}
Some("list") => {
list_installed();
}
Some("build-image") => {
let dir = args.get(2).context("usage: weft-pack build-image <dir>")?;
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 <img>")?;
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 <dir>")?;
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 <archive>")?;
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 <dir> --key <keyfile>")?;
let key = args
.windows(2)
.find(|w| w[0] == "--key")
.map(|w| &w[1])
.context("missing --key <keyfile>")?;
sign_package(Path::new(dir), Path::new(key))?;
}
Some("verify") => {
let dir = args
.get(2)
.context("usage: weft-pack verify <dir> --key <pubkeyfile>")?;
let key = args
.windows(2)
.find(|w| w[0] == "--key")
.map(|w| &w[1])
.context("missing --key <pubkeyfile>")?;
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 <dir> validate a package directory");
eprintln!(" weft-pack info <dir> print package metadata");
eprintln!(" weft-pack install <dir> install package to app store");
eprintln!(" weft-pack uninstall <app_id> remove installed package");
eprintln!(" weft-pack list list installed packages");
eprintln!(
" weft-pack build-image <dir> [--out <img>] create EROFS image with mkfs.erofs"
);
eprintln!(" weft-pack build-verity <img> [--out <hash>] add dm-verity hash tree");
eprintln!(" weft-pack bundle <dir> [--out <dir>] create .app.tar.zst archive");
eprintln!(" weft-pack unbundle <archive> [--out <dir>] extract .app.tar.zst");
eprintln!(" weft-pack generate-key [<outdir>] generate Ed25519 keypair");
eprintln!(" weft-pack sign <dir> --key <key> sign package with private key");
eprintln!(" weft-pack verify <dir> --key <pub> verify package signature");
std::process::exit(1);
}
}
Ok(())
}
fn check_package(dir: &Path) -> anyhow::Result<String> {
let mut errors: Vec<String> = 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()));
}
}
if errors.is_empty() {
Ok("OK".into())
} else {
Err(anyhow::anyhow!("{}", errors.join("\n")))
}
}
fn load_manifest(dir: &Path) -> anyhow::Result<Manifest> {
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<PathBuf> {
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(dir: &Path) -> anyhow::Result<()> {
let root = resolve_install_root()?;
install_package_to(dir, &root)
}
fn install_package_to(dir: &Path, store_root: &Path) -> anyhow::Result<()> {
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<PathBuf> {
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::<Manifest>(&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<bool> {
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"<!DOCTYPE html>").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"<!DOCTYPE html>").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 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"<!DOCTYPE html>").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"<!DOCTYPE html>").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"<!DOCTYPE html>").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"))
);
}
}