diff --git a/src/commands/build.rs b/src/commands/build.rs index 06ded39..e12317a 100644 --- a/src/commands/build.rs +++ b/src/commands/build.rs @@ -74,10 +74,7 @@ static TIPS: &[&str] = &[ pub fn cmd_build(vfs: PathBuf, args: &BuildArgs) -> anyhow::Result<()> { init_vfs(&vfs).context("init vfs")?; let config = Config::load(vfs, &args.root).context("load project config")?; - if config.author_id == "joearms" { - println!("⚠️ author_id in firefly.tom has the default value."); - println!(" Please, change it before sharing the app with the world."); - } + check_provenance(&config); if !args.no_tip { show_tip(); } @@ -115,6 +112,18 @@ pub fn cmd_build(vfs: PathBuf, args: &BuildArgs) -> anyhow::Result<()> { Ok(()) } +/// Emit warnings for suspicious config. +fn check_provenance(c: &Config) { + if c.author_id == "joearms" { + println!("⚠️ author_id in firefly.toml has the default value."); + println!(" Please, change it before sharing the app with the world."); + } + if (c.launcher || c.sudo) && c.author_id != "sys" { + println!("⚠️ The app uses privileged system access. Make sure you trust the author."); + } + // TODO(@orsinium): Validate that "sys" apps are cloned from the official repos. +} + /// Serialize and write the ROM meta information. fn write_meta(config: &Config) -> anyhow::Result> { use firefly_types::{Meta, validate_id, validate_name}; diff --git a/src/commands/import.rs b/src/commands/import.rs index 5d5664f..ee1c40d 100644 --- a/src/commands/import.rs +++ b/src/commands/import.rs @@ -28,14 +28,24 @@ pub fn cmd_import(vfs: &Path, args: &ImportArgs) -> Result<()> { let meta_raw = read_meta_raw(&mut archive)?; let meta = Meta::decode(&meta_raw).context("parse meta")?; + if !id_matches(&args.path, &meta) { + bail!( + "app ID ({}.{}) doesn't match the expected ID", + meta.author_id, + meta.app_id + ); + } + if (meta.launcher || meta.sudo) && meta.author_id != "sys" { + println!("⚠️ The app uses privileged system access. Make sure you trust the author."); + } let rom_path = vfs.join("roms").join(meta.author_id).join(meta.app_id); init_vfs(vfs).context("init VFS")?; _ = fs::remove_dir_all(&rom_path); create_dir_all(&rom_path).context("create ROM dir")?; archive.extract(&rom_path).context("extract archive")?; - if let Err(err) = verify(&rom_path) { - println!("⚠️ verification failed: {err}"); + if let Err(err) = verify_hash(&rom_path) { + println!("⚠️ hash verification failed: {err}"); } create_data_dir(&meta, vfs).context("create app data directory")?; write_stats(&meta, vfs).context("create app stats file")?; @@ -47,14 +57,35 @@ pub fn cmd_import(vfs: &Path, args: &ImportArgs) -> Result<()> { Ok(()) } +/// Check if the ID from the ID/path/URL that the user provided matches the app ID in meta. +/// +/// Currently verifies ID only if the app source is the catalog. +/// For installation from URL/file we let the URL/file to have any name. +fn id_matches(given: &str, meta: &Meta<'_>) -> bool { + let is_catalog = !given.ends_with(".zip"); + if !is_catalog { + return true; + } + if given == "launcher" { + return meta.author_id == "sys" && meta.app_id == "launcher"; + } + let full_id = format!("{}.{}", meta.author_id, meta.app_id); + given == full_id +} + +/// Fetch the given app archive as a file. +/// +/// * If file path is given, this path will be returned without any file modification. +/// * If URL is given, the file will be downloaded. +/// * If app ID is given, try downloading the app from the catalog. fn fetch_archive(path: &str) -> Result { - let mut path = path.to_string(); + let mut path = path; if path == "launcher" { - path = "https://github.com/firefly-zero/firefly-launcher/releases/latest/download/sys.launcher.zip".to_string(); + path = "https://github.com/firefly-zero/firefly-launcher/releases/latest/download/sys.launcher.zip"; } + let mut path = path.to_string(); // App ID is given. Fetch download URL from the catalog. - #[expect(clippy::case_sensitive_file_extension_comparisons)] if !path.ends_with(".zip") { let Some((author_id, app_id)) = path.split_once('.') else { bail!("app ID must contain dot"); @@ -90,6 +121,7 @@ fn fetch_archive(path: &str) -> Result { Ok(out_path) } +/// Read and parse app metadata from the app archive. fn read_meta_raw(archive: &mut ZipArchive) -> Result> { let mut meta_raw = Vec::new(); let mut meta_file = if archive.index_for_name(META).is_some() { @@ -134,14 +166,14 @@ fn reset_launcher_cache(vfs_path: &Path) -> anyhow::Result<()> { } /// Verify SHA256 hash. -fn verify(rom_path: &Path) -> anyhow::Result<()> { +fn verify_hash(rom_path: &Path) -> anyhow::Result<()> { let hash_path = rom_path.join(HASH); let hash_expected: &[u8] = &fs::read(hash_path).context("read hash file")?; let hash_actual: &[u8] = &hash_dir(rom_path).context("calculate hash")?[..]; if hash_actual != hash_expected { let exp = HEXLOWER.encode(hash_expected); let act = HEXLOWER.encode(hash_actual); - bail!("invalid hash:\n expected: {exp}\n got: {act}"); + bail!("expected: {exp}, got: {act}"); } Ok(()) } @@ -319,4 +351,27 @@ mod tests { dirs_eq(&vfs.join("roms"), &vfs2.join("roms")); dirs_eq(&vfs.join("data"), &vfs2.join("data")); } + + #[test] + fn test_id_matches() { + let meta = Meta { + author_id: "sys", + app_id: "launcher", + + app_name: "", + author_name: "", + launcher: true, + sudo: true, + version: 1, + }; + assert!(id_matches("launcher", &meta)); + assert!(id_matches("sys.launcher", &meta)); + assert!(id_matches("sys.launcher.zip", &meta)); + assert!(id_matches("/tmp/sys.launcher.zip", &meta)); + let url = "https://github.com/firefly-zero/firefly-launcher/releases/latest/download/sys.launcher.zip"; + assert!(id_matches(url, &meta)); + + assert!(!id_matches("lux.snek", &meta)); + assert!(!id_matches("snek", &meta)); + } } diff --git a/src/main.rs b/src/main.rs index 44524e6..1fd5971 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,7 +8,11 @@ clippy::nursery, clippy::allow_attributes )] -#![allow(clippy::enum_glob_use, clippy::wildcard_imports)] +#![allow( + clippy::enum_glob_use, + clippy::wildcard_imports, + clippy::case_sensitive_file_extension_comparisons +)] #![expect(clippy::option_if_let_else)] mod args;