Files
core/test/rust/native/dependency/npm/receipt.rs
T
2026-11-03 03:32:07 +00:00

261 lines
9.0 KiB
Rust

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();
}