use super::*; #[test] fn test_init_repo_schema() { let mut conn = Connection::open_in_memory().unwrap(); let manager = RepoManager::new(PathBuf::from(".")); manager.init_repo_schema(&mut conn).unwrap(); // Check if table exists let exists: bool = conn .query_row( "SELECT count(*) FROM sqlite_master WHERE type='table' AND name='packages'", [], |r| r.get(0), ) .unwrap(); assert!(exists); let has_sha512: bool = conn .query_row( "SELECT COUNT(*) FROM pragma_table_info('packages') WHERE name = 'sha512'", [], |r| { let n: i64 = r.get(0)?; Ok(n > 0) }, ) .unwrap(); assert!(has_sha512); let has_completed_at: bool = conn .query_row( "SELECT COUNT(*) FROM pragma_table_info('packages') WHERE name = 'completed_at'", [], |r| { let n: i64 = r.get(0)?; Ok(n > 0) }, ) .unwrap(); assert!(has_completed_at); } #[test] fn test_index_package() { let tmp = tempfile::tempdir().unwrap(); let repo_dir = tmp.path(); let pkg_path = repo_dir.join("test-1.0-1-x86_64.depot.pkg.tar.zst"); // Create a valid .tar.zst with .metadata.toml let file = fs::File::create(&pkg_path).unwrap(); let encoder = zstd::stream::write::Encoder::new(file, 3).unwrap(); let mut tar = tar::Builder::new(encoder); let metadata = r#" name = "test" real_name = "icu" version = "1.0" revision = 1 abi_breaking = true built_against = ["icu78"] description = "test description" homepage = "https://example.com" license = "MIT" completed_at = "2026-03-10T12:34:56Z" provides = ["test-feature"] [dependencies] runtime = [] optional = [] "#; let mut header = tar::Header::new_gnu(); header.set_path(".metadata.toml").unwrap(); header.set_size(metadata.len() as u64); header.set_mode(0o644); header.set_cksum(); tar.append(&header, metadata.as_bytes()).unwrap(); let encoder = tar.into_inner().unwrap(); encoder.finish().unwrap(); filetime::set_file_mtime( &pkg_path, filetime::FileTime::from_unix_time(1_700_000_000, 0), ) .unwrap(); let mut conn = Connection::open_in_memory().unwrap(); let manager = RepoManager::new(repo_dir.to_path_buf()); manager.init_repo_schema(&mut conn).unwrap(); let indexed = manager.read_indexed_package(&pkg_path).unwrap(); manager.insert_indexed_package(&mut conn, indexed).unwrap(); type PackageRow = ( String, Option, String, i64, i64, String, Option, Option, Option, String, String, ); let ( name, real_name, version, revision, abi_breaking, built_against, desc, home, lic, sha256, sha512, ): PackageRow = conn .query_row( "SELECT name, real_name, version, revision, abi_breaking, built_against, description, homepage, license, sha256, sha512 FROM packages", [], |r| { Ok(( r.get(0)?, r.get(1)?, r.get(2)?, r.get(3)?, r.get(4)?, r.get(5)?, r.get(6)?, r.get(7)?, r.get(8)?, r.get(9)?, r.get(10)?, )) }, ) .unwrap(); assert_eq!(name, "test"); assert_eq!(real_name, Some("icu".to_string())); assert_eq!(version, "1.0"); assert_eq!(revision, 1); assert_eq!(abi_breaking, 1); assert_eq!(built_against, "icu78"); assert_eq!(desc, Some("test description".to_string())); assert_eq!(home, Some("https://example.com".to_string())); assert_eq!(lic, Some("MIT".to_string())); assert_eq!(sha256.len(), 64); assert_eq!(sha512.len(), 128); let completed_at: Option = conn .query_row("SELECT completed_at FROM packages", [], |r| r.get(0)) .unwrap(); assert_eq!(completed_at, Some(1_773_146_096)); let provides_count: i64 = conn .query_row("SELECT count(*) FROM provides", [], |r| r.get(0)) .unwrap(); assert_eq!(provides_count, 1); } #[test] fn test_index_package_with_multiple_licenses() { let tmp = tempfile::tempdir().unwrap(); let repo_dir = tmp.path(); let pkg_path = repo_dir.join("test-1.0-1-x86_64.depot.pkg.tar.zst"); let file = fs::File::create(&pkg_path).unwrap(); let encoder = zstd::stream::write::Encoder::new(file, 3).unwrap(); let mut tar = tar::Builder::new(encoder); let metadata = r#" name = "test" version = "1.0" revision = 1 license = ["MIT", "Apache-2.0"] "#; let mut header = tar::Header::new_gnu(); header.set_path(".metadata.toml").unwrap(); header.set_size(metadata.len() as u64); header.set_mode(0o644); header.set_cksum(); tar.append(&header, metadata.as_bytes()).unwrap(); let encoder = tar.into_inner().unwrap(); encoder.finish().unwrap(); let mut conn = Connection::open_in_memory().unwrap(); let manager = RepoManager::new(repo_dir.to_path_buf()); manager.init_repo_schema(&mut conn).unwrap(); let indexed = manager.read_indexed_package(&pkg_path).unwrap(); manager.insert_indexed_package(&mut conn, indexed).unwrap(); let lic: Option = conn .query_row("SELECT license FROM packages", [], |r| r.get(0)) .unwrap(); assert_eq!(lic, Some("MIT, Apache-2.0".to_string())); } #[test] fn test_index_package_records_symlink_paths_for_repo_owns() { let tmp = tempfile::tempdir().unwrap(); let repo_dir = tmp.path(); let pkg_path = repo_dir.join("test-1.0-1-x86_64.depot.pkg.tar.zst"); let file = fs::File::create(&pkg_path).unwrap(); let encoder = zstd::stream::write::Encoder::new(file, 3).unwrap(); let mut tar = tar::Builder::new(encoder); let metadata = r#" name = "test" version = "1.0" revision = 1 "#; let mut header = tar::Header::new_gnu(); header.set_path(".metadata.toml").unwrap(); header.set_size(metadata.len() as u64); header.set_mode(0o644); header.set_cksum(); tar.append(&header, metadata.as_bytes()).unwrap(); let mut file_header = tar::Header::new_gnu(); file_header.set_path("usr/bin/coreutils").unwrap(); file_header.set_size(4); file_header.set_mode(0o755); file_header.set_cksum(); tar.append(&file_header, &b"test"[..]).unwrap(); let mut link_header = tar::Header::new_gnu(); link_header.set_entry_type(tar::EntryType::Symlink); link_header.set_path("usr/bin/ls").unwrap(); link_header.set_link_name("coreutils").unwrap(); link_header.set_size(0); link_header.set_mode(0o777); link_header.set_cksum(); tar.append(&link_header, std::io::empty()).unwrap(); let encoder = tar.into_inner().unwrap(); encoder.finish().unwrap(); let mut conn = Connection::open_in_memory().unwrap(); let manager = RepoManager::new(repo_dir.to_path_buf()); manager.init_repo_schema(&mut conn).unwrap(); let indexed = manager.read_indexed_package(&pkg_path).unwrap(); manager.insert_indexed_package(&mut conn, indexed).unwrap(); let db_path = repo_dir.join("repo.db"); let mut file_conn = Connection::open(&db_path).unwrap(); manager.init_repo_schema(&mut file_conn).unwrap(); let indexed = manager.read_indexed_package(&pkg_path).unwrap(); manager .insert_indexed_package(&mut file_conn, indexed) .unwrap(); drop(file_conn); let hits = cached_binary_repo_owns_path("repo", &db_path, "usr/bin/ls").unwrap(); assert_eq!(hits.len(), 1); assert_eq!(hits[0].package_name, "test"); assert_eq!(hits[0].path, "usr/bin/ls"); } #[test] fn test_search_cached_binary_repo_db_matches_name_and_provides() { let tmp = tempfile::tempdir().unwrap(); let db_path = tmp.path().join("repo.db"); let mut conn = Connection::open(&db_path).unwrap(); let manager = RepoManager::new(tmp.path().to_path_buf()); manager.init_repo_schema(&mut conn).unwrap(); conn.execute( "INSERT INTO packages (id, name, version, revision, description, homepage, license, filename, size, sha256, sha512) VALUES (1, 'foo', '1.2.3', 1, 'Foo package', 'https://example.test', 'MIT', 'foo-1.2.3-1-x86_64.depot.pkg.tar.zst', 1234, 'a', 'b')", [], ) .unwrap(); conn.execute( "INSERT INTO provides (package_id, name) VALUES (1, 'libfoo.so')", [], ) .unwrap(); drop(conn); let name_hits = search_cached_binary_repo_db("testrepo", &db_path, "foo").unwrap(); assert_eq!(name_hits.len(), 1); assert_eq!(name_hits[0].name, "foo"); assert_eq!(name_hits[0].repo_name, "testrepo"); assert!(name_hits[0].provides.iter().any(|p| p == "libfoo.so")); let provide_hits = search_cached_binary_repo_db("testrepo", &db_path, "libfoo").unwrap(); assert_eq!(provide_hits.len(), 1); assert_eq!(provide_hits[0].name, "foo"); } #[test] fn test_find_cached_binary_repo_packages_by_group() { let tmp = tempfile::tempdir().unwrap(); let db_path = tmp.path().join("repo.db"); let mut conn = Connection::open(&db_path).unwrap(); let manager = RepoManager::new(tmp.path().to_path_buf()); manager.init_repo_schema(&mut conn).unwrap(); conn.execute( "INSERT INTO packages (id, name, version, revision, description, homepage, license, filename, size, sha256, sha512) VALUES (1, 'foo', '1.2.3', 1, 'Foo package', 'https://example.test', 'MIT', 'foo-1.2.3-1-x86_64.depot.pkg.tar.zst', 1234, 'a', 'b')", [], ) .unwrap(); conn.execute( "INSERT INTO groups (package_id, name) VALUES (1, 'base')", [], ) .unwrap(); drop(conn); let hits = find_cached_binary_repo_packages_by_group("testrepo", &db_path, "base").unwrap(); assert_eq!(hits.len(), 1); assert_eq!(hits[0].name, "foo"); assert_eq!(hits[0].groups, vec!["base".to_string()]); } #[test] #[cfg(unix)] fn test_repo_owns_query_candidates_follow_rootfs_symlink_targets() { let rootfs = tempfile::tempdir().unwrap(); let usr_bin = rootfs.path().join("usr/bin"); fs::create_dir_all(&usr_bin).unwrap(); fs::write(usr_bin.join("coreutils"), b"payload").unwrap(); std::os::unix::fs::symlink("coreutils", usr_bin.join("ls")).unwrap(); std::os::unix::fs::symlink("usr/bin", rootfs.path().join("bin")).unwrap(); let ls_candidates = repo_owns_query_candidates(rootfs.path(), "/usr/bin/ls"); assert!( ls_candidates .iter() .any(|candidate| candidate == "usr/bin/ls") ); assert!( ls_candidates .iter() .any(|candidate| candidate == "usr/bin/coreutils") ); let bin_candidates = repo_owns_query_candidates(rootfs.path(), "/bin/ls"); assert!(bin_candidates.iter().any(|candidate| candidate == "bin/ls")); assert!( bin_candidates .iter() .any(|candidate| candidate == "usr/bin/coreutils") ); } #[test] fn test_find_cached_binary_repo_package_prefers_exact_name() { let tmp = tempfile::tempdir().unwrap(); let db_path = tmp.path().join("repo.db"); let mut conn = Connection::open(&db_path).unwrap(); let manager = RepoManager::new(tmp.path().to_path_buf()); manager.init_repo_schema(&mut conn).unwrap(); conn.execute( "INSERT INTO packages (id, name, version, revision, description, homepage, license, filename, size, sha256, sha512) VALUES (1, 'foo', '1.0', 1, NULL, NULL, NULL, 'foo-1.0-1.depot.pkg.tar.zst', 10, 'aa', 'bb')", [], ) .unwrap(); conn.execute( "INSERT INTO packages (id, name, version, revision, description, homepage, license, filename, size, sha256, sha512) VALUES (2, 'bar', '1.0', 1, NULL, NULL, NULL, 'bar-1.0-1.depot.pkg.tar.zst', 10, 'cc', 'dd')", [], ) .unwrap(); conn.execute( "INSERT INTO provides (package_id, name) VALUES (2, 'foo')", [], ) .unwrap(); drop(conn); let recs = find_cached_binary_repo_packages("repo", &db_path, "foo").unwrap(); let rec = recs.first().expect("expected a match"); assert_eq!(rec.name, "foo"); assert_eq!(rec.filename, "foo-1.0-1.depot.pkg.tar.zst"); } #[test] fn test_find_cached_binary_repo_packages_matches_real_name_and_built_against() { let tmp = tempfile::tempdir().unwrap(); let db_path = tmp.path().join("repo.db"); let mut conn = Connection::open(&db_path).unwrap(); let manager = RepoManager::new(tmp.path().to_path_buf()); manager.init_repo_schema(&mut conn).unwrap(); conn.execute( "INSERT INTO packages (id, name, real_name, version, revision, abi_breaking, built_against, description, homepage, license, filename, size, sha256, sha512) VALUES (1, 'icu78', 'icu', '78.1', 1, 0, '', NULL, NULL, NULL, 'icu78.pkg', 10, 'aa', 'bb')", [], ) .unwrap(); conn.execute( "INSERT INTO packages (id, name, real_name, version, revision, abi_breaking, built_against, description, homepage, license, filename, size, sha256, sha512) VALUES (2, 'app', NULL, '1.0', 1, 0, 'icu78', NULL, NULL, NULL, 'app.pkg', 10, 'cc', 'dd')", [], ) .unwrap(); conn.execute( "INSERT INTO dependencies (package_id, kind, name) VALUES (2, 'runtime', 'icu')", [], ) .unwrap(); drop(conn); let icu_matches = find_cached_binary_repo_packages("repo", &db_path, "icu").unwrap(); assert_eq!(icu_matches.len(), 1); assert_eq!(icu_matches[0].name, "icu78"); assert_eq!(icu_matches[0].real_name.as_deref(), Some("icu")); let app = find_cached_binary_repo_packages("repo", &db_path, "app") .unwrap() .pop() .unwrap(); assert_eq!(app.runtime_dependencies, vec!["icu".to_string()]); assert_eq!(app.built_against, vec!["icu78".to_string()]); } #[test] fn test_verify_binary_package_record_checksums_accepts_valid_hashes() { use sha2::{Digest, Sha512}; let tmp = tempfile::tempdir().unwrap(); let pkg = tmp.path().join("pkg.depot.pkg.tar.zst"); fs::write(&pkg, b"payload").unwrap(); let sha512 = { let mut h = Sha512::new(); h.update(b"payload"); crate::hex::encode_lower(h.finalize()) }; let rec = BinaryRepoPackageRecord { repo_name: "repo".into(), name: "pkg".into(), real_name: None, version: "1.0".into(), revision: 1, abi_breaking: false, built_against: Vec::new(), completed_at: None, filename: "pkg.depot.pkg.tar.zst".into(), size: 7, sha512, description: None, homepage: None, license: None, provides: Vec::new(), conflicts: Vec::new(), replaces: Vec::new(), runtime_dependencies: Vec::new(), optional_dependencies: Vec::new(), groups: Vec::new(), }; verify_binary_package_record_checksums(&pkg, &rec).unwrap(); } #[test] fn test_verify_binary_package_record_checksums_requires_valid_sha512() { use sha2::{Digest, Sha512}; let tmp = tempfile::tempdir().unwrap(); let pkg = tmp.path().join("pkg.depot.pkg.tar.zst"); fs::write(&pkg, b"payload").unwrap(); let mut rec = test_record_for_payload("pkg.depot.pkg.tar.zst", b"payload"); verify_binary_package_record_checksums(&pkg, &rec).unwrap(); let mut wrong_sha512 = Sha512::new(); wrong_sha512.update(b"different payload"); rec.sha512 = crate::hex::encode_lower(wrong_sha512.finalize()); let err = verify_binary_package_record_checksums(&pkg, &rec).unwrap_err(); assert!(err.to_string().contains("SHA-512 mismatch")); } fn test_record_for_payload(filename: &str, payload: &[u8]) -> BinaryRepoPackageRecord { use sha2::{Digest, Sha512}; let sha512 = { let mut h = Sha512::new(); h.update(payload); crate::hex::encode_lower(h.finalize()) }; BinaryRepoPackageRecord { repo_name: "repo".into(), name: "pkg".into(), real_name: None, version: "1.0".into(), revision: 1, abi_breaking: false, built_against: Vec::new(), completed_at: None, filename: filename.to_string(), size: payload.len() as u64, sha512, description: None, homepage: None, license: None, provides: Vec::new(), conflicts: Vec::new(), replaces: Vec::new(), runtime_dependencies: Vec::new(), optional_dependencies: Vec::new(), groups: Vec::new(), } } #[test] fn test_fetch_binary_package_archive_requires_signature_when_unsigned_disallowed() { let rootfs = tempfile::tempdir().unwrap(); let repo_dir = tempfile::tempdir().unwrap(); let cache_dir = tempfile::tempdir().unwrap(); let filename = "pkg-1.0-1-x86_64.depot.pkg.tar.zst"; let payload = b"package payload"; std::fs::write(repo_dir.path().join(filename), payload).unwrap(); let rec = test_record_for_payload(filename, payload); let repo_url = url::Url::from_directory_path(repo_dir.path()) .expect("file URL") .to_string(); let repo_cfg = crate::config::BinaryRepo { url: repo_url, allow_unsigned: false, ..Default::default() }; let err = fetch_binary_package_archive("repo", &repo_cfg, rootfs.path(), &rec, cache_dir.path()) .expect_err("missing detached signature should fail"); assert!(err.to_string().to_ascii_lowercase().contains("signature")); } #[test] fn test_fetch_binary_package_archive_verifies_signature_and_checksum() { let rootfs = tempfile::tempdir().unwrap(); let repo_dir = tempfile::tempdir().unwrap(); let cache_dir = tempfile::tempdir().unwrap(); let trusted_dir = crate::signing::trusted_public_keys_dir(rootfs.path()); std::fs::create_dir_all(&trusted_dir).unwrap(); let keypair = minisign::KeyPair::generate_unencrypted_keypair().unwrap(); std::fs::write( trusted_dir.join("repo.pub"), keypair.pk.to_box().unwrap().to_bytes(), ) .unwrap(); let filename = "pkg-1.0-1-x86_64.depot.pkg.tar.zst"; let payload = b"signed package payload"; let package_path = repo_dir.path().join(filename); std::fs::write(&package_path, payload).unwrap(); let sig = minisign::sign( Some(&keypair.pk), &keypair.sk, std::fs::File::open(&package_path).unwrap(), None, Some("test signature"), ) .unwrap(); std::fs::write(format!("{}.sig", package_path.display()), sig.to_bytes()).unwrap(); let rec = test_record_for_payload(filename, payload); let repo_url = url::Url::from_directory_path(repo_dir.path()) .expect("file URL") .to_string(); let repo_cfg = crate::config::BinaryRepo { url: repo_url, allow_unsigned: false, ..Default::default() }; let fetched = fetch_binary_package_archive("repo", &repo_cfg, rootfs.path(), &rec, cache_dir.path()) .unwrap(); assert_eq!(std::fs::read(&fetched).unwrap(), payload); assert!(PathBuf::from(format!("{}.sig", fetched.display())).exists()); } #[test] fn test_cache_binary_package_archive_supports_combined_integrity_verification() { let rootfs = tempfile::tempdir().unwrap(); let repo_dir = tempfile::tempdir().unwrap(); let cache_dir = tempfile::tempdir().unwrap(); let trusted_dir = crate::signing::trusted_public_keys_dir(rootfs.path()); std::fs::create_dir_all(&trusted_dir).unwrap(); let keypair = minisign::KeyPair::generate_unencrypted_keypair().unwrap(); std::fs::write( trusted_dir.join("repo.pub"), keypair.pk.to_box().unwrap().to_bytes(), ) .unwrap(); let filename = "pkg-1.0-1-x86_64.depot.pkg.tar.zst"; let payload = b"staged verification payload"; let package_path = repo_dir.path().join(filename); std::fs::write(&package_path, payload).unwrap(); let sig = minisign::sign( Some(&keypair.pk), &keypair.sk, std::fs::File::open(&package_path).unwrap(), None, Some("test signature"), ) .unwrap(); std::fs::write(format!("{}.sig", package_path.display()), sig.to_bytes()).unwrap(); let rec = test_record_for_payload(filename, payload); let repo_url = url::Url::from_directory_path(repo_dir.path()) .expect("file URL") .to_string(); let repo_cfg = crate::config::BinaryRepo { url: repo_url, allow_unsigned: false, ..Default::default() }; let cached = cache_binary_package_archive("repo", &repo_cfg, &rec, cache_dir.path()) .expect("cache should succeed"); assert!(cached.package_path.exists()); assert!(cached.signature_path.exists()); verify_binary_package_archive_checksums(&cached.package_path, &rec) .expect("checksum verification should succeed"); let trusted_keys = crate::signing::load_trusted_public_keys(rootfs.path()).unwrap(); verify_binary_package_archive_integrity_with_trusted_keys( "repo", &repo_cfg, &rec, &cached.package_path, &cached.signature_path, &trusted_keys, ) .expect("combined integrity verification should succeed"); let mut wrong_record = rec.clone(); wrong_record.sha512 = crate::hex::encode_lower(Sha512::digest(b"wrong payload")); let error = verify_binary_package_archive_integrity_with_trusted_keys( "repo", &repo_cfg, &wrong_record, &cached.package_path, &cached.signature_path, &trusted_keys, ) .expect_err("combined verification must reject a checksum mismatch"); assert!(error.to_string().contains("SHA-512 mismatch")); } #[test] fn test_fetch_binary_package_archive_allows_missing_signature_when_configured() { let rootfs = tempfile::tempdir().unwrap(); let repo_dir = tempfile::tempdir().unwrap(); let cache_dir = tempfile::tempdir().unwrap(); let filename = "pkg-1.0-1-x86_64.depot.pkg.tar.zst"; let payload = b"unsigned package payload"; std::fs::write(repo_dir.path().join(filename), payload).unwrap(); let rec = test_record_for_payload(filename, payload); let repo_url = url::Url::from_directory_path(repo_dir.path()) .expect("file URL") .to_string(); let repo_cfg = crate::config::BinaryRepo { url: repo_url, allow_unsigned: true, ..Default::default() }; let fetched = fetch_binary_package_archive("repo", &repo_cfg, rootfs.path(), &rec, cache_dir.path()) .unwrap(); assert_eq!(std::fs::read(&fetched).unwrap(), payload); assert!(!PathBuf::from(format!("{}.sig", fetched.display())).exists()); } #[test] fn test_copy_file_url_to_path_supports_file_scheme() { let tmp = tempfile::tempdir().unwrap(); let src = tmp.path().join("repo.db.zst"); let dst = tmp.path().join("copy.zst"); fs::write(&src, b"repo-db").unwrap(); let url = format!("file://{}", src.display()); let outcome = copy_file_url_to_path(&url, &dst).unwrap(); assert_eq!(outcome, FileUrlCopyOutcome::Copied); assert_eq!(fs::read(&dst).unwrap(), b"repo-db"); } #[test] fn test_copy_file_url_to_path_reports_missing_file() { let tmp = tempfile::tempdir().unwrap(); let missing = tmp.path().join("missing.db.zst"); let dst = tmp.path().join("copy.zst"); let url = format!("file://{}", missing.display()); let outcome = copy_file_url_to_path(&url, &dst).unwrap(); assert_eq!(outcome, FileUrlCopyOutcome::Missing); assert!(!dst.exists()); } #[test] fn test_repo_db_fetch_cache_roundtrip_and_prunes_stale_entry() { let tmp = tempfile::tempdir().unwrap(); let db_path = tmp.path().join("repo.db"); fs::write(&db_path, b"db").unwrap(); let key = RepoDbFetchCacheKey { repo_name: "core".to_string(), base_url: "https://repo.example.test/core".to_string(), repo_db_rel: "repo.db.zst".to_string(), rootfs: PathBuf::from("/tmp/rootfs-test"), package_cache_dir: PathBuf::from("/tmp/pkg-cache-test"), }; remember_repo_db_path(key.clone(), db_path.clone()); assert_eq!(get_cached_repo_db_path(&key), Some(db_path.clone())); fs::remove_file(&db_path).unwrap(); assert_eq!(get_cached_repo_db_path(&key), None); } #[test] fn test_extract_html_href_targets_parses_common_forms() { let html = r#" alpha beta gamma parent "#; let hrefs = extract_html_href_targets(html); assert!(hrefs.iter().any(|h| h == "alpha.pub")); assert!(hrefs.iter().any(|h| h == "nested/beta.pub")); assert!(hrefs.iter().any(|h| h == "gamma.pub")); assert!(hrefs.iter().any(|h| h == "../")); } #[test] fn test_list_repo_public_key_urls_reads_file_repo_keys_dir() { let tmp = tempfile::tempdir().unwrap(); let repo_dir = tmp.path().join("repo"); let keys_dir = repo_dir.join("keys"); fs::create_dir_all(&keys_dir).unwrap(); fs::write(keys_dir.join("repo.pub"), b"pubkey").unwrap(); fs::write(keys_dir.join("ignore.txt"), b"nope").unwrap(); fs::create_dir_all(keys_dir.join("subdir")).unwrap(); let base_url = url::Url::from_directory_path(&repo_dir) .expect("file URL") .to_string(); let client = reqwest::blocking::Client::builder().build().unwrap(); let keys = list_repo_public_key_urls(&base_url, &client).unwrap(); assert_eq!(keys.len(), 1); assert_eq!(keys[0].0, "repo.pub"); assert!(keys[0].1.ends_with("/repo.pub")); } #[test] fn test_list_repo_public_key_urls_probes_common_names_when_index_missing() { use std::io::{BufRead, BufReader, Write}; use std::net::TcpListener; use std::thread; let listener = TcpListener::bind("127.0.0.1:0").unwrap(); let addr = listener.local_addr().unwrap(); let server = thread::spawn(move || { for _ in 0..7 { let (mut stream, _) = listener.accept().unwrap(); let mut reader = BufReader::new(stream.try_clone().unwrap()); let mut request_line = String::new(); reader.read_line(&mut request_line).unwrap(); loop { let mut line = String::new(); reader.read_line(&mut line).unwrap(); if line == "\r\n" || line.is_empty() { break; } } let path = request_line .split_whitespace() .nth(1) .unwrap_or_default() .to_string(); let (status, body) = if path == "/core/keys/vertex.pub" { ("200 OK", "trusted-key") } else { ("404 Not Found", "missing") }; write!( stream, "HTTP/1.1 {status}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}", body.len(), body ) .unwrap(); stream.flush().unwrap(); } }); let base_url = format!("http://{}/core", addr); let client = reqwest::blocking::Client::builder().build().unwrap(); let keys = list_repo_public_key_urls(&base_url, &client).unwrap(); server.join().unwrap(); assert_eq!(keys.len(), 1); assert_eq!(keys[0].0, "vertex.pub"); assert!(keys[0].1.ends_with("/core/keys/vertex.pub")); } #[test] fn test_fetch_binary_repo_db_can_recover_from_stale_trusted_key() { use std::io::Write; struct AssumeYesReset; impl Drop for AssumeYesReset { fn drop(&mut self) { crate::ui::set_assume_yes(false); } } crate::ui::set_assume_yes(true); let _reset = AssumeYesReset; let rootfs = tempfile::tempdir().unwrap(); let repo_dir = tempfile::tempdir().unwrap(); let cache_dir = tempfile::tempdir().unwrap(); let stale_keypair = minisign::KeyPair::generate_unencrypted_keypair().unwrap(); let repo_keypair = minisign::KeyPair::generate_unencrypted_keypair().unwrap(); let trusted_dir = crate::signing::trusted_public_keys_dir(rootfs.path()); fs::create_dir_all(&trusted_dir).unwrap(); fs::write( trusted_dir.join("vertex.pub"), stale_keypair.pk.to_box().unwrap().to_bytes(), ) .unwrap(); let repo_keys_dir = repo_dir.path().join("keys"); fs::create_dir_all(&repo_keys_dir).unwrap(); fs::write( repo_keys_dir.join("vertex.pub"), repo_keypair.pk.to_box().unwrap().to_bytes(), ) .unwrap(); let repo_db_path = repo_dir.path().join("repo.db.zst"); let mut encoder = zstd::stream::write::Encoder::new(fs::File::create(&repo_db_path).unwrap(), 3).unwrap(); encoder.write_all(b"repo-db-content").unwrap(); encoder.finish().unwrap(); let sig = minisign::sign( Some(&repo_keypair.pk), &repo_keypair.sk, fs::File::open(&repo_db_path).unwrap(), None, Some("repo db signature"), ) .unwrap(); fs::write(repo_dir.path().join("repo.db.zst.sig"), sig.to_bytes()).unwrap(); let repo_cfg = crate::config::BinaryRepo { url: url::Url::from_directory_path(repo_dir.path()) .expect("file URL") .to_string(), allow_unsigned: false, ..Default::default() }; let sqlite_db = fetch_binary_repo_db("core", &repo_cfg, rootfs.path(), cache_dir.path()).unwrap(); assert_eq!(fs::read(sqlite_db).unwrap(), b"repo-db-content"); let installed_key = trusted_dir.join("core-vertex.pub"); assert!(installed_key.exists()); assert_eq!( fs::read(installed_key).unwrap(), repo_keypair.pk.to_box().unwrap().to_bytes() ); } #[test] fn test_normalize_git_mirror_url_converts_file_scheme() { let tmp = tempfile::tempdir().unwrap(); let repo_dir = tmp.path().join("repo.git"); fs::create_dir_all(&repo_dir).unwrap(); let url = format!("file://{}", repo_dir.display()); let normalized = normalize_git_mirror_url(&url).unwrap(); assert_eq!(normalized, repo_dir.to_string_lossy()); }