1408 lines
47 KiB
Rust
1408 lines
47 KiB
Rust
use std::collections::BTreeSet;
|
|
use std::ffi::OsString;
|
|
use std::fs;
|
|
#[cfg(unix)]
|
|
use std::io::Read;
|
|
use std::io::Write;
|
|
use std::path::{Path, PathBuf};
|
|
use std::process::{Child, Command};
|
|
use std::sync::atomic::{AtomicU64, Ordering};
|
|
|
|
use dever_cli::toolchain::{
|
|
Artifact, BuildIdentity, CacheStore, CommandResult, Layout, MachineManager, PlatformContract,
|
|
ReleaseManifest, Version, artifact_get, artifact_put, execute_in, platform_contract,
|
|
};
|
|
use ring::rand::SystemRandom;
|
|
use ring::signature::{Ed25519KeyPair, KeyPair};
|
|
use sha2::{Digest, Sha256};
|
|
|
|
static NEXT_TEMPORARY: AtomicU64 = AtomicU64::new(0);
|
|
|
|
#[cfg(unix)]
|
|
#[path = "shared_toolchain_extensions.rs"]
|
|
mod extensions;
|
|
|
|
#[cfg(target_os = "linux")]
|
|
#[path = "project_skill.rs"]
|
|
mod project_skill;
|
|
|
|
struct TemporaryDirectory(PathBuf);
|
|
|
|
impl TemporaryDirectory {
|
|
fn new() -> Self {
|
|
loop {
|
|
let path = std::env::temp_dir().join(format!(
|
|
"dever-toolchain-test-{}-{}",
|
|
std::process::id(),
|
|
NEXT_TEMPORARY.fetch_add(1, Ordering::Relaxed),
|
|
));
|
|
match fs::create_dir(&path) {
|
|
Ok(()) => return Self(path),
|
|
Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => continue,
|
|
Err(error) => panic!("cannot create test directory: {error}"),
|
|
}
|
|
}
|
|
}
|
|
|
|
fn path(&self) -> &Path {
|
|
&self.0
|
|
}
|
|
}
|
|
|
|
impl Drop for TemporaryDirectory {
|
|
fn drop(&mut self) {
|
|
let _ = fs::remove_dir_all(&self.0);
|
|
}
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
struct TestDaemon(Child);
|
|
|
|
#[cfg(unix)]
|
|
impl TestDaemon {
|
|
fn start(layout: &Layout) -> Self {
|
|
let child = Command::new(env!("CARGO_BIN_EXE_deverd"))
|
|
.env_clear()
|
|
.args(["--root", layout.root().to_str().unwrap()])
|
|
.spawn()
|
|
.unwrap();
|
|
let mut daemon = Self(child);
|
|
for _ in 0..100 {
|
|
if dever_cli::toolchain::cache_status(layout).is_ok() {
|
|
return daemon;
|
|
}
|
|
assert!(
|
|
daemon.0.try_wait().unwrap().is_none(),
|
|
"deverd exited before becoming ready"
|
|
);
|
|
std::thread::sleep(std::time::Duration::from_millis(10));
|
|
}
|
|
panic!("deverd did not publish its endpoint");
|
|
}
|
|
|
|
fn stop(&mut self) {
|
|
self.0.kill().unwrap();
|
|
self.0.wait().unwrap();
|
|
}
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
impl Drop for TestDaemon {
|
|
fn drop(&mut self) {
|
|
let _ = self.0.kill();
|
|
let _ = self.0.wait();
|
|
}
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
fn raw_request(layout: &Layout, token: &str, operation: &str) -> serde_json::Value {
|
|
raw_operation(layout, token, serde_json::Value::String(operation.into()))
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
fn raw_operation(layout: &Layout, token: &str, operation: serde_json::Value) -> serde_json::Value {
|
|
use std::os::unix::net::UnixStream;
|
|
|
|
let mut stream = UnixStream::connect(layout.service_socket()).unwrap();
|
|
stream
|
|
.set_read_timeout(Some(std::time::Duration::from_secs(2)))
|
|
.unwrap();
|
|
let payload = serde_json::to_vec(&serde_json::json!({
|
|
"token": token,
|
|
"operation": operation,
|
|
"installed": [],
|
|
}))
|
|
.unwrap();
|
|
stream
|
|
.write_all(&(payload.len() as u32).to_be_bytes())
|
|
.unwrap();
|
|
stream.write_all(&payload).unwrap();
|
|
read_response(&mut stream)
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
#[test]
|
|
fn compilation_control_rejects_client_ir_paths_and_oversized_bodies() {
|
|
let temporary = TemporaryDirectory::new();
|
|
let layout = Layout::new(temporary.path().join("machine"));
|
|
layout.initialize().unwrap();
|
|
let _daemon = TestDaemon::start(&layout);
|
|
let token = fs::read_to_string(layout.service_token()).unwrap();
|
|
for extra in ["ir", "output", "cache_key", "compiler"] {
|
|
let mut operation = serde_json::json!({"compile":{"bytes":1}});
|
|
operation["compile"][extra] = serde_json::json!("caller-controlled");
|
|
let response = raw_operation(&layout, token.trim(), operation);
|
|
assert_eq!(response["ok"], false);
|
|
assert!(
|
|
response["error"]
|
|
.as_str()
|
|
.unwrap()
|
|
.contains("unknown field"),
|
|
"{response}"
|
|
);
|
|
}
|
|
for bytes in [0, 384 * 1024 * 1024 + 1, u64::MAX] {
|
|
let response = raw_operation(
|
|
&layout,
|
|
token.trim(),
|
|
serde_json::json!({"compile":{"bytes":bytes}}),
|
|
);
|
|
assert_eq!(response["ok"], false);
|
|
assert!(
|
|
response["error"]
|
|
.as_str()
|
|
.unwrap()
|
|
.contains("transfer limit")
|
|
);
|
|
}
|
|
assert_eq!(
|
|
dever_cli::toolchain::cache_status(&layout).unwrap().entries,
|
|
0
|
|
);
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
fn read_response(stream: &mut std::os::unix::net::UnixStream) -> serde_json::Value {
|
|
let mut header = [0_u8; 4];
|
|
stream.read_exact(&mut header).unwrap();
|
|
let mut payload = vec![0; u32::from_be_bytes(header) as usize];
|
|
stream.read_exact(&mut payload).unwrap();
|
|
serde_json::from_slice(&payload).unwrap()
|
|
}
|
|
|
|
#[test]
|
|
fn runtime_accepts_only_an_exact_project_toolchain_version() {
|
|
let temporary = TemporaryDirectory::new();
|
|
let config = temporary.path().join("config");
|
|
fs::create_dir(&config).unwrap();
|
|
fs::write(
|
|
config.join("setting.json"),
|
|
r#"{"dever":{"version":"1.2.3"}}"#,
|
|
)
|
|
.unwrap();
|
|
let settings = dever_runtime::config::Settings::load_project(temporary.path()).unwrap();
|
|
assert_eq!(settings.dever_version(), Some("1.2.3"));
|
|
|
|
for invalid in [
|
|
r#"{"dever":{"version":"latest"}}"#,
|
|
r#"{"dever":{"version":"01.2.3"}}"#,
|
|
r#"{"dever":{"version":"1.2"}}"#,
|
|
r#"{"dever":{"version":"1.2.3","channel":"stable"}}"#,
|
|
r#"{"dever":{"version":"1.2.3","version":"2.0.0"}}"#,
|
|
] {
|
|
fs::write(config.join("setting.json"), invalid).unwrap();
|
|
assert!(
|
|
dever_runtime::config::Settings::load_project(temporary.path()).is_err(),
|
|
"accepted {invalid}"
|
|
);
|
|
}
|
|
assert!(Version::parse("1.10.0").unwrap() > Version::parse("1.2.0").unwrap());
|
|
}
|
|
|
|
#[test]
|
|
fn all_supported_platforms_have_one_machine_entry_and_service_contract() {
|
|
for (os, expected) in [
|
|
("linux", ("/opt/dever", "/usr/local/bin/dever")),
|
|
(
|
|
"macos",
|
|
("/Library/Application Support/Dever", "/usr/local/bin/dever"),
|
|
),
|
|
(
|
|
"windows",
|
|
(
|
|
r"C:\Program Files\Dever",
|
|
r"C:\Program Files\Dever\bin\dever.exe",
|
|
),
|
|
),
|
|
] {
|
|
let PlatformContract {
|
|
machine_root,
|
|
public_entry,
|
|
service_definition,
|
|
ipc_endpoint,
|
|
} = platform_contract(os).expect("supported platform contract");
|
|
assert_eq!((machine_root, public_entry), expected);
|
|
assert!(!service_definition.is_empty());
|
|
assert!(!ipc_endpoint.is_empty());
|
|
}
|
|
assert!(platform_contract("freebsd").is_none());
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
#[test]
|
|
fn signed_versions_install_select_dispatch_update_and_rollback_atomically() {
|
|
use std::os::unix::fs::PermissionsExt;
|
|
|
|
let temporary = TemporaryDirectory::new();
|
|
let layout = Layout::new(temporary.path().join("machine"));
|
|
layout.initialize().unwrap();
|
|
let key = signing_key();
|
|
fs::write(
|
|
layout.state().join("trusted-release-key"),
|
|
format!("{}\n", hex(key.public_key().as_ref())),
|
|
)
|
|
.unwrap();
|
|
let dispatch_log = temporary.path().join("dispatch.log");
|
|
for (version, healthy, valid_signature) in [
|
|
("1.0.0", true, true),
|
|
("2.0.0", true, true),
|
|
("3.0.0", false, true),
|
|
("4.0.0", true, false),
|
|
] {
|
|
write_release(
|
|
&layout,
|
|
&key,
|
|
version,
|
|
&dispatch_log,
|
|
healthy,
|
|
valid_signature,
|
|
);
|
|
}
|
|
fs::write(layout.downloads().join("latest"), "2.0.0\n").unwrap();
|
|
let manager = MachineManager::new(layout.clone());
|
|
|
|
fs::write(
|
|
layout.state().join("active-version"),
|
|
"dever-active-version-v1\n\npartial",
|
|
)
|
|
.unwrap();
|
|
assert_eq!(manager.active_version().unwrap(), None);
|
|
assert_eq!(
|
|
manager.install_requested("1.0.0").unwrap().as_str(),
|
|
"1.0.0"
|
|
);
|
|
assert_eq!(
|
|
fs::metadata(layout.versions().join("1.0.0/dever-core"))
|
|
.unwrap()
|
|
.permissions()
|
|
.mode()
|
|
& 0o777,
|
|
0o755
|
|
);
|
|
assert_eq!(
|
|
fs::metadata(layout.versions().join("1.0.0/runtime.pack"))
|
|
.unwrap()
|
|
.permissions()
|
|
.mode()
|
|
& 0o777,
|
|
0o644
|
|
);
|
|
assert_eq!(manager.active_version().unwrap().unwrap().as_str(), "1.0.0");
|
|
fs::OpenOptions::new()
|
|
.append(true)
|
|
.open(layout.state().join("active-version"))
|
|
.unwrap()
|
|
.write_all(b"\npartial-record")
|
|
.unwrap();
|
|
assert_eq!(manager.active_version().unwrap().unwrap().as_str(), "1.0.0");
|
|
manager.install_requested("2.0.0").unwrap();
|
|
assert_eq!(manager.active_version().unwrap().unwrap().as_str(), "1.0.0");
|
|
|
|
let skill_path = |layout: &Layout| {
|
|
let CommandResult::Message(path) =
|
|
execute_in(layout, &["skill".into(), "path".into()]).unwrap()
|
|
else {
|
|
panic!("skill path must return the active signed directory")
|
|
};
|
|
PathBuf::from(path)
|
|
};
|
|
assert_eq!(
|
|
fs::read_to_string(skill_path(&layout).join("SKILL.md")).unwrap(),
|
|
"skill 1.0.0\n"
|
|
);
|
|
fs::copy(env!("CARGO_BIN_EXE_dever-launcher"), layout.bin().join("dever")).unwrap();
|
|
let active_project = temporary.path().join("active-project");
|
|
fs::create_dir_all(active_project.join("module")).unwrap();
|
|
fs::create_dir(active_project.join("config")).unwrap();
|
|
fs::write(active_project.join("config/setting.json"), "{}").unwrap();
|
|
install_project_skill(&layout, &active_project);
|
|
let rules = fs::read(active_project.join("AGENTS.md")).unwrap();
|
|
assert_eq!(rules, project_rules("1.0.0").as_bytes());
|
|
install_project_skill(&layout, &active_project);
|
|
assert_eq!(fs::read(active_project.join("AGENTS.md")).unwrap(), rules);
|
|
|
|
let new_project = temporary.path().join("not-created-yet");
|
|
let CommandResult::Exited(code) = execute_in(
|
|
&layout,
|
|
&[
|
|
"new".into(),
|
|
new_project.as_os_str().into(),
|
|
"--markdown".into(),
|
|
],
|
|
)
|
|
.unwrap() else {
|
|
panic!("new must dispatch without project settings")
|
|
};
|
|
assert_eq!(code, 0);
|
|
assert_eq!(
|
|
fs::read_to_string(&dispatch_log).unwrap(),
|
|
format!("new\n{}\n--markdown\n", new_project.display())
|
|
);
|
|
|
|
let project = temporary.path().join("project");
|
|
fs::create_dir_all(project.join("config")).unwrap();
|
|
fs::create_dir(project.join("module")).unwrap();
|
|
fs::write(
|
|
project.join("config/setting.json"),
|
|
r#"{"dever":{"version":"2.0.0"}}"#,
|
|
)
|
|
.unwrap();
|
|
let arguments = ["check", project.to_str().unwrap()].map(OsString::from);
|
|
let CommandResult::Message(pinned_skill) = execute_in(
|
|
&layout,
|
|
&["skill".into(), "path".into(), project.as_os_str().into()],
|
|
)
|
|
.unwrap() else {
|
|
panic!("project skill must match its selected compiler")
|
|
};
|
|
assert_eq!(
|
|
Path::new(&pinned_skill),
|
|
layout.versions().join("2.0.0/skills/dever-language")
|
|
);
|
|
install_project_skill(&layout, &project);
|
|
assert_eq!(
|
|
fs::read_to_string(project.join("AGENTS.md")).unwrap(),
|
|
project_rules("2.0.0")
|
|
);
|
|
let CommandResult::Exited(code) = execute_in(&layout, &arguments).unwrap() else {
|
|
panic!("project command must dispatch")
|
|
};
|
|
assert_eq!(code, 0);
|
|
assert_eq!(
|
|
fs::read_to_string(&dispatch_log).unwrap(),
|
|
format!("check\n{}\n", project.display())
|
|
);
|
|
|
|
let clean_arguments = ["clean", project.to_str().unwrap()].map(OsString::from);
|
|
let CommandResult::Exited(code) = execute_in(&layout, &clean_arguments).unwrap() else {
|
|
panic!("clean command must use the selected project core")
|
|
};
|
|
assert_eq!(code, 0);
|
|
assert_eq!(
|
|
fs::read_to_string(&dispatch_log).unwrap(),
|
|
format!("clean\n{}\n", project.display())
|
|
);
|
|
|
|
assert_eq!(manager.update().unwrap().as_str(), "2.0.0");
|
|
assert_eq!(manager.active_version().unwrap().unwrap().as_str(), "2.0.0");
|
|
assert_eq!(
|
|
fs::read_to_string(skill_path(&layout).join("SKILL.md")).unwrap(),
|
|
"skill 2.0.0\n"
|
|
);
|
|
assert_eq!(fs::read(active_project.join("AGENTS.md")).unwrap(), rules);
|
|
install_project_skill(&layout, &active_project);
|
|
assert_eq!(
|
|
fs::read_to_string(active_project.join("AGENTS.md")).unwrap(),
|
|
project_rules("2.0.0")
|
|
);
|
|
fs::write(layout.downloads().join("latest"), "1.0.0\n").unwrap();
|
|
assert!(
|
|
manager
|
|
.update()
|
|
.unwrap_err()
|
|
.contains("refusing to downgrade")
|
|
);
|
|
assert_eq!(manager.active_version().unwrap().unwrap().as_str(), "2.0.0");
|
|
fs::write(layout.downloads().join("latest"), "2.0.0\n").unwrap();
|
|
assert!(
|
|
manager
|
|
.uninstall(&Version::parse("2.0.0").unwrap())
|
|
.unwrap_err()
|
|
.contains("active")
|
|
);
|
|
manager.activate(&Version::parse("1.0.0").unwrap()).unwrap();
|
|
assert_eq!(
|
|
fs::read_to_string(skill_path(&layout).join("SKILL.md")).unwrap(),
|
|
"skill 1.0.0\n"
|
|
);
|
|
manager
|
|
.uninstall(&Version::parse("2.0.0").unwrap())
|
|
.unwrap();
|
|
assert!(!layout.versions().join("2.0.0").exists());
|
|
|
|
let before = manager.active_version().unwrap();
|
|
assert!(
|
|
manager
|
|
.install_requested("3.0.0")
|
|
.unwrap_err()
|
|
.contains("health check")
|
|
);
|
|
assert_eq!(manager.active_version().unwrap(), before);
|
|
assert!(!layout.versions().join("3.0.0").exists());
|
|
assert!(
|
|
manager
|
|
.install_requested("4.0.0")
|
|
.unwrap_err()
|
|
.contains("signature")
|
|
);
|
|
assert_eq!(manager.active_version().unwrap(), before);
|
|
write_release(&layout, &key, "5.0.0", &dispatch_log, true, true);
|
|
fs::write(
|
|
layout.downloads().join("5.0.0/runtime.pack"),
|
|
"tampered runtime",
|
|
)
|
|
.unwrap();
|
|
assert!(
|
|
manager
|
|
.install_requested("5.0.0")
|
|
.unwrap_err()
|
|
.contains("integrity")
|
|
);
|
|
assert_eq!(fs::read_dir(layout.staging()).unwrap().count(), 0);
|
|
|
|
let install_lock = fs::OpenOptions::new()
|
|
.read(true)
|
|
.write(true)
|
|
.open(layout.state().join("install.lock"))
|
|
.unwrap();
|
|
fs2::FileExt::lock_exclusive(&install_lock).unwrap();
|
|
assert!(
|
|
manager
|
|
.install_requested("1.0.0")
|
|
.unwrap_err()
|
|
.contains("active")
|
|
);
|
|
fs2::FileExt::unlock(&install_lock).unwrap();
|
|
drop(install_lock);
|
|
|
|
fs::set_permissions(layout.state(), fs::Permissions::from_mode(0o755)).unwrap();
|
|
fs::write(
|
|
project.join("config/setting.json"),
|
|
r#"{"dever":{"version":"9.9.9"}}"#,
|
|
)
|
|
.unwrap();
|
|
let error = match execute_in(&layout, &arguments) {
|
|
Ok(_) => panic!("missing locked version must fail"),
|
|
Err(error) => error,
|
|
};
|
|
assert!(error.contains("dever install 9.9.9"), "{error}");
|
|
|
|
fs::set_permissions(
|
|
layout.versions().join("1.0.0/dever-core"),
|
|
fs::Permissions::from_mode(0o777),
|
|
)
|
|
.unwrap();
|
|
let error = manager
|
|
.resolve_core(&Version::parse("1.0.0").unwrap())
|
|
.unwrap_err();
|
|
assert!(
|
|
error.contains("not writable by group or other users"),
|
|
"{error}"
|
|
);
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
#[test]
|
|
fn installed_launcher_is_one_shared_entry_for_multiple_projects() {
|
|
let temporary = TemporaryDirectory::new();
|
|
let layout = Layout::new(temporary.path().join("machine"));
|
|
layout.initialize().unwrap();
|
|
let key = signing_key();
|
|
fs::write(
|
|
layout.state().join("trusted-release-key"),
|
|
format!("{}\n", hex(key.public_key().as_ref())),
|
|
)
|
|
.unwrap();
|
|
let dispatch_log = temporary.path().join("dispatch.log");
|
|
write_release(&layout, &key, "1.0.0", &dispatch_log, true, true);
|
|
MachineManager::new(layout.clone())
|
|
.install_requested("1.0.0")
|
|
.unwrap();
|
|
|
|
let launcher = layout.bin().join("dever");
|
|
fs::copy(env!("CARGO_BIN_EXE_dever-launcher"), &launcher).unwrap();
|
|
let version = Command::new(&launcher)
|
|
.env_clear()
|
|
.arg("version")
|
|
.output()
|
|
.unwrap();
|
|
assert!(
|
|
version.status.success(),
|
|
"{}",
|
|
String::from_utf8_lossy(&version.stderr)
|
|
);
|
|
assert_eq!(version.stdout, b"1.0.0\n");
|
|
|
|
for name in ["first-project", "second-project"] {
|
|
let project = temporary.path().join(name);
|
|
fs::create_dir_all(project.join("config")).unwrap();
|
|
fs::write(project.join("config/setting.json"), "{}\n").unwrap();
|
|
for prefix in [vec!["check"], vec!["lib", "list"], vec!["package", "list"]] {
|
|
let output = Command::new(&launcher)
|
|
.env_clear()
|
|
.env("DEVER_TEST_SENTINEL", "must-not-reach-version-core")
|
|
.args(&prefix)
|
|
.arg(&project)
|
|
.output()
|
|
.unwrap();
|
|
assert!(
|
|
output.status.success(),
|
|
"{}",
|
|
String::from_utf8_lossy(&output.stderr)
|
|
);
|
|
assert_eq!(
|
|
fs::read_to_string(&dispatch_log).unwrap(),
|
|
format!("{}\n{}\n", prefix.join("\n"), project.display())
|
|
);
|
|
}
|
|
}
|
|
let project = temporary.path().join("exit-42");
|
|
fs::create_dir_all(project.join("config")).unwrap();
|
|
fs::write(project.join("config/setting.json"), "{}\n").unwrap();
|
|
let status = Command::new(&launcher)
|
|
.env_clear()
|
|
.args(["check", project.to_str().unwrap()])
|
|
.status()
|
|
.unwrap();
|
|
assert_eq!(status.code(), Some(42));
|
|
}
|
|
|
|
#[test]
|
|
fn shared_cache_uses_complete_crypto_identity_integrity_leases_and_safe_cleanup() {
|
|
let temporary = TemporaryDirectory::new();
|
|
let layout = Layout::new(temporary.path().join("machine"));
|
|
layout.initialize().unwrap();
|
|
let version = Version::parse("1.0.0").unwrap();
|
|
let program = temporary.path().join("program");
|
|
fs::write(&program, b"verified executable bytes").unwrap();
|
|
let build = identity(&version, b"generated one", "linux-x86_64");
|
|
let service = CacheStore::new(&layout);
|
|
|
|
let keys = std::thread::scope(|scope| {
|
|
let first = scope.spawn(|| CacheStore::new(&layout).publish(&build, &program).unwrap());
|
|
let second = scope.spawn(|| CacheStore::new(&layout).publish(&build, &program).unwrap());
|
|
[first.join().unwrap(), second.join().unwrap()]
|
|
});
|
|
assert_eq!(keys[0], keys[1]);
|
|
let restored = service.restore(&build).unwrap().expect("cache hit");
|
|
assert_eq!(restored.as_bytes(), b"verified executable bytes");
|
|
assert!(
|
|
service
|
|
.restore(&identity(&version, b"generated two", "linux-x86_64"))
|
|
.unwrap()
|
|
.is_none()
|
|
);
|
|
for target in ["linux-aarch64", "windows-x86_64"] {
|
|
assert!(
|
|
service
|
|
.restore(&identity(&version, b"generated one", target))
|
|
.unwrap()
|
|
.is_none()
|
|
);
|
|
}
|
|
|
|
let status = service.status().unwrap();
|
|
assert_eq!((status.entries, status.corrupt_entries), (1, 0));
|
|
assert_eq!(status.versions.get("1.0.0"), Some(&1));
|
|
service.clean(&BTreeSet::new()).unwrap();
|
|
assert_eq!(
|
|
service.status().unwrap().entries,
|
|
1,
|
|
"leased entry was deleted"
|
|
);
|
|
drop(restored);
|
|
fs::create_dir_all(layout.native_cache().join("operations")).unwrap();
|
|
fs::write(
|
|
layout.native_cache().join("operations/active"),
|
|
"fixture operation",
|
|
)
|
|
.unwrap();
|
|
assert!(
|
|
service
|
|
.clean(&BTreeSet::new())
|
|
.unwrap_err()
|
|
.contains("busy")
|
|
);
|
|
fs::remove_file(layout.native_cache().join("operations/active")).unwrap();
|
|
service.clean(&BTreeSet::new()).unwrap();
|
|
assert_eq!(service.status().unwrap().entries, 0);
|
|
|
|
let key = service.publish(&build, &program).unwrap();
|
|
fs::write(
|
|
layout
|
|
.native_cache()
|
|
.join("entries")
|
|
.join(&key)
|
|
.join("program"),
|
|
b"tampered",
|
|
)
|
|
.unwrap();
|
|
let error = match service.restore(&build) {
|
|
Ok(_) => panic!("modified cache entry must fail"),
|
|
Err(error) => error,
|
|
};
|
|
assert!(error.contains("modified"), "{error}");
|
|
assert_eq!(service.status().unwrap().corrupt_entries, 1);
|
|
let installed = BTreeSet::from([version]);
|
|
service.clean(&installed).unwrap();
|
|
assert_eq!(service.status().unwrap().corrupt_entries, 0);
|
|
}
|
|
|
|
#[test]
|
|
fn launcher_does_not_bypass_an_unavailable_authenticated_cache_service() {
|
|
let temporary = TemporaryDirectory::new();
|
|
let layout = Layout::new(temporary.path().join("machine"));
|
|
layout.initialize().unwrap();
|
|
for arguments in [["cache", "status"], ["cache", "clean"]] {
|
|
let arguments = arguments.map(OsString::from);
|
|
let error = match execute_in(&layout, &arguments) {
|
|
Ok(_) => panic!("cache command must not open the service store directly"),
|
|
Err(error) => error,
|
|
};
|
|
assert!(error.contains("authenticated deverd"), "{error}");
|
|
}
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
#[test]
|
|
fn deverd_authenticates_cache_commands_over_local_ipc() {
|
|
use std::os::unix::fs::PermissionsExt;
|
|
|
|
let temporary = TemporaryDirectory::new();
|
|
let layout = Layout::new(temporary.path().join("machine"));
|
|
layout.initialize().unwrap();
|
|
let _daemon = TestDaemon::start(&layout);
|
|
let status = execute_in(
|
|
&layout,
|
|
&[OsString::from("cache"), OsString::from("status")],
|
|
)
|
|
.unwrap();
|
|
let CommandResult::Message(status) = status else {
|
|
panic!("cache status must return a message");
|
|
};
|
|
let status: serde_json::Value = serde_json::from_str(&status).unwrap();
|
|
assert_eq!(status["entries"], 0);
|
|
let version = Version::parse("1.0.0").unwrap();
|
|
let artifact = temporary.path().join("program");
|
|
fs::write(&artifact, b"authenticated cleanup fixture").unwrap();
|
|
CacheStore::new(&layout)
|
|
.publish(&identity(&version, b"auth", "linux-x86_64"), &artifact)
|
|
.unwrap();
|
|
let token = fs::read_to_string(layout.service_token()).unwrap();
|
|
let padded = format!("{}{}", token.trim(), "\0".repeat(256));
|
|
assert_eq!(raw_request(&layout, &padded, "clean")["ok"], false);
|
|
assert_eq!(
|
|
dever_cli::toolchain::cache_status(&layout).unwrap().entries,
|
|
1
|
|
);
|
|
assert_eq!(raw_request(&layout, token.trim(), "status")["ok"], true);
|
|
let moved = layout.state().join("deverd.token.saved");
|
|
fs::rename(layout.service_token(), &moved).unwrap();
|
|
assert_eq!(raw_request(&layout, "", "clean")["ok"], false);
|
|
assert!(dever_cli::toolchain::cache_clean(&layout, &BTreeSet::new()).is_err());
|
|
assert!(!layout.service_token().exists());
|
|
fs::rename(moved, layout.service_token()).unwrap();
|
|
fs::set_permissions(layout.service_token(), fs::Permissions::from_mode(0o644)).unwrap();
|
|
assert_eq!(raw_request(&layout, token.trim(), "clean")["ok"], false);
|
|
fs::set_permissions(layout.service_token(), fs::Permissions::from_mode(0o600)).unwrap();
|
|
assert_eq!(
|
|
dever_cli::toolchain::cache_status(&layout).unwrap().entries,
|
|
1
|
|
);
|
|
fs::write(layout.service_token(), format!("{token}tampered")).unwrap();
|
|
let error = execute_in(
|
|
&layout,
|
|
&[OsString::from("cache"), OsString::from("status")],
|
|
)
|
|
.unwrap_err();
|
|
assert!(
|
|
error.contains("authentication token") || error.contains("invalid"),
|
|
"{error}"
|
|
);
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
#[test]
|
|
fn deverd_rejects_a_second_instance_and_recovers_its_socket_after_exit() {
|
|
use std::os::unix::fs::{MetadataExt, PermissionsExt};
|
|
|
|
let temporary = TemporaryDirectory::new();
|
|
let layout = Layout::new(temporary.path().join("machine"));
|
|
layout.initialize().unwrap();
|
|
let mut daemon = TestDaemon::start(&layout);
|
|
let socket_inode = fs::symlink_metadata(layout.service_socket()).unwrap().ino();
|
|
let second = Command::new(env!("CARGO_BIN_EXE_deverd"))
|
|
.args(["--root", layout.root().to_str().unwrap()])
|
|
.output()
|
|
.unwrap();
|
|
assert!(!second.status.success());
|
|
assert!(String::from_utf8_lossy(&second.stderr).contains("already running"));
|
|
assert_eq!(
|
|
fs::symlink_metadata(layout.service_socket()).unwrap().ino(),
|
|
socket_inode
|
|
);
|
|
assert!(dever_cli::toolchain::cache_status(&layout).is_ok());
|
|
|
|
daemon.stop();
|
|
assert!(layout.service_socket().exists());
|
|
let lock = layout.state().join("deverd.lock");
|
|
fs::set_permissions(&lock, fs::Permissions::from_mode(0o644)).unwrap();
|
|
let insecure = Command::new(env!("CARGO_BIN_EXE_deverd"))
|
|
.args(["--root", layout.root().to_str().unwrap()])
|
|
.output()
|
|
.unwrap();
|
|
assert!(!insecure.status.success());
|
|
assert!(String::from_utf8_lossy(&insecure.stderr).contains("deverd lock"));
|
|
fs::set_permissions(&lock, fs::Permissions::from_mode(0o600)).unwrap();
|
|
let _recovered = TestDaemon::start(&layout);
|
|
assert!(dever_cli::toolchain::cache_status(&layout).is_ok());
|
|
}
|
|
|
|
#[cfg(target_os = "linux")]
|
|
#[test]
|
|
fn deverd_bounds_workers_when_clients_stall() {
|
|
use std::os::unix::net::UnixStream;
|
|
|
|
let temporary = TemporaryDirectory::new();
|
|
let layout = Layout::new(temporary.path().join("machine"));
|
|
layout.initialize().unwrap();
|
|
let daemon = TestDaemon::start(&layout);
|
|
let clients: Vec<_> = (0..48)
|
|
.map(|_| {
|
|
let mut stream = UnixStream::connect(layout.service_socket()).unwrap();
|
|
let _ = stream.write_all(&[0]);
|
|
stream
|
|
})
|
|
.collect();
|
|
let status = fs::read_to_string(format!("/proc/{}/status", daemon.0.id())).unwrap();
|
|
let threads: usize = status
|
|
.lines()
|
|
.find_map(|line| {
|
|
line.strip_prefix("Threads:")
|
|
.map(|value| value.trim().parse().unwrap())
|
|
})
|
|
.unwrap();
|
|
assert!(
|
|
threads <= 10,
|
|
"deverd created {threads} threads for stalled clients"
|
|
);
|
|
drop(clients);
|
|
let recovered = (0..100).any(|_| {
|
|
if dever_cli::toolchain::cache_status(&layout).is_ok() {
|
|
return true;
|
|
}
|
|
std::thread::sleep(std::time::Duration::from_millis(10));
|
|
false
|
|
});
|
|
assert!(recovered);
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
#[test]
|
|
fn deverd_keeps_serving_after_bad_and_slow_clients() {
|
|
use std::net::Shutdown;
|
|
use std::os::unix::net::UnixStream;
|
|
use std::time::{Duration, Instant};
|
|
|
|
let temporary = TemporaryDirectory::new();
|
|
let layout = Layout::new(temporary.path().join("machine"));
|
|
layout.initialize().unwrap();
|
|
let _daemon = TestDaemon::start(&layout);
|
|
|
|
let mut slow = UnixStream::connect(layout.service_socket()).unwrap();
|
|
slow.write_all(&[0]).unwrap();
|
|
let start = Instant::now();
|
|
assert!(dever_cli::toolchain::cache_status(&layout).is_ok());
|
|
assert!(start.elapsed() < Duration::from_secs(2));
|
|
|
|
let mut oversized = UnixStream::connect(layout.service_socket()).unwrap();
|
|
oversized
|
|
.set_read_timeout(Some(Duration::from_secs(2)))
|
|
.unwrap();
|
|
oversized.write_all(&u32::MAX.to_be_bytes()).unwrap();
|
|
assert_eq!(read_response(&mut oversized)["ok"], false);
|
|
|
|
let mut partial = UnixStream::connect(layout.service_socket()).unwrap();
|
|
partial
|
|
.set_read_timeout(Some(Duration::from_secs(2)))
|
|
.unwrap();
|
|
partial.write_all(&100_u32.to_be_bytes()).unwrap();
|
|
partial.shutdown(Shutdown::Write).unwrap();
|
|
assert_eq!(read_response(&mut partial)["ok"], false);
|
|
|
|
let mut disconnected = UnixStream::connect(layout.service_socket()).unwrap();
|
|
disconnected.write_all(&0_u32.to_be_bytes()).unwrap();
|
|
disconnected.shutdown(Shutdown::Both).unwrap();
|
|
drop(slow);
|
|
assert!(dever_cli::toolchain::cache_status(&layout).is_ok());
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
#[test]
|
|
fn deverd_transfers_verified_artifacts_without_client_cache_paths() {
|
|
let temporary = TemporaryDirectory::new();
|
|
let layout = Layout::new(temporary.path().join("machine"));
|
|
layout.initialize().unwrap();
|
|
let _daemon = TestDaemon::start(&layout);
|
|
// Larger than a transport scratch buffer; the daemon streams to staging.
|
|
let bytes = (0..262_147)
|
|
.map(|index| (index % 251) as u8)
|
|
.collect::<Vec<_>>();
|
|
let receipt = artifact_put(&layout, &bytes).unwrap();
|
|
assert_eq!(receipt.bytes, bytes.len() as u64);
|
|
assert_eq!(
|
|
artifact_get(&layout, &receipt.sha256, receipt.bytes).unwrap(),
|
|
bytes
|
|
);
|
|
std::thread::scope(|scope| {
|
|
for _ in 0..6 {
|
|
scope.spawn(|| assert_eq!(artifact_put(&layout, &bytes).unwrap(), receipt));
|
|
}
|
|
});
|
|
let status = dever_cli::toolchain::cache_status(&layout).unwrap();
|
|
assert_eq!(status.artifact_entries, 1);
|
|
assert_eq!(status.artifact_bytes, receipt.bytes);
|
|
assert_eq!(status.staging_entries, 0);
|
|
assert!(artifact_get(&layout, "../outside", 1).is_err());
|
|
assert!(artifact_get(&layout, &receipt.sha256, receipt.bytes + 1).is_err());
|
|
assert!(artifact_get(&layout, &receipt.sha256, 64 * 1024 * 1024 + 1).is_err());
|
|
fs::write(
|
|
layout
|
|
.native_cache()
|
|
.join("artifacts")
|
|
.join(&receipt.sha256),
|
|
b"tampered",
|
|
)
|
|
.unwrap();
|
|
assert!(artifact_get(&layout, &receipt.sha256, receipt.bytes).is_err());
|
|
assert_eq!(
|
|
dever_cli::toolchain::cache_status(&layout)
|
|
.unwrap()
|
|
.corrupt_artifacts,
|
|
1
|
|
);
|
|
dever_cli::toolchain::cache_clean(&layout, &BTreeSet::new()).unwrap();
|
|
assert!(artifact_get(&layout, &receipt.sha256, receipt.bytes).is_err());
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
#[test]
|
|
fn deverd_rejects_bad_and_incomplete_artifact_uploads_before_publication() {
|
|
use std::os::unix::net::UnixStream;
|
|
|
|
let temporary = TemporaryDirectory::new();
|
|
let layout = Layout::new(temporary.path().join("machine"));
|
|
layout.initialize().unwrap();
|
|
let _daemon = TestDaemon::start(&layout);
|
|
let token = fs::read_to_string(layout.service_token()).unwrap();
|
|
for (bytes, digest, body, complete) in [
|
|
(
|
|
4_u64,
|
|
hex(&Sha256::digest(b"good")),
|
|
b"bad!".as_slice(),
|
|
true,
|
|
),
|
|
(4, hex(&Sha256::digest(b"good")), b"go".as_slice(), false),
|
|
] {
|
|
let mut stream = UnixStream::connect(layout.service_socket()).unwrap();
|
|
stream
|
|
.set_read_timeout(Some(std::time::Duration::from_secs(2)))
|
|
.unwrap();
|
|
let request = serde_json::to_vec(&serde_json::json!({
|
|
"token": token.trim(), "operation": {"artifact_put": {"sha256": digest, "bytes": bytes}}, "installed": []
|
|
})).unwrap();
|
|
stream
|
|
.write_all(&(request.len() as u32).to_be_bytes())
|
|
.unwrap();
|
|
stream.write_all(&request).unwrap();
|
|
assert_eq!(read_response(&mut stream)["ok"], true);
|
|
stream.write_all(body).unwrap();
|
|
if complete {
|
|
assert_eq!(read_response(&mut stream)["ok"], false);
|
|
}
|
|
drop(stream);
|
|
for attempt in 0..100 {
|
|
let status = dever_cli::toolchain::cache_status(&layout).unwrap();
|
|
if status.staging_entries == 0 {
|
|
assert_eq!(status.artifact_entries, 0);
|
|
break;
|
|
}
|
|
assert!(attempt < 99, "incomplete upload staging was not removed");
|
|
std::thread::sleep(std::time::Duration::from_millis(10));
|
|
}
|
|
}
|
|
let receipt = artifact_put(&layout, b"good").unwrap();
|
|
assert_eq!(
|
|
artifact_get(&layout, &receipt.sha256, receipt.bytes).unwrap(),
|
|
b"good"
|
|
);
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
#[test]
|
|
fn shared_artifact_quota_counts_bytes_and_entries_and_cleans_failed_staging() {
|
|
for (entries, artifact_bytes) in [(32_u32, 64 * 1024 * 1024), (4096, 0)] {
|
|
let temporary = TemporaryDirectory::new();
|
|
let layout = Layout::new(temporary.path().join("machine"));
|
|
layout.initialize().unwrap();
|
|
let artifacts = layout.native_cache().join("artifacts");
|
|
fs::create_dir(&artifacts).unwrap();
|
|
// Sparse owned fixtures test byte and entry quotas without allocating 2 GiB.
|
|
for index in 0_u32..entries {
|
|
let file = fs::File::create(artifacts.join(hex(&Sha256::digest(index.to_le_bytes()))))
|
|
.unwrap();
|
|
file.set_len(artifact_bytes).unwrap();
|
|
}
|
|
let body = b"new verified artifact";
|
|
let receipt = dever_cli::toolchain::ArtifactReceipt {
|
|
sha256: hex(&Sha256::digest(body)),
|
|
bytes: body.len() as u64,
|
|
};
|
|
let store = CacheStore::new(&layout);
|
|
assert!(
|
|
store
|
|
.publish_artifact(&receipt, &mut body.as_slice())
|
|
.unwrap_err()
|
|
.contains("cache is full")
|
|
);
|
|
assert!(!artifacts.join(&receipt.sha256).exists());
|
|
assert_eq!(
|
|
fs::read_dir(layout.native_cache().join("staging"))
|
|
.unwrap()
|
|
.count(),
|
|
0
|
|
);
|
|
assert_eq!(
|
|
fs::read_dir(layout.native_cache().join("operations"))
|
|
.unwrap()
|
|
.count(),
|
|
0
|
|
);
|
|
}
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
#[test]
|
|
fn shared_cache_summary_cannot_claim_payload_integrity() {
|
|
let temporary = TemporaryDirectory::new();
|
|
let layout = Layout::new(temporary.path().join("machine"));
|
|
layout.initialize().unwrap();
|
|
let store = CacheStore::new(&layout);
|
|
let bytes = b"good";
|
|
let receipt = dever_cli::toolchain::ArtifactReceipt {
|
|
sha256: hex(&Sha256::digest(bytes)),
|
|
bytes: bytes.len() as u64,
|
|
};
|
|
store
|
|
.publish_artifact(&receipt, &mut bytes.as_slice())
|
|
.unwrap();
|
|
fs::write(
|
|
layout
|
|
.native_cache()
|
|
.join("artifacts")
|
|
.join(&receipt.sha256),
|
|
b"bad!",
|
|
)
|
|
.unwrap();
|
|
let summary = store.summary().unwrap();
|
|
assert!(!summary.verified);
|
|
assert_eq!(summary.artifact_entries, 1);
|
|
assert_eq!(summary.corrupt_artifacts, 0);
|
|
let checked = store.status().unwrap();
|
|
assert!(checked.verified);
|
|
assert_eq!(checked.artifact_entries, 0);
|
|
assert_eq!(checked.corrupt_artifacts, 1);
|
|
assert!(store.artifact(&receipt).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn compiled_cache_has_independent_byte_entry_and_output_limits() {
|
|
for (count, bytes) in [(32, 64 * 1024 * 1024), (4096, 1)] {
|
|
let temporary = TemporaryDirectory::new();
|
|
let layout = Layout::new(temporary.path().join("machine"));
|
|
layout.initialize().unwrap();
|
|
let version = Version::parse("0.1.0").unwrap();
|
|
for index in 0..count {
|
|
let key = format!("{index:064x}");
|
|
let directory = layout.native_cache().join("entries").join(&key);
|
|
fs::create_dir_all(&directory).unwrap();
|
|
fs::File::create(directory.join("program"))
|
|
.unwrap()
|
|
.set_len(bytes)
|
|
.unwrap();
|
|
let entry = dever_cli::toolchain::CacheEntry {
|
|
format: "dever-machine-cache-entry-v1".into(),
|
|
key,
|
|
version: version.clone(),
|
|
target: "linux-x86_64".into(),
|
|
bytes,
|
|
sha256: "0".repeat(64),
|
|
};
|
|
fs::write(
|
|
directory.join("entry.json"),
|
|
serde_json::to_vec(&entry).unwrap(),
|
|
)
|
|
.unwrap();
|
|
}
|
|
let program = temporary.path().join("program");
|
|
fs::write(&program, b"new compiled output").unwrap();
|
|
let store = CacheStore::new(&layout);
|
|
assert!(
|
|
store
|
|
.publish(&identity(&version, b"new", "linux-x86_64"), &program)
|
|
.unwrap_err()
|
|
.contains("quota exceeded")
|
|
);
|
|
fs::File::create(&program)
|
|
.unwrap()
|
|
.set_len(320 * 1024 * 1024 + 1)
|
|
.unwrap();
|
|
assert!(
|
|
store
|
|
.publish(&identity(&version, b"large", "linux-x86_64"), &program)
|
|
.unwrap_err()
|
|
.contains("320 MiB")
|
|
);
|
|
assert_eq!(
|
|
fs::read_dir(layout.native_cache().join("staging"))
|
|
.unwrap()
|
|
.count(),
|
|
0
|
|
);
|
|
assert_eq!(
|
|
fs::read_dir(layout.native_cache().join("operations"))
|
|
.unwrap()
|
|
.count(),
|
|
0
|
|
);
|
|
}
|
|
}
|
|
|
|
#[cfg(target_os = "linux")]
|
|
#[test]
|
|
#[ignore = "requires root; drops only an owned skill client to an unprivileged uid"]
|
|
fn active_skill_is_readable_by_other_machine_users_without_install_lock_access() {
|
|
use std::os::unix::fs::PermissionsExt;
|
|
use std::os::unix::process::CommandExt;
|
|
|
|
assert_eq!(rustix::process::geteuid().as_raw(), 0);
|
|
let temporary = TemporaryDirectory::new();
|
|
let layout = Layout::new(temporary.path().join("machine"));
|
|
layout.initialize().unwrap();
|
|
let key = signing_key();
|
|
fs::write(
|
|
layout.state().join("trusted-release-key"),
|
|
format!("{}\n", hex(key.public_key().as_ref())),
|
|
)
|
|
.unwrap();
|
|
write_release(
|
|
&layout,
|
|
&key,
|
|
"1.0.0",
|
|
&temporary.path().join("dispatch"),
|
|
true,
|
|
true,
|
|
);
|
|
MachineManager::new(layout.clone())
|
|
.install_requested("1.0.0")
|
|
.unwrap();
|
|
let launcher = layout.bin().join("dever");
|
|
fs::copy(env!("CARGO_BIN_EXE_dever-launcher"), &launcher).unwrap();
|
|
fs::set_permissions(&launcher, fs::Permissions::from_mode(0o755)).unwrap();
|
|
let output = Command::new(&launcher)
|
|
.args(["skill", "path"])
|
|
.uid(65534)
|
|
.gid(65534)
|
|
.env_clear()
|
|
.output()
|
|
.unwrap();
|
|
assert!(
|
|
output.status.success(),
|
|
"{}",
|
|
String::from_utf8_lossy(&output.stderr)
|
|
);
|
|
let path = String::from_utf8(output.stdout).unwrap();
|
|
assert_eq!(
|
|
Path::new(path.trim()),
|
|
layout.versions().join("1.0.0/skills/dever-language")
|
|
);
|
|
let read = Command::new("/bin/cat")
|
|
.arg(Path::new(path.trim()).join("SKILL.md"))
|
|
.uid(65534)
|
|
.gid(65534)
|
|
.env_clear()
|
|
.output()
|
|
.unwrap();
|
|
assert!(read.status.success());
|
|
assert_eq!(read.stdout, b"skill 1.0.0\n");
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
#[test]
|
|
fn corrupt_skill_cannot_be_published_or_resolved() {
|
|
let temporary = TemporaryDirectory::new();
|
|
let layout = Layout::new(temporary.path().join("machine"));
|
|
layout.initialize().unwrap();
|
|
let key = signing_key();
|
|
fs::write(
|
|
layout.state().join("trusted-release-key"),
|
|
format!("{}\n", hex(key.public_key().as_ref())),
|
|
)
|
|
.unwrap();
|
|
for version in ["1.0.0", "2.0.0"] {
|
|
write_release(
|
|
&layout,
|
|
&key,
|
|
version,
|
|
&temporary.path().join("dispatch"),
|
|
true,
|
|
true,
|
|
);
|
|
}
|
|
let manager = MachineManager::new(layout.clone());
|
|
manager.install_requested("1.0.0").unwrap();
|
|
fs::write(
|
|
layout
|
|
.downloads()
|
|
.join("2.0.0/skills/dever-language/SKILL.md"),
|
|
"tampered",
|
|
)
|
|
.unwrap();
|
|
fs::write(layout.downloads().join("latest"), "2.0.0\n").unwrap();
|
|
assert!(manager.update().unwrap_err().contains("integrity"));
|
|
assert_eq!(manager.active_version().unwrap().unwrap().as_str(), "1.0.0");
|
|
assert!(!layout.versions().join("2.0.0").exists());
|
|
fs::write(
|
|
layout
|
|
.versions()
|
|
.join("1.0.0/skills/dever-language/SKILL.md"),
|
|
"tampered",
|
|
)
|
|
.unwrap();
|
|
assert!(
|
|
execute_in(&layout, &["skill".into(), "path".into()])
|
|
.unwrap_err()
|
|
.contains("integrity")
|
|
);
|
|
}
|
|
|
|
#[cfg(target_os = "linux")]
|
|
#[test]
|
|
#[ignore = "requires root; drops only owned test clients to an unprivileged uid"]
|
|
fn deverd_uses_kernel_credentials_for_other_machine_users() {
|
|
use std::os::unix::fs::PermissionsExt;
|
|
use std::os::unix::process::CommandExt;
|
|
|
|
assert_eq!(
|
|
rustix::process::geteuid().as_raw(),
|
|
0,
|
|
"this scoped acceptance requires root"
|
|
);
|
|
let temporary = TemporaryDirectory::new();
|
|
let layout = Layout::new(temporary.path().join("machine"));
|
|
layout.initialize().unwrap();
|
|
let _daemon = TestDaemon::start(&layout);
|
|
let launcher = layout.bin().join("dever");
|
|
fs::copy(env!("CARGO_BIN_EXE_dever-launcher"), &launcher).unwrap();
|
|
fs::set_permissions(&launcher, fs::Permissions::from_mode(0o755)).unwrap();
|
|
let status = Command::new(&launcher)
|
|
.args(["cache", "status"])
|
|
.uid(65534)
|
|
.gid(65534)
|
|
.env_clear()
|
|
.output()
|
|
.unwrap();
|
|
assert!(
|
|
status.status.success(),
|
|
"{}",
|
|
String::from_utf8_lossy(&status.stderr)
|
|
);
|
|
assert_eq!(
|
|
serde_json::from_slice::<serde_json::Value>(&status.stdout).unwrap()["verified"],
|
|
false
|
|
);
|
|
let clean = Command::new(&launcher)
|
|
.args(["cache", "clean"])
|
|
.uid(65534)
|
|
.gid(65534)
|
|
.env_clear()
|
|
.output()
|
|
.unwrap();
|
|
assert!(!clean.status.success());
|
|
assert!(String::from_utf8_lossy(&clean.stderr).contains("service owner"));
|
|
let client = r#"
|
|
import hashlib, json, socket, struct, sys
|
|
endpoint, token_path = sys.argv[1:]
|
|
try:
|
|
open(token_path, 'rb')
|
|
except PermissionError:
|
|
pass
|
|
else:
|
|
raise AssertionError('foreign user read the owner token')
|
|
def response(peer):
|
|
def read(n):
|
|
out = b''
|
|
while len(out) < n:
|
|
part = peer.recv(n - len(out))
|
|
if not part: raise AssertionError('incomplete response')
|
|
out += part
|
|
return out
|
|
length, = struct.unpack('>I', read(4))
|
|
return json.loads(read(length))
|
|
held = []
|
|
try:
|
|
for _ in range(2):
|
|
peer = socket.socket(socket.AF_UNIX)
|
|
peer.settimeout(2)
|
|
peer.connect(endpoint)
|
|
held.append(peer)
|
|
with socket.socket(socket.AF_UNIX) as excess:
|
|
excess.settimeout(2)
|
|
excess.connect(endpoint)
|
|
assert excess.recv(1) == b'', 'foreign UID exceeded its two-client admission'
|
|
request = json.dumps(dict(operation='status')).encode()
|
|
for peer in held:
|
|
peer.sendall(struct.pack('>I', len(request)) + request)
|
|
assert response(peer)['status']['verified'] is False
|
|
finally:
|
|
for peer in held:
|
|
peer.close()
|
|
body = b'shared across machine users'
|
|
identity = dict(sha256=hashlib.sha256(body).hexdigest(), bytes=len(body))
|
|
for operation in ('artifact_put', 'artifact_get'):
|
|
with socket.socket(socket.AF_UNIX) as peer:
|
|
peer.settimeout(5)
|
|
peer.connect(endpoint)
|
|
request = json.dumps(dict(operation={operation: identity})).encode()
|
|
peer.sendall(struct.pack('>I', len(request)) + request)
|
|
assert response(peer)['artifact'] == identity
|
|
if operation == 'artifact_put':
|
|
peer.sendall(body)
|
|
assert response(peer)['artifact'] == identity
|
|
else:
|
|
read = b''
|
|
while len(read) < len(body):
|
|
part = peer.recv(len(body) - len(read))
|
|
assert part
|
|
read += part
|
|
assert read == body
|
|
"#;
|
|
let result = Command::new("/usr/bin/python3")
|
|
.args(["-I", "-c", client])
|
|
.arg(layout.service_socket())
|
|
.arg(layout.service_token())
|
|
.uid(65534)
|
|
.gid(65534)
|
|
.env_clear()
|
|
.output()
|
|
.unwrap();
|
|
assert!(
|
|
result.status.success(),
|
|
"{}",
|
|
String::from_utf8_lossy(&result.stderr)
|
|
);
|
|
let status = dever_cli::toolchain::cache_status(&layout).unwrap();
|
|
assert_eq!(status.artifact_entries, 1);
|
|
}
|
|
|
|
fn identity<'a>(version: &'a Version, generated: &'a [u8], target: &'a str) -> BuildIdentity<'a> {
|
|
BuildIdentity {
|
|
version,
|
|
compiler: b"compiler identity",
|
|
runtime: b"runtime identity",
|
|
target,
|
|
generated,
|
|
options: b"release options",
|
|
environment: b"backend environment",
|
|
}
|
|
}
|
|
|
|
fn signing_key() -> Ed25519KeyPair {
|
|
let document = Ed25519KeyPair::generate_pkcs8(&SystemRandom::new()).unwrap();
|
|
Ed25519KeyPair::from_pkcs8(document.as_ref()).unwrap()
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
fn write_release(
|
|
layout: &Layout,
|
|
key: &Ed25519KeyPair,
|
|
version: &str,
|
|
dispatch_log: &Path,
|
|
healthy: bool,
|
|
valid_signature: bool,
|
|
) {
|
|
use std::os::unix::fs::PermissionsExt;
|
|
|
|
let root = layout.downloads().join(version);
|
|
fs::create_dir(&root).unwrap();
|
|
let core = root.join("dever-core");
|
|
let log = dispatch_log.display().to_string().replace('\'', "'\\''");
|
|
fs::write(
|
|
&core,
|
|
format!(
|
|
"#!/bin/sh\nif [ \"${{DEVER_TEST_SENTINEL+x}}\" = x ]; then exit 98; fi\nif [ \"$1\" = \"--dever-health\" ]; then exit {}; fi\nif [ \"${{2##*/}}\" = \"exit-42\" ]; then exit 42; fi\nprintf '%s\\n' \"$@\" > '{}'\n",
|
|
if healthy { 0 } else { 9 },
|
|
log,
|
|
),
|
|
).unwrap();
|
|
fs::set_permissions(&core, fs::Permissions::from_mode(0o755)).unwrap();
|
|
let runtime = root.join("runtime.pack");
|
|
fs::write(&runtime, format!("runtime {version}")).unwrap();
|
|
fs::set_permissions(&runtime, fs::Permissions::from_mode(0o666)).unwrap();
|
|
fs::set_permissions(&core, fs::Permissions::from_mode(0o777)).unwrap();
|
|
let skill = root.join("skills/dever-language/SKILL.md");
|
|
fs::create_dir_all(skill.parent().unwrap()).unwrap();
|
|
fs::write(&skill, format!("skill {version}\n")).unwrap();
|
|
let rules = root.join("skills/dever-language/assets/template/AGENTS.md");
|
|
fs::create_dir_all(rules.parent().unwrap()).unwrap();
|
|
fs::write(&rules, project_rules(version)).unwrap();
|
|
let manifest = ReleaseManifest::new(
|
|
Version::parse(version).unwrap(),
|
|
vec![
|
|
artifact(&core, "dever-core"),
|
|
artifact(&runtime, "runtime.pack"),
|
|
artifact(&skill, "skills/dever-language/SKILL.md"),
|
|
artifact(&rules, "skills/dever-language/assets/template/AGENTS.md"),
|
|
],
|
|
);
|
|
let bytes = serde_json::to_vec(&manifest).unwrap();
|
|
fs::write(root.join("manifest.json"), &bytes).unwrap();
|
|
let mut signature = key.sign(&bytes).as_ref().to_vec();
|
|
if !valid_signature {
|
|
signature[0] ^= 1;
|
|
}
|
|
fs::write(root.join("manifest.sig"), format!("{}\n", hex(&signature))).unwrap();
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
fn project_rules(version: &str) -> String {
|
|
include_str!("../../skills/assets/template/AGENTS.md").replace(
|
|
"<!-- dever-language:end -->",
|
|
&format!("<!-- fixture version {version} -->\n<!-- dever-language:end -->"),
|
|
)
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
fn install_project_skill(layout: &Layout, project: &Path) {
|
|
let output = Command::new(layout.bin().join("dever"))
|
|
.args(["skill", "install"])
|
|
.current_dir(project)
|
|
.env_clear()
|
|
.output()
|
|
.unwrap();
|
|
assert!(
|
|
output.status.success(),
|
|
"{}",
|
|
String::from_utf8_lossy(&output.stderr)
|
|
);
|
|
}
|
|
|
|
fn artifact(path: &Path, relative: &str) -> Artifact {
|
|
let bytes = fs::read(path).unwrap();
|
|
Artifact {
|
|
path: relative.into(),
|
|
bytes: bytes.len() as u64,
|
|
sha256: hex(&Sha256::digest(&bytes)),
|
|
}
|
|
}
|
|
|
|
fn hex(bytes: &[u8]) -> String {
|
|
const DIGITS: &[u8; 16] = b"0123456789abcdef";
|
|
let mut output = String::with_capacity(bytes.len() * 2);
|
|
for byte in bytes {
|
|
output.push(DIGITS[usize::from(byte >> 4)] as char);
|
|
output.push(DIGITS[usize::from(byte & 0x0f)] as char);
|
|
}
|
|
output
|
|
}
|