1use std::{
14 collections::BTreeMap,
15 fs,
16 io::Write,
17 path::{Path, PathBuf},
18};
19
20use base64::Engine as _;
21use serde_json::Value;
22
23use crate::{
24 integrity::{FrontendIntegrityInputs, create_explicit_run_integrity},
25 lifecycle::{note_kept_evidence, remove_stored_tree_deferred},
26 run_store::{RunIntegrity, RunMetadata},
27 rust_project::PreparedRustProject,
28 rust_wasm_evidence::{Owner, WasmRecord, WasmRun},
29 served_tests::ServedTest,
30};
31
32fn wasm_crates(root: &Path) -> Vec<PathBuf> {
36 const SKIPPED: &[&str] = &[
37 "node_modules",
38 "target",
39 "build",
40 "dist",
41 "pkg",
42 ".git",
43 ".supercov",
44 ".wrangler",
45 ];
46 let mut found = Vec::new();
47 let mut directories = vec![(root.to_path_buf(), 0)];
48 while let Some((directory, depth)) = directories.pop() {
49 let Ok(entries) = fs::read_dir(&directory) else {
50 continue;
51 };
52 for entry in entries.flatten() {
53 let path = entry.path();
54 let name = entry.file_name();
55 let Some(name) = name.to_str() else {
56 continue;
57 };
58 let Ok(kind) = entry.file_type() else {
59 continue;
60 };
61 if kind.is_dir() {
62 if depth < 6 && !SKIPPED.contains(&name) {
63 directories.push((path, depth + 1));
64 }
65 continue;
66 }
67 if kind.is_file() && name == "Cargo.toml" && is_wasm_library(&path) {
68 found.push(path);
69 }
70 }
71 }
72 found.sort();
73 found
74}
75
76fn is_wasm_library(manifest: &Path) -> bool {
77 let Ok(text) = fs::read_to_string(manifest) else {
78 return false;
79 };
80 let Ok(document) = text.parse::<toml::Table>() else {
81 return false;
82 };
83 let cdylib = document
84 .get("lib")
85 .and_then(|lib| lib.get("crate-type"))
86 .and_then(toml::Value::as_array)
87 .is_some_and(|types| types.iter().any(|kind| kind.as_str() == Some("cdylib")));
88 let depends = |table: Option<&toml::Value>| {
89 table
90 .and_then(|dependencies| dependencies.get("wasm-bindgen"))
91 .is_some()
92 };
93 let bindgen = depends(document.get("dependencies"))
95 || document
96 .get("target")
97 .and_then(toml::Value::as_table)
98 .is_some_and(|targets| {
99 targets
100 .values()
101 .any(|target| depends(target.get("dependencies")))
102 });
103 let worker = ["wrangler.toml", "wrangler.json", "wrangler.jsonc"]
104 .iter()
105 .any(|name| manifest.with_file_name(name).is_file());
106 cdylib && bindgen && !worker
107}
108
109pub struct RustWasmServer {
111 project: PreparedRustProject,
112 integrity: RunIntegrity,
113 source_inputs: crate::source_manifest::Inputs,
114 source_roots: Option<Vec<String>>,
115 workspace: PathBuf,
117 records: PathBuf,
119 directory: PathBuf,
121}
122
123impl RustWasmServer {
124 pub fn present(root: &Path) -> bool {
126 !wasm_crates(root).is_empty()
127 }
128
129 pub fn prepare(
131 root: &Path,
132 workspace: &Path,
133 server_evidence_root: &Path,
134 run_id: &str,
135 command: &[String],
136 ) -> Result<Vec<Self>, String> {
137 let mut by_workspace = BTreeMap::<PathBuf, ()>::new();
138 for manifest in wasm_crates(workspace) {
139 let crate_directory = manifest.parent().unwrap_or(workspace).to_owned();
140 let cargo_root = crate::rust_worker::cargo_workspace_root(&crate_directory)
141 .unwrap_or(crate_directory);
142 by_workspace.insert(cargo_root, ());
143 }
144 let mut servers = Vec::new();
145 for (index, cargo_root) in by_workspace.into_keys().enumerate() {
146 let Ok(relative) = cargo_root.strip_prefix(workspace) else {
147 continue;
148 };
149 let source_roots = crate::source_discovery::configured_source_roots(
150 &std::env::vars().collect::<BTreeMap<_, _>>(),
151 );
152 let mut integrity_inputs = crate::rust_run::collect_integrity_inputs(root, command)?;
155 crate::source_discovery::fold_roots_into_configuration(
156 &mut integrity_inputs.execution_configuration,
157 source_roots.as_deref(),
158 );
159 let source_inputs =
160 crate::source_capture::capture(root, "rust", integrity_inputs.assertion_paths())?;
161 let integrity = create_explicit_run_integrity(
162 root,
163 &integrity_inputs,
164 &FrontendIntegrityInputs::embedded_rust(),
165 )
166 .map_err(|error| error.to_string())?;
167 let cfg = crate::rust_worker::wasm_target_cfg(&cargo_root);
168 let mut project =
169 crate::rust_project::prepare_rust_project_for(&cargo_root, None, cfg.as_ref())
170 .map_err(|error| error.to_string())?;
171 crate::rust_wasm_evidence::root_paths(&mut project, relative);
172 let directory = root
173 .join(".supercov/work")
174 .join(run_id)
175 .join(format!("rust-wasm-{index}"));
176 fs::create_dir_all(&directory).map_err(|error| error.to_string())?;
177 servers.push(Self {
178 project,
179 integrity,
180 source_inputs,
181 source_roots,
182 workspace: workspace.to_owned(),
183 records: server_evidence_root.join("wasm"),
184 directory,
185 });
186 }
187 Ok(servers)
188 }
189
190 pub fn source_files(&self) -> usize {
191 self.project.source_files.len()
192 }
193
194 pub fn source_fingerprint(&self) -> String {
196 self.integrity.fingerprint.source.clone()
197 }
198
199 pub fn environment(&self) -> BTreeMap<String, String> {
203 let mut rustflags = std::env::var("RUSTFLAGS").unwrap_or_default();
204 if !rustflags.is_empty() {
205 rustflags.push(' ');
206 }
207 rustflags.push_str("--cap-lints=warn -A unused_parens");
208 BTreeMap::from([
209 ("SUPERCOV_WASM_REGIONS".into(), "1".into()),
210 ("RUSTFLAGS".into(), rustflags),
211 ])
212 }
213
214 pub fn publish(
217 self,
218 root: &Path,
219 evidence_run_id: &str,
220 metadata: RunMetadata,
221 _served: &[ServedTest],
222 diagnostics: &mut dyn Write,
223 ) -> Result<Option<(PathBuf, RunMetadata)>, String> {
224 let tests = crate::served_tests::read_attempts(
226 &self
227 .workspace
228 .join(".supercov/evidence")
229 .join(evidence_run_id),
230 );
231 let result = read_records(&self.records).and_then(|records| {
232 crate::rust_wasm_evidence::publish(
233 root,
234 WasmRun {
235 language: "Rust wasm",
236 archive: "rust-wasm-evidence.raw.gz",
237 project: &self.project,
238 integrity: &self.integrity,
239 source_inputs: &self.source_inputs,
240 source_roots: self.source_roots.clone(),
241 },
242 records,
243 metadata,
244 &tests,
245 diagnostics,
246 )
247 });
248 let result = result.map_err(|error| {
249 note_kept_evidence(
250 root,
251 &self.records,
252 &format!("{evidence_run_id}-rust-wasm"),
253 error,
254 )
255 });
256 let _ = remove_stored_tree_deferred(root, &self.directory);
257 result
258 }
259}
260
261fn read_records(directory: &Path) -> Result<Vec<WasmRecord>, String> {
263 let mut files = fs::read_dir(directory)
264 .map(|entries| {
265 entries
266 .filter_map(Result::ok)
267 .map(|entry| entry.path())
268 .filter(|path| {
269 path.extension()
270 .is_some_and(|extension| extension == "jsonl")
271 })
272 .collect::<Vec<_>>()
273 })
274 .unwrap_or_default();
275 files.sort();
276 let mut records = Vec::new();
277 for file in &files {
278 let text =
279 fs::read_to_string(file).map_err(|error| format!("{}: {error}", file.display()))?;
280 for line in text.lines() {
282 let Ok(value) = serde_json::from_str::<Value>(line) else {
283 continue;
284 };
285 let owner = match value.get("owner").and_then(Value::as_str) {
286 Some(attempt) => Owner::Attempt(attempt.to_owned()),
287 None => Owner::Nobody,
288 };
289 if let Some(regions) = regions_from(&value) {
290 records.push(WasmRecord { owner, regions });
291 }
292 }
293 }
294 Ok(records)
295}
296
297pub(crate) fn regions_from(value: &Value) -> Option<Vec<(String, Vec<u8>)>> {
299 let mut regions = Vec::new();
300 for region in value.get("regions")?.as_array()? {
301 let crate_key = region.get(0)?.as_str()?.to_owned();
302 let length = usize::try_from(region.get(1)?.as_u64()?).ok()?;
303 let mut bytes = vec![0_u8; length];
304 for segment in region.get(2)?.as_array()? {
305 let offset = usize::try_from(segment.get(0)?.as_u64()?).ok()?;
306 let data = base64::engine::general_purpose::STANDARD
307 .decode(segment.get(1)?.as_str()?)
308 .ok()?;
309 bytes
310 .get_mut(offset..offset + data.len())?
311 .copy_from_slice(&data);
312 }
313 regions.push((crate_key, bytes));
314 }
315 Some(regions)
316}
317
318#[cfg(test)]
319mod tests {
320 use super::*;
321
322 #[test]
323 fn a_wasm_library_is_a_cdylib_that_depends_on_wasm_bindgen() {
324 let root = std::env::temp_dir().join(format!(
325 "supercov-rust-wasm-crates-{}-{}",
326 std::process::id(),
327 std::time::SystemTime::now()
328 .duration_since(std::time::UNIX_EPOCH)
329 .unwrap()
330 .as_nanos()
331 ));
332 let write = |relative: &str, text: &str| {
333 let path = root.join(relative);
334 fs::create_dir_all(path.parent().unwrap()).unwrap();
335 fs::write(path, text).unwrap();
336 };
337 write(
338 "rust/Cargo.toml",
339 "[package]\nname = \"a\"\n[lib]\ncrate-type = [\"cdylib\", \"rlib\"]\n[dependencies]\nwasm-bindgen = \"0.2\"\n",
340 );
341 write(
344 "native/Cargo.toml",
345 "[package]\nname = \"b\"\n[dependencies]\nwasm-bindgen = \"0.2\"\n",
346 );
347 write(
348 "plugin/Cargo.toml",
349 "[package]\nname = \"c\"\n[lib]\ncrate-type = [\"cdylib\"]\n",
350 );
351 write(
352 "worker/Cargo.toml",
353 "[package]\nname = \"d\"\n[lib]\ncrate-type = [\"cdylib\"]\n[dependencies]\nwasm-bindgen = \"0.2\"\n",
354 );
355 write("worker/wrangler.toml", "main = \"x\"\n");
356 write(
357 "node_modules/e/Cargo.toml",
358 "[package]\nname = \"e\"\n[lib]\ncrate-type = [\"cdylib\"]\n[dependencies]\nwasm-bindgen = \"0.2\"\n",
359 );
360 let found = wasm_crates(&root)
361 .into_iter()
362 .map(|path| path.strip_prefix(&root).unwrap().to_owned())
363 .collect::<Vec<_>>();
364 assert_eq!(found, [PathBuf::from("rust/Cargo.toml")]);
365 fs::remove_dir_all(root).unwrap();
366 }
367}