Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 13 additions & 4 deletions src/commands/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand Down Expand Up @@ -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<firefly_types::Meta<'_>> {
use firefly_types::{Meta, validate_id, validate_name};
Expand Down
69 changes: 62 additions & 7 deletions src/commands/import.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")?;
Expand All @@ -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<PathBuf> {
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");
Expand Down Expand Up @@ -90,6 +121,7 @@ fn fetch_archive(path: &str) -> Result<PathBuf> {
Ok(out_path)
}

/// Read and parse app metadata from the app archive.
fn read_meta_raw(archive: &mut ZipArchive<File>) -> Result<Vec<u8>> {
let mut meta_raw = Vec::new();
let mut meta_file = if archive.index_for_name(META).is_some() {
Expand Down Expand Up @@ -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(())
}
Expand Down Expand Up @@ -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));
}
}
6 changes: 5 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down