use crate::package::test_support::*;
use crate::package::*;
#[test]
fn concurrent_materialization_serializes_package_tree_updates() {
let (_repo_tmp, repo, _branch) = create_git_package_repo();
let project_tmp = tempfile::tempdir().unwrap();
let root = project_tmp.path();
let workspace = TestWorkspace::new(root);
fs::create_dir_all(root.join(".git")).unwrap();
let git = normalize_git_url(repo.to_string_lossy().as_ref()).unwrap();
fs::write(
root.join(MANIFEST),
format!(
r#"
[package]
name = "workspace"
version = "0.1.0"
[dependencies]
acme-lib = {{ git = "{git}", tag = "v1.0.0" }}
"#
),
)
.unwrap();
install_packages_in(workspace.env(), false, None, false).unwrap();
fs::write(
current_packages_dir(root).join("acme-lib").join("lib.harn"),
"pub fn value() -> string { return \"stale\" }\n",
)
.unwrap();
let ctx = workspace.env().load_manifest_context().unwrap();
let lock = LockFile::load(&ctx.lock_path()).unwrap().unwrap();
let workspace_env = workspace.env().clone();
let handles = (0..8)
.map(|_| {
let workspace_env = workspace_env.clone();
let ctx = ctx.clone();
let lock = lock.clone();
std::thread::spawn(move || {
materialize_dependencies_from_lock(&workspace_env, &ctx, &lock, None, false)
})
})
.collect::<Vec<_>>();
for handle in handles {
handle.join().unwrap().unwrap();
}
let materialized =
fs::read_to_string(current_packages_dir(root).join("acme-lib").join("lib.harn")).unwrap();
assert!(materialized.contains("return \"v1\""));
}
#[test]
fn materialization_rejects_lock_alias_path_traversal_before_removing_paths() {
let tmp = tempfile::tempdir().unwrap();
let dep = tmp.path().join("dep");
fs::create_dir_all(&dep).unwrap();
fs::write(dep.join("lib.harn"), "pub fn dep() { 1 }\n").unwrap();
let victim = tmp.path().join("victim");
fs::create_dir_all(&victim).unwrap();
fs::write(victim.join("keep.txt"), "keep").unwrap();
let manifest: Manifest = toml::from_str("[package]\nname = \"root\"\n").unwrap();
let ctx = ManifestContext {
manifest,
dir: tmp.path().to_path_buf(),
};
let workspace = TestWorkspace::new(tmp.path());
let lock = LockFile {
packages: vec![LockEntry {
name: "../victim".to_string(),
source: path_source_uri(&dep).unwrap(),
..LockEntry::default()
}],
..LockFile::default()
};
let error =
materialize_dependencies_from_lock(workspace.env(), &ctx, &lock, None, false).unwrap_err();
assert!(error.to_string().contains("invalid dependency alias"));
assert!(
victim.join("keep.txt").exists(),
"malicious alias should not remove paths outside the materialization root"
);
}
#[test]
fn read_only_materialization_accepts_v4_git_hashes_without_rewriting_the_lock() {
let (_repo_tmp, repo, _branch) = create_git_package_repo();
let project_tmp = tempfile::tempdir().unwrap();
let root = project_tmp.path();
let workspace = TestWorkspace::new(root);
fs::create_dir_all(root.join(".git")).unwrap();
let git = normalize_git_url(repo.to_string_lossy().as_ref()).unwrap();
fs::write(
root.join(MANIFEST),
format!(
r#"
[package]
name = "workspace"
version = "0.1.0"
[dependencies]
acme-lib = {{ git = "{git}", tag = "v1.0.0" }}
"#
),
)
.unwrap();
install_packages_in(workspace.env(), false, None, false).unwrap();
let lock_path = root.join(LOCK_FILE);
let current = LockFile::load(&lock_path).unwrap().unwrap();
let entry = current.find("acme-lib").unwrap();
let canonical_hash = entry.content_hash.as_deref().unwrap();
let cache_dir = git_cache_dir_in(
workspace.env(),
&entry.source,
entry.commit.as_deref().unwrap(),
)
.unwrap();
let v4_hash = compute_archive_content_hash(&cache_dir).unwrap();
let v4_lock = fs::read_to_string(&lock_path)
.unwrap()
.replace("version = 5", "version = 4")
.replace(canonical_hash, &v4_hash);
fs::write(&lock_path, &v4_lock).unwrap();
ensure_dependencies_materialized_in(workspace.env(), root).unwrap();
assert_eq!(fs::read_to_string(&lock_path).unwrap(), v4_lock);
assert!(current_packages_dir(root)
.join("acme-lib")
.join("lib.harn")
.is_file());
let snapshot = harn_modules::package_snapshot::PackageSnapshot::acquire(root)
.unwrap()
.unwrap();
let runtime_lock = fs::read_to_string(snapshot.lock_path()).unwrap();
assert!(runtime_lock.contains(canonical_hash));
assert!(!runtime_lock.contains(&v4_hash));
}