feat: publish Dever core with project scaffolding and versioned AI skills

This commit is contained in:
Antigravity committed 2026-10-04 13:37:30 +00:00
1 parent 70fa73cdc2
commit ac458ffa7f
960 files changed
+221319 -1163

No files matched your search

+468
View File
@@ -0,0 +1,468 @@
use super::*;
use dever_cli::libs::build::{Inputs, Session, pack};
use serde::{Deserialize, Serialize};
#[path = "../../dever-tests/tests/support/sandbox.rs"]
pub(super) mod sandbox;
#[derive(Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
struct Settings {
runtime: RegistryRuntime,
runtime_file: String,
build: pack::Descriptor,
build_file: String,
}
struct AuthorInputs {
root: PathBuf,
settings: Settings,
}
#[test]
fn signed_build_pack_binds_runtime_layout_tools_and_archive_digest() {
let runtime = registry_runtime(Ecosystem::Pip);
let mut descriptor = pack::Descriptor {
format: "dever-registry-build-v1".into(),
ecosystem: Ecosystem::Pip,
target: runtime.target.clone(),
runtime_sha256: runtime.pack.sha256.clone(),
python_runtime_sha256: None,
sha256: "0".repeat(64),
};
descriptor.validate(&runtime, None).unwrap();
let manifest = pack::Manifest {
format: "dever-lib-build-v1".into(),
ecosystem: Ecosystem::Pip,
target: runtime.target.clone(),
runtime_sha256: runtime.pack.sha256.clone(),
python_runtime_sha256: None,
executables: ["sh", "cc", "c++", "as", "ld", "ar", "ranlib", "make"]
.into_iter()
.map(|name| format!("rootfs/usr/bin/{name}"))
.collect(),
};
let mut files = manifest
.executables
.iter()
.map(|path| (path.clone(), b"#!/bin/sh\nexit 0\n".to_vec()))
.collect::<Vec<_>>();
files.push((
"dever-build.json".into(),
serde_json::to_vec(&manifest).unwrap(),
));
manifest.validate(&files, &descriptor).unwrap();
let mut changed = descriptor.clone();
changed.runtime_sha256 = "f".repeat(64);
assert!(changed.validate(&runtime, None).is_err());
assert!(manifest.validate(&files, &changed).is_err());
let mut changed = files.clone();
changed.push(("rootfs/etc/ld.so.preload".into(), b"injected".to_vec()));
assert!(manifest.validate(&changed, &descriptor).is_err());
let mut changed = manifest.clone();
changed.executables.push("rootfs/usr/bin/absent".into());
assert!(changed.validate(&files, &descriptor).is_err());
let mut archive = tar::Builder::new(flate2::write::GzEncoder::new(
Vec::new(),
flate2::Compression::default(),
));
for (path, bytes) in files {
let mut header = tar::Header::new_gnu();
header.set_size(bytes.len() as u64);
header.set_mode(0o644);
header.set_cksum();
archive
.append_data(&mut header, path, bytes.as_slice())
.unwrap();
}
let bytes = archive.into_inner().unwrap().finish().unwrap();
descriptor.sha256 = digest(&bytes);
pack::unpack(&bytes, &descriptor).unwrap();
let mut changed = bytes;
changed[0] ^= 1;
assert!(pack::unpack(&changed, &descriptor).is_err());
}
impl Inputs for AuthorInputs {
fn diagnostic(&self, output: &[u8]) {
eprintln!("{}", String::from_utf8_lossy(output));
}
fn runtime(&self, ecosystem: Ecosystem) -> Result<(RegistryRuntime, Vec<u8>), String> {
if ecosystem != Ecosystem::Pip {
return Err("fixture only supplies Python".into());
}
let bytes = fs::read(self.root.join(&self.settings.runtime_file))
.map_err(|error| error.to_string())?;
if digest(&bytes) != self.settings.runtime.pack.sha256 {
return Err("author runtime drift".into());
}
Ok((self.settings.runtime.clone(), bytes))
}
fn tools(&self, ecosystem: Ecosystem) -> Result<(pack::Descriptor, Vec<u8>), String> {
if ecosystem != Ecosystem::Pip {
return Err("fixture only supplies Python".into());
}
Ok((
self.settings.build.clone(),
fs::read(self.root.join(&self.settings.build_file))
.map_err(|error| error.to_string())?,
))
}
fn assets(&self) -> Result<Vec<(String, Vec<u8>)>, String> {
let workspace = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..");
Ok(sandbox::assets(&workspace)
.into_iter()
.map(|asset| {
let _mode = asset.executable;
(
asset.path.trim_start_matches("sandbox/").into(),
asset.bytes,
)
})
.collect())
}
}
#[test]
#[ignore = "explicit author operation: assembles previously hashed native-build-inputs; no network or host execution"]
fn prepare_source_build_tool_fixture() {
let workspace = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..");
let runtime: RegistryRuntime = serde_json::from_value(
serde_json::from_slice::<serde_json::Value>(
&fs::read(workspace.join("target/python-managed/config/setting.json")).unwrap(),
)
.unwrap()["runtime"]
.clone(),
)
.unwrap();
let bytes = fs::read(workspace.join("target/python-managed/runtime.pack")).unwrap();
assert_eq!(digest(&bytes), runtime.pack.sha256);
drop(bytes);
let root = workspace.join("target/source-build");
let descriptor = prepare_tool_pack(Ecosystem::Pip, &runtime, None, &root);
fs::write(
root.join("config/setting.json"),
serde_json::to_vec_pretty(&Settings {
runtime,
runtime_file: "../python-managed/runtime.pack".into(),
build: descriptor,
build_file: "build.pack".into(),
})
.unwrap(),
)
.unwrap();
}
pub(super) fn prepare_tool_pack(
ecosystem: Ecosystem,
runtime: &RegistryRuntime,
python: Option<&RegistryRuntime>,
root: &std::path::Path,
) -> pack::Descriptor {
let workspace = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..");
let inputs = workspace.join("target/native-build-inputs");
let document: serde_json::Value =
serde_json::from_slice(&fs::read(inputs.join("inputs.json")).unwrap()).unwrap();
let mut descriptor = pack::Descriptor {
format: "dever-registry-build-v1".into(),
ecosystem: ecosystem.clone(),
target: runtime.target.clone(),
runtime_sha256: runtime.pack.sha256.clone(),
python_runtime_sha256: python.map(|runtime| runtime.pack.sha256.clone()),
sha256: "0".repeat(64),
};
descriptor.validate(runtime, python).unwrap();
let manifest = pack::Manifest {
format: "dever-lib-build-v1".into(),
ecosystem: ecosystem.clone(),
target: runtime.target.clone(),
runtime_sha256: runtime.pack.sha256.clone(),
python_runtime_sha256: descriptor.python_runtime_sha256.clone(),
executables: serde_json::from_value(document[ecosystem.as_str()]["executables"].clone())
.unwrap(),
};
let mut files: Vec<(String, Vec<u8>)> = vec![(
"dever-build.json".into(),
serde_json::to_vec(&manifest).unwrap(),
)];
for input in document[ecosystem.as_str()]["files"].as_array().unwrap() {
let source = inputs.join(input["source"].as_str().unwrap());
assert!(fs::symlink_metadata(&source).unwrap().is_file());
let bytes = fs::read(&source).unwrap();
assert_eq!(digest(&bytes), input["sha256"].as_str().unwrap());
files.push((input["path"].as_str().unwrap().into(), bytes));
}
files.sort_by(|left, right| left.0.cmp(&right.0));
manifest.validate(&files, &descriptor).unwrap();
let assets = sandbox::assets(&workspace)
.into_iter()
.map(|asset| {
(
asset.path.trim_start_matches("sandbox/").into(),
asset.bytes,
)
})
.collect::<Vec<_>>();
pack::validate_tools(&manifest, &files, &assets).unwrap();
fs::create_dir_all(root.join("config")).unwrap();
let encoder = flate2::GzBuilder::new()
.mtime(0)
.operating_system(255)
.write(
fs::File::create(root.join("build.pack")).unwrap(),
flate2::Compression::default(),
);
let mut archive = tar::Builder::new(encoder);
for (path, bytes) in files {
let mut header = tar::Header::new_gnu();
header.set_size(bytes.len() as u64);
header.set_mode(if manifest.executables.contains(&path) {
0o755
} else {
0o644
});
header.set_uid(0);
header.set_gid(0);
header.set_mtime(0);
header.set_cksum();
archive
.append_data(&mut header, path, bytes.as_slice())
.unwrap();
}
archive.into_inner().unwrap().finish().unwrap();
descriptor.sha256 = digest(&fs::read(root.join("build.pack")).unwrap());
descriptor
}
struct SourceOnly(dever_cli::libs::HttpRegistry);
#[test]
fn incompatible_sdist_candidate_backtracks_before_loading_build_tools() {
let mut registry = LocalRegistry(BTreeMap::new());
python_release(&mut registry, "app", "1.0.0", &[], &["backend>=1"]);
python_release(&mut registry, "backend", "1.0.0", &[], &[]);
python_release(&mut registry, "backend", "2.0.0", &[], &[]);
let path = (Ecosystem::Pip, "/pypi/backend/2.0.0/json".into());
let mut release: serde_json::Value = serde_json::from_slice(&registry.0[&path]).unwrap();
release["urls"] = serde_json::json!([{"packagetype":"sdist","requires_python":">=3.13","filename":"backend-2.0.0.tar.gz","url":"must-not-fetch","digests":{"sha256":"0".repeat(64)}}]);
registry
.0
.insert(path, serde_json::to_vec(&release).unwrap());
let lock = python_lock(&registry, &["pip:app@1.0.0"]).unwrap();
assert!(
lock.libs
.iter()
.any(|lib| lib.spec.key() == "pip:backend@1.0.0")
);
}
fn backend_fixture(mutate: bool) -> LocalRegistry {
let mut registry = LocalRegistry(BTreeMap::new());
python_release(&mut registry, "buildhelper", "1.0.0", &[], &[]);
registry.0.insert(
(Ecosystem::Pip, "/pypi/sample/json".into()),
serde_json::to_vec(&serde_json::json!({"releases":{"1.0.0":[]}})).unwrap(),
);
let helper = wheel_fixture::pack(
"buildhelper",
"1.0.0",
"Wheel-Version: 1.0\nRoot-Is-Purelib: true\nTag: py3-none-any\n",
&wheel_fixture::metadata("buildhelper", "1.0.0", &[], &[]),
&[
(
"buildhelper.py",
b"def cli():\n print('isolated-cli')\n".to_vec(),
),
(
"buildhelper-1.0.0.dist-info/entry_points.txt",
b"[console_scripts]\nbuild-probe = buildhelper:cli\n".to_vec(),
),
],
);
let key = (Ecosystem::Pip, "/pypi/buildhelper/1.0.0/json".into());
let mut release: serde_json::Value = serde_json::from_slice(&registry.0[&key]).unwrap();
let url = release["urls"][0]["url"].as_str().unwrap().to_owned();
release["urls"][0]["digests"]["sha256"] = serde_json::json!(digest(&helper));
registry
.0
.insert(key, serde_json::to_vec(&release).unwrap());
registry.0.insert((Ecosystem::Pip, url), helper);
let original = wheel_fixture::metadata("sample", "1.0.0", &[], &[]);
let changed = format!("{original}Summary: changed by wheel hook\n");
let wheel = wheel_fixture::pack(
"sample",
"1.0.0",
"Wheel-Version: 1.0\nRoot-Is-Purelib: true\nTag: py3-none-any\n",
if mutate { &changed } else { &original },
&[],
);
let backend = format!(
r#"import os, shutil, subprocess
from pathlib import Path
def get_requires_for_build_wheel(config_settings=None):
try:
import buildhelper
return []
except ImportError:
return ['buildhelper==1.0.0']
def prepare_metadata_for_build_wheel(directory, config_settings=None):
assert subprocess.check_output(['build-probe'], text=True).strip() == 'isolated-cli'
metadata = Path(directory) / 'sample-1.0.0.dist-info'
metadata.mkdir()
shutil.copy('original-metadata', metadata / 'METADATA')
os.chdir('/')
return metadata.name
def build_wheel(directory, config_settings=None, metadata_directory=None):
assert Path.cwd().name == 'source', 'previous hook leaked its cwd'
if {mutate}:
shutil.copy('changed-metadata', Path(metadata_directory) / 'METADATA')
name = 'sample-1.0.0-py3-none-any.whl'
shutil.copy('result.whl', Path(directory) / name)
return name
"#,
mutate = if mutate { "True" } else { "False" }
);
let sourcefiles = vec![
(
"sample-1.0.0/pyproject.toml",
b"[build-system]\nrequires = []\nbuild-backend = 'backend'\nbackend-path = ['.']\n"
.to_vec(),
),
("sample-1.0.0/backend.py", backend.into_bytes()),
("sample-1.0.0/original-metadata", original.into_bytes()),
("sample-1.0.0/changed-metadata", changed.into_bytes()),
("sample-1.0.0/result.whl", wheel),
];
let mut tar = tar::Builder::new(flate2::write::GzEncoder::new(
Vec::new(),
flate2::Compression::default(),
));
for (name, bytes) in sourcefiles {
let mut header = tar::Header::new_gnu();
header.set_size(bytes.len() as u64);
header.set_mode(0o644);
header.set_cksum();
tar.append_data(&mut header, name, bytes.as_slice())
.unwrap();
}
let source = tar.into_inner().unwrap().finish().unwrap();
registry.0.insert((Ecosystem::Pip,"/pypi/sample/1.0.0/json".into()),serde_json::to_vec(&serde_json::json!({"info":{},"urls":[{"packagetype":"sdist","filename":"sample-1.0.0.tar.gz","url":"sample-source","digests":{"sha256":digest(&source)}}]})).unwrap());
registry
.0
.insert((Ecosystem::Pip, "sample-source".into()), source);
registry
}
#[test]
#[ignore = "isolated offline PEP517 contracts; requires explicit target/source-build tool/runtime fixture"]
fn pep517_hooks_keep_requirements_cli_and_metadata_boundaries() {
let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../target/source-build");
let settings: Settings =
serde_json::from_slice(&fs::read(root.join("config/setting.json")).unwrap()).unwrap();
let inputs = AuthorInputs { root, settings };
for mutate in [false, true] {
let session = Session::new(&inputs);
let transport = backend_fixture(mutate);
let store = FixtureArtifactStore::default();
let resolver = RegistryResolver {
transport: &transport,
store: &store,
runtimes: BTreeMap::from([(Ecosystem::Pip, inputs.settings.runtime.clone())]),
go_sumdb: dever_cli::libs::sumdb::ChecksumDatabase::official(None).unwrap(),
build: Some(&session),
};
let result = dever_cli::libs::LibResolver::resolve(
&resolver,
&["pip:sample@1.0.0".parse().unwrap()],
);
if mutate {
assert!(result.unwrap_err().contains("metadata differs"));
continue;
}
let lock = result.unwrap();
dever_cli::libs::doctor(&lock).unwrap();
assert_eq!(lock.builds[0].dynamic_requires, vec!["buildhelper==1.0.0"]);
for mutation in 0..3 {
let mut altered = lock.clone();
match mutation {
0 => altered.builds[0].tools.sha256 = "not-a-hash".into(),
1 => altered.builds[0].dynamic_requires = vec!["buildhelper>=2".into()],
_ => *altered.builds[0].dependencies = LockFile::new(vec![]).unwrap(),
}
altered.libs[0].build = Some(altered.builds[0].identity().unwrap());
assert!(
dever_cli::libs::doctor(&altered).is_err(),
"accepted contradictory receipt {mutation}"
);
}
}
}
impl RegistryTransport for SourceOnly {
fn get(&self, ecosystem: Ecosystem, path: &str, limit: usize) -> Result<Vec<u8>, String> {
let bytes = self.0.get(ecosystem, path, limit)?;
// Explicit author acceptance forces this one package's official sdist.
// Backend requirements still use their ordinary authenticated wheels.
if path == "/pypi/simplejson/3.20.1/json" {
let mut document: serde_json::Value =
serde_json::from_slice(&bytes).map_err(|error| error.to_string())?;
document["urls"]
.as_array_mut()
.ok_or("official release lacks files")?
.retain(|file| file["packagetype"] == "sdist");
serde_json::to_vec(&document).map_err(|error| error.to_string())
} else {
Ok(bytes)
}
}
}
#[test]
#[ignore = "explicit official PyPI source build: downloads fixed source/build requirements and executes signed managed build tools"]
fn builds_official_python_source_with_isolated_pep517() {
let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../target/source-build");
let settings: Settings =
serde_json::from_slice(&fs::read(root.join("config/setting.json")).unwrap()).unwrap();
let inputs = AuthorInputs {
root: root.clone(),
settings,
};
let session = Session::new(&inputs);
let transport = SourceOnly(dever_cli::libs::HttpRegistry::official());
let store = FixtureArtifactStore::default();
let resolver = RegistryResolver {
transport: &transport,
store: &store,
runtimes: BTreeMap::from([(Ecosystem::Pip, inputs.settings.runtime.clone())]),
go_sumdb: dever_cli::libs::sumdb::ChecksumDatabase::official(None).unwrap(),
build: Some(&session),
};
let requested = vec!["pip:simplejson@3.20.1".parse().unwrap()];
let lock = dever_cli::libs::LibResolver::resolve(&resolver, &requested).unwrap();
dever_cli::libs::doctor(&lock).unwrap();
assert_eq!(lock.builds.len(), 1);
assert!(!lock.builds[0].dependencies.libs.is_empty());
let artifact = &lock
.libs
.iter()
.find(|lib| lib.spec.name == "simplejson")
.unwrap()
.artifacts[0];
let wheel = store
.verify_exact(&artifact.sha256, artifact.bytes, &artifact.target)
.unwrap();
let mut archive = zip::ZipArchive::new(Cursor::new(wheel)).unwrap();
assert!((0..archive.len()).any(|index| {
let file = archive.by_index(index).unwrap();
file.name().contains("_speedups.") && file.name().ends_with(".so")
}));
assert_eq!(
lock,
dever_cli::libs::LibResolver::resolve(&resolver, &requested).unwrap(),
"same session must replay one exact built artifact"
);
publish_fixture_artifacts(&root.join("dependency"), &lock, &store);
let mut altered = lock.clone();
altered.builds[0].frontend = "0".repeat(64);
assert!(dever_cli::libs::doctor(&altered).is_err());
}
+691
View File
@@ -0,0 +1,691 @@
use super::*;
use dever_cli::libs::{LibResolver, LockedWorker, npm, resolve_workers};
pub(super) fn archive(
name: &str,
version: &str,
mut manifest: serde_json::Value,
extra: &[(&str, Vec<u8>)],
) -> Vec<u8> {
manifest["name"] = name.into();
manifest["version"] = version.into();
let encoder = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::default());
let mut writer = tar::Builder::new(encoder);
for (name, bytes) in [
(
"package/package.json",
serde_json::to_vec(&manifest).unwrap(),
),
(
"package/index.js",
format!("module.exports = '{name}/{version}';\n").into_bytes(),
),
]
.into_iter()
.chain(extra.iter().map(|(name, bytes)| (*name, bytes.clone())))
{
let mut header = tar::Header::new_gnu();
header.set_size(bytes.len() as u64);
header.set_mode(0o644);
header.set_cksum();
writer
.append_data(&mut header, name, bytes.as_slice())
.unwrap();
}
writer.into_inner().unwrap().finish().unwrap()
}
pub(super) fn release(
registry: &mut LocalRegistry,
name: &str,
version: &str,
manifest: serde_json::Value,
extra: &[(&str, Vec<u8>)],
) {
let archive = archive(name, version, manifest.clone(), extra);
let index_path = (Ecosystem::Npm, format!("/{name}"));
let mut index = registry
.0
.get(&index_path)
.map(|bytes| serde_json::from_slice::<serde_json::Value>(bytes).unwrap())
.unwrap_or(serde_json::json!({"versions":{}}));
index["versions"][version] = serde_json::json!({});
registry
.0
.insert(index_path, serde_json::to_vec(&index).unwrap());
let mut document = manifest;
document["name"] = name.into();
document["version"] = version.into();
let url = format!("https://mock/{name}/{version}.tgz");
document["dist"] = serde_json::json!({"tarball":url,"integrity":format!("sha512-{}", base64::engine::general_purpose::STANDARD.encode(Sha512::digest(&archive)))});
registry.0.insert(
(Ecosystem::Npm, format!("/{name}/{version}")),
serde_json::to_vec(&document).unwrap(),
);
registry.0.insert((Ecosystem::Npm, url), archive);
}
fn resolved(registry: &LocalRegistry, requested: &[&str]) -> (LockFile, FixtureArtifactStore) {
let store = FixtureArtifactStore::default();
let lock = local_resolver(registry, &store)
.resolve(
&requested
.iter()
.map(|spec| spec.parse().unwrap())
.collect::<Vec<_>>(),
)
.unwrap();
doctor(&lock).unwrap();
assert_eq!(LockFile::decode(&lock.encode().unwrap()).unwrap(), lock);
(lock, store)
}
fn files(lock: &LockFile, store: &FixtureArtifactStore, index: usize) -> BTreeMap<String, Vec<u8>> {
let archives = lock
.libs
.iter()
.map(|lib| {
(
lib.spec.clone(),
store
.verify_exact(
&lib.artifacts[0].sha256,
lib.artifacts[0].bytes,
&lib.artifacts[0].target,
)
.unwrap(),
)
})
.collect::<BTreeMap<_, _>>();
npm::files(&lock.npm[index], lock, |lib| {
Ok(archives[&lib.spec].as_slice())
})
.unwrap()
.into_iter()
.collect()
}
#[test]
fn nested_versions_and_shared_packages_keep_distinct_peer_contexts() {
let mut registry = LocalRegistry(BTreeMap::new());
for (name, dependencies) in [
("left", serde_json::json!({"plugin":"1.0.0","host":"1.0.0"})),
(
"right",
serde_json::json!({"plugin":"1.0.0","host":"2.0.0"}),
),
] {
release(
&mut registry,
name,
"1.0.0",
serde_json::json!({"dependencies":dependencies}),
&[],
);
}
release(
&mut registry,
"plugin",
"1.0.0",
serde_json::json!({"peerDependencies":{"host":">=1.0.0 <3.0.0"}}),
&[],
);
for version in ["1.0.0", "2.0.0"] {
release(&mut registry, "host", version, serde_json::json!({}), &[]);
}
let (lock, store) = resolved(&registry, &["npm:left@1.0.0", "npm:right@1.0.0"]);
assert_eq!(lock.libs.len(), 5);
let files = files(&lock, &store, 0);
for (side, version) in [("left", "1.0.0"), ("right", "2.0.0")] {
let node = lock.npm[0]
.instances
.iter()
.find(|node| node.path == format!("node_modules/{side}/node_modules/plugin"))
.unwrap();
assert_eq!(
node.edges[0].target.as_deref(),
Some(format!("node_modules/{side}/node_modules/host").as_str())
);
assert!(
String::from_utf8(
files[&format!("node_modules/{side}/node_modules/host/index.js")].clone()
)
.unwrap()
.contains(version)
);
}
}
#[test]
fn optional_dependencies_override_regular_edges_and_record_target_or_absence() {
let mut registry = LocalRegistry(BTreeMap::new());
release(
&mut registry,
"root",
"1.0.0",
serde_json::json!({"dependencies":{"child":"1.0.0"},"optionalDependencies":{"child":"2.0.0","missing":"*","darwin":"*","musl":"*"}}),
&[],
);
release(&mut registry, "child", "2.0.0", serde_json::json!({}), &[]);
release(
&mut registry,
"darwin",
"1.0.0",
serde_json::json!({"os":["darwin"]}),
&[],
);
release(
&mut registry,
"musl",
"1.0.0",
serde_json::json!({"os":["linux"],"libc":["musl"]}),
&[],
);
let (lock, store) = resolved(&registry, &["npm:root@1.0.0"]);
assert_eq!(lock.libs.len(), 2);
let root = lock.npm[0]
.instances
.iter()
.find(|node| node.spec.name == "root")
.unwrap();
assert_eq!(
root.edges
.iter()
.find(|edge| edge.name == "child")
.unwrap()
.kind,
npm::Kind::Optional
);
for name in ["darwin", "musl"] {
assert_eq!(
root.edges
.iter()
.find(|edge| edge.name == name)
.unwrap()
.omission,
Some(npm::Omission::Target)
);
}
assert_eq!(
root.edges
.iter()
.find(|edge| edge.name == "missing")
.unwrap()
.omission,
Some(npm::Omission::Unavailable)
);
assert!(
!files(&lock, &store, 0)
.keys()
.any(|path| path.contains("darwin") || path.contains("musl"))
);
}
#[test]
fn optional_integrity_and_protocol_failures_are_never_omissions() {
let mut registry = LocalRegistry(BTreeMap::new());
release(
&mut registry,
"root",
"1.0.0",
serde_json::json!({"optionalDependencies":{"child":"*"}}),
&[],
);
release(&mut registry, "child", "1.0.0", serde_json::json!({}), &[]);
registry.0.insert(
(Ecosystem::Npm, "https://mock/child/1.0.0.tgz".into()),
b"tampered".to_vec(),
);
let store = FixtureArtifactStore::default();
assert!(
local_resolver(&registry, &store)
.resolve(&["npm:root@1.0.0".parse().unwrap()])
.unwrap_err()
.contains("integrity mismatch")
);
release(
&mut registry,
"child",
"1.0.0",
serde_json::json!({"dependencies":{"other":"not-a-semver"}}),
&[],
);
assert!(
local_resolver(&registry, &store)
.resolve(&["npm:root@1.0.0".parse().unwrap()])
.unwrap_err()
.contains("invalid npm range")
);
}
#[test]
fn required_peers_auto_install_and_backtrack_but_optional_peers_do_not() {
let mut registry = LocalRegistry(BTreeMap::new());
release(
&mut registry,
"root",
"1.0.0",
serde_json::json!({"dependencies":{"plugin":"*","host":"1.0.0"}}),
&[],
);
release(
&mut registry,
"plugin",
"2.0.0",
serde_json::json!({"peerDependencies":{"host":"2.0.0"}}),
&[],
);
release(
&mut registry,
"plugin",
"1.0.0",
serde_json::json!({"peerDependencies":{"host":"1.0.0","absent":"*"},"peerDependenciesMeta":{"absent":{"optional":true}}}),
&[],
);
for version in ["1.0.0", "2.0.0"] {
release(&mut registry, "host", version, serde_json::json!({}), &[]);
}
let (lock, _) = resolved(&registry, &["npm:root@1.0.0"]);
let plugin = lock.npm[0]
.instances
.iter()
.find(|node| node.spec.name == "plugin")
.unwrap();
assert_eq!(plugin.spec.version, "1.0.0");
assert_eq!(
plugin
.edges
.iter()
.find(|edge| edge.name == "absent")
.unwrap()
.omission,
Some(npm::Omission::OptionalPeerAbsent)
);
let (lock, _) = resolved(&registry, &["npm:plugin@1.0.0"]);
assert!(
lock.npm[0]
.instances
.iter()
.any(|node| node.path == "node_modules/host")
);
let store = FixtureArtifactStore::default();
assert!(
local_resolver(&registry, &store)
.resolve(&[
"npm:plugin@2.0.0".parse().unwrap(),
"npm:host@1.0.0".parse().unwrap()
])
.is_err()
);
}
#[test]
fn bundled_transitive_packages_are_bound_to_parent_and_never_downloaded() {
let mut registry = LocalRegistry(BTreeMap::new());
let extra = vec![
(
"package/node_modules/b/package.json",
br#"{"name":"b","version":"1.0.0","dependencies":{"c":"2.0.0"}}"#.to_vec(),
),
(
"package/node_modules/b/index.js",
b"module.exports = require('c');".to_vec(),
),
(
"package/node_modules/c/package.json",
br#"{"name":"c","version":"2.0.0"}"#.to_vec(),
),
(
"package/node_modules/c/index.js",
b"module.exports = 42;".to_vec(),
),
];
release(
&mut registry,
"root",
"1.0.0",
serde_json::json!({"dependencies":{"b":"1.0.0"},"bundleDependencies":["b"]}),
&extra,
);
let (lock, store) = resolved(&registry, &["npm:root@1.0.0"]);
assert_eq!(lock.libs.len(), 1);
assert_eq!(lock.npm[0].instances.len(), 3);
assert_eq!(
files(&lock, &store, 0)["node_modules/root/node_modules/c/index.js"],
b"module.exports = 42;"
);
release(
&mut registry,
"root",
"1.0.0",
serde_json::json!({}),
&extra,
);
assert!(
local_resolver(&registry, &store)
.resolve(&["npm:root@1.0.0".parse().unwrap()])
.unwrap_err()
.contains("undeclared")
);
}
#[test]
fn worker_root_sets_keep_separate_npm_environments_and_detect_lock_tampering() {
let mut registry = LocalRegistry(BTreeMap::new());
release(
&mut registry,
"plugin",
"1.0.0",
serde_json::json!({"peerDependencies":{"host":"*"}}),
&[],
);
for version in ["1.0.0", "2.0.0"] {
release(&mut registry, "host", version, serde_json::json!({}), &[]);
}
let store = FixtureArtifactStore::default();
let worker = |adapter: &str, version: &str| LockedWorker {
port: "sample.worker".into(),
adapter: adapter.into(),
ecosystem: "npm".into(),
runtime: Some(registry_runtime(Ecosystem::Npm).pack),
entry: format!("worker/{adapter}.mjs"),
schema: "checked-port".into(),
capabilities: vec![],
operations: vec!["read".into()],
libs: vec![
"npm:plugin@1.0.0".parse().unwrap(),
format!("npm:host@{version}").parse().unwrap(),
],
};
let lock = resolve_workers(
&local_resolver(&registry, &store),
&[],
vec![worker("first", "1.0.0"), worker("second", "2.0.0")],
)
.unwrap();
assert_eq!(lock.npm.len(), 2);
for index in 0..2 {
assert!(files(&lock, &store, index).contains_key("node_modules/plugin/index.js"));
}
let mut changed = lock.clone();
changed.npm[0].instances[0].path = "../escape".into();
assert!(doctor(&changed).is_err());
let mut changed = lock.clone();
changed.npm[0]
.instances
.iter_mut()
.find(|node| node.spec.name == "plugin")
.unwrap()
.edges[0]
.target = Some("node_modules/missing".into());
assert!(doctor(&changed).is_err());
let mut changed = lock;
changed.npm[0]
.instances
.iter_mut()
.find(|node| node.spec.name == "plugin")
.unwrap()
.edges
.clear();
let archives = changed
.libs
.iter()
.map(|lib| {
(
lib.spec.clone(),
store
.verify_exact(
&lib.artifacts[0].sha256,
lib.artifacts[0].bytes,
&lib.artifacts[0].target,
)
.unwrap(),
)
})
.collect::<BTreeMap<_, _>>();
assert!(
npm::files(&changed.npm[0], &changed, |lib| Ok(
archives[&lib.spec].as_slice()
))
.unwrap_err()
.contains("differs from package.json")
);
}
#[test]
fn bundled_instances_cannot_switch_to_another_parents_same_version_package() {
let mut registry = LocalRegistry(BTreeMap::new());
for name in ["a", "c"] {
release(
&mut registry,
name,
"1.0.0",
serde_json::json!({"dependencies":{"b":"1.0.0"},"bundleDependencies":["b"]}),
&[
(
"package/node_modules/b/package.json",
br#"{"name":"b","version":"1.0.0"}"#.to_vec(),
),
(
"package/node_modules/b/index.js",
format!("module.exports = '{name}';").into_bytes(),
),
],
);
}
release(&mut registry, "b", "1.0.0", serde_json::json!({}), &[]);
let (original, store) = resolved(&registry, &["npm:a@1.0.0", "npm:c@1.0.0", "npm:b@1.0.0"]);
let archives = original
.libs
.iter()
.map(|lib| {
(
lib.spec.clone(),
store
.verify_exact(
&lib.artifacts[0].sha256,
lib.artifacts[0].bytes,
&lib.artifacts[0].target,
)
.unwrap(),
)
})
.collect::<BTreeMap<_, _>>();
for (archive, prefix) in [
("npm:c@1.0.0", "package/node_modules/b/"),
("npm:b@1.0.0", "package/"),
] {
let mut changed = original.clone();
changed.npm[0]
.instances
.iter_mut()
.find(|node| node.path == "node_modules/a/node_modules/b")
.unwrap()
.source = npm::Source {
archive: archive.parse().unwrap(),
prefix: prefix.into(),
};
assert!(
doctor(&changed)
.unwrap_err()
.contains("parent archive installation")
);
assert!(
npm::files(&changed.npm[0], &changed, |lib| Ok(
archives[&lib.spec].as_slice()
))
.unwrap_err()
.contains("parent archive installation")
);
// Removing the locked layout cannot hide the original archive binding.
for node in &mut changed.npm[0].instances {
node.bundled.clear();
}
assert!(
npm::files(&changed.npm[0], &changed, |lib| Ok(
archives[&lib.spec].as_slice()
))
.is_err()
);
}
}
#[test]
fn production_and_optional_dependencies_override_same_named_peers() {
let mut registry = LocalRegistry(BTreeMap::new());
release(&mut registry, "host", "2.0.0", serde_json::json!({}), &[]);
release(
&mut registry,
"plugin",
"1.0.0",
serde_json::json!({"dependencies":{"host":"2.0.0"},"peerDependencies":{"host":"1.0.0"}}),
&[],
);
let (lock, _) = resolved(&registry, &["npm:plugin@1.0.0", "npm:host@2.0.0"]);
let plugin = lock.npm[0]
.instances
.iter()
.find(|node| node.spec.name == "plugin")
.unwrap();
assert_eq!(plugin.edges.len(), 1);
assert_eq!(plugin.edges[0].kind, npm::Kind::Dependency);
assert_eq!(
plugin.edges[0].target.as_deref(),
Some("node_modules/plugin/node_modules/host")
);
release(
&mut registry,
"plugin",
"1.0.0",
serde_json::json!({"optionalDependencies":{"absent":"*"},"peerDependencies":{"absent":"1.0.0"}}),
&[],
);
let (lock, _) = resolved(&registry, &["npm:plugin@1.0.0"]);
assert_eq!(lock.npm[0].instances[0].edges.len(), 1);
assert_eq!(lock.npm[0].instances[0].edges[0].kind, npm::Kind::Optional);
assert_eq!(
lock.npm[0].instances[0].edges[0].omission,
Some(npm::Omission::Unavailable)
);
}
#[test]
fn removing_a_root_prunes_its_entire_dependency_cycle() {
for bundled in [false, true] {
let mut registry = LocalRegistry(BTreeMap::new());
release(&mut registry, "z", "1.0.0", serde_json::json!({}), &[]);
let b = serde_json::json!({"name":"b","version":"1.0.0","dependencies":{"a":"1.0.0"}});
let mut a = serde_json::json!({"dependencies":{"b":"1.0.0"}});
let extra = if bundled {
a["bundleDependencies"] = serde_json::json!(["b"]);
vec![(
"package/node_modules/b/package.json",
serde_json::to_vec(&b).unwrap(),
)]
} else {
release(&mut registry, "b", "1.0.0", b, &[]);
vec![]
};
release(&mut registry, "a", "1.0.0", a, &extra);
let (lock, _) = resolved(&registry, &["npm:a@1.0.0", "npm:z@1.0.0"]);
let root = TemporaryDirectory::new();
fs::create_dir(root.path().join("config")).unwrap();
fs::write(
root.path().join("config/setting.json"),
br#"{"lib":["npm:a@1.0.0","npm:z@1.0.0"]}"#,
)
.unwrap();
lock.write_atomic(root.path()).unwrap();
dever_cli::libs::execute("remove", root.path(), &["npm:a@1.0.0".into()]).unwrap();
let retained =
LockFile::decode(&fs::read(root.path().join("dever.lock")).unwrap()).unwrap();
assert_eq!(retained.libs.len(), 1);
assert_eq!(retained.libs[0].spec.name, "z");
assert_eq!(retained.npm[0].instances.len(), 1);
doctor(&retained).unwrap();
}
}
#[test]
fn removing_a_dependency_still_required_by_a_remaining_root_is_rejected() {
let mut registry = LocalRegistry(BTreeMap::new());
release(
&mut registry,
"a",
"1.0.0",
serde_json::json!({"dependencies":{"b":"1.0.0"}}),
&[],
);
release(&mut registry, "b", "1.0.0", serde_json::json!({}), &[]);
let (lock, _) = resolved(&registry, &["npm:a@1.0.0", "npm:b@1.0.0"]);
let root = TemporaryDirectory::new();
fs::create_dir(root.path().join("config")).unwrap();
let setting = br#"{"lib":["npm:a@1.0.0","npm:b@1.0.0"]}"#;
fs::write(root.path().join("config/setting.json"), setting).unwrap();
lock.write_atomic(root.path()).unwrap();
let before = fs::read(root.path().join("dever.lock")).unwrap();
assert!(
dever_cli::libs::execute("remove", root.path(), &["npm:b@1.0.0".into()])
.unwrap_err()
.contains("required by")
);
assert_eq!(fs::read(root.path().join("dever.lock")).unwrap(), before);
assert_eq!(
fs::read(root.path().join("config/setting.json")).unwrap(),
setting
);
}
#[test]
fn repeated_archive_instances_are_bounded_before_materialization_clones() {
let mut registry = LocalRegistry(BTreeMap::new());
release(
&mut registry,
"leaf",
"1.0.0",
serde_json::json!({}),
&[("package/payload.js", vec![b'x'; 2 * 1024 * 1024])],
);
let mut roots = Vec::new();
for number in 0..129 {
let name = format!("root-{number}");
release(
&mut registry,
&name,
"1.0.0",
serde_json::json!({"dependencies":{"leaf":"1.0.0"}}),
&[],
);
roots.push(format!("npm:{name}@1.0.0"));
}
let (lock, store) = resolved(
&registry,
&roots.iter().map(String::as_str).collect::<Vec<_>>(),
);
assert_eq!(lock.npm[0].instances.len(), 258);
let archives = lock
.libs
.iter()
.map(|lib| {
(
lib.spec.clone(),
store
.verify_exact(
&lib.artifacts[0].sha256,
lib.artifacts[0].bytes,
&lib.artifacts[0].target,
)
.unwrap(),
)
})
.collect::<BTreeMap<_, _>>();
assert!(
npm::files(
&lock.npm[0],
&lock,
|lib| Ok(archives[&lib.spec].as_slice())
)
.unwrap_err()
.contains("tree file/byte budget")
);
}
@@ -0,0 +1,590 @@
use super::*;
use dever_cli::libs::build::{Inputs, NativeRejection, Receipt, Session, pack};
use dever_cli::libs::{LibResolver, npm};
use serde::{Deserialize, Serialize};
#[derive(Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
struct Settings {
runtime: RegistryRuntime,
runtime_file: String,
python: RegistryRuntime,
python_file: String,
build: pack::Descriptor,
build_file: String,
}
struct AuthorInputs {
root: PathBuf,
settings: Settings,
}
#[test]
fn npm_lifecycle_output_preserves_locked_identity_and_instance_ownership() {
let environment = npm::Environment::single("npm:app@1.0.0".parse().unwrap());
let files = vec![
(
"node_modules/app/package.json".into(),
br#"{"name":"app","version":"1.0.0"}"#.to_vec(),
),
(
"node_modules/app/generated.js".into(),
b"module.exports=42;".to_vec(),
),
];
npm::validate_built_files(&environment, &files).unwrap();
let mut changed = files.clone();
changed[0].1 =
br#"{"name":"app","version":"1.0.0","dependencies":{"injected":"1.0.0"}}"#.to_vec();
assert!(npm::validate_built_files(&environment, &changed).is_err());
let mut changed = files.clone();
changed.push(("node_modules/injected/index.js".into(), vec![]));
assert!(npm::validate_built_files(&environment, &changed).is_err());
let mut changed = files.clone();
changed.push(("node_modules/app/../escape".into(), vec![]));
assert!(npm::validate_built_files(&environment, &changed).is_err());
assert!(npm::validate_built_files(&environment, &files[1..]).is_err());
}
#[test]
fn npm_registry_install_hooks_do_not_run_publishing_lifecycle() {
let metadata = |script: &str| {
vec![(
"node_modules/app/package.json".into(),
serde_json::to_vec(&serde_json::json!({"scripts":{script:"node command.js"}})).unwrap(),
)]
};
for script in ["preinstall", "install", "postinstall"] {
assert!(npm::requires_build(&metadata(script)).unwrap());
}
for script in ["prepare", "prepack", "prepublish"] {
assert!(!npm::requires_build(&metadata(script)).unwrap());
}
assert!(
npm::requires_build(&[("node_modules/app/binding.gyp".into(), b"{}".to_vec())]).unwrap()
);
}
fn locked_installation_fixture() -> (LockFile, Vec<u8>) {
let mut registry = LocalRegistry(BTreeMap::new());
npm_tests::release(
&mut registry,
"app",
"1.0.0",
serde_json::json!({}),
&[(
"package/prebuilds/foreign/addon.node",
b"foreign-object".to_vec(),
)],
);
npm_tests::release(&mut registry, "other", "1.0.0", serde_json::json!({}), &[]);
let store = FixtureArtifactStore::default();
let mut lock = local_resolver(&registry, &store)
.resolve(&[
"npm:app@1.0.0".parse().unwrap(),
"npm:other@1.0.0".parse().unwrap(),
])
.unwrap();
let sources = lock
.libs
.iter()
.flat_map(|lib| lib.artifacts.clone())
.collect();
let archives = lock
.libs
.iter()
.map(|lib| {
(
lib.spec.clone(),
store
.verify_exact(
&lib.artifacts[0].sha256,
lib.artifacts[0].bytes,
&lib.artifacts[0].target,
)
.unwrap(),
)
})
.collect::<BTreeMap<_, _>>();
let files = npm::build_files(
&lock.npm[0],
&lock,
|lib| Ok(archives[&lib.spec].as_slice()),
)
.unwrap();
let mut writer = tar::Builder::new(flate2::write::GzEncoder::new(
Vec::new(),
flate2::Compression::default(),
));
for (path, bytes) in files {
let mut header = tar::Header::new_gnu();
header.set_size(bytes.len() as u64);
header.set_mode(0o644);
header.set_cksum();
writer
.append_data(&mut header, path, bytes.as_slice())
.unwrap();
}
let bytes = writer.into_inner().unwrap().finish().unwrap();
let runtime = registry_runtime(Ecosystem::Npm);
let python = registry_runtime(Ecosystem::Pip);
let hash = digest(&bytes);
let receipt = Receipt {
ecosystem: Ecosystem::Npm,
roots: lock.npm[0].roots.clone(),
sources,
graph: Some(lock.npm[0].clone()),
runtime: runtime.pack.clone(),
auxiliary_runtime: Some(python.pack.clone()),
native_entries: Default::default(),
native_rejections: BTreeMap::from([(
"node_modules/app/prebuilds/foreign/addon.node".into(),
NativeRejection::Format,
)]),
python_markers: None,
tools: pack::Descriptor {
format: "dever-registry-build-v1".into(),
ecosystem: Ecosystem::Npm,
target: runtime.target.clone(),
runtime_sha256: runtime.pack.sha256.clone(),
python_runtime_sha256: Some(python.pack.sha256),
sha256: "a".repeat(64),
},
frontend: digest(
&[
include_bytes!("../../../crates/dever-cli/src/libs/build/npm.js").as_slice(),
include_bytes!("../../../crates/dever-cli/src/libs/build/npm/probe.cjs").as_slice(),
]
.concat(),
),
python: None,
dynamic_requires: vec![],
dependencies: Box::new(LockFile::new(vec![]).unwrap()),
config_settings: BTreeMap::new(),
output: dever_cli::libs::LockedArtifact {
target: runtime.target,
path: format!("build/npm/{hash}/installation.tgz"),
bytes: bytes.len() as u64,
sha256: hash,
},
};
lock.npm[0].build = Some(receipt.identity().unwrap());
lock.builds.push(receipt);
doctor(&lock).unwrap();
(lock, bytes)
}
#[test]
fn npm_expanded_outputs_are_pruned_only_when_no_raw_consumer_remains() {
use dever_cli::libs::{LockedWorker, prune_expanded_resources};
use dever_core::native::EmbeddedResource;
let (mut lock, bytes) = locked_installation_fixture();
let roots = lock.npm[0].roots.clone();
let other = vec!["npm:other@1.0.0".parse().unwrap()];
lock.npm = npm::retain(&lock, &[roots.clone(), other.clone()]).unwrap();
let output = &lock.builds[0].output;
let managed = LockedWorker {
port: "sample.worker".into(),
adapter: "default".into(),
ecosystem: "npm".into(),
runtime: Some(registry_runtime(Ecosystem::Npm).pack),
entry: "worker/main.cjs".into(),
schema: "checked".into(),
capabilities: vec![],
operations: vec!["probe".into()],
libs: roots.clone(),
};
let resource = EmbeddedResource {
path: output.path.clone(),
bytes,
sha256: output.sha256.clone(),
executable: false,
};
let mut resources = vec![resource.clone()];
prune_expanded_resources(&lock, std::slice::from_ref(&managed), &[], &mut resources).unwrap();
assert!(
resources.is_empty(),
"expanded installation archive leaked into program"
);
// A projected environment shares its original build output with the full
// Worker. Retention follows current consumers, not the receipt's old roots.
for declared in [roots, other] {
let mut resources = vec![resource.clone()];
prune_expanded_resources(
&lock,
std::slice::from_ref(&managed),
&declared,
&mut resources,
)
.unwrap();
assert_eq!(resources.len(), 1);
}
let mut exec = managed.clone();
exec.ecosystem = "exec".into();
exec.runtime = None;
let mut resources = vec![resource];
prune_expanded_resources(&lock, &[managed, exec], &[], &mut resources).unwrap();
assert_eq!(resources.len(), 1);
}
#[test]
fn npm_native_admission_accounts_every_addon_and_prunes_unsupported_objects() {
let (lock, bytes) = locked_installation_fixture();
let files = dever_cli::libs::build::npm_installation(&lock, &lock.npm[0], &bytes).unwrap();
assert!(files.iter().all(|(path, _, _)| !path.ends_with(".node")));
let mut missing = lock.clone();
missing.builds[0].native_rejections.clear();
missing.npm[0].build = Some(missing.builds[0].identity().unwrap());
assert!(
dever_cli::libs::build::npm_installation(&missing, &missing.npm[0], &bytes)
.unwrap_err()
.contains("exact addon files")
);
let mut corrupted = bytes.clone();
corrupted[10] ^= 1;
assert!(dever_cli::libs::build::npm_installation(&lock, &lock.npm[0], &corrupted).is_err());
for change in ["tools", "graph", "runtime"] {
let mut changed = lock.clone();
match change {
"tools" => changed.builds[0].tools.sha256 = "not-a-hash".into(),
"graph" => changed.builds[0].graph.as_mut().unwrap().instances.clear(),
_ => changed.builds[0].runtime.sha256 = "b".repeat(64),
}
changed.npm[0].build = Some(changed.builds[0].identity().unwrap());
assert!(
doctor(&changed).is_err(),
"accepted internally inconsistent {change}"
);
}
}
#[test]
fn npm_locked_output_projects_retained_roots_without_rerunning_hooks() {
let (lock, bytes) = locked_installation_fixture();
let project = TemporaryDirectory::new();
fs::create_dir(project.path().join("config")).unwrap();
fs::write(
project.path().join("config/setting.json"),
br#"{"lib":["npm:app@1.0.0","npm:other@1.0.0"]}"#,
)
.unwrap();
lock.write_atomic(project.path()).unwrap();
dever_cli::libs::execute("remove", project.path(), &["npm:app@1.0.0".into()]).unwrap();
let retained = LockFile::decode(&fs::read(project.path().join("dever.lock")).unwrap()).unwrap();
doctor(&retained).unwrap();
let files =
dever_cli::libs::build::npm_installation(&retained, &retained.npm[0], &bytes).unwrap();
assert_eq!(retained.builds, lock.builds);
assert!(
files
.iter()
.all(|(path, _, _)| path.starts_with("node_modules/other/"))
);
}
impl AuthorInputs {
fn load() -> Self {
let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../target/npm-build");
let settings =
serde_json::from_slice(&fs::read(root.join("config/setting.json")).unwrap()).unwrap();
Self { root, settings }
}
}
impl Inputs for AuthorInputs {
fn runtime(&self, ecosystem: Ecosystem) -> Result<(RegistryRuntime, Vec<u8>), String> {
let (runtime, path) = match ecosystem {
Ecosystem::Pip => (&self.settings.python, &self.settings.python_file),
Ecosystem::Npm => (&self.settings.runtime, &self.settings.runtime_file),
_ => return Err("fixture only supplies Node and auxiliary Python".into()),
};
let bytes = fs::read(self.root.join(path)).map_err(|error| error.to_string())?;
if digest(&bytes) != runtime.pack.sha256 {
return Err("author runtime drift".into());
}
Ok((runtime.clone(), bytes))
}
fn tools(&self, ecosystem: Ecosystem) -> Result<(pack::Descriptor, Vec<u8>), String> {
if ecosystem != Ecosystem::Npm {
return Err("fixture only supplies npm tools".into());
}
Ok((
self.settings.build.clone(),
fs::read(self.root.join(&self.settings.build_file))
.map_err(|error| error.to_string())?,
))
}
fn assets(&self) -> Result<Vec<(String, Vec<u8>)>, String> {
let workspace = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..");
Ok(build_tests::sandbox::assets(&workspace)
.into_iter()
.map(|asset| {
(
asset.path.trim_start_matches("sandbox/").into(),
asset.bytes,
)
})
.collect())
}
fn diagnostic(&self, output: &[u8]) {
eprintln!("{}", String::from_utf8_lossy(output));
}
}
#[test]
#[ignore = "explicit author operation: assembles hashed npm/compiler/header inputs; no network or host execution"]
fn prepare_npm_build_tool_fixture() {
let workspace = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..");
let runtime = |name: &str| {
let root = workspace.join(format!("target/{name}-managed"));
let config: serde_json::Value =
serde_json::from_slice(&fs::read(root.join("config/setting.json")).unwrap()).unwrap();
let runtime: RegistryRuntime = serde_json::from_value(config["runtime"].clone()).unwrap();
assert_eq!(
digest(&fs::read(root.join("runtime.pack")).unwrap()),
runtime.pack.sha256
);
runtime
};
let node = runtime("npm");
let python = runtime("python");
let root = workspace.join("target/npm-build");
let build = build_tests::prepare_tool_pack(Ecosystem::Npm, &node, Some(&python), &root);
let settings = Settings {
runtime: node,
runtime_file: "../npm-managed/runtime.pack".into(),
python,
python_file: "../python-managed/runtime.pack".into(),
build,
build_file: "build.pack".into(),
};
fs::write(
root.join("config/setting.json"),
serde_json::to_vec_pretty(&settings).unwrap(),
)
.unwrap();
}
fn resolver<'a>(
inputs: &AuthorInputs,
session: &'a Session<'a>,
transport: &'a dyn RegistryTransport,
store: &'a FixtureArtifactStore,
) -> RegistryResolver<'a> {
RegistryResolver {
transport,
store,
runtimes: BTreeMap::from([(Ecosystem::Npm, inputs.settings.runtime.clone())]),
go_sumdb: dever_cli::libs::sumdb::ChecksumDatabase::official(None).unwrap(),
build: Some(session),
}
}
#[test]
#[ignore = "explicit official npm native source build; network only in registry resolver, never in lifecycle sandbox"]
fn builds_official_npm_addon_with_locked_lifecycle() {
let inputs = AuthorInputs::load();
let session = Session::new(&inputs);
let transport = dever_cli::libs::HttpRegistry::official();
let store = FixtureArtifactStore::default();
let resolver = resolver(&inputs, &session, &transport, &store);
let roots = vec!["npm:bufferutil@4.0.9".parse().unwrap()];
let lock = resolver.resolve(&roots).unwrap();
doctor(&lock).unwrap();
assert_eq!(lock.builds.len(), 1);
let receipt = &lock.builds[0];
assert!(
receipt
.native_entries
.contains("node_modules/bufferutil/build/Release/bufferutil.node")
);
let bytes = store
.verify_exact(
&receipt.output.sha256,
receipt.output.bytes,
&receipt.output.target,
)
.unwrap();
let files = dever_cli::libs::build::npm_installation(&lock, &lock.npm[0], &bytes).unwrap();
assert!(files.iter().any(
|(path, bytes, _)| path.ends_with("/build/Release/bufferutil.node")
&& bytes.starts_with(b"\x7fELF")
));
assert_eq!(lock, resolver.resolve(&roots).unwrap());
publish_fixture_artifacts(&inputs.root.join("dependency"), &lock, &store);
}
#[test]
#[ignore = "explicit managed npm hooks; uses local registry and real signed author runtime/tools"]
fn npm_lifecycle_preserves_optional_failures_bins_and_offline_remove() {
let inputs = AuthorInputs::load();
let session = Session::new(&inputs);
let mut transport = LocalRegistry(BTreeMap::new());
npm_tests::release(&mut transport, "helper", "1.0.0", serde_json::json!({"bin":{"build-probe":"cli.js"}}), &[("package/cli.js", b"#!/usr/bin/env node\nrequire('fs').writeFileSync('generated.js','module.exports = 42;');\n".to_vec())]);
npm_tests::release(
&mut transport,
"broken",
"1.0.0",
serde_json::json!({"dependencies":{"cycle":"1.0.0"},"scripts":{"install":"node -e 'process.exit(7)'"}}),
&[],
);
npm_tests::release(
&mut transport,
"cycle",
"1.0.0",
serde_json::json!({"dependencies":{"broken":"1.0.0"}}),
&[],
);
npm_tests::release(
&mut transport,
"app",
"1.0.0",
serde_json::json!({"dependencies":{"helper":"1.0.0"},"optionalDependencies":{"broken":"1.0.0"},"scripts":{"postinstall":"build-probe"}}),
&[],
);
npm_tests::release(&mut transport, "other", "1.0.0", serde_json::json!({}), &[]);
let store = FixtureArtifactStore::default();
let resolver = resolver(&inputs, &session, &transport, &store);
let lock = resolver
.resolve(&[
"npm:app@1.0.0".parse().unwrap(),
"npm:other@1.0.0".parse().unwrap(),
])
.unwrap();
doctor(&lock).unwrap();
assert!(
!lock
.libs
.iter()
.any(|lib| matches!(lib.spec.name.as_str(), "broken" | "cycle"))
);
assert!(
lock.npm[0]
.instances
.iter()
.flat_map(|node| &node.edges)
.any(|edge| edge.omission == Some(npm::Omission::BuildFailure))
);
let receipt = &lock.builds[0];
let bytes = store
.verify_exact(
&receipt.output.sha256,
receipt.output.bytes,
&receipt.output.target,
)
.unwrap();
let files = dever_cli::libs::build::npm_installation(&lock, &lock.npm[0], &bytes).unwrap();
assert!(
files
.iter()
.any(|(path, bytes, _)| path == "node_modules/app/generated.js"
&& bytes == b"module.exports = 42;")
);
assert!(
resolver
.resolve(&["npm:broken@1.0.0".parse().unwrap()])
.unwrap_err()
.contains("failed")
);
let project = TemporaryDirectory::new();
fs::create_dir(project.path().join("config")).unwrap();
fs::write(
project.path().join("config/setting.json"),
br#"{"lib":["npm:app@1.0.0","npm:other@1.0.0"]}"#,
)
.unwrap();
lock.write_atomic(project.path()).unwrap();
dever_cli::libs::execute("remove", project.path(), &["npm:app@1.0.0".into()]).unwrap();
let retained = LockFile::decode(&fs::read(project.path().join("dever.lock")).unwrap()).unwrap();
doctor(&retained).unwrap();
let files =
dever_cli::libs::build::npm_installation(&retained, &retained.npm[0], &bytes).unwrap();
assert!(
files
.iter()
.all(|(path, _, _)| path.starts_with("node_modules/other/"))
);
}
#[test]
#[ignore = "explicit managed npm hook: sparse scratch overflow and child cleanup; no network"]
fn npm_build_scratch_budget_kills_owned_hook_and_cleans_stage() {
let inputs = AuthorInputs::load();
let session = Session::new(&inputs);
let mut transport = LocalRegistry(BTreeMap::new());
npm_tests::release(&mut transport,"oversized","1.0.0",serde_json::json!({"scripts":{"install":"node fill.js"}}),&[("package/fill.js",b"const fs=require('fs');fs.writeFileSync('large',Buffer.alloc(1));fs.truncateSync('large',600*1024*1024);setInterval(()=>{},1000);".to_vec())]);
let store = FixtureArtifactStore::default();
let resolver = resolver(&inputs, &session, &transport, &store);
let owned_stages = || {
fs::read_dir(std::env::temp_dir())
.unwrap()
.map(|entry| entry.unwrap().file_name().to_string_lossy().into_owned())
.filter(|name| name.starts_with(&format!("dever-lib-build-{}-", std::process::id())))
.collect::<std::collections::BTreeSet<_>>()
};
let before = owned_stages();
let error = resolver
.resolve(&["npm:oversized@1.0.0".parse().unwrap()])
.unwrap_err();
assert!(error.contains("scratch budget"), "{error}");
assert_eq!(
owned_stages(),
before,
"failed hook leaked its owned staging tree"
);
}
#[test]
#[ignore = "explicit managed npm hook: internal output aliases are flattened; aliases outside output are rejected"]
fn npm_build_output_hard_links_stay_inside_the_exported_tree() {
let inputs = AuthorInputs::load();
let session = Session::new(&inputs);
for (name, source, accepted) in [
(
"insidealias",
"const fs=require('fs');fs.writeFileSync('original.txt','same-bytes');fs.linkSync('original.txt','alias.txt');",
true,
),
(
"outsidealias",
"const fs=require('fs');fs.writeFileSync('/data/build/outside.txt','outside');fs.linkSync('/data/build/outside.txt','alias.txt');",
false,
),
] {
let mut transport = LocalRegistry(BTreeMap::new());
npm_tests::release(
&mut transport,
name,
"1.0.0",
serde_json::json!({"scripts":{"install":"node aliases.js"}}),
&[("package/aliases.js", source.as_bytes().to_vec())],
);
let store = FixtureArtifactStore::default();
let result =
resolver(&inputs, &session, &transport, &store)
.resolve(&[format!("npm:{name}@1.0.0").parse().unwrap()]);
if accepted {
let lock = result.unwrap();
doctor(&lock).unwrap();
let receipt = &lock.builds[0];
let bytes = store
.verify_exact(
&receipt.output.sha256,
receipt.output.bytes,
&receipt.output.target,
)
.unwrap();
let files =
dever_cli::libs::build::npm_installation(&lock, &lock.npm[0], &bytes).unwrap();
for leaf in ["original.txt", "alias.txt"] {
assert!(files.iter().any(|(path, bytes, _)| path
== &format!("node_modules/{name}/{leaf}")
&& bytes == b"same-bytes"));
}
} else {
assert!(result.unwrap_err().contains("hard-link alias outside"));
}
}
}
+754
View File
@@ -0,0 +1,754 @@
use super::*;
use dever_cli::libs::sumdb::{self, ChecksumDatabase, Evidence, Verifier};
use ring::signature::{Ed25519KeyPair, KeyPair};
fn key() -> Ed25519KeyPair {
Ed25519KeyPair::from_seed_unchecked(&[47; 32]).unwrap()
}
pub(super) fn verifier() -> Verifier {
let mut encoded = vec![1];
encoded.extend(key().public_key().as_ref());
let mut hash = Sha256::new();
hash.update(b"dever-sumdb-test\n");
hash.update(&encoded);
let id = hash.finalize();
Verifier::new(&format!(
"dever-sumdb-test+{}+{}",
id[..4]
.iter()
.map(|byte| format!("{byte:02x}"))
.collect::<String>(),
base64::engine::general_purpose::STANDARD.encode(encoded)
))
.unwrap()
}
fn signed(size: usize, root: [u8; 32]) -> String {
let text = format!(
"go.sum database tree\n{size}\n{}\n",
base64::engine::general_purpose::STANDARD.encode(root)
);
let mut hash = Sha256::new();
hash.update(b"dever-sumdb-test\n");
hash.update([1]);
hash.update(key().public_key().as_ref());
let mut signature = hash.finalize()[..4].to_vec();
signature.extend(key().sign(text.as_bytes()).as_ref());
format!(
"{text}\n— dever-sumdb-test {}\n",
base64::engine::general_purpose::STANDARD.encode(signature)
)
}
fn leaf(record: &[u8]) -> [u8; 32] {
let mut hash = Sha256::new();
hash.update([0]);
hash.update(record);
hash.finalize().into()
}
fn root(hashes: &[[u8; 32]]) -> [u8; 32] {
if hashes.is_empty() {
return Sha256::digest([]).into();
}
if hashes.len() == 1 {
return hashes[0];
}
let split = hashes.len().next_power_of_two() / 2;
let mut hash = Sha256::new();
hash.update([1]);
hash.update(root(&hashes[..split]));
hash.update(root(&hashes[split..]));
hash.finalize().into()
}
fn unescape(path: &str) -> String {
let mut chars = path.chars();
let mut output = String::new();
while let Some(ch) = chars.next() {
output.push(if ch == '!' {
chars.next().unwrap().to_ascii_uppercase()
} else {
ch
});
}
output
}
pub(super) fn seal(registry: &mut LocalRegistry) {
registry
.0
.retain(|(_, path), _| !path.starts_with("https://sum.golang.org/"));
let records = registry
.0
.iter()
.filter_map(|((ecosystem, path), bytes)| {
if *ecosystem != Ecosystem::Go || !path.ends_with(".mod") {
return None;
}
let (module, version) = path.strip_prefix('/')?.split_once("/@v/v")?;
let version = version.strip_suffix(".mod")?;
let module = unescape(module);
let archive = registry.0.get(&(
Ecosystem::Go,
format!("{}zip", path.strip_suffix("mod").unwrap()),
))?;
let record = format!(
"{module} v{version} {}\n{module} v{version}/go.mod {}\n",
sumdb::zip_hash(&module, version, archive).unwrap(),
sumdb::mod_hash(bytes)
);
Some((
format!(
"/lookup/{}@v{version}",
path.strip_prefix('/')
.unwrap()
.split("/@v/")
.next()
.unwrap()
),
record,
))
})
.collect::<Vec<_>>();
let hashes = records
.iter()
.map(|(_, record)| leaf(record.as_bytes()))
.collect::<Vec<_>>();
let checkpoint = signed(records.len(), root(&hashes));
registry.0.insert(
(Ecosystem::Go, "https://sum.golang.org/latest".into()),
checkpoint.as_bytes().to_vec(),
);
for (id, (path, record)) in records.iter().enumerate() {
registry.0.insert(
(Ecosystem::Go, format!("https://sum.golang.org{path}")),
format!("{id}\n{record}\n{checkpoint}").into_bytes(),
);
}
// Fixtures deliberately remain below one tile. Larger trees use official
// captured vectors, not a second production tile-address implementation.
assert!(hashes.len() < 256);
if !hashes.is_empty() {
registry.0.insert(
(
Ecosystem::Go,
format!("https://sum.golang.org/tile/8/0/000.p/{}", hashes.len()),
),
hashes.iter().flatten().copied().collect(),
);
}
}
pub(super) struct Transport<'a> {
pub registry: &'a dyn RegistryTransport,
pub checksums: &'a LocalRegistry,
}
impl RegistryTransport for Transport<'_> {
fn get(&self, ecosystem: Ecosystem, path: &str, limit: usize) -> Result<Vec<u8>, String> {
if path.starts_with("https://sum.golang.org/") {
self.checksums.get(ecosystem, path, limit)
} else {
self.registry.get(ecosystem, path, limit)
}
}
}
fn resolved(
registry: &LocalRegistry,
previous: Option<String>,
roots: &[&str],
) -> Result<LockFile, String> {
let cache = FixtureArtifactStore::default();
let mut resolver = local_resolver(registry, &cache);
resolver.go_sumdb = ChecksumDatabase::new(verifier(), previous)?;
dever_cli::libs::LibResolver::resolve(
&resolver,
&roots
.iter()
.map(|root| root.parse().unwrap())
.collect::<Vec<_>>(),
)
}
#[test]
fn authenticated_modules_replay_offline_and_reject_content_or_proof_drift() {
let registry = local_registry();
let lock = resolved(&registry, None, &["go:example.com/a@1.0.0"]).unwrap();
let evidence = &lock.go_sumdb[0];
let checked = evidence.verify(&verifier()).unwrap();
let archive = &registry.0[&(Ecosystem::Go, "/example.com/a/@v/v1.0.0.zip".into())];
checked
.verify_zip("example.com/a", "1.0.0", archive)
.unwrap();
assert!(
evidence
.verify(&Verifier::official())
.unwrap_err()
.contains("trusted signature")
);
let mut changed = evidence.clone();
changed.records[0].go_mod.push_str("// changed\n");
assert!(changed.verify(&verifier()).unwrap_err().contains("go.mod"));
let mut changed = evidence.clone();
changed.records[0].module = "wrong.example/a".into();
assert!(
changed
.verify(&verifier())
.unwrap_err()
.contains("different module/version")
);
let mut changed = evidence.clone();
changed.records[0].inclusion[0] = base64::engine::general_purpose::STANDARD.encode([0; 32]);
assert!(
changed
.verify(&verifier())
.unwrap_err()
.contains("inclusion proof")
);
let mut changed = evidence.clone();
changed.checkpoint = changed
.checkpoint
.replace("go.sum database tree", "go.sum database free");
assert!(
changed
.verify(&verifier())
.unwrap_err()
.contains("signature verification")
);
assert!(
checked
.verify_zip(
"example.com/a",
"1.0.0",
&zip_file("example.com/a@v1.0.0/changed.go")
)
.unwrap_err()
.contains("sumdb h1")
);
}
#[test]
fn checkpoint_updates_require_consistency_and_never_move_backwards() {
let mut first = local_registry();
first
.0
.remove(&(Ecosystem::Go, "/example.com/b/@v/v1.2.0.mod".into()));
first.0.insert(
(Ecosystem::Go, "/example.com/a/@v/v1.0.0.mod".into()),
b"module example.com/a\n".to_vec(),
);
seal(&mut first);
let old = resolved(&first, None, &["go:example.com/a@1.0.0"]).unwrap();
let checkpoint = old.go_sumdb[0].verify(&verifier()).unwrap().checkpoint;
let mut next = first;
next.0.insert(
(Ecosystem::Go, "/example.com/z/@v/v1.0.0.mod".into()),
b"module example.com/z\n".to_vec(),
);
next.0.insert(
(Ecosystem::Go, "/example.com/z/@v/v1.0.0.zip".into()),
zip_file("example.com/z@v1.0.0/z.go"),
);
seal(&mut next);
let new = resolved(&next, Some(checkpoint.clone()), &["go:example.com/z@1.0.0"]).unwrap();
let current = new.go_sumdb[0].verify(&verifier()).unwrap().checkpoint;
assert!(resolved(&next, Some(current.clone()), &["go:example.com/z@1.0.0"]).is_ok());
next.0.insert(
(Ecosystem::Go, "https://sum.golang.org/latest".into()),
checkpoint.into_bytes(),
);
assert!(
resolved(&next, Some(current), &["go:example.com/a@1.0.0"])
.unwrap_err()
.contains("rollback")
);
let mut fork = new.go_sumdb[0].clone();
fork.consistency[0] = base64::engine::general_purpose::STANDARD.encode([0; 32]);
assert!(
fork.verify(&verifier())
.unwrap_err()
.contains("conflicting history")
);
}
#[test]
fn sumdb_verifier_key_and_upstream_signed_note_vectors() {
let note = "go.sum database tree\n63512409\nF7eHRPqN/cqPtqti14NKdGGaI1Giw8coWP3KxihFZbg=\n\n— sum.golang.org Az3gri1OBHKT87Xj8UXU8zjoHGy/X+7Nl94f8TZkELVaasolmGApUsWqwVG9vE6hgqYb/ph/w2vdkLIVRH7bMB/FPQ4=\n";
let evidence = Evidence {
previous: None,
checkpoint: note.into(),
consistency: vec![],
records: vec![],
};
evidence.verify(&Verifier::official()).unwrap();
assert!(evidence.verify(&verifier()).is_err());
assert!(Verifier::new(&sumdb::OFFICIAL_VERIFIER.replace("033de0ae", "033de0af")).is_err());
}
#[test]
fn official_large_tree_inclusion_and_consistency_vectors_verify_offline() {
// Captured from sum.golang.org/lookup/github.com/google/uuid@v1.6.0
// and its immutable height-8 tiles on 2026-10-02. Hashes were assembled
// independently of this Rust implementation. Both notes retain Google's
// original Ed25519 signatures; no fixture signer can mint these vectors.
let evidence: Evidence = serde_json::from_str(include_str!("sumdb_uuid.json")).unwrap();
let trusted = Verifier::official();
let verified = evidence.verify(&trusted).unwrap();
assert!(verified.contains("github.com/google/uuid", "1.6.0"));
assert_eq!(
sumdb::mod_hash(b"module github.com/google/uuid\n"),
"h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo="
);
for index in 0..evidence.records[0].inclusion.len() {
let mut changed = evidence.clone();
changed.records[0].inclusion[index] =
base64::engine::general_purpose::STANDARD.encode([0; 32]);
assert!(
changed.verify(&trusted).is_err(),
"accepted corrupt inclusion hash {index}"
);
}
for index in 0..evidence.consistency.len() {
let mut changed = evidence.clone();
changed.consistency[index] = base64::engine::general_purpose::STANDARD.encode([0; 32]);
assert!(
changed.verify(&trusted).is_err(),
"accepted corrupt consistency hash {index}"
);
}
}
#[test]
fn go_h1_ignores_zip_order_and_compression_and_preserves_module_case() {
let archive = |reverse, compression| {
let mut writer = zip::ZipWriter::new(Cursor::new(Vec::new()));
let mut files = vec![
("a.go", b"package a\n".as_slice()),
("LICENSE", b"fixture license\n".as_slice()),
];
if reverse {
files.reverse();
}
for (name, content) in files {
writer
.start_file(
format!("example.com/Mixed@v1.0.0/{name}"),
zip::write::SimpleFileOptions::default().compression_method(compression),
)
.unwrap();
writer.write_all(content).unwrap();
}
writer.finish().unwrap().into_inner()
};
let first = archive(false, zip::CompressionMethod::Stored);
let second = archive(true, zip::CompressionMethod::Deflated);
assert_ne!(digest(&first), digest(&second));
assert_eq!(
sumdb::zip_hash("example.com/Mixed", "1.0.0", &first).unwrap(),
sumdb::zip_hash("example.com/Mixed", "1.0.0", &second).unwrap()
);
let mut registry = LocalRegistry(BTreeMap::from([
(
(Ecosystem::Go, "/example.com/!mixed/@v/v1.0.0.mod".into()),
b"module example.com/Mixed\n".to_vec(),
),
(
(Ecosystem::Go, "/example.com/!mixed/@v/v1.0.0.zip".into()),
first,
),
]));
seal(&mut registry);
let lock = resolved(&registry, None, &["go:example.com/Mixed@1.0.0"]).unwrap();
assert_eq!(lock.libs[0].spec.name, "example.com/Mixed");
assert!(
lock.go_sumdb[0]
.verify(&verifier())
.unwrap()
.contains("example.com/Mixed", "1.0.0")
);
}
#[test]
fn every_go_mod_consulted_by_mvs_is_authenticated_even_when_not_selected() {
let mut registry = LocalRegistry(BTreeMap::new());
for (name, version, source) in [
(
"a",
"1.0.0",
"module example.com/a\nrequire (\nexample.com/b v1.0.0\nexample.com/c v1.0.0\n)\n",
),
("b", "1.0.0", "module example.com/b\n"),
("b", "2.0.0", "module example.com/b\n"),
(
"c",
"1.0.0",
"module example.com/c\nrequire example.com/b v2.0.0\n",
),
] {
registry.0.insert(
(
Ecosystem::Go,
format!("/example.com/{name}/@v/v{version}.mod"),
),
source.as_bytes().to_vec(),
);
registry.0.insert(
(
Ecosystem::Go,
format!("/example.com/{name}/@v/v{version}.zip"),
),
zip_file(&format!("example.com/{name}@v{version}/{name}.go")),
);
}
seal(&mut registry);
let lock = resolved(&registry, None, &["go:example.com/a@1.0.0"]).unwrap();
assert!(
!lock
.libs
.iter()
.any(|lib| lib.spec.key() == "go:example.com/b@1.0.0")
);
assert!(
lock.go_sumdb[0]
.records
.iter()
.any(|record| record.module == "example.com/b" && record.version == "1.0.0")
);
registry
.0
.get_mut(&(Ecosystem::Go, "/example.com/b/@v/v1.0.0.mod".into()))
.unwrap()
.extend(b"// modified discarded metadata\n");
let cache = FixtureArtifactStore::default();
let resolver = local_resolver(&registry, &cache);
assert!(
dever_cli::libs::LibResolver::resolve(
&resolver,
&["go:example.com/a@1.0.0".parse().unwrap()]
)
.unwrap_err()
.contains("go.mod does not match")
);
for ((_, path), bytes) in &registry.0 {
if path.ends_with(".zip") {
assert!(!cache.has(&digest(bytes), "linux-x86_64").unwrap());
}
}
}
#[test]
fn go_hashzip_includes_empty_directory_entries() {
let archive = |directory| {
let mut writer = zip::ZipWriter::new(Cursor::new(Vec::new()));
writer
.start_file(
"example.com/a@v1.0.0/a.go",
zip::write::SimpleFileOptions::default(),
)
.unwrap();
writer.write_all(b"package a\n").unwrap();
if directory {
writer
.add_directory(
"example.com/a@v1.0.0/empty/",
zip::write::SimpleFileOptions::default(),
)
.unwrap();
}
writer.finish().unwrap().into_inner()
};
// Independent Hash1 vectors: sorted "sha256(content) filename\n" records,
// including the directory's trailing slash and SHA-256 of empty content.
assert_eq!(
sumdb::zip_hash("example.com/a", "1.0.0", &archive(false)).unwrap(),
"h1:VEUcYKYLbKBYq0eA/VEJ3mMQBzi4jjWpusjuduGJcBQ="
);
assert_eq!(
sumdb::zip_hash("example.com/a", "1.0.0", &archive(true)).unwrap(),
"h1:y9ZWKWCmVsKnLxdFkIVoxr4MyXyHgBUvp1hgBzZ+IfE="
);
}
#[test]
fn project_updates_and_removing_last_lib_preserve_official_checkpoint_anchor() {
let root = TemporaryDirectory::new();
let evidence: Evidence = serde_json::from_str(include_str!("sumdb_uuid.json")).unwrap();
let mut lock = LockFile::new(Vec::new()).unwrap();
lock.go_sumdb.push(evidence.clone());
lock.write_atomic(root.path()).unwrap();
let spec = vec!["pip:fixture@1.0.0".into()];
for command in ["add", "update", "remove"] {
dever_cli::libs::execute(command, root.path(), &spec).unwrap();
let lock = LockFile::decode(&fs::read(root.path().join("dever.lock")).unwrap()).unwrap();
assert_eq!(lock.go_sumdb, vec![evidence.clone()]);
let current = sumdb::verify_chain(&lock.go_sumdb, &Verifier::official()).unwrap();
let previous = evidence.previous.clone().unwrap();
// A subsequent online resolution cannot accept the older official tree.
let mut registry = local_registry();
registry.0.insert(
(Ecosystem::Go, "https://sum.golang.org/latest".into()),
previous.into_bytes(),
);
let cache = FixtureArtifactStore::default();
let mut resolver = local_resolver(&registry, &cache);
resolver.go_sumdb = ChecksumDatabase::official(Some(current.checkpoint)).unwrap();
assert!(
dever_cli::libs::LibResolver::resolve(
&resolver,
&["go:example.com/a@1.0.0".parse().unwrap()]
)
.unwrap_err()
.contains("rollback")
);
}
}
#[test]
fn lock_publication_rejects_offline_byte_and_element_overflow_before_replacement() {
let root = TemporaryDirectory::new();
let original = LockFile::new(Vec::new()).unwrap();
original.write_atomic(root.path()).unwrap();
let path = root.path().join("dever.lock");
let before = fs::read(&path).unwrap();
let receipt = Evidence {
previous: None,
checkpoint: "x".into(),
consistency: Vec::new(),
records: Vec::new(),
};
for overflow in [true, false] {
let mut lock = original.clone();
let mut receipt = receipt.clone();
if overflow {
receipt.checkpoint = "x".repeat(dever_runtime::wire::MAX_BYTES);
} else {
receipt.consistency = vec!["x".into(); dever_runtime::wire::MAX_ELEMENTS];
}
lock.go_sumdb.push(receipt);
assert!(lock.encode().unwrap_err().contains("invalid dever.lock"));
assert!(lock.write_atomic(root.path()).is_err());
assert_eq!(fs::read(&path).unwrap(), before);
assert_eq!(fs::read_dir(root.path()).unwrap().count(), 1);
}
}
#[test]
fn truncated_surplus_and_out_of_bounds_sumdb_proofs_are_rejected() {
let original: Evidence = serde_json::from_str(include_str!("sumdb_uuid.json")).unwrap();
let trusted = Verifier::official();
let mut changed = original.clone();
changed.records[0].inclusion.pop();
assert!(changed.verify(&trusted).is_err());
let mut changed = original.clone();
changed.records[0]
.inclusion
.push(base64::engine::general_purpose::STANDARD.encode([0; 32]));
assert!(changed.verify(&trusted).is_err());
for id in ["66321505", "09223372036854775807", "9223372036854775808"] {
let mut changed = original.clone();
let (_, suffix) = changed.records[0].lookup.split_once('\n').unwrap();
changed.records[0].lookup = format!("{id}\n{suffix}");
assert!(changed.verify(&trusted).is_err());
}
let mut changed = original.clone();
changed.checkpoint.pop();
assert!(changed.verify(&trusted).is_err());
let mut disconnected = original.clone();
disconnected.previous = None;
disconnected.consistency.clear();
assert!(
sumdb::verify_chain(&[original, disconnected], &trusted)
.unwrap_err()
.contains("not connected")
);
}
#[cfg(unix)]
#[test]
fn lib_and_package_mutations_share_a_stable_nonblocking_project_lock() {
use std::os::unix::fs::OpenOptionsExt;
let root = TemporaryDirectory::new();
let requested = vec!["pip:fixture@1.0.0".into()];
dever_cli::libs::execute("add", root.path(), &requested).unwrap();
let lock_path = root.path().join("dever.lock");
let config_path = root.path().join("config/setting.json");
let before_lock = fs::read(&lock_path).unwrap();
let before_config = fs::read(&config_path).unwrap();
let guard = fs::OpenOptions::new()
.read(true)
.write(true)
.mode(0o600)
.open(root.path().join(".dever-project.lock"))
.unwrap();
fs2::FileExt::try_lock_exclusive(&guard).unwrap();
for command in ["add", "update", "remove"] {
assert!(
dever_cli::libs::execute(command, root.path(), &requested)
.unwrap_err()
.contains("mutation is busy")
);
assert!(
dever_cli::packages::execute(command, root.path(), &[])
.unwrap_err()
.contains("mutation is busy")
);
struct UnusedTransport;
impl dever_cli::packages::PackageTransport for UnusedTransport {
fn get(&self, _: &str, _: usize) -> Result<Vec<u8>, String> {
panic!("busy mutation must not contact a registry")
}
}
assert!(
dever_cli::packages::execute_with(
command,
root.path(),
&[],
&UnusedTransport,
&FixtureArtifactStore::default()
)
.unwrap_err()
.contains("mutation is busy")
);
}
assert_eq!(fs::read(&lock_path).unwrap(), before_lock);
assert_eq!(fs::read(&config_path).unwrap(), before_config);
drop(guard);
dever_cli::libs::execute("update", root.path(), &requested).unwrap();
assert_eq!(fs::read(&lock_path).unwrap(), before_lock);
}
#[cfg(unix)]
#[test]
fn project_mutation_rejects_linked_or_unprotected_lock_paths() {
use std::os::unix::fs::{PermissionsExt, symlink};
let root = TemporaryDirectory::new();
let outside = TemporaryDirectory::new();
let target = outside.path().join("untouched");
fs::write(&target, "keep").unwrap();
let path = root.path().join(".dever-project.lock");
symlink(&target, &path).unwrap();
let requested = vec!["pip:fixture@1.0.0".into()];
assert!(
dever_cli::libs::execute("add", root.path(), &requested)
.unwrap_err()
.contains("without symlinks")
);
assert_eq!(fs::read(&target).unwrap(), b"keep");
fs::remove_file(&path).unwrap();
fs::hard_link(&target, &path).unwrap();
assert!(
dever_cli::libs::execute("add", root.path(), &requested)
.unwrap_err()
.contains("private and unlinked")
);
fs::remove_file(&path).unwrap();
fs::write(&path, "").unwrap();
fs::set_permissions(&path, fs::Permissions::from_mode(0o666)).unwrap();
assert!(
dever_cli::libs::execute("add", root.path(), &requested)
.unwrap_err()
.contains("private and unlinked")
);
fs::set_permissions(root.path(), fs::Permissions::from_mode(0o777)).unwrap();
assert!(
dever_cli::libs::execute("add", root.path(), &requested)
.unwrap_err()
.contains("not group/other writable")
);
}
#[test]
fn independent_go_workers_merge_connected_offline_evidence() {
use dever_cli::libs::{LockedWorker, resolve_workers};
let registry = local_registry();
let cache = FixtureArtifactStore::default();
let resolver = local_resolver(&registry, &cache);
let worker = |adapter: &str, spec: &str| LockedWorker {
port: "sample.worker".into(),
adapter: adapter.into(),
ecosystem: "go".into(),
runtime: Some(registry_runtime(Ecosystem::Go).pack),
entry: format!("worker/{adapter}.go"),
schema: "checked-port".into(),
capabilities: vec![],
operations: vec!["read".into()],
libs: vec![spec.parse().unwrap()],
};
let lock = resolve_workers(
&resolver,
&[],
vec![
worker("first", "go:example.com/a@1.0.0"),
worker("second", "go:example.com/b@1.2.0"),
],
)
.unwrap();
assert_eq!(lock.go_sumdb.len(), 2);
assert_eq!(
lock.go_sumdb[1].previous.as_ref(),
Some(&lock.go_sumdb[0].verify(&verifier()).unwrap().checkpoint)
);
let decoded = LockFile::decode(&lock.encode().unwrap()).unwrap();
let verified = sumdb::verify_chain(&decoded.go_sumdb, &verifier()).unwrap();
assert!(verified.contains("example.com/a", "1.0.0"));
assert!(verified.contains("example.com/b", "1.2.0"));
verify_locked_artifacts(&decoded, &cache, "linux-x86_64").unwrap();
}
#[test]
#[ignore = "author operation: explicitly fetches official Go proxy/sumdb; requires target/go-managed/config/setting.json and runtime.pack"]
fn prepare_official_go_dependency_fixture() {
#[derive(serde::Deserialize)]
#[serde(deny_unknown_fields)]
struct Settings {
runtime: RegistryRuntime,
}
let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../target/go-managed");
let settings: Settings = serde_json::from_slice(
&fs::read(root.join("config/setting.json"))
.expect("prepare the explicit fixture settings first"),
)
.unwrap();
let pack =
fs::read(root.join("runtime.pack")).expect("prepare the explicit Go runtime pack first");
assert_eq!(
settings.runtime.pack.sha256,
digest(&pack),
"configured runtime does not match runtime.pack"
);
assert_eq!(
settings.runtime.target,
dever_cli::toolchain::platform_identity()
);
let output = root.join("dependency");
let lock_path = output.join("dever.lock");
let previous = if lock_path.exists() {
let lock = LockFile::decode(&fs::read(&lock_path).unwrap()).unwrap();
dever_cli::libs::doctor(&lock).unwrap();
let verified = sumdb::verify_chain(&lock.go_sumdb, &Verifier::official()).unwrap();
(!verified.checkpoint.is_empty()).then_some(verified.checkpoint)
} else {
None
};
let target = settings.runtime.target.clone();
let store = FixtureArtifactStore::default();
let transport = dever_cli::libs::HttpRegistry::official();
let resolver = RegistryResolver {
build: None,
transport: &transport,
store: &store,
runtimes: BTreeMap::from([(Ecosystem::Go, settings.runtime)]),
go_sumdb: ChecksumDatabase::official(previous).unwrap(),
};
let lock = dever_cli::libs::LibResolver::resolve(
&resolver,
&["go:github.com/google/uuid@1.6.0".parse().unwrap()],
)
.unwrap();
// The same official verifier and offline artifact checks protect normal builds.
dever_cli::libs::doctor(&lock).unwrap();
dever_cli::libs::verify_locked_artifacts(&lock, &store, &target).unwrap();
publish_fixture_artifacts(&output, &lock, &store);
}
@@ -0,0 +1,68 @@
{
"previous": "go.sum database tree\n63512409\nF7eHRPqN/cqPtqti14NKdGGaI1Giw8coWP3KxihFZbg=\n\n— sum.golang.org Az3gri1OBHKT87Xj8UXU8zjoHGy/X+7Nl94f8TZkELVaasolmGApUsWqwVG9vE6hgqYb/ph/w2vdkLIVRH7bMB/FPQ4=\n",
"checkpoint": "go.sum database tree\n66321505\nnL/ZK4zIq2TWUhMi/cDJQRq6hXj67qFBbyEqxks3epY=\n\n— sum.golang.org Az3gruvaP9rIy6foXnttmnViaXM55mqUcP66AeP7N5r+tpFo2ll40wDr7sN0guXk6rXp0fL63fe30pchMCTqmbRo+Ak=\n",
"consistency": [
"3OyjS9XS6r41pxaFWGnW/WSmX7b1ik3nupquDNipdZ8=",
"zX5IsB02+IutT35ka/MwY7gFiPSAlihfnt2cu6smUjw=",
"Qc0CqO89A+/onGz1u4ZsDoMOfRIgPZpayRGTETOv8kk=",
"JhdrRO/y8Iu+MDeUupFG0uM4+odAADMjVH0mOgLOA/c=",
"lOgywJvTdrBWol0uphM6ZulvAebwJGNlekm8MIwiwKM=",
"FRf0N51Fw+8eSLHEa/e/AsnDway0EkKo4jI0E1Ne7dk=",
"Ak0gyC+RS6/ahVBCBuSBDA1wFN9mgq3acIbYm9uJHQ4=",
"FFJ8WrKTTQgCjTuAl5CukBdjDaQ7opSdHBpNSolRYWw=",
"rdNcd+MUjUxzlX/nlVGeQP5BkkZyTT89sn/yfg0AXQ0=",
"gJzOh5y5rP1BILUrFJw+aGjod840PErDnjy4T0BoUE4=",
"7I8BPI+ADxeJmu19mQxO+xt6bNhg3Ueq1j2dIfOppwA=",
"urhcUF9QOyTRAYRjJjsRHjHdwIc+BxfiQ6a+NuYeC6E=",
"p9eGEUHXeKLFnrIPB7VXHHMuwtaBnvHG4CYwgC0wq+w=",
"Gcry0ScaNfFmQujPMT95RQ3rbzKDSynQsL546C8CkfU=",
"dIpym+JRT/+z9/tlhpuIRyv+uoWGqgND/MHw0f1MtqA=",
"cQrKLJIBbPoWDnRnbGfAEMfpsau0h+Aw3aqDOJ5LAVU=",
"C46761Ui7jL+lf4vRXBlveoxuFS6tykAd6mMEfNttPE=",
"45Kz4HcZ9iZ84ySMHCy21d4vd2MvibvfNMzYm0hKYRs=",
"4JZTJ+4VUtLMTEs11jn4GmCxoz56Lm5HDwkYmkmAWlw=",
"2hJV4h1jZ2YujjyXUw+7YYCwLXGYZy+ondhRUgxKmXI=",
"lrn/kRfgYdYSDxgPdSQ8/GnZw6u/joKu9++AR+Saa5M=",
"hnm9+kaHQdhHWrAcyrvY5fbv8QB+GwVjRMFeobtTg8A=",
"rPFmKL/vD6Edsk/0DmpvDa/nMOqWgjfpPGRjBUL9yKs=",
"2IzbyezmwwGZZPJ276ejAQZiAnar+RcjAUU3MEMtbIA=",
"k3kfr9AECmkV+UA3/D6EMqaBDr67aubL5hXGsvJ+Nng=",
"/WnQtddEqG03mbcU1XID1iws1nlKHVkJcaREWknCOiE=",
"xJ20xXM4QItt5vxFOtolZ5l18p7ej1+RimNkgWmoRFM="
],
"records": [{
"module": "github.com/google/uuid",
"version": "1.6.0",
"go_mod": "module github.com/google/uuid\n",
"lookup": "22152757\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\n\ngo.sum database tree\n66321505\nnL/ZK4zIq2TWUhMi/cDJQRq6hXj67qFBbyEqxks3epY=\n\n— sum.golang.org Az3gruvaP9rIy6foXnttmnViaXM55mqUcP66AeP7N5r+tpFo2ll40wDr7sN0guXk6rXp0fL63fe30pchMCTqmbRo+Ak=\n",
"inclusion": [
"vYU6vRfhcMyd5/U04n+AYg52mm3BQrjwyN+27saiSnU=",
"syWiyNbj6gybIdLKoAFd7xF3qNltS7OhQjtp4y8rCFg=",
"Zqpa9zJsOg/sI/mv0UkXO9d7gvPHLLjMAKnZCPzeiNM=",
"tGbcXa1AQlYBIm5gM2KDy8JvdAFDizj4lUop18Eehxs=",
"/w9FG1Sae/omKxZdOvj719sT1rCow3tlP8VNo0fDQtE=",
"dR6Bk+gh/hyJx7bu65HfhQdfuvT/OdVv/G7RUsOc26Y=",
"fRC86/2mWWtFWNPz/Vou6f7hdyvRLHFPo9KaOlVOVmc=",
"jJS2b3XVP2sst4CS5srpnF34agHpiluU63h6UQyFPDc=",
"cOL2+zAXTEOaAiyvmCrL3Ned6soAg08CJJJ2VW3+a18=",
"In5NzPUzIAZfV6bnUL11Bkm4Aqoit/ZG7gJ8nxOdB2w=",
"r+8IH0m7Ig3BS+uwCVVG1CZT0I/Rro3bbSUVYcSrN2Q=",
"wOgTamt00bdokE1Nbv3aHYu2um06V1xdLFEmOBjt0GI=",
"21woobbbCvogV/ZXACdAozfVAFjRv5xMID/iLcryHzw=",
"5+o2gn9/zVod+MYr4UuoeMzuz68vt/hA5+9H4YZaVME=",
"040B+RrLQeFCJhPU69kzoKr4BTqhSwdrnYEVhknZ+I0=",
"ioI6UByPBEr2lEZB0dI6pCXgcNss6UkKxm+TYAADujg=",
"HOJ0dLK4hnl0tFeu1tmT7TRrNKRIbi/T8KI/2s4DXDs=",
"BBcHtB4mS19jHff/LQTNI/+WCVBZ1Up52gNqyoln71U=",
"Oz/OlF1Lj7wkqqE15p324ZSX1XHcnLzDSMKNtUx6rwY=",
"ofpmTAcvFvNzlwjZnsrMqeG4iIBCP2lt5wIusvZ2gG4=",
"IRpVS0aap6n0qb02jyOy6ikkOPxdFIQtWwckaGF2MAQ=",
"liRusDUG3TanNZLGkfjb58ppX/6d40M7X2s3wptjxMw=",
"ROjUzjXHDN0Yo/I6EeqHhpszFgIs7FCABVeQniliPTk=",
"NTjIv8CJqIIGrOHhNQWSP9FV6XEPL6uGA4lmMfNKGJg=",
"bwqUQG2wJXGxo1QLA2CFb3chH17xvlqXe6eQy43p0HY=",
"bqa+4xGvzGcI/PYne2fXfUvdULgy8o3zQmD13q/gu1Y="
],
"consistency": []
}]
}
+531
View File
@@ -0,0 +1,531 @@
use super::*;
use dever_cli::workers::python_wheel::Wheel;
fn validate_native(
target: &str,
suffixes: &[String],
interpreter: &str,
files: &BTreeMap<String, &[u8]>,
) -> Result<(), String> {
let wheel_files = files
.keys()
.filter(|path| !path.starts_with("runtime/") && !path.starts_with("sandbox/"))
.cloned()
.collect();
dever_cli::workers::python_wheel::validate_native(
target,
suffixes,
interpreter,
files,
&wheel_files,
)
}
fn spec() -> LibSpec {
"pip:sample@1.0.0".parse().unwrap()
}
#[test]
fn wheel_console_scripts_use_the_same_private_prefix_and_record_generated_files() {
let bytes=wheel_fixture::pack("sample","1.0.0","Wheel-Version: 1.0\nRoot-Is-Purelib: true\nTag: py3-none-any\n",
&wheel_fixture::metadata("sample","1.0.0",&[],&[]),&[("sample-1.0.0.dist-info/entry_points.txt",b"[console_scripts]\nsample-tool = sample.cli:main\n[gui_scripts]\nsample-gui = sample.gui:App.run [desktop]\n".to_vec())]);
let installed = Wheel::parse(&spec(), None, &tags(), &bytes)
.unwrap()
.install("runtime/bin/python3", "3.12")
.unwrap();
let script = installed
.iter()
.find(|file| file.path == "runtime/bin/sample-tool")
.unwrap();
assert!(script.executable);
assert!(
script
.bytes
.starts_with(b"#!/worker/runtime/bin/python3 -I\n")
);
assert!(String::from_utf8_lossy(&script.bytes).contains("sample.cli"));
let record = installed
.iter()
.find(|file| file.path.ends_with(".dist-info/RECORD"))
.unwrap();
assert!(String::from_utf8_lossy(&record.bytes).contains("../../../bin/sample-tool,sha256="));
let collision = wheel_fixture::pack(
"sample",
"1.0.0",
"Wheel-Version: 1.0\nRoot-Is-Purelib: true\nTag: py3-none-any\n",
&wheel_fixture::metadata("sample", "1.0.0", &[], &[]),
&[
(
"sample-1.0.0.dist-info/entry_points.txt",
b"[console_scripts]\ntool = sample:main\n".to_vec(),
),
("sample-1.0.0.data/scripts/tool", b"#!python\n".to_vec()),
],
);
assert!(
Wheel::parse(&spec(), None, &tags(), &collision)
.unwrap()
.install("runtime/bin/python3", "3.12")
.is_err()
);
}
fn tags() -> Vec<String> {
vec!["py3-none-any".into()]
}
fn valid() -> Vec<u8> {
pure_wheel("sample", "1.0.0", &[], &[])
}
fn unpack(bytes: &[u8]) -> Vec<(String, Vec<u8>)> {
use std::io::Read;
let mut archive = zip::ZipArchive::new(Cursor::new(bytes)).unwrap();
(0..archive.len())
.map(|index| {
let mut file = archive.by_index(index).unwrap();
let name = file.name().to_owned();
let mut bytes = vec![];
file.read_to_end(&mut bytes).unwrap();
(name, bytes)
})
.collect()
}
#[test]
fn wheel_identity_and_all_compressed_filename_tags_are_bound_to_metadata() {
Wheel::parse(
&spec(),
Some("sample-1.0.0-py3-none-any.whl"),
&tags(),
&valid(),
)
.unwrap();
for filename in [
"other-1.0.0-py3-none-any.whl",
"sample-2.0.0-py3-none-any.whl",
"sample-1.0.0-py2.py3-none-any.whl",
"sample-1.0.0-1-py3-none-any.whl",
] {
assert!(
Wheel::parse(&spec(), Some(filename), &tags(), &valid()).is_err(),
"{filename}"
);
}
let compressed = wheel_fixture::pack(
"sample",
"1.0.0",
"Wheel-Version: 1.0\nRoot-Is-Purelib: false\nTag: py2-none-any\nTag: py3-none-any\n",
&wheel_fixture::metadata("sample", "1.0.0", &[], &[]),
&[],
);
Wheel::parse(
&spec(),
Some("sample-1.0.0-py2.py3-none-any.whl"),
&tags(),
&compressed,
)
.unwrap();
assert!(
Wheel::parse(
&spec(),
None,
&["cp312-cp312-linux_x86_64".into()],
&valid()
)
.is_err()
);
let forged_metadata = wheel_fixture::pack(
"sample",
"1.0.0",
"Wheel-Version: 1.0\nRoot-Is-Purelib: true\nTag: py3-none-any\n",
&wheel_fixture::metadata("other", "1.0.0", &[], &[]),
&[],
);
assert!(Wheel::parse(&spec(), None, &tags(), &forged_metadata).is_err());
let hidden_binary = wheel_fixture::pack(
"sample",
"1.0.0",
"Wheel-Version: 1.0\nRoot-Is-Purelib: true\nTag: py3-none-any\n",
&wheel_fixture::metadata("sample", "1.0.0", &[], &[]),
&[("sample/native.so", elf(62, None, None))],
);
assert!(Wheel::parse(&spec(), None, &tags(), &hidden_binary).is_err());
}
#[test]
#[ignore = "author operation: explicitly fetches PyPI simplejson; requires target/python-managed/config/setting.json and runtime.pack"]
fn prepare_official_native_python_fixture() {
#[derive(serde::Deserialize)]
#[serde(deny_unknown_fields)]
struct Settings {
runtime: RegistryRuntime,
}
let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../target/python-managed");
let settings: Settings =
serde_json::from_slice(&fs::read(root.join("config/setting.json")).unwrap()).unwrap();
let pack = fs::read(root.join("runtime.pack")).unwrap();
assert_eq!(digest(&pack), settings.runtime.pack.sha256);
assert_eq!(
settings.runtime.target,
dever_cli::toolchain::platform_identity()
);
let target = settings.runtime.target.clone();
let store = FixtureArtifactStore::default();
let transport = dever_cli::libs::HttpRegistry::official();
let resolver = RegistryResolver {
build: None,
transport: &transport,
store: &store,
runtimes: BTreeMap::from([(Ecosystem::Pip, settings.runtime)]),
go_sumdb: dever_cli::libs::sumdb::ChecksumDatabase::official(None).unwrap(),
};
let lock = dever_cli::libs::LibResolver::resolve(
&resolver,
&["pip:simplejson@3.20.1".parse().unwrap()],
)
.unwrap();
dever_cli::libs::doctor(&lock).unwrap();
dever_cli::libs::verify_locked_artifacts(&lock, &store, &target).unwrap();
let artifact = &lock.libs[0].artifacts[0];
let wheel = store
.verify_exact(&artifact.sha256, artifact.bytes, &target)
.unwrap();
assert!(
unpack(&wheel)
.iter()
.any(|(path, bytes)| path.contains("_speedups.")
&& path.ends_with(".so")
&& bytes.starts_with(b"\x7fELF")),
"fixture must use native speedups, not the optional pure-Python fallback"
);
publish_fixture_artifacts(&root.join("dependency"), &lock, &store);
}
#[test]
fn wheel_record_requires_exact_files_hashes_sizes_and_unique_csv_paths() {
for mutation in 0..6 {
let mut files = unpack(&valid());
match mutation {
0 => files
.iter_mut()
.find(|(path, _)| path.ends_with("__init__.py"))
.unwrap()
.1
.push(b'!'),
1 => files.push(("sample/unlisted.py".into(), vec![b'x'])),
2 => {
files.retain(|(path, _)| !path.ends_with("__init__.py"));
}
_ => {
let record = &mut files
.iter_mut()
.find(|(path, _)| path.ends_with("/RECORD"))
.unwrap()
.1;
let mut content = String::from_utf8(record.clone()).unwrap();
match mutation {
3 => content = content.replace("sha256=", "sha1="),
4 => content.push_str("sample-1.0.0.dist-info/RECORD,,\n"),
5 => content.push_str("../outside.py,sha256=abc,1\n"),
_ => unreachable!(),
}
*record = content.into_bytes();
}
}
assert!(
Wheel::parse(&spec(), None, &tags(), &wheel_fixture::zip(&files)).is_err(),
"mutation {mutation}"
);
}
}
#[test]
fn wheel_installs_all_standard_schemes_and_rewrites_installed_record() {
let bytes = wheel_fixture::pack(
"sample",
"1.0.0",
"Wheel-Version: 1.0\nRoot-Is-Purelib: false\nTag: py3-none-any\n",
&wheel_fixture::metadata("sample", "1.0.0", &[], &[]),
&[
("sample-1.0.0.data/purelib/plain.py", b"plain".to_vec()),
(
"sample-1.0.0.data/platlib/platform.py",
b"platform".to_vec(),
),
(
"sample-1.0.0.data/scripts/tool",
b"#!python\nprint('ok')\n".to_vec(),
),
("sample-1.0.0.data/headers/public.h", b"header".to_vec()),
(
"sample-1.0.0.data/data/share/sample/schema.json",
b"{}".to_vec(),
),
("sample/name,with\"quotes.py", b"quoted".to_vec()),
],
);
let files = Wheel::parse(&spec(), None, &tags(), &bytes)
.unwrap()
.install("runtime/bin/python3", "3.12")
.unwrap();
for path in [
"runtime/lib/python3.12/site-packages/plain.py",
"runtime/lib/python3.12/site-packages/platform.py",
"runtime/include/python3.12/sample/public.h",
"runtime/share/sample/schema.json",
"runtime/lib/python3.12/site-packages/sample/name,with\"quotes.py",
] {
assert!(files.iter().any(|file| file.path == path), "{path}");
}
let script = files
.iter()
.find(|file| file.path == "runtime/bin/tool")
.unwrap();
assert!(script.executable);
assert!(
script
.bytes
.starts_with(b"#!/worker/runtime/bin/python3 -I\n")
);
let record = files
.iter()
.find(|file| file.path.ends_with("/RECORD"))
.unwrap();
let rows = csv::ReaderBuilder::new()
.has_headers(false)
.from_reader(record.bytes.as_slice())
.records()
.collect::<Result<Vec<_>, _>>()
.unwrap();
let installed_script = rows
.iter()
.find(|row| &row[0] == "../../../bin/tool")
.unwrap();
assert_eq!(
installed_script[1].to_owned(),
format!(
"sha256={}",
base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(Sha256::digest(&script.bytes))
)
);
}
#[test]
fn wheel_rejects_scheme_collisions_and_uses_artifact_dependencies() {
let bytes = wheel_fixture::pack(
"sample",
"1.0.0",
"Wheel-Version: 1.0\nRoot-Is-Purelib: true\nTag: py3-none-any\n",
&wheel_fixture::metadata("sample", "1.0.0", &[], &[]),
&[
("same.py", vec![b'a']),
("sample-1.0.0.data/platlib/same.py", vec![b'b']),
],
);
assert!(
Wheel::parse(&spec(), None, &tags(), &bytes)
.unwrap()
.install("runtime/bin/python3", "3.12")
.is_err()
);
let mut registry = local_registry();
let release = registry
.0
.get_mut(&(Ecosystem::Pip, "/pypi/a/1.0.0/json".into()))
.unwrap();
let mut document: serde_json::Value = serde_json::from_slice(release).unwrap();
document["info"]["requires_dist"] = serde_json::json!([]);
*release = serde_json::to_vec(&document).unwrap();
let lock = python_lock(&registry, &["pip:a@1.0.0"]).unwrap();
assert_eq!(dependency_keys(&lock, "pip:a@1.0.0"), ["pip:b@1.1.0"]);
}
#[test]
fn solver_backtracks_on_selected_wheel_metadata_and_its_actual_extras() {
let mut registry = LocalRegistry(BTreeMap::new());
python_release(
&mut registry,
"root",
"1.0.0",
&[],
&["a[x] >=1", "b ==1.0.0"],
);
python_release(
&mut registry,
"a",
"2.0.0",
&["x"],
&["b ==2.0.0; extra == 'x'"],
);
python_release(
&mut registry,
"a",
"1.0.0",
&["x"],
&["b ==1.0.0; extra == 'x'"],
);
python_release(&mut registry, "b", "2.0.0", &[], &[]);
python_release(&mut registry, "b", "1.0.0", &[], &[]);
for version in ["1.0.0", "2.0.0"] {
let json = registry
.0
.get_mut(&(Ecosystem::Pip, format!("/pypi/a/{version}/json")))
.unwrap();
let mut release: serde_json::Value = serde_json::from_slice(json).unwrap();
release["info"] = serde_json::json!({"requires_dist":null,"provides_extra":null});
*json = serde_json::to_vec(&release).unwrap();
}
let lock = python_lock(&registry, &["pip:root@1.0.0"]).unwrap();
assert!(
lock.libs
.iter()
.any(|lib| lib.spec.key() == "pip:a[x]@1.0.0")
);
assert!(!lock.libs.iter().any(|lib| lib.spec.version == "2.0.0"));
assert_eq!(dependency_keys(&lock, "pip:a[x]@1.0.0"), ["pip:b@1.0.0"]);
}
/// ELF64 protocol fixture with PT_LOAD and optional PT_DYNAMIC/DT_STRTAB.
/// It is parsed only; no executable bytes are launched by this test.
fn elf(machine: u16, needed: Option<&str>, rpath: Option<&str>) -> Vec<u8> {
let mut bytes = vec![0u8; 1024];
bytes[..7].copy_from_slice(b"\x7fELF\x02\x01\x01");
bytes[16..18].copy_from_slice(&3u16.to_le_bytes());
bytes[18..20].copy_from_slice(&machine.to_le_bytes());
bytes[20..24].copy_from_slice(&1u32.to_le_bytes());
bytes[32..40].copy_from_slice(&64u64.to_le_bytes());
bytes[52..54].copy_from_slice(&64u16.to_le_bytes());
bytes[54..56].copy_from_slice(&56u16.to_le_bytes());
bytes[56..58].copy_from_slice(&2u16.to_le_bytes());
bytes[64..68].copy_from_slice(&1u32.to_le_bytes());
bytes[96..104].copy_from_slice(&1024u64.to_le_bytes());
bytes[104..112].copy_from_slice(&1024u64.to_le_bytes());
bytes[120..124].copy_from_slice(&2u32.to_le_bytes());
bytes[128..136].copy_from_slice(&176u64.to_le_bytes());
bytes[136..144].copy_from_slice(&176u64.to_le_bytes());
let mut strings = vec![0];
let mut dynamic = vec![];
for (tag, value) in [(1u64, needed), (15u64, rpath)] {
if let Some(value) = value {
dynamic.push((tag, strings.len() as u64));
strings.extend(value.bytes());
strings.push(0);
}
}
dynamic.extend([(5, 512), (10, strings.len() as u64), (0, 0)]);
bytes[152..160].copy_from_slice(&((dynamic.len() * 16) as u64).to_le_bytes());
bytes[160..168].copy_from_slice(&((dynamic.len() * 16) as u64).to_le_bytes());
for (index, (tag, value)) in dynamic.into_iter().enumerate() {
bytes[176 + index * 16..184 + index * 16].copy_from_slice(&tag.to_le_bytes());
bytes[184 + index * 16..192 + index * 16].copy_from_slice(&value.to_le_bytes());
}
bytes[512..512 + strings.len()].copy_from_slice(&strings);
bytes
}
#[test]
fn native_wheel_checks_target_suffix_and_closed_library_search() {
let runtime = elf(62, None, None);
let library = elf(62, None, None);
let native = elf(62, Some("libsample.so.1"), Some("$ORIGIN/../sample.libs"));
let suffixes = vec![
".cpython-312-x86_64-linux-gnu.so".into(),
".abi3.so".into(),
".so".into(),
];
let mut files = BTreeMap::from([
("runtime/bin/python3".into(), runtime.as_slice()),
(
"site-packages/sample/_native.cpython-312-x86_64-linux-gnu.so".into(),
native.as_slice(),
),
(
"site-packages/sample.libs/libsample.so.1".into(),
library.as_slice(),
),
]);
validate_native("linux-x86_64", &suffixes, "runtime/bin/python3", &files).unwrap();
files.remove("site-packages/sample.libs/libsample.so.1");
assert!(
validate_native("linux-x86_64", &suffixes, "runtime/bin/python3", &files)
.unwrap_err()
.contains("packaged dependency closure")
);
files.insert("sandbox/lib/libsample.so.1".into(), library.as_slice());
validate_native("linux-x86_64", &suffixes, "runtime/bin/python3", &files).unwrap();
let old = files
.remove("site-packages/sample/_native.cpython-312-x86_64-linux-gnu.so")
.unwrap();
files.insert(
"site-packages/sample/_native.cpython-311-x86_64-linux-gnu.so".into(),
old,
);
assert!(
validate_native("linux-x86_64", &suffixes, "runtime/bin/python3", &files)
.unwrap_err()
.contains("suffix")
);
for (machine, rpath, message) in [
(183, "$ORIGIN", "target"),
(62, "/usr/lib", "$ORIGIN"),
(62, "$ORIGIN/../../../..", "escapes"),
] {
let rejected = elf(machine, None, Some(rpath));
let files = BTreeMap::from([
("runtime/bin/python3".into(), runtime.as_slice()),
("site-packages/sample/native.so".into(), rejected.as_slice()),
]);
assert!(
validate_native("linux-x86_64", &suffixes, "runtime/bin/python3", &files)
.unwrap_err()
.contains(message)
);
}
}
#[test]
fn native_runpath_outranks_ancestor_rpath_and_helpers_keep_import_context() {
let runtime = elf(62, None, Some("$ORIGIN/../lib"));
let shared = elf(62, None, None);
let wrong_target = elf(183, None, None);
let mut extension = elf(
62,
Some("libsample.so.1"),
Some("$ORIGIN/../../runtime/alternate"),
);
// Replace DT_RPATH with DT_RUNPATH in the independently encoded ELF vector.
for offset in (176..256).step_by(16) {
if u64::from_le_bytes(extension[offset..offset + 8].try_into().unwrap()) == 15 {
extension[offset..offset + 8].copy_from_slice(&29u64.to_le_bytes());
}
}
let suffixes = vec![".cpython-312-x86_64-linux-gnu.so".into()];
let files = BTreeMap::from([
("runtime/bin/python3".into(), runtime.as_slice()),
("runtime/lib/libsample.so.1".into(), shared.as_slice()),
(
"runtime/alternate/libsample.so.1".into(),
wrong_target.as_slice(),
),
(
"site-packages/pkg/mod.cpython-312-x86_64-linux-gnu.so".into(),
extension.as_slice(),
),
]);
assert!(
validate_native("linux-x86_64", &suffixes, "runtime/bin/python3", &files)
.unwrap_err()
.contains("target")
);
let extension = elf(62, Some("liba.so.1"), Some("$ORIGIN/../vendor"));
let helper = elf(62, Some("libb.so.1"), None);
let files = BTreeMap::from([
("runtime/bin/python3".into(), runtime.as_slice()),
(
"site-packages/pkg/mod.cpython-312-x86_64-linux-gnu.so".into(),
extension.as_slice(),
),
("site-packages/vendor/liba.so.1".into(), helper.as_slice()),
("site-packages/vendor/libb.so.1".into(), shared.as_slice()),
]);
validate_native("linux-x86_64", &suffixes, "runtime/bin/python3", &files).unwrap();
}