use std::fs::Metadata;
use std::hash::{Hash, Hasher};
use std::path::{Path, PathBuf};
use std::time::{Duration, SystemTime};
use wasmtime::component::Component;
use wasmtime::Engine;
use crate::murmur_home::FileStamp;
const COMPILED_DIR: &str = "compiled";
const FORM_RETENTION: Duration = Duration::from_secs(30 * 24 * 60 * 60);
const TEMP_RETENTION: Duration = Duration::from_secs(60 * 60);
#[derive(Clone)]
pub(crate) struct CompiledForms {
capsule_writable: Option<PathBuf>,
engine_key: u64,
}
impl CompiledForms {
pub(crate) fn new(engine: &Engine, capsule_writable: &Path) -> Self {
Self::with_ceiling(
engine,
capsule_writable,
murmur_artifact::zip_guard::max_artifact_decompressed_bytes(),
)
}
pub(crate) fn without_capsule(engine: &Engine) -> Self {
Self {
capsule_writable: None,
engine_key: engine_key(
engine,
murmur_artifact::zip_guard::max_artifact_decompressed_bytes(),
),
}
}
fn with_ceiling(engine: &Engine, capsule_writable: &Path, ceiling: u64) -> Self {
Self {
capsule_writable: Some(capsule_writable.to_path_buf()),
engine_key: engine_key(engine, ceiling),
}
}
pub(crate) fn load(&self, engine: &Engine, key_sha256: &str) -> Option<Component> {
if !is_key(key_sha256) {
return None;
}
let dir = self.dir(false)?;
load(engine, &dir.join(self.file_name(key_sha256)))
}
pub(crate) fn compile(
&self,
engine: &Engine,
key_sha256: &str,
wasm: &[u8],
) -> wasmtime::Result<Component> {
if !is_key(key_sha256) {
return Component::new(engine, wasm);
}
if let Some(component) = self.load(engine, key_sha256) {
return Ok(component);
}
let component = Component::new(engine, wasm)?;
if let (Some(dir), Ok(bytes)) = (self.dir(true), component.serialize()) {
store(&dir, &self.file_name(key_sha256), &bytes);
prune_stale(&dir, SystemTime::now());
}
Ok(component)
}
fn file_name(&self, key_sha256: &str) -> String {
format!("{key_sha256}-{:016x}.cwasm", self.engine_key)
}
fn dir(&self, create: bool) -> Option<PathBuf> {
let home = if create {
crate::murmur_home::ensure_murmur_home().ok()?
} else {
crate::state_store::murmur_home_dir().ok()?
};
if self.reachable_from_capsule(&home) {
return None;
}
let dir = home.join(COMPILED_DIR);
let mut widened = false;
if create {
match std::fs::symlink_metadata(&dir) {
Ok(metadata) if metadata.is_dir() => widened = !held_private(&dir),
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
_ => return None,
}
crate::murmur_home::hold_private_dir(&dir).ok()?;
}
if !held_private(&dir) || self.reachable_from_capsule(&dir) {
return None;
}
if widened {
for (path, _) in files(&dir) {
let _ = std::fs::remove_file(path);
}
}
Some(dir)
}
fn reachable_from_capsule(&self, dir: &Path) -> bool {
self.capsule_writable
.as_deref()
.is_some_and(|root| reachable_from(dir, root))
}
}
fn engine_key(engine: &Engine, ceiling: u64) -> u64 {
let mut hasher = std::collections::hash_map::DefaultHasher::new();
engine.precompile_compatibility_hash().hash(&mut hasher);
ceiling.hash(&mut hasher);
hasher.finish()
}
fn is_key(key_sha256: &str) -> bool {
key_sha256.len() == 64
&& key_sha256
.bytes()
.all(|byte| matches!(byte, b'0'..=b'9' | b'a'..=b'f'))
}
fn sidecar(path: &Path) -> PathBuf {
path.with_extension("cwasm.sha256")
}
fn reachable_from(dir: &Path, root: &Path) -> bool {
match (dir.canonicalize(), root.canonicalize()) {
(Ok(dir), Ok(root)) => dir.starts_with(root),
(Ok(_), Err(err)) => err.kind() != std::io::ErrorKind::NotFound,
(Err(_), _) => true,
}
}
fn held_private(dir: &Path) -> bool {
std::fs::symlink_metadata(dir).is_ok_and(|metadata| {
metadata.is_dir()
&& owned_within(
&metadata,
current_euid(),
crate::murmur_home::MURMUR_HOME_DIR_MODE,
)
})
}
fn owned_within(metadata: &Metadata, euid: u32, allowed: u32) -> bool {
use std::os::unix::fs::MetadataExt;
metadata.uid() == euid && !crate::murmur_home::is_wider_than(metadata.mode(), allowed)
}
#[allow(unsafe_code)]
fn current_euid() -> u32 {
unsafe { libc::geteuid() }
}
fn open_private(path: &Path) -> Option<(std::fs::File, Metadata)> {
let (file, metadata) = crate::resource_plane::open_no_follow(path).ok()?;
(metadata.is_file()
&& owned_within(
&metadata,
current_euid(),
crate::murmur_home::PRIVATE_FILE_MODE,
))
.then_some((file, metadata))
}
fn store(dir: &Path, file_name: &str, bytes: &[u8]) {
let path = dir.join(file_name);
let written = crate::murmur_home::write_private_file(&path, bytes).and_then(|()| {
crate::murmur_home::write_private_file(
&sidecar(&path),
murmur_artifact::sha256_hex(bytes).as_bytes(),
)
});
if written.is_ok() {
let _ = read_verified(&path);
}
}
fn read_verified(path: &Path) -> Option<Vec<u8>> {
use std::io::Read;
let (mut file, before) = open_private(path)?;
let mut bytes = Vec::with_capacity(usize::try_from(before.len()).ok()?);
file.read_to_end(&mut bytes).ok()?;
let mut recorded = String::new();
open_private(&sidecar(path))?
.0
.read_to_string(&mut recorded)
.ok()?;
let mut lines = recorded.lines();
let sha256 = lines.next()?.trim();
let unchanged = Some(FileStamp::of(&before))
.filter(|before| {
file.metadata()
.is_ok_and(|after| FileStamp::of(&after) == *before)
})
.map(|stamp| stamp.to_string());
if unchanged.is_some() && lines.next() == unchanged.as_deref() {
return Some(bytes);
}
if murmur_artifact::sha256_hex(&bytes) != sha256 {
return None;
}
if let Some(unchanged) = unchanged {
let _ = crate::murmur_home::write_private_file(
&sidecar(path),
format!("{sha256}\n{unchanged}\n").as_bytes(),
);
}
Some(bytes)
}
fn load(engine: &Engine, path: &Path) -> Option<Component> {
let bytes = read_verified(path)?;
#[allow(unsafe_code)]
let component = unsafe { Component::deserialize(engine, &bytes) };
component.ok()
}
fn prune_stale(dir: &Path, now: SystemTime) {
use std::os::unix::ffi::OsStrExt;
let unused = |used: Option<SystemTime>, retention: Duration| {
used.is_some_and(|used| {
now.duration_since(used)
.is_ok_and(|unused| unused > retention)
})
};
for (path, metadata) in files(dir) {
let Some(name) = path.file_name().map(OsStrExt::as_bytes) else {
continue;
};
if name.ends_with(b".cwasm") {
let sidecar = sidecar(&path);
let sidecar_metadata = std::fs::symlink_metadata(&sidecar)
.ok()
.filter(|metadata| !metadata.is_dir());
let used = last_used(&metadata).max(sidecar_metadata.as_ref().and_then(last_used));
if unused(used, FORM_RETENTION)
&& std::fs::remove_file(&path).is_ok()
&& sidecar_metadata.is_some()
{
let _ = std::fs::remove_file(&sidecar);
}
continue;
}
if is_sidecar_of_a_form(&path) {
continue;
}
let retention = if name.starts_with(b".") && name.ends_with(b".tmp") {
TEMP_RETENTION
} else {
FORM_RETENTION
};
if unused(last_used(&metadata), retention) {
let _ = std::fs::remove_file(&path);
}
}
}
fn is_sidecar_of_a_form(path: &Path) -> bool {
let form = path.with_extension("");
sidecar(&form) == path
&& std::fs::symlink_metadata(&form).is_ok_and(|metadata| !metadata.is_dir())
}
fn last_used(metadata: &Metadata) -> Option<SystemTime> {
match (metadata.accessed(), metadata.modified()) {
(Ok(accessed), Ok(modified)) => Some(accessed.max(modified)),
(Ok(used), Err(_)) | (Err(_), Ok(used)) => Some(used),
(Err(_), Err(_)) => None,
}
}
fn files(dir: &Path) -> impl Iterator<Item = (PathBuf, Metadata)> {
std::fs::read_dir(dir)
.into_iter()
.flatten()
.filter_map(Result::ok)
.filter_map(|entry| {
let path = entry.path();
let metadata = std::fs::symlink_metadata(&path).ok()?;
(!metadata.is_dir()).then_some((path, metadata))
})
}
#[cfg(test)]
pub(crate) const EMPTY_COMPONENT: &[u8] = b"\0asm\x0d\x00\x01\x00";
#[cfg(test)]
mod tests {
use super::*;
use std::fs::{FileTimes, Permissions};
use std::os::unix::fs::PermissionsExt;
fn engine() -> Engine {
let mut config = wasmtime::Config::new();
config.wasm_component_model(true);
Engine::new(&config).unwrap()
}
fn write_form(dir: &Path, bytes: &[u8]) -> PathBuf {
let path = dir.join("form.cwasm");
std::fs::write(&path, bytes).unwrap();
std::fs::set_permissions(&path, Permissions::from_mode(0o600)).unwrap();
std::fs::write(sidecar(&path), murmur_artifact::sha256_hex(bytes)).unwrap();
std::fs::set_permissions(sidecar(&path), Permissions::from_mode(0o600)).unwrap();
path
}
fn set_times(path: &Path, accessed: SystemTime, modified: SystemTime) {
std::fs::File::open(path)
.unwrap()
.set_times(
FileTimes::new()
.set_accessed(accessed)
.set_modified(modified),
)
.unwrap();
}
fn inode(path: &Path) -> u64 {
use std::os::unix::fs::MetadataExt;
std::fs::symlink_metadata(path)
.unwrap_or_else(|err| panic!("{}: {err}", path.display()))
.ino()
}
fn serialized_empty_component(engine: &Engine) -> Vec<u8> {
Component::new(engine, EMPTY_COMPONENT)
.unwrap()
.serialize()
.unwrap()
}
#[test]
fn load_returns_a_form_whose_sidecar_matches_its_bytes() {
let engine = engine();
let temp = tempfile::tempdir().unwrap();
let path = write_form(temp.path(), &serialized_empty_component(&engine));
assert!(load(&engine, &path).is_some());
}
#[test]
fn load_refuses_a_form_whose_bytes_differ_from_its_sidecar() {
let engine = engine();
let temp = tempfile::tempdir().unwrap();
let mut bytes = serialized_empty_component(&engine);
let path = write_form(temp.path(), &bytes);
let middle = bytes.len() / 2;
bytes[middle] ^= 0xff;
std::fs::write(&path, &bytes).unwrap();
assert!(load(&engine, &path).is_none());
}
#[test]
fn load_refuses_a_form_with_no_sidecar() {
let engine = engine();
let temp = tempfile::tempdir().unwrap();
let path = write_form(temp.path(), &serialized_empty_component(&engine));
std::fs::remove_file(sidecar(&path)).unwrap();
assert!(load(&engine, &path).is_none());
}
#[test]
fn load_refuses_a_form_readable_beyond_its_owner() {
let engine = engine();
let temp = tempfile::tempdir().unwrap();
let path = write_form(temp.path(), &serialized_empty_component(&engine));
std::fs::set_permissions(&path, Permissions::from_mode(0o640)).unwrap();
assert!(load(&engine, &path).is_none());
}
#[test]
fn load_refuses_a_form_whose_sidecar_is_readable_beyond_its_owner() {
let engine = engine();
let temp = tempfile::tempdir().unwrap();
let path = write_form(temp.path(), &serialized_empty_component(&engine));
std::fs::set_permissions(sidecar(&path), Permissions::from_mode(0o644)).unwrap();
assert!(load(&engine, &path).is_none());
}
#[test]
fn load_refuses_a_form_reached_through_a_symlink() {
let engine = engine();
let temp = tempfile::tempdir().unwrap();
let path = write_form(temp.path(), &serialized_empty_component(&engine));
let link = temp.path().join("link.cwasm");
std::os::unix::fs::symlink(&path, &link).unwrap();
std::fs::copy(sidecar(&path), sidecar(&link)).unwrap();
assert!(load(&engine, &link).is_none());
}
fn break_recorded_sha256(path: &Path) {
let recorded = std::fs::read_to_string(sidecar(path)).unwrap();
let stamp = recorded.lines().nth(1).unwrap_or_default();
assert!(!stamp.is_empty(), "no stamp recorded: {recorded:?}");
std::fs::write(sidecar(path), format!("{}\n{stamp}\n", "0".repeat(64))).unwrap();
}
fn recorded_stamp(path: &Path) -> String {
let recorded = std::fs::read_to_string(sidecar(path)).unwrap();
recorded.lines().nth(1).unwrap_or_default().to_owned()
}
fn current_stamp(path: &Path) -> String {
FileStamp::of(&std::fs::metadata(path).unwrap()).to_string()
}
#[test]
fn store_records_the_stamp_of_the_bytes_it_wrote() {
let engine = engine();
let temp = tempfile::tempdir().unwrap();
std::fs::set_permissions(temp.path(), Permissions::from_mode(0o700)).unwrap();
let bytes = serialized_empty_component(&engine);
store(temp.path(), "form.cwasm", &bytes);
let path = temp.path().join("form.cwasm");
assert_eq!(std::fs::read(&path).unwrap(), bytes);
assert_eq!(
std::fs::read_to_string(sidecar(&path)).unwrap(),
format!(
"{}\n{}\n",
murmur_artifact::sha256_hex(&bytes),
current_stamp(&path)
),
);
}
#[test]
fn a_file_whose_stamp_is_unchanged_is_taken_without_hashing() {
let engine = engine();
let temp = tempfile::tempdir().unwrap();
let path = write_form(temp.path(), &serialized_empty_component(&engine));
assert!(read_verified(&path).is_some());
break_recorded_sha256(&path);
assert!(load(&engine, &path).is_some());
}
#[test]
fn a_file_rewritten_since_its_stamp_is_hashed_again() {
let engine = engine();
let temp = tempfile::tempdir().unwrap();
let bytes = serialized_empty_component(&engine);
let path = write_form(temp.path(), &bytes);
assert!(read_verified(&path).is_some());
break_recorded_sha256(&path);
std::fs::write(&path, [bytes.as_slice(), b"\0"].concat()).unwrap();
assert!(read_verified(&path).is_none());
}
#[test]
fn a_file_rewritten_in_place_with_its_times_restored_is_hashed_again() {
use std::os::unix::fs::{FileExt, MetadataExt};
let engine = engine();
let temp = tempfile::tempdir().unwrap();
let bytes = serialized_empty_component(&engine);
let path = write_form(temp.path(), &bytes);
assert!(read_verified(&path).is_some());
break_recorded_sha256(&path);
let earlier = std::fs::metadata(&path).unwrap();
let file = std::fs::OpenOptions::new().write(true).open(&path).unwrap();
let middle = bytes.len() / 2;
file.write_all_at(&[!bytes[middle]], middle as u64).unwrap();
file.set_times(
FileTimes::new()
.set_accessed(earlier.accessed().unwrap())
.set_modified(earlier.modified().unwrap()),
)
.unwrap();
drop(file);
let later = std::fs::metadata(&path).unwrap();
assert_eq!(later.ino(), earlier.ino());
assert_eq!(later.len(), earlier.len());
assert_eq!(later.modified().unwrap(), earlier.modified().unwrap());
assert!(read_verified(&path).is_none());
}
#[test]
fn a_file_replaced_since_its_stamp_is_hashed_again() {
let engine = engine();
let temp = tempfile::tempdir().unwrap();
let bytes = serialized_empty_component(&engine);
let path = write_form(temp.path(), &bytes);
assert!(read_verified(&path).is_some());
break_recorded_sha256(&path);
let replacement = temp.path().join("replacement");
std::fs::write(&replacement, &bytes).unwrap();
std::fs::set_permissions(&replacement, Permissions::from_mode(0o600)).unwrap();
std::fs::rename(&replacement, &path).unwrap();
assert!(read_verified(&path).is_none());
}
#[test]
fn a_file_chmodded_since_its_stamp_is_hashed_again() {
let engine = engine();
let temp = tempfile::tempdir().unwrap();
let path = write_form(temp.path(), &serialized_empty_component(&engine));
assert!(read_verified(&path).is_some());
break_recorded_sha256(&path);
std::fs::set_permissions(&path, Permissions::from_mode(0o400)).unwrap();
assert!(read_verified(&path).is_none());
}
#[test]
fn a_touched_file_whose_bytes_still_match_is_restamped() {
let engine = engine();
let temp = tempfile::tempdir().unwrap();
let path = write_form(temp.path(), &serialized_empty_component(&engine));
assert!(read_verified(&path).is_some());
let stamped = recorded_stamp(&path);
std::fs::File::open(&path)
.unwrap()
.set_times(FileTimes::new().set_modified(SystemTime::now()))
.unwrap();
assert_ne!(current_stamp(&path), stamped);
assert!(read_verified(&path).is_some());
assert_eq!(recorded_stamp(&path), current_stamp(&path));
}
#[test]
fn a_digest_only_sidecar_is_hashed_and_stamped() {
let engine = engine();
let temp = tempfile::tempdir().unwrap();
let bytes = serialized_empty_component(&engine);
let path = write_form(temp.path(), &bytes);
assert_eq!(recorded_stamp(&path), "");
assert!(read_verified(&path).is_some());
assert_eq!(recorded_stamp(&path), current_stamp(&path));
assert_eq!(
std::fs::read_to_string(sidecar(&path))
.unwrap()
.lines()
.next(),
Some(murmur_artifact::sha256_hex(&bytes).as_str()),
);
}
#[test]
fn owned_within_refuses_an_owner_other_than_the_expected_uid() {
let temp = tempfile::tempdir().unwrap();
let path = temp.path().join("form.cwasm");
std::fs::write(&path, b"form").unwrap();
std::fs::set_permissions(&path, Permissions::from_mode(0o600)).unwrap();
let metadata = std::fs::metadata(&path).unwrap();
assert!(owned_within(&metadata, current_euid(), 0o600));
assert!(!owned_within(
&metadata,
current_euid().wrapping_add(1),
0o600
));
}
#[test]
fn held_private_accepts_only_an_owner_only_directory_reached_without_a_symlink() {
let temp = tempfile::tempdir().unwrap();
let dir = temp.path().join("forms");
std::fs::create_dir(&dir).unwrap();
std::fs::set_permissions(&dir, Permissions::from_mode(0o700)).unwrap();
assert!(held_private(&dir));
let link = temp.path().join("link");
std::os::unix::fs::symlink(&dir, &link).unwrap();
assert!(!held_private(&link));
std::fs::set_permissions(&dir, Permissions::from_mode(0o750)).unwrap();
assert!(!held_private(&dir));
}
#[test]
fn a_directory_beneath_the_capsule_workdir_is_reachable_from_it() {
let temp = tempfile::tempdir().unwrap();
let workdir = temp.path().join("work");
let inside = workdir.join(".murmur");
let outside = temp.path().join("home");
std::fs::create_dir_all(&inside).unwrap();
std::fs::create_dir(&outside).unwrap();
assert!(reachable_from(&workdir, &workdir));
assert!(reachable_from(&inside, &workdir));
assert!(!reachable_from(&outside, &workdir));
assert!(!reachable_from(&outside, &temp.path().join("not-created")));
assert!(reachable_from(&temp.path().join("missing"), &workdir));
}
#[test]
fn prune_removes_only_entries_unused_for_the_retention_period() {
let temp = tempfile::tempdir().unwrap();
let dir = temp.path();
let now = SystemTime::now();
let day = Duration::from_secs(24 * 60 * 60);
let stale = now - 31 * day;
let set_times = |path: &Path, accessed: SystemTime, modified: SystemTime| {
std::fs::File::open(path)
.unwrap()
.set_times(
FileTimes::new()
.set_accessed(accessed)
.set_modified(modified),
)
.unwrap();
};
let unused = dir.join("a.cwasm");
let read_recently = dir.join("b.cwasm");
let fresh = dir.join("c.cwasm");
let subdir = dir.join("d");
std::fs::write(&unused, b"a").unwrap();
std::fs::write(&read_recently, b"b").unwrap();
std::fs::write(&fresh, b"c").unwrap();
std::fs::create_dir(&subdir).unwrap();
set_times(&unused, stale, stale);
set_times(&read_recently, now - day, stale);
set_times(&subdir, stale, stale);
prune_stale(dir, now);
assert!(!unused.exists());
assert!(read_recently.exists());
assert!(fresh.exists());
assert!(subdir.is_dir());
}
#[test]
fn the_form_key_carries_the_decompression_ceiling() {
let engine = engine();
let workdir = Path::new("/nonexistent-workdir");
let sha = murmur_artifact::sha256_hex(EMPTY_COMPONENT);
let name = |ceiling| CompiledForms::with_ceiling(&engine, workdir, ceiling).file_name(&sha);
assert_ne!(name(1_000_000), name(2_000_000));
assert_eq!(name(1_000_000), name(1_000_000));
let engine_key = name(1_000_000)
.strip_prefix(&format!("{sha}-"))
.and_then(|rest| rest.strip_suffix(".cwasm"))
.map(str::to_owned)
.expect("<sha>-<engine key>.cwasm");
assert_eq!(engine_key.len(), 16);
assert!(engine_key
.bytes()
.all(|byte| matches!(byte, b'0'..=b'9' | b'a'..=b'f')));
assert_eq!(
CompiledForms::new(&engine, workdir).file_name(&sha),
name(murmur_artifact::zip_guard::max_artifact_decompressed_bytes()),
);
}
#[test]
fn load_returns_only_a_form_stored_under_the_same_key_and_ceiling() {
let home = tempfile::tempdir().unwrap();
crate::murmur_home::run_with_home(
"compiled_forms::tests::inner_load_returns_only_a_form_stored_under_the_same_key_and_ceiling",
home.path(),
);
}
#[test]
#[ignore = "run by load_returns_only_a_form_stored_under_the_same_key_and_ceiling"]
fn inner_load_returns_only_a_form_stored_under_the_same_key_and_ceiling() {
if !crate::murmur_home::in_scratch_home() {
return;
}
let engine = engine();
let workdir = tempfile::tempdir().unwrap();
let ceiling = 1_000_000;
let forms = CompiledForms::with_ceiling(&engine, workdir.path(), ceiling);
let sha = murmur_artifact::sha256_hex(b"a payload");
let compiled = crate::state_store::murmur_home_dir()
.unwrap()
.join(COMPILED_DIR);
assert!(forms.load(&engine, &sha).is_none());
assert!(!compiled.exists(), "{} was created", compiled.display());
forms.compile(&engine, &sha, EMPTY_COMPONENT).unwrap();
assert!(forms.load(&engine, &sha).is_some());
assert!(
CompiledForms::with_ceiling(&engine, workdir.path(), ceiling + 1)
.load(&engine, &sha)
.is_none()
);
assert!(forms.load(&engine, &sha.to_uppercase()).is_none());
assert!(forms.load(&engine, "not-a-key").is_none());
assert!(forms.compile(&engine, &sha, b"\0asm\x01\0\0\0").is_ok());
}
#[test]
fn prune_treats_a_form_and_its_sidecar_as_one_set() {
let temp = tempfile::tempdir().unwrap();
let dir = temp.path();
let now = SystemTime::now();
let day = Duration::from_secs(24 * 60 * 60);
let (stale, recent) = (now - 31 * day, now - day);
let entry = |name: &str, accessed: SystemTime, modified: SystemTime| {
let path = dir.join(name);
std::fs::write(&path, name).unwrap();
set_times(&path, accessed, modified);
path
};
let unused = [
entry("1.cwasm", stale, stale),
entry("1.cwasm.sha256", stale, stale),
];
let sidecar_read = [
entry("2.cwasm", stale, stale),
entry("2.cwasm.sha256", recent, stale),
];
let form_read = [
entry("3.cwasm", recent, stale),
entry("3.cwasm.sha256", stale, stale),
];
let stale_orphan = entry("4.cwasm.sha256", stale, stale);
let recent_orphan = entry("5.cwasm.sha256", recent, recent);
prune_stale(dir, now);
for path in unused.iter().chain([&stale_orphan]) {
assert!(!path.exists(), "{} survived", path.display());
}
for path in sidecar_read
.iter()
.chain(&form_read)
.chain([&recent_orphan])
{
assert!(path.exists(), "{} was removed", path.display());
}
}
#[test]
fn prune_removes_temp_files_left_by_a_crashed_write() {
let temp = tempfile::tempdir().unwrap();
let dir = temp.path();
let now = SystemTime::now();
let entry = |name: &str, age: Duration| {
let path = dir.join(name);
std::fs::write(&path, name).unwrap();
set_times(&path, now - age, now - age);
path
};
let crashed = entry(
".a.cwasm.1.0192d3f4a5b67c8d9e0f1a2b3c4d5e6f.tmp",
Duration::from_secs(2 * 60 * 60),
);
let in_flight = entry(
".b.cwasm.2.0192d3f4a5b67c8d9e0f1a2b3c4d5e70.tmp",
Duration::from_secs(60),
);
let form = entry("c.cwasm", Duration::from_secs(2 * 60 * 60));
prune_stale(dir, now);
assert!(!crashed.exists());
assert!(in_flight.exists());
assert!(form.exists());
}
#[test]
fn two_engine_keys_used_alternately_keep_and_load_their_own_forms() {
let home = tempfile::tempdir().unwrap();
crate::murmur_home::run_with_home(
"compiled_forms::tests::inner_two_engine_keys_used_alternately_keep_and_load_their_own_forms",
home.path(),
);
}
#[test]
#[ignore = "run by two_engine_keys_used_alternately_keep_and_load_their_own_forms"]
fn inner_two_engine_keys_used_alternately_keep_and_load_their_own_forms() {
if !crate::murmur_home::in_scratch_home() {
return;
}
const X: &[u8] = EMPTY_COMPONENT;
const Y: &[u8] = b"\0asm\x0d\x00\x01\x00\x00\x02\x01y";
const Z: &[u8] = b"\0asm\x0d\x00\x01\x00\x00\x02\x01z";
let a_engine = engine();
let b_engine = {
let mut config = wasmtime::Config::new();
config.wasm_component_model(true);
config.cranelift_opt_level(wasmtime::OptLevel::None);
Engine::new(&config).unwrap()
};
let workdir = tempfile::tempdir().unwrap();
let a = (CompiledForms::new(&a_engine, workdir.path()), &a_engine);
let b = (CompiledForms::new(&b_engine, workdir.path()), &b_engine);
let compile = |(forms, engine): &(CompiledForms, &Engine), wasm: &[u8]| {
forms
.compile(engine, &murmur_artifact::sha256_hex(wasm), wasm)
.unwrap();
};
let dir = PathBuf::from(std::env::var_os("HOME").unwrap())
.join(".murmur")
.join(COMPILED_DIR);
let set = |(forms, _): &(CompiledForms, &Engine), wasm: &[u8]| {
let form = dir.join(format!(
"{}-{:016x}.cwasm",
murmur_artifact::sha256_hex(wasm),
forms.engine_key
));
[sidecar(&form), form]
};
let inodes = |sets: &[[PathBuf; 2]]| -> Vec<u64> {
sets.iter().flatten().map(|path| inode(path)).collect()
};
assert_ne!(set(&a, X), set(&b, X));
compile(&a, X);
compile(&b, X);
for path in set(&a, X).iter().chain(&set(&b, X)) {
assert!(path.is_file(), "{} missing", path.display());
}
let x_inodes = inodes(&[set(&a, X), set(&b, X)]);
for forms in [&a, &b, &a, &b] {
compile(forms, X);
}
assert_eq!(inodes(&[set(&a, X), set(&b, X)]), x_inodes);
compile(&a, Y);
assert_eq!(inodes(&[set(&b, X)]), x_inodes[2..]);
let a_xy_inodes = inodes(&[set(&a, X), set(&a, Y)]);
let stale = SystemTime::now() - Duration::from_secs(31 * 24 * 60 * 60);
for path in set(&b, X) {
set_times(&path, stale, stale);
}
compile(&a, Z);
for path in set(&b, X) {
assert!(!path.exists(), "{} survived", path.display());
}
for path in set(&a, Z) {
assert!(path.is_file(), "{} missing", path.display());
}
assert_eq!(inodes(&[set(&a, X), set(&a, Y)]), a_xy_inodes);
compile(&b, X);
for path in set(&b, X) {
assert!(path.is_file(), "{} missing", path.display());
}
}
#[test]
fn compile_errors_exactly_as_component_new_and_writes_nothing() {
let home = tempfile::tempdir().unwrap();
crate::murmur_home::run_with_home(
"compiled_forms::tests::inner_compile_errors_exactly_as_component_new",
home.path(),
);
let compiled = home.path().join(".murmur").join(COMPILED_DIR);
let forms = std::fs::read_dir(&compiled)
.map(|entries| {
entries
.filter_map(Result::ok)
.filter(|entry| entry.path().extension().is_some_and(|ext| ext == "cwasm"))
.count()
})
.unwrap_or(0);
assert_eq!(forms, 0, "{} holds a form", compiled.display());
}
#[test]
#[ignore = "run by compile_errors_exactly_as_component_new_and_writes_nothing"]
fn inner_compile_errors_exactly_as_component_new() {
if !crate::murmur_home::in_scratch_home() {
return;
}
let engine = engine();
let workdir = tempfile::tempdir().unwrap();
let forms = CompiledForms::new(&engine, workdir.path());
let wasm: &[u8] = b"\0asm\x01\0\0\0";
let key = murmur_artifact::sha256_hex(wasm);
let cached = forms
.compile(&engine, &key, wasm)
.expect_err("compile fails");
let direct = Component::new(&engine, wasm).expect_err("Component::new fails");
assert_eq!(cached.to_string(), direct.to_string());
}
}