use std::{
collections::BTreeMap,
fs,
io::Write,
path::{Path, PathBuf},
};
use base64::Engine as _;
use serde_json::Value;
use crate::{
integrity::{FrontendIntegrityInputs, create_explicit_run_integrity},
lifecycle::{note_kept_evidence, remove_stored_tree_deferred},
run_store::{RunIntegrity, RunMetadata},
rust_project::PreparedRustProject,
rust_wasm_evidence::{Owner, WasmRecord, WasmRun},
served_tests::ServedTest,
};
fn wasm_crates(root: &Path) -> Vec<PathBuf> {
const SKIPPED: &[&str] = &[
"node_modules",
"target",
"build",
"dist",
"pkg",
".git",
".supercov",
".wrangler",
];
let mut found = Vec::new();
let mut directories = vec![(root.to_path_buf(), 0)];
while let Some((directory, depth)) = directories.pop() {
let Ok(entries) = fs::read_dir(&directory) else {
continue;
};
for entry in entries.flatten() {
let path = entry.path();
let name = entry.file_name();
let Some(name) = name.to_str() else {
continue;
};
let Ok(kind) = entry.file_type() else {
continue;
};
if kind.is_dir() {
if depth < 6 && !SKIPPED.contains(&name) {
directories.push((path, depth + 1));
}
continue;
}
if kind.is_file() && name == "Cargo.toml" && is_wasm_library(&path) {
found.push(path);
}
}
}
found.sort();
found
}
fn is_wasm_library(manifest: &Path) -> bool {
let Ok(text) = fs::read_to_string(manifest) else {
return false;
};
let Ok(document) = text.parse::<toml::Table>() else {
return false;
};
let cdylib = document
.get("lib")
.and_then(|lib| lib.get("crate-type"))
.and_then(toml::Value::as_array)
.is_some_and(|types| types.iter().any(|kind| kind.as_str() == Some("cdylib")));
let depends = |table: Option<&toml::Value>| {
table
.and_then(|dependencies| dependencies.get("wasm-bindgen"))
.is_some()
};
let bindgen = depends(document.get("dependencies"))
|| document
.get("target")
.and_then(toml::Value::as_table)
.is_some_and(|targets| {
targets
.values()
.any(|target| depends(target.get("dependencies")))
});
let worker = ["wrangler.toml", "wrangler.json", "wrangler.jsonc"]
.iter()
.any(|name| manifest.with_file_name(name).is_file());
cdylib && bindgen && !worker
}
pub struct RustWasmServer {
project: PreparedRustProject,
integrity: RunIntegrity,
source_inputs: crate::source_manifest::Inputs,
source_roots: Option<Vec<String>>,
workspace: PathBuf,
records: PathBuf,
directory: PathBuf,
}
impl RustWasmServer {
pub fn present(root: &Path) -> bool {
!wasm_crates(root).is_empty()
}
pub fn prepare(
root: &Path,
workspace: &Path,
server_evidence_root: &Path,
run_id: &str,
command: &[String],
) -> Result<Vec<Self>, String> {
let mut by_workspace = BTreeMap::<PathBuf, ()>::new();
for manifest in wasm_crates(workspace) {
let crate_directory = manifest.parent().unwrap_or(workspace).to_owned();
let cargo_root = crate::rust_worker::cargo_workspace_root(&crate_directory)
.unwrap_or(crate_directory);
by_workspace.insert(cargo_root, ());
}
let mut servers = Vec::new();
for (index, cargo_root) in by_workspace.into_keys().enumerate() {
let Ok(relative) = cargo_root.strip_prefix(workspace) else {
continue;
};
let source_roots = crate::source_discovery::configured_source_roots(
&std::env::vars().collect::<BTreeMap<_, _>>(),
);
let mut integrity_inputs = crate::rust_run::collect_integrity_inputs(root, command)?;
crate::source_discovery::fold_roots_into_configuration(
&mut integrity_inputs.execution_configuration,
source_roots.as_deref(),
);
let source_inputs =
crate::source_capture::capture(root, "rust", integrity_inputs.assertion_paths())?;
let integrity = create_explicit_run_integrity(
root,
&integrity_inputs,
&FrontendIntegrityInputs::embedded_rust(),
)
.map_err(|error| error.to_string())?;
let cfg = crate::rust_worker::wasm_target_cfg(&cargo_root);
let mut project =
crate::rust_project::prepare_rust_project_for(&cargo_root, None, cfg.as_ref())
.map_err(|error| error.to_string())?;
crate::rust_wasm_evidence::root_paths(&mut project, relative);
let directory = root
.join(".supercov/work")
.join(run_id)
.join(format!("rust-wasm-{index}"));
fs::create_dir_all(&directory).map_err(|error| error.to_string())?;
servers.push(Self {
project,
integrity,
source_inputs,
source_roots,
workspace: workspace.to_owned(),
records: server_evidence_root.join("wasm"),
directory,
});
}
Ok(servers)
}
pub fn source_files(&self) -> usize {
self.project.source_files.len()
}
pub fn source_fingerprint(&self) -> String {
self.integrity.fingerprint.source.clone()
}
pub fn environment(&self) -> BTreeMap<String, String> {
let mut rustflags = std::env::var("RUSTFLAGS").unwrap_or_default();
if !rustflags.is_empty() {
rustflags.push(' ');
}
rustflags.push_str("--cap-lints=warn -A unused_parens");
BTreeMap::from([
("SUPERCOV_WASM_REGIONS".into(), "1".into()),
("RUSTFLAGS".into(), rustflags),
])
}
pub fn publish(
self,
root: &Path,
evidence_run_id: &str,
metadata: RunMetadata,
_served: &[ServedTest],
diagnostics: &mut dyn Write,
) -> Result<Option<(PathBuf, RunMetadata)>, String> {
let tests = crate::served_tests::read_attempts(
&self
.workspace
.join(".supercov/evidence")
.join(evidence_run_id),
);
let result = read_records(&self.records).and_then(|records| {
crate::rust_wasm_evidence::publish(
root,
WasmRun {
language: "Rust wasm",
archive: "rust-wasm-evidence.raw.gz",
project: &self.project,
integrity: &self.integrity,
source_inputs: &self.source_inputs,
source_roots: self.source_roots.clone(),
},
records,
metadata,
&tests,
diagnostics,
)
});
let result = result.map_err(|error| {
note_kept_evidence(
root,
&self.records,
&format!("{evidence_run_id}-rust-wasm"),
error,
)
});
let _ = remove_stored_tree_deferred(root, &self.directory);
result
}
}
fn read_records(directory: &Path) -> Result<Vec<WasmRecord>, String> {
let mut files = fs::read_dir(directory)
.map(|entries| {
entries
.filter_map(Result::ok)
.map(|entry| entry.path())
.filter(|path| {
path.extension()
.is_some_and(|extension| extension == "jsonl")
})
.collect::<Vec<_>>()
})
.unwrap_or_default();
files.sort();
let mut records = Vec::new();
for file in &files {
let text =
fs::read_to_string(file).map_err(|error| format!("{}: {error}", file.display()))?;
for line in text.lines() {
let Ok(value) = serde_json::from_str::<Value>(line) else {
continue;
};
let owner = match value.get("owner").and_then(Value::as_str) {
Some(attempt) => Owner::Attempt(attempt.to_owned()),
None => Owner::Nobody,
};
if let Some(regions) = regions_from(&value) {
records.push(WasmRecord { owner, regions });
}
}
}
Ok(records)
}
pub(crate) fn regions_from(value: &Value) -> Option<Vec<(String, Vec<u8>)>> {
let mut regions = Vec::new();
for region in value.get("regions")?.as_array()? {
let crate_key = region.get(0)?.as_str()?.to_owned();
let length = usize::try_from(region.get(1)?.as_u64()?).ok()?;
let mut bytes = vec![0_u8; length];
for segment in region.get(2)?.as_array()? {
let offset = usize::try_from(segment.get(0)?.as_u64()?).ok()?;
let data = base64::engine::general_purpose::STANDARD
.decode(segment.get(1)?.as_str()?)
.ok()?;
bytes
.get_mut(offset..offset + data.len())?
.copy_from_slice(&data);
}
regions.push((crate_key, bytes));
}
Some(regions)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_wasm_library_is_a_cdylib_that_depends_on_wasm_bindgen() {
let root = std::env::temp_dir().join(format!(
"supercov-rust-wasm-crates-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
let write = |relative: &str, text: &str| {
let path = root.join(relative);
fs::create_dir_all(path.parent().unwrap()).unwrap();
fs::write(path, text).unwrap();
};
write(
"rust/Cargo.toml",
"[package]\nname = \"a\"\n[lib]\ncrate-type = [\"cdylib\", \"rlib\"]\n[dependencies]\nwasm-bindgen = \"0.2\"\n",
);
write(
"native/Cargo.toml",
"[package]\nname = \"b\"\n[dependencies]\nwasm-bindgen = \"0.2\"\n",
);
write(
"plugin/Cargo.toml",
"[package]\nname = \"c\"\n[lib]\ncrate-type = [\"cdylib\"]\n",
);
write(
"worker/Cargo.toml",
"[package]\nname = \"d\"\n[lib]\ncrate-type = [\"cdylib\"]\n[dependencies]\nwasm-bindgen = \"0.2\"\n",
);
write("worker/wrangler.toml", "main = \"x\"\n");
write(
"node_modules/e/Cargo.toml",
"[package]\nname = \"e\"\n[lib]\ncrate-type = [\"cdylib\"]\n[dependencies]\nwasm-bindgen = \"0.2\"\n",
);
let found = wasm_crates(&root)
.into_iter()
.map(|path| path.strip_prefix(&root).unwrap().to_owned())
.collect::<Vec<_>>();
assert_eq!(found, [PathBuf::from("rust/Cargo.toml")]);
fs::remove_dir_all(root).unwrap();
}
}