Initial Dever 0.0.1

This commit is contained in:
Codex committed 2026-11-03 03:32:07 +00:00
commit 81233d098c
1479 files changed
+163782

No files matched your search

+163
View File
@@ -0,0 +1,163 @@
use super::{Fixture, tools};
use dever::compiler::project::dependency::{
Lock,
library::{command, npm},
};
use serde_json::json;
use std::{collections::BTreeMap, fs, os::unix::fs::DirBuilderExt};
#[test]
fn npm_signed_hooks_native_addon_and_missing_output_replay_are_real() {
let mut fixture = Fixture::new();
tools::prepare(&mut fixture);
let script=br#"const fs=require('node:fs');const path=require('node:path');const child=require('node:child_process');
const tools=process.env.PATH.split(':').find(p=>p.endsWith('/rootfs/usr/bin'));
if(!tools)throw Error('signed tools PATH missing');
if((fs.statSync('executable.cjs').mode&0o111)===0)throw Error('archive executable bit lost');
if((fs.statSync('addon.c').mode&0o111)!==0)throw Error('archive executable bit invented');
if(fs.readFileSync(path.join(path.dirname(process.execPath),'..','shared.txt'),'utf8')!=='shared runtime fixture')throw Error('shared runtime path changed');
if(child.execFileSync(path.join(tools,'fixture-node'),{encoding:'utf8'})!=='signed script')throw Error('signed script did not execute');
fs.mkdirSync('build/Release',{recursive:true});
child.execFileSync(path.join(tools,'cc'),['-B'+tools+'/','-fno-use-linker-plugin','-fno-lto','-shared','-fPIC','-nostdlib','addon.c','-o','build/Release/addon.node'],{stdio:'inherit'});
fs.writeFileSync('generated.js','module.exports=42;');
"#;
let source=br#"typedef void* napi_env;typedef void* napi_value;typedef unsigned long size_t;
extern int napi_create_string_utf8(napi_env,const char*,size_t,napi_value*);
__attribute__((visibility("default"))) napi_value napi_register_module_v1(napi_env env,napi_value exports){napi_value result;napi_create_string_utf8(env,"signed addon",12,&result);return result;}
"#;
fixture.package(json!({"name":"built","version":"1.0.0","optionalDependencies":{"unavailable":"1.0.0"},"scripts":{"install":"node build.cjs","prepare":"node must-not-run.cjs"}}),BTreeMap::from([("package/build.cjs".into(),script.to_vec()),("package/addon.c".into(),source.to_vec()),("package/executable.cjs".into(),b"#!/usr/bin/env node\n".to_vec())]));
fixture.package(json!({"name":"unavailable","version":"1.0.0","scripts":{"install":"node -e 'process.exit(7)'"},"bin":{"fixture-bin":"index.js"}}), BTreeMap::new());
let project = fixture.directory.0.join("project");
fs::DirBuilder::new().mode(0o700).create(&project).unwrap();
fs::create_dir(project.join("config")).unwrap();
fs::write(project.join("config/setting.json"), "{\"lib\":[]}").unwrap();
command::execute_with(
"add",
&project,
&["npm:built@1.0.0".into()],
fixture.target,
&fixture,
&fixture,
Some(&fixture.signed),
)
.unwrap();
let lock_file = fs::read_to_string(project.join("dever.lock")).unwrap();
let setting = fs::read(project.join("config/setting.json")).unwrap();
fixture.lock = Lock::decode(&lock_file).unwrap();
let files = fixture.install().unwrap();
assert_eq!(
files["node_modules/built/generated.js"],
b"module.exports=42;"
);
assert!(files["node_modules/built/build/Release/addon.node"].starts_with(b"\x7fELF"));
assert_eq!(fixture.graph().instances.len(), 1);
assert_eq!(
fixture.graph().instances[0].edges[0].omission,
Some(npm::Omission::BuildFailure)
);
assert_eq!(fixture.lock.libs.len(), 1);
let receipt = &fixture.lock.builds[0];
assert_eq!(
receipt["native_entries"],
json!(["node_modules/built/build/Release/addon.node"])
);
let output = receipt["output"]["sha256"].as_str().unwrap().to_owned();
let original = fixture.cache.borrow_mut().remove(&output).unwrap();
let sources = receipt["sources"].as_array().unwrap();
assert_eq!(sources.len(), 2);
let mut expected_urls = vec![];
for source in sources {
fixture
.cache
.borrow_mut()
.remove(source["sha256"].as_str().unwrap())
.unwrap();
expected_urls.push(source["source"]["locator"].as_str().unwrap().to_owned());
}
let lock_before = fixture.lock.encode().unwrap();
let calls = fixture.calls.borrow().len();
command::execute_with(
"install",
&project,
&[],
fixture.target,
&fixture,
&fixture,
Some(&fixture.signed),
)
.unwrap();
assert_eq!(fixture.cache.borrow()[&output], original);
assert_eq!(fixture.lock.encode().unwrap(), lock_before);
assert_eq!(
fs::read_to_string(project.join("dever.lock")).unwrap(),
lock_file
);
assert_eq!(
fs::read(project.join("config/setting.json")).unwrap(),
setting
);
let mut fetched = fixture.calls.borrow()[calls..].to_vec();
expected_urls.sort();
fetched.sort();
assert_eq!(fetched, expected_urls);
nondeterministic_replay_is_rejected(&mut fixture);
failed_native_build_is_not_published(&mut fixture);
}
fn nondeterministic_replay_is_rejected(fixture: &mut Fixture) {
fixture.lock = Lock::new(vec![]);
fixture.package(json!({"name":"unstable","version":"1.0.0","scripts":{"install":"node -e \"require('node:fs').writeFileSync('nonce',require('node:crypto').randomBytes(16))\""}}), BTreeMap::new());
fixture.resolve(&["npm:unstable@1.0.0"]).unwrap();
let mut lock = fixture.lock.clone();
fixture.reads.borrow_mut().clear();
npm::prepare(&mut lock, fixture.target, &fixture.signed, fixture).unwrap();
for lib in &lock.libs {
let artifact = &lib.artifacts[0];
let reads = fixture.reads.borrow();
let source_reads = reads
.iter()
.filter(|(hash, _)| hash == &artifact.sha256)
.collect::<Vec<_>>();
eprintln!(
"npm build: fixture={} bytes={} reads={}",
artifact.sha256,
artifact.bytes,
source_reads.len()
);
assert_eq!(source_reads.len(), 1);
assert_eq!(source_reads[0].1 as u64, artifact.bytes);
}
fixture.lock = lock;
let digest = fixture.lock.builds[0]["output"]["sha256"]
.as_str()
.unwrap()
.to_owned();
fixture.cache.borrow_mut().remove(&digest).unwrap();
let before = fixture.lock.encode().unwrap();
let cache_entries = fixture.cache.borrow().len();
let error = npm::restore(
&fixture.lock,
&fixture.graph(),
fixture.target,
&fixture.signed,
fixture,
fixture,
)
.unwrap_err();
assert!(error.contains("not byte reproducible"), "{error}");
assert_eq!(fixture.cache.borrow().len(), cache_entries);
assert_eq!(fixture.lock.encode().unwrap(), before);
}
fn failed_native_build_is_not_published(fixture: &mut Fixture) {
fixture.lock = Lock::new(vec![]);
fixture.package(json!({"name":"broken","version":"1.0.0","scripts":{"install":"node -e \"const fs=require('node:fs');fs.mkdirSync('build/Release',{recursive:true});fs.writeFileSync('build/Release/broken.node','invalid addon');\""}}), BTreeMap::new());
fixture.resolve(&["npm:broken@1.0.0"]).unwrap();
let mut lock = fixture.lock.clone();
let before = lock.encode().unwrap();
let cache_entries = fixture.cache.borrow().len();
let error = npm::prepare(&mut lock, fixture.target, &fixture.signed, fixture).unwrap_err();
assert!(error.contains("Lib build failed"), "{error}");
assert_eq!(lock.encode().unwrap(), before);
assert_eq!(fixture.cache.borrow().len(), cache_entries);
}
+195
View File
@@ -0,0 +1,195 @@
use super::super::super::Directory;
use base64::{Engine, engine::general_purpose::STANDARD};
use dever::compiler::{
project::dependency::{
Lock, Store,
library::{self, Spec, registry::Transport, runtime},
worker::signed::SignedResources,
},
toolchain::BuildTarget,
};
use ring::signature::{Ed25519KeyPair, KeyPair};
use serde_json::{Value, json};
use sha2::{Digest, Sha256, Sha512};
use std::{cell::RefCell, collections::BTreeMap, io::Cursor};
pub(super) fn hash(bytes: &[u8]) -> String {
format!("{:x}", Sha256::digest(bytes))
}
pub(super) fn archive(files: &BTreeMap<String, Vec<u8>>) -> Vec<u8> {
let gzip = flate2::GzBuilder::new()
.mtime(0)
.write(Vec::new(), flate2::Compression::fast());
let mut builder = tar::Builder::new(gzip);
for (path, bytes) in files {
let mut header = tar::Header::new_gnu();
header.set_mode(if bytes.starts_with(b"#!") {
0o755
} else {
0o644
});
header.set_size(bytes.len() as u64);
header.set_cksum();
builder
.append_data(&mut header, path, Cursor::new(bytes))
.unwrap();
}
builder.into_inner().unwrap().finish().unwrap()
}
pub(super) struct Fixture {
pub directory: Directory,
pub documents: BTreeMap<String, Vec<u8>>,
pub cache: RefCell<BTreeMap<String, Vec<u8>>>,
pub reads: RefCell<Vec<(String, usize)>>,
pub calls: RefCell<Vec<String>>,
pub metadata: runtime::Metadata,
pub target: BuildTarget,
pub signed: SignedResources,
pub lock: Lock,
}
impl Fixture {
pub fn new() -> Self {
let directory = Directory::new();
let target = BuildTarget::host().unwrap();
let metadata = json!({"pack":{"name":"node","version":"22.1.0","sha256":hash(b"node pack")},"target":target.name(),"python_markers":null,"python_wheel_tags":[],"npm_libc":"glibc"});
let signed = signed(
&directory,
&BTreeMap::from([(
format!("runtime/npm/{}/manifest.json", target.name()),
serde_json::to_vec(
&json!({"format":"dever-registry-runtime-v1","runtime":metadata}),
)
.unwrap(),
)]),
);
Self {
directory,
documents: BTreeMap::new(),
cache: RefCell::new(BTreeMap::new()),
reads: RefCell::new(vec![]),
calls: RefCell::new(vec![]),
metadata: serde_json::from_value(metadata).unwrap(),
target,
signed,
lock: Lock::new(vec![]),
}
}
pub fn package(&mut self, document: Value, extra: BTreeMap<String, Vec<u8>>) {
let name = document["name"].as_str().unwrap();
let version = document["version"].as_str().unwrap();
let base = format!("https://registry.npmjs.org/{name}");
let locator = format!("{base}/-/archive-{version}.tgz");
let mut files = extra;
files.insert(
"package/package.json".into(),
serde_json::to_vec(&document).unwrap(),
);
files.insert("package/index.js".into(), b"module.exports = 42;".to_vec());
let bytes = archive(&files);
let mut metadata = document.clone();
metadata["dist"] = json!({"tarball":locator,"integrity":format!("sha512-{}",STANDARD.encode(Sha512::digest(&bytes)))});
let mut index: Value = self
.documents
.get(&base)
.map(|bytes| serde_json::from_slice(bytes).unwrap())
.unwrap_or_else(|| json!({"name":name,"versions":{}}));
index["versions"][version] = metadata.clone();
self.documents
.insert(base.clone(), serde_json::to_vec(&index).unwrap());
self.documents.insert(
format!("{base}/{version}"),
serde_json::to_vec(&metadata).unwrap(),
);
self.documents.insert(locator, bytes);
}
pub fn resolve(&mut self, roots: &[&str]) -> Result<(), String> {
let specs = roots
.iter()
.map(|value| value.parse::<Spec>())
.collect::<Result<Vec<_>, _>>()?;
let mut lock = self.lock.clone();
library::npm::resolve(&specs, self.target, &self.metadata, self, self, &mut lock)?;
self.lock = lock;
Ok(())
}
pub fn graph(&self) -> library::npm::Environment {
serde_json::from_value(self.lock.npm[0].clone()).unwrap()
}
pub fn install(&self) -> Result<BTreeMap<String, Vec<u8>>, String> {
library::npm::install(
&self.lock,
&self.graph().roots,
self.target,
self,
&self.signed,
)
}
}
impl Transport for Fixture {
fn get(&self, url: &str, limit: usize) -> Result<Vec<u8>, String> {
library::registry::official(url)?;
self.calls.borrow_mut().push(url.into());
let bytes = self
.documents
.get(url)
.ok_or_else(|| "registry returned HTTP 404".to_owned())?;
assert!(bytes.len() <= limit);
Ok(bytes.clone())
}
}
impl Store for Fixture {
fn get(&self, digest: &str, _: u64) -> Result<Option<Vec<u8>>, String> {
let bytes = self.cache.borrow().get(digest).cloned();
self.reads
.borrow_mut()
.push((digest.into(), bytes.as_ref().map_or(0, Vec::len)));
Ok(bytes)
}
fn put(&self, bytes: &[u8]) -> Result<(), String> {
self.cache.borrow_mut().insert(hash(bytes), bytes.to_vec());
Ok(())
}
}
pub(super) fn signed(directory: &Directory, files: &BTreeMap<String, Vec<u8>>) -> SignedResources {
let root = directory.0.join("signed");
let key = Ed25519KeyPair::from_seed_unchecked(&[72; 32]).unwrap();
let hex = |bytes: &[u8]| {
bytes
.iter()
.map(|byte| format!("{byte:02x}"))
.collect::<String>()
};
let write = |path: &str, bytes: &[u8]| {
let path = root.join(path);
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
std::fs::write(path, bytes).unwrap();
};
write(
"state/trusted-release-key",
hex(key.public_key().as_ref()).as_bytes(),
);
let mut artifacts = vec![json!({"path":"base.txt","bytes":4,"sha256":hash(b"base")})];
let target = BuildTarget::host().unwrap();
for (path, bytes) in files {
artifacts.push(json!({"path":path,"bytes":bytes.len(),"sha256":hash(bytes)}));
write(&format!("versions/0.0.1/{path}"), bytes);
}
write("versions/0.0.1/base.txt", b"base");
let catalog=serde_json::to_vec(&json!({"format":"dever-release-v2","version":"0.0.1","platform":target.name(),"artifacts":artifacts,"extensions":[]})).unwrap();
write("versions/0.0.1/manifest.json", &catalog);
write(
"versions/0.0.1/manifest.sig",
hex(key.sign(&catalog).as_ref()).as_bytes(),
);
SignedResources::load(&root, "0.0.1").unwrap()
}
+276
View File
@@ -0,0 +1,276 @@
use super::Fixture;
use dever::compiler::project::dependency::library::npm::{self, Kind, Omission};
use serde_json::json;
use std::collections::BTreeMap;
#[test]
fn npm_scoped_metadata_uses_strict_official_url_policy() {
let mut fixture = Fixture::new();
fixture.package(
json!({"name":"app","version":"1.0.0","dependencies":{"@types/node":"^22"}}),
BTreeMap::new(),
);
fixture.package(
json!({"name":"@types/node","version":"22.10.0"}),
BTreeMap::new(),
);
fixture.resolve(&["npm:app@1.0.0"]).unwrap();
for address in [
"https://registry.npmjs.org/@types/node",
"https://registry.npmjs.org/@types/node/22.10.0",
"https://registry.npmjs.org/@types/node/-/archive-22.10.0.tgz",
] {
assert!(fixture.calls.borrow().iter().any(|url| url == address));
}
let graph = fixture.graph();
let node = graph
.instances
.iter()
.find(|node| node.spec.name == "@types/node")
.unwrap();
let files = fixture.install().unwrap();
assert_eq!(
files[&format!("{}/index.js", node.path)],
b"module.exports = 42;"
);
let lib = fixture
.lock
.libs
.iter()
.find(|lib| lib.spec.name == "@types/node")
.unwrap();
let mut source = lib.artifacts[0].source.clone().unwrap();
for locator in [
"https://registry.npmjs.org/@types%2fnode/-/archive-22.10.0.tgz",
"https://registry.npmjs.org/@types%2Fnode/-/archive-22.10.0.tgz",
"https://registry.npmjs.org/@types/node/%2e%2e/archive-22.10.0.tgz",
] {
source.locator = locator.into();
assert!(source.validate(&lib.spec).is_err());
}
}
#[test]
fn npm_nested_instances_keep_peer_context_and_install_offline() {
let mut fixture = Fixture::new();
for document in [
json!({"name":"left","version":"1.0.0","dependencies":{"plugin":"1.0.0","engine":"1.0.0"}}),
json!({"name":"right","version":"1.0.0","dependencies":{"plugin":"1.0.0","engine":"2.0.0"}}),
json!({"name":"plugin","version":"1.0.0","peerDependencies":{"engine":">=1 <3"}}),
json!({"name":"engine","version":"1.0.0"}),
json!({"name":"engine","version":"2.0.0"}),
] {
fixture.package(document, BTreeMap::new());
}
fixture
.resolve(&["npm:left@1.0.0", "npm:right@1.0.0"])
.unwrap();
let graph = fixture.graph();
for root in ["left", "right"] {
let plugin = graph
.instances
.iter()
.find(|node| node.path == format!("node_modules/{root}/node_modules/plugin"))
.unwrap();
assert_eq!(plugin.edges[0].kind, Kind::Peer);
assert_eq!(
plugin.edges[0].target.as_deref(),
Some(format!("node_modules/{root}/node_modules/engine").as_str())
);
}
fixture.documents.clear();
let calls = fixture.calls.borrow().len();
let files = fixture.install().unwrap();
assert_eq!(files.len(), 12);
assert_eq!(fixture.calls.borrow().len(), calls);
assert_eq!(fixture.lock.libs.len(), 5);
}
#[test]
fn npm_peer_conflict_backtracks_versions_and_incompatible_roots_fail_atomically() {
let mut fixture = Fixture::new();
for document in [
json!({"name":"app","version":"1.0.0","dependencies":{"plugin":"*","engine":"1.0.0"}}),
json!({"name":"plugin","version":"2.0.0","peerDependencies":{"engine":"2.0.0"}}),
json!({"name":"plugin","version":"1.0.0","peerDependencies":{"engine":"1.0.0"}}),
json!({"name":"engine","version":"1.0.0"}),
json!({"name":"engine","version":"2.0.0"}),
] {
fixture.package(document, BTreeMap::new());
}
fixture.resolve(&["npm:app@1.0.0"]).unwrap();
assert_eq!(
fixture
.graph()
.instances
.iter()
.find(|node| node.spec.name == "plugin")
.unwrap()
.spec
.version,
"1.0.0"
);
let before = fixture.lock.encode().unwrap();
assert!(
fixture
.resolve(&["npm:engine@1.0.0", "npm:engine@2.0.0"])
.unwrap_err()
.contains("incompatible")
);
assert_eq!(fixture.lock.encode().unwrap(), before);
}
#[test]
fn npm_optional_target_and_absent_peer_are_explicit_but_integrity_errors_are_fatal() {
let mut fixture = Fixture::new();
fixture.package(json!({"name":"app","version":"1.0.0","optionalDependencies":{"wrong":"*","missing":"*"},"peerDependencies":{"peer":"*"},"peerDependenciesMeta":{"peer":{"optional":true}}}),BTreeMap::new());
fixture.package(
json!({"name":"wrong","version":"1.0.0","os":["!linux"]}),
BTreeMap::new(),
);
fixture.resolve(&["npm:app@1.0.0"]).unwrap();
let graph = fixture.graph();
let edges = &graph.instances[0].edges;
for (name, reason) in [
("wrong", Omission::Target),
("missing", Omission::Unavailable),
("peer", Omission::OptionalPeerAbsent),
] {
assert_eq!(
edges
.iter()
.find(|edge| edge.name == name)
.unwrap()
.omission,
Some(reason)
);
}
fixture.install().unwrap();
let before = fixture.lock.encode().unwrap();
assert!(
fixture
.resolve(&["npm:missing@1.0.0"])
.unwrap_err()
.contains("no npm version")
);
assert_eq!(fixture.lock.encode().unwrap(), before);
fixture.documents.insert(
"https://registry.npmjs.org/wrong/-/archive-1.0.0.tgz".into(),
b"corrupt optional archive".to_vec(),
);
assert!(
fixture
.resolve(&["npm:app@1.0.0"])
.unwrap_err()
.contains("integrity")
);
assert_eq!(fixture.lock.encode().unwrap(), before);
}
#[test]
fn npm_one_version_cannot_change_bytes_between_worker_groups() {
let mut fixture = Fixture::new();
let metadata = json!({"name":"shared","version":"1.0.0"});
fixture.package(metadata.clone(), BTreeMap::new());
fixture.resolve(&["npm:shared@1.0.0"]).unwrap();
let before = fixture.lock.encode().unwrap();
fixture.package(
metadata,
BTreeMap::from([("package/changed.js".into(), b"different bytes".to_vec())]),
);
let error = fixture.resolve(&["npm:shared@1.0.0"]).unwrap_err();
assert!(error.contains("artifact differs"), "{error}");
assert_eq!(fixture.lock.encode().unwrap(), before);
}
#[test]
fn npm_bundled_packages_have_one_owner_and_tampered_manifest_or_graph_fails() {
let mut fixture = Fixture::new();
fixture.package(json!({"name":"app","version":"1.0.0","dependencies":{"tiny":"1.0.0"},"bundleDependencies":["tiny"]}),BTreeMap::from([
("package/node_modules/tiny/package.json".into(),serde_json::to_vec(&json!({"name":"tiny","version":"1.0.0"})).unwrap()),
("package/node_modules/tiny/index.js".into(),b"module.exports=7".to_vec()),
]));
fixture.resolve(&["npm:app@1.0.0"]).unwrap();
assert_eq!(fixture.lock.libs.len(), 1);
assert_eq!(fixture.install().unwrap().len(), 4);
let original = fixture.lock.npm[0].clone();
fixture.lock.npm[0]["instances"][0]["edges"][0]["requirement"] = json!("2.0.0");
assert!(fixture.install().unwrap_err().contains("range"));
fixture.lock.npm[0] = original;
let mut graph = fixture.graph();
graph.instances[1].source.prefix = "package/".into();
assert!(
npm::validate(&graph, &fixture.lock)
.unwrap_err()
.contains("owned")
);
}
#[test]
fn npm_registry_and_archive_metadata_must_agree_and_urls_are_official() {
let mut fixture = Fixture::new();
fixture.package(json!({"name":"app","version":"1.0.0"}), BTreeMap::new());
let address = "https://registry.npmjs.org/app/1.0.0";
let original = fixture.documents[address].clone();
let mut metadata: serde_json::Value = serde_json::from_slice(&original).unwrap();
metadata["dependencies"] = json!({"hidden":"1.0.0"});
fixture
.documents
.insert(address.into(), serde_json::to_vec(&metadata).unwrap());
assert!(
fixture
.resolve(&["npm:app@1.0.0"])
.unwrap_err()
.contains("metadata")
);
let mut metadata: serde_json::Value = serde_json::from_slice(&original).unwrap();
metadata["dist"]["tarball"] = json!("https://attacker.invalid/app.tgz");
fixture
.documents
.insert(address.into(), serde_json::to_vec(&metadata).unwrap());
assert!(
fixture
.resolve(&["npm:app@1.0.0"])
.unwrap_err()
.contains("official")
);
assert!(
!fixture
.calls
.borrow()
.iter()
.any(|url| url.contains("attacker"))
);
}
#[test]
fn npm_cycles_close_without_infinite_nested_copies_and_missing_build_receipt_fails() {
let mut fixture = Fixture::new();
fixture.package(
json!({"name":"alpha","version":"1.0.0","dependencies":{"beta":"1.0.0"}}),
BTreeMap::new(),
);
fixture.package(
json!({"name":"beta","version":"1.0.0","dependencies":{"alpha":"1.0.0"}}),
BTreeMap::new(),
);
fixture.resolve(&["npm:alpha@1.0.0"]).unwrap();
assert_eq!(fixture.graph().instances.len(), 2);
assert_eq!(fixture.install().unwrap().len(), 4);
fixture.package(
json!({"name":"hook","version":"1.0.0","scripts":{"install":"must not run"}}),
BTreeMap::new(),
);
fixture.resolve(&["npm:hook@1.0.0"]).unwrap();
let graph = fixture
.lock
.npm
.iter()
.find(|value| value["roots"][0]["name"] == "hook")
.unwrap()
.clone();
fixture.lock.npm = vec![graph];
assert!(fixture.install().unwrap_err().contains("lifecycle"));
fixture.lock.npm[0]["build"] = json!("0".repeat(64));
assert!(fixture.install().unwrap_err().contains("receipt"));
}
+10
View File
@@ -0,0 +1,10 @@
//! 注入 registry、签名 catalog 与真实 tgz;不运行宿主 Node/npm,不访问公网。
mod build;
mod fixture;
mod graph;
mod process;
mod receipt;
mod tools;
use fixture::Fixture;
@@ -0,0 +1,98 @@
//! 共享执行器回归使用 Pod 内的真实子进程,不依赖线上服务。
use super::super::super::Directory;
use dever::compiler::project::dependency::library::build::process;
use std::{fs, path::Path, process::Command};
fn child_gone(path: &Path) {
let pid = fs::read_to_string(path).unwrap();
assert!(
!Path::new("/proc").join(pid.trim()).exists(),
"child {pid} was not reaped"
);
}
#[test]
fn lib_build_output_overflow_drains_pipes_and_reaps_its_group() {
let directory = Directory::new();
let mut command = Command::new("/bin/sh");
command.current_dir(&directory.0).args(["-c", "echo $$ > parent; /bin/sleep 300 & echo $! > child; /usr/bin/head -c 33554432 /dev/zero; wait"]);
let error = process::run(command, &directory.0).unwrap_err();
assert!(error.contains("16 MiB"), "{error}");
child_gone(&directory.0.join("parent"));
child_gone(&directory.0.join("child"));
assert!(
fs::read_dir(&directory.0)
.unwrap()
.all(|entry| entry.unwrap().metadata().unwrap().len() < 32)
);
}
#[test]
fn lib_build_success_reaps_descendants_holding_output_open() {
let directory = Directory::new();
let mut command = Command::new("/bin/sh");
command
.current_dir(&directory.0)
.args(["-c", "/bin/sleep 300 & echo $! > child; exit 0"]);
assert!(process::run(command, &directory.0).unwrap().success());
child_gone(&directory.0.join("child"));
}
#[test]
fn lib_build_scratch_budget_kills_and_reaps_before_returning() {
let directory = Directory::new();
let mut command = Command::new("/bin/sh");
command.current_dir(&directory.0).args(["-c", "echo $$ > parent; /bin/sleep 300 & echo $! > child; /usr/bin/truncate -s 536870913 excess; wait"]);
let error = process::run(command, &directory.0).unwrap_err();
assert!(error.contains("512 MiB"), "{error}");
child_gone(&directory.0.join("parent"));
child_gone(&directory.0.join("child"));
}
#[test]
fn lib_build_probe_distinguishes_unloadable_exit_and_signal_from_budget_errors() {
let directory = Directory::new();
for script in ["exit 10", "ulimit -c 0; kill -SEGV $$"] {
let mut command = Command::new("/bin/sh");
command.args(["-c", script]);
let status = process::status(command, &directory.0).unwrap();
assert!(!status.success());
}
let mut command = Command::new("/bin/sh");
command.args(["-c", "exit 10"]);
assert!(
process::run(command, &directory.0)
.unwrap_err()
.contains("Lib build failed")
);
}
#[test]
fn lib_build_script_header_cannot_select_host_tools_or_inject_arguments() {
let fixture = super::Fixture::new();
for header in [
"#!/bin/bash",
"#!/usr/bin/env node --eval unsafe",
"#!/tmp/untrusted",
] {
let mut stage = dever::compiler::project::dependency::library::build::stage::Stage::new(
&fixture.signed,
)
.unwrap();
stage
.write(
"input/rootfs/usr/bin/script",
format!("{header}\nexit 0\n").into_bytes(),
true,
)
.unwrap();
let tools = ["rootfs/usr/bin/script".to_owned()].into_iter().collect();
assert!(
stage
.prepare_tools(&tools)
.unwrap_err()
.contains("signed interpreter")
);
}
}
+260
View File
@@ -0,0 +1,260 @@
use super::{
Fixture,
fixture::{archive, hash, signed},
};
use dever::compiler::project::dependency::{
Lock, Store,
library::{
Spec,
lock::{Artifact, Library, Runtime},
npm::Environment,
},
};
use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
use std::collections::{BTreeMap, BTreeSet};
// Fixture 按既有 v6 协议编码,独立于实现中的私有 Receipt 类型。
#[derive(Deserialize, Serialize)]
struct Inputs {
format: String,
toolchain: String,
libs: Vec<Library>,
workers: Vec<Value>,
npm: Vec<Environment>,
}
#[derive(Deserialize, Serialize)]
struct Tools {
format: String,
ecosystem: String,
target: String,
runtime_sha256: String,
python_runtime_sha256: Option<String>,
sha256: String,
}
#[derive(Serialize)]
struct Receipt {
ecosystem: String,
roots: Vec<Spec>,
sources: Vec<Artifact>,
graph: Option<Environment>,
runtime: Runtime,
auxiliary_runtime: Option<Runtime>,
native_entries: BTreeSet<String>,
native_rejections: BTreeMap<String, String>,
python_markers: Option<Value>,
tools: Tools,
frontend: String,
python: Option<Value>,
dynamic_requires: Vec<String>,
inputs: Inputs,
config_settings: BTreeMap<String, Value>,
output: Artifact,
}
fn prepare(fixture: &mut Fixture, addons: bool) -> Receipt {
let node_pack = archive(&BTreeMap::from([(
"fixture/node.txt".into(),
b"author fixture runtime".to_vec(),
)]));
let python_pack = archive(&BTreeMap::from([(
"fixture/python.txt".into(),
b"author fixture auxiliary runtime".to_vec(),
)]));
let build_pack = archive(&BTreeMap::from([(
"fixture/tools.txt".into(),
b"already prepared output; no hooks run".to_vec(),
)]));
fixture.metadata.pack.sha256 = hash(&node_pack);
let mut extra = BTreeMap::new();
if addons {
extra.insert(
"package/foreign.node".into(),
b"prebuilt foreign format".to_vec(),
);
}
fixture.package(
json!({"name":"built","version":"1.0.0","scripts":{"install":"must never run"}}),
extra,
);
fixture.resolve(&["npm:built@1.0.0"]).unwrap();
let graph = fixture.graph();
let mut output = BTreeMap::from([
(
"node_modules/built/package.json".into(),
serde_json::to_vec(
&json!({"name":"built","version":"1.0.0","scripts":{"install":"must never run"}}),
)
.unwrap(),
),
(
"node_modules/built/generated.js".into(),
b"module.exports=42".to_vec(),
),
]);
if addons {
output.insert(
"node_modules/built/foreign.node".into(),
b"prebuilt foreign format".to_vec(),
);
}
let bytes = archive(&output);
fixture.put(&bytes).unwrap();
let python = Runtime {
name: "python".into(),
version: "3.12.1".into(),
sha256: hash(&python_pack),
};
let tools = Tools {
format: "dever-registry-build-v1".into(),
ecosystem: "npm".into(),
target: fixture.target.name().into(),
runtime_sha256: fixture.metadata.pack.sha256.clone(),
python_runtime_sha256: Some(python.sha256.clone()),
sha256: hash(&build_pack),
};
let target = fixture.target.name();
let files=BTreeMap::from([
(format!("runtime/npm/{target}/runtime.pack"),node_pack),
(format!("runtime/npm/{target}/manifest.json"),serde_json::to_vec(&json!({"format":"dever-registry-runtime-v1","runtime":{"pack":fixture.metadata.pack,"target":target,"python_markers":null,"python_wheel_tags":[],"npm_libc":"glibc"}})).unwrap()),
(format!("runtime/pip/{target}/runtime.pack"),python_pack),
(format!("runtime/pip/{target}/manifest.json"),serde_json::to_vec(&json!({"format":"dever-registry-runtime-v1","runtime":{"pack":python,"target":target,"python_markers":{"implementation_name":"cpython","implementation_version":"3.12.1","os_name":"posix","platform_machine":"x86_64","platform_python_implementation":"CPython","platform_release":"","platform_system":"Linux","platform_version":"","python_full_version":"3.12.1","python_version":"3.12","sys_platform":"linux"},"python_wheel_tags":["py3-none-any"]}})).unwrap()),
(format!("build/npm/{target}/build.pack"),build_pack),
(format!("build/npm/{target}/manifest.json"),serde_json::to_vec(&tools).unwrap()),
]);
fixture.signed = signed(&fixture.directory, &files);
Receipt {
ecosystem: "npm".into(),
roots: graph.roots.clone(),
sources: fixture
.lock
.libs
.iter()
.flat_map(|lib| lib.artifacts.clone())
.collect(),
graph: Some(graph),
runtime: fixture.metadata.pack.clone(),
auxiliary_runtime: Some(python),
native_entries: BTreeSet::new(),
native_rejections: if addons {
BTreeMap::from([("node_modules/built/foreign.node".into(), "format".into())])
} else {
BTreeMap::new()
},
python_markers: None,
tools,
frontend: "cfe67ebbc939762cd4ba7328943bf97a72d06a4b79071f056ebd8e5ab566a721".into(),
python: None,
dynamic_requires: vec![],
inputs: serde_json::from_value(serde_json::to_value(&fixture.lock).unwrap()).unwrap(),
config_settings: BTreeMap::new(),
output: Artifact {
target: target.into(),
path: format!("build/npm/{}/installation.tgz", hash(&bytes)),
bytes: bytes.len() as u64,
sha256: hash(&bytes),
source: None,
},
}
}
fn attach(fixture: &mut Fixture, receipt: &Receipt) {
fixture.lock.npm[0]["build"] = json!(hash(&serde_json::to_vec(receipt).unwrap()));
fixture.lock.builds = vec![serde_json::to_value(receipt).unwrap()];
}
#[test]
fn npm_lifecycle_receipt_consumes_signed_inputs_without_network_or_hooks() {
let mut fixture = Fixture::new();
let receipt = prepare(&mut fixture, false);
attach(&mut fixture, &receipt);
fixture.documents.clear();
let calls = fixture.calls.borrow().len();
let files = fixture.install().unwrap();
assert_eq!(
files["node_modules/built/generated.js"],
b"module.exports=42"
);
assert_eq!(fixture.calls.borrow().len(), calls);
let text = fixture.lock.encode().unwrap();
let reparsed = Lock::decode(&text).unwrap();
fixture.lock = reparsed;
fixture.install().unwrap();
fixture.lock.builds[0]["frontend"] = json!("0".repeat(64));
assert!(fixture.install().unwrap_err().contains("receipt"));
}
#[test]
fn npm_native_receipt_accounts_for_all_addons_and_never_admits_failed_build() {
let mut fixture = Fixture::new();
let mut receipt = prepare(&mut fixture, true);
attach(&mut fixture, &receipt);
assert!(
!fixture
.install()
.unwrap()
.keys()
.any(|path| path.ends_with(".node"))
);
receipt.native_rejections.clear();
attach(&mut fixture, &receipt);
assert!(fixture.install().unwrap_err().contains("admission"));
receipt
.native_entries
.insert("node_modules/built/foreign.node".into());
attach(&mut fixture, &receipt);
assert!(fixture.install().unwrap_err().contains("ELF"));
receipt.native_entries.clear();
receipt
.native_rejections
.insert("node_modules/built/new.node".into(), "unloadable".into());
attach(&mut fixture, &receipt);
assert!(fixture.install().unwrap_err().contains("admission"));
}
#[test]
fn npm_replay_rejects_malformed_binding_before_fetching_or_executing() {
let mut fixture = Fixture::new();
let mut receipt = prepare(&mut fixture, false);
receipt.tools.runtime_sha256 = "0".repeat(64);
attach(&mut fixture, &receipt);
fixture.cache.borrow_mut().clear();
let calls = fixture.calls.borrow().len();
let error = dever::compiler::project::dependency::library::npm::restore(
&fixture.lock,
&fixture.graph(),
fixture.target,
&fixture.signed,
&fixture,
&fixture,
)
.unwrap_err();
assert!(error.contains("binding differs"), "{error}");
assert_eq!(fixture.calls.borrow().len(), calls);
assert!(fixture.cache.borrow().is_empty());
}
#[test]
fn npm_cached_receipt_rejects_tampering_but_needs_no_build_tools() {
let mut fixture = Fixture::new();
let receipt = prepare(&mut fixture, false);
attach(&mut fixture, &receipt);
let original = fixture.cache.borrow()[&receipt.output.sha256].clone();
fixture
.cache
.borrow_mut()
.insert(receipt.output.sha256.clone(), b"tampered".to_vec());
assert!(fixture.install().unwrap_err().contains("digest"));
fixture
.cache
.borrow_mut()
.insert(receipt.output.sha256.clone(), original);
std::fs::remove_file(fixture.directory.0.join(format!(
"signed/versions/0.0.1/build/npm/{}/build.pack",
fixture.target.name()
)))
.unwrap();
fixture.install().unwrap();
}
+110
View File
@@ -0,0 +1,110 @@
//! 只在开发 Pod 执行:读取已安装工具,组装临时签名输入;不安装或激活软件。
use super::{
Fixture,
fixture::{archive, hash, signed},
};
use serde_json::json;
use std::{collections::BTreeMap, fs, path::Path, process::Command};
pub(super) fn prepare(fixture: &mut Fixture) {
let target = fixture.target.name();
let mut assets = BTreeMap::new();
let mut dependencies = BTreeMap::new();
let node = Path::new("/usr/local/bin/node");
let python = Path::new("/usr/bin/python3");
let node_bytes = fs::read(node).expect("Node must already exist in the development Pod");
let python_bytes = fs::read(python).expect("Python must already exist in the development Pod");
libraries(node, &mut dependencies);
libraries(python, &mut dependencies);
let markers = json!({"implementation_name":"cpython","implementation_version":"3.13.0","os_name":"posix","platform_machine":"x86_64","platform_python_implementation":"CPython","platform_release":"","platform_system":"Linux","platform_version":"","python_full_version":"3.13.0","python_version":"3.13","sys_platform":"linux"});
let node_pack=archive(&BTreeMap::from([
("bin/node".into(),node_bytes),
("shared.txt".into(),b"shared runtime fixture".to_vec()),
("dever-runtime.json".into(),serde_json::to_vec(&json!({"format":"dever-worker-runtime-v1","ecosystem":"npm","target":target,"executable":"bin/node","arguments":[],"build":null,"python_wheel_tags":[],"python_extension_suffixes":[],"python_markers":null})).unwrap()),
]));
let python_pack=archive(&BTreeMap::from([
("bin/python3".into(),python_bytes),
("shared.txt".into(),b"shared runtime fixture".to_vec()),
("dever-runtime.json".into(),serde_json::to_vec(&json!({"format":"dever-worker-runtime-v1","ecosystem":"pip","target":target,"executable":"bin/python3","arguments":["-I","-S","-B"],"build":null,"python_wheel_tags":["py3-none-any"],"python_extension_suffixes":[".cpython-313-x86_64-linux-gnu.so"],"python_markers":markers})).unwrap()),
]));
fixture.metadata.pack.sha256 = hash(&node_pack);
fixture.metadata.pack.version = "24.21.0".into();
let python_identity = json!({"name":"python","version":"3.13.0","sha256":hash(&python_pack)});
let mut tools = BTreeMap::new();
let mut executables = Vec::new();
for (name, path) in [
("sh", "/bin/sh"),
("cc", "/usr/bin/cc"),
("c++", "/usr/bin/c++"),
("as", "/usr/bin/as"),
("ld", "/usr/bin/ld"),
("ar", "/usr/bin/ar"),
("ranlib", "/usr/bin/ranlib"),
("make", "/usr/bin/make"),
("cc1", "/usr/libexec/gcc/x86_64-linux-gnu/14/cc1"),
("collect2", "/usr/libexec/gcc/x86_64-linux-gnu/14/collect2"),
] {
let path = Path::new(path);
libraries(path, &mut dependencies);
let destination = format!("rootfs/usr/bin/{name}");
tools.insert(destination.clone(), fs::read(path).unwrap());
executables.push(destination);
}
files(
Path::new("/usr/local/lib/node_modules/npm"),
"npm/frontend/node_modules/npm/",
&mut tools,
);
tools.insert(
"rootfs/usr/bin/fixture-node".into(),
b"#!/usr/bin/env node\nprocess.stdout.write('signed script');\n".to_vec(),
);
executables.push("rootfs/usr/bin/fixture-node".into());
tools.insert("dever-build.json".into(),serde_json::to_vec(&json!({"format":"dever-lib-build-v1","ecosystem":"npm","target":target,"runtime_sha256":fixture.metadata.pack.sha256,"python_runtime_sha256":python_identity["sha256"],"executables":executables})).unwrap());
let tools_pack = archive(&tools);
assets.insert(format!("build/npm/{target}/manifest.json"),serde_json::to_vec(&json!({"format":"dever-registry-build-v1","ecosystem":"npm","target":target,"runtime_sha256":fixture.metadata.pack.sha256,"python_runtime_sha256":python_identity["sha256"],"sha256":hash(&tools_pack)})).unwrap());
assets.insert(format!("build/npm/{target}/build.pack"), tools_pack);
assets.insert(format!("runtime/npm/{target}/runtime.pack"), node_pack);
assets.insert(format!("runtime/pip/{target}/runtime.pack"), python_pack);
assets.insert(format!("runtime/npm/{target}/manifest.json"),serde_json::to_vec(&json!({"format":"dever-registry-runtime-v1","runtime":{"pack":fixture.metadata.pack,"target":target,"python_markers":null,"python_wheel_tags":[],"npm_libc":"glibc"}})).unwrap());
assets.insert(format!("runtime/pip/{target}/manifest.json"),serde_json::to_vec(&json!({"format":"dever-registry-runtime-v1","runtime":{"pack":python_identity,"target":target,"python_markers":markers,"python_wheel_tags":["py3-none-any"]}})).unwrap());
for (name, bytes) in dependencies {
assets.insert(format!("process-runtime/{target}/lib/{name}"), bytes);
}
fixture.signed = signed(&fixture.directory, &assets);
}
fn files(root: &Path, prefix: &str, output: &mut BTreeMap<String, Vec<u8>>) {
for entry in fs::read_dir(root).unwrap() {
let entry = entry.unwrap();
let name = entry.file_name().into_string().unwrap();
let kind = entry.file_type().unwrap();
if kind.is_dir() {
files(&entry.path(), &format!("{prefix}{name}/"), output);
} else if kind.is_file() {
output.insert(format!("{prefix}{name}"), fs::read(entry.path()).unwrap());
}
}
}
fn libraries(program: &Path, files: &mut BTreeMap<String, Vec<u8>>) {
let output = Command::new("/usr/bin/ldd").arg(program).output().unwrap();
assert!(
output.status.success(),
"ldd fixture input {}",
program.display()
);
for path in String::from_utf8(output.stdout)
.unwrap()
.split_whitespace()
.filter(|value| value.starts_with('/'))
{
let path = Path::new(path);
let name = path.file_name().unwrap().to_str().unwrap().to_owned();
let bytes = fs::read(path).unwrap();
if let Some(previous) = files.insert(name, bytes.clone()) {
assert_eq!(previous, bytes);
}
}
}