Skip to main content

supercov_engine/
rust_libtest_companion.rs

1//! Deterministic source preparation for Supercov's exact-toolchain libtest
2//! companion.
3//!
4//! Published Supercov platform packages will contain the already-built rlib.
5//! This module is the release/development builder: it consumes the selected
6//! toolchain's exact `library/test` source, rejects unrecognized layouts, and
7//! atomically publishes a patched tree whose identity contains no scratch
8//! paths.
9
10use std::{
11    ffi::OsString,
12    fs::{self, OpenOptions},
13    io::{Cursor, Write},
14    path::{Component, Path, PathBuf},
15    process::Command,
16    thread,
17    time::{Duration, Instant, SystemTime, UNIX_EPOCH},
18};
19
20use ar_archive_writer::{
21    ArchiveKind as WritableArchiveKind, DEFAULT_OBJECT_READER, NewArchiveMember,
22    write_archive_to_stream,
23};
24use object::read::archive::{ArchiveFile, ArchiveKind as ReadableArchiveKind};
25use serde::{Deserialize, Serialize};
26use sha2::{Digest, Sha256};
27use supercov_contracts::{
28    RUST_LIBTEST_COMPANION_BUNDLE_SCHEMA_VERSION, RUST_LIBTEST_EVENT_PROTOCOL_VERSION,
29    RustCompilerIdentity,
30};
31
32use crate::{
33    rust_compiler_selection::{
34        SelectedRustCompilerCompanion, probe_rustc_identity, select_rust_compiler_companion,
35    },
36    rust_libtest_events::rust_libtest_event_runtime_source,
37};
38
39const SOURCE_IDENTITY_FILE: &str = "supercov-libtest-source.json";
40const BUILD_LOCK_TIMEOUT: Duration = Duration::from_secs(300);
41
42const LIB_ANCHOR: &str = "mod console;";
43const LIB_REPLACEMENT: &str = "mod console;\nmod supercov_events;";
44const CONSOLE_ANCHOR: &str = "fn on_test_event(\n    event: &TestEvent,\n    st: &mut ConsoleTestState,\n    out: &mut dyn OutputFormatter,\n) -> io::Result<()> {\n    match (*event).clone() {";
45const CONSOLE_REPLACEMENT: &str = "fn on_test_event(\n    event: &TestEvent,\n    st: &mut ConsoleTestState,\n    out: &mut dyn OutputFormatter,\n) -> io::Result<()> {\n    crate::supercov_events::emit(event)?;\n    match (*event).clone() {";
46const LISTING_ANCHOR: &str =
47    "    out.write_discovery_start()?;\n    for test in filter_tests(opts, tests).into_iter() {";
48const LISTING_REPLACEMENT: &str = "    out.write_discovery_start()?;\n    let tests_len = tests.len();\n    let filtered_tests = filter_tests(opts, tests);\n    crate::supercov_events::emit_listing(tests_len - filtered_tests.len(), filtered_tests.len())?;\n    for test in filtered_tests {";
49const IN_PROCESS_ANCHOR: &str = "    // Buffer for capturing standard I/O\n    let data =";
50const IN_PROCESS_REPLACEMENT: &str = "    let _supercov_context = crate::supercov_events::enter_test(desc.name.as_slice())\n        .expect(\"Supercov could not enter the exact libtest context\");\n\n    // Buffer for capturing standard I/O\n    let data =";
51const SPAWNED_PROCESS_ANCHOR: &str = "fn run_test_in_spawned_subprocess(desc: TestDesc, runnable_test: RunnableTest) -> ! {\n    let builtin_panic_hook";
52const SPAWNED_PROCESS_REPLACEMENT: &str = "fn run_test_in_spawned_subprocess(desc: TestDesc, runnable_test: RunnableTest) -> ! {\n    let _supercov_context = crate::supercov_events::enter_test(desc.name.as_slice())\n        .expect(\"Supercov could not enter the exact spawned libtest context\");\n    let builtin_panic_hook";
53const BENCH_ANCHOR: &str = "        Runnable::Bench(runnable_bench) => {\n            // Benchmarks aren't expected to panic, so we run them all in-process.\n            runnable_bench.run";
54const BENCH_REPLACEMENT: &str = "        Runnable::Bench(runnable_bench) => {\n            // Benchmarks aren't expected to panic, so we run them all in-process.\n            let _supercov_context = crate::supercov_events::enter_test(desc.name.as_slice())\n                .expect(\"Supercov could not enter the exact benchmark context\");\n            runnable_bench.run";
55
56#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
57#[serde(rename_all = "camelCase", deny_unknown_fields)]
58pub struct RustLibtestCompanionSourceIdentity {
59    pub event_protocol_version: u32,
60    pub rustc_commit_hash: String,
61    pub rustc_release: String,
62    pub host_triple: String,
63    pub original_source_sha256: String,
64    pub event_runtime_sha256: String,
65    pub patched_source_sha256: String,
66}
67
68#[derive(Debug, Clone, PartialEq, Eq)]
69pub struct RustLibtestCompanionBuildPlan {
70    pub source: PathBuf,
71    pub output: PathBuf,
72    pub arguments: Vec<OsString>,
73    pub rustc_bootstrap: OsString,
74}
75
76#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
77#[serde(rename_all = "camelCase", deny_unknown_fields)]
78pub struct RustLibtestCompanionBundle {
79    pub schema_version: u32,
80    pub event_protocol_version: u32,
81    pub compiler_companion_build_id: String,
82    pub rustc_commit_hash: String,
83    pub host_triple: String,
84    pub original_source_sha256: String,
85    pub event_runtime_sha256: String,
86    pub patched_source_sha256: String,
87    pub artifact_file: String,
88    pub artifact_sha256: String,
89}
90
91#[derive(Debug, Clone, PartialEq, Eq)]
92pub struct SelectedRustLibtestCompanion {
93    pub bundle_path: PathBuf,
94    pub artifact_path: PathBuf,
95    pub bundle: RustLibtestCompanionBundle,
96}
97
98#[derive(Debug, Clone, PartialEq, Eq)]
99pub enum RustLibtestCompanionError {
100    Io {
101        path: PathBuf,
102        reason: String,
103    },
104    UnsafeSource(PathBuf),
105    NonUtf8Path(PathBuf),
106    UnrecognizedSource {
107        path: PathBuf,
108        anchor: &'static str,
109    },
110    DependencyMetadata {
111        directory: PathBuf,
112        crate_name: &'static str,
113        count: usize,
114    },
115    InvalidBundle {
116        path: PathBuf,
117        reason: String,
118    },
119    InvalidArchive(String),
120    BundleMismatch(String),
121    BuildFailed {
122        program: PathBuf,
123        status: Option<i32>,
124        stdout: String,
125        stderr: String,
126    },
127    LockTimeout(PathBuf),
128}
129
130impl std::fmt::Display for RustLibtestCompanionError {
131    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
132        match self {
133            Self::Io { path, reason } => write!(formatter, "{}: {reason}", path.display()),
134            Self::UnsafeSource(path) => write!(
135                formatter,
136                "exact libtest source contains a symlink or special file: {}",
137                path.display()
138            ),
139            Self::NonUtf8Path(path) => write!(
140                formatter,
141                "exact libtest source contains a non-UTF-8 path: {}",
142                path.display()
143            ),
144            Self::UnrecognizedSource { path, anchor } => write!(
145                formatter,
146                "selected toolchain libtest source {} does not contain exactly one {anchor} patch anchor",
147                path.display()
148            ),
149            Self::DependencyMetadata {
150                directory,
151                crate_name,
152                count,
153            } => write!(
154                formatter,
155                "expected exactly one full {crate_name} metadata file in {}, found {count}",
156                directory.display()
157            ),
158            Self::InvalidBundle { path, reason } => write!(
159                formatter,
160                "invalid libtest companion bundle {}: {reason}",
161                path.display()
162            ),
163            Self::InvalidArchive(reason) => {
164                write!(formatter, "invalid libtest companion rlib: {reason}")
165            }
166            Self::BundleMismatch(reason) => {
167                write!(formatter, "libtest companion bundle mismatch: {reason}")
168            }
169            Self::BuildFailed {
170                program,
171                status,
172                stdout,
173                stderr,
174            } => write!(
175                formatter,
176                "{} failed with status {status:?}: {stderr}{stdout}",
177                program.display()
178            ),
179            Self::LockTimeout(path) => write!(
180                formatter,
181                "timed out waiting for the exact libtest builder lock {}",
182                path.display()
183            ),
184        }
185    }
186}
187
188impl std::error::Error for RustLibtestCompanionError {}
189
190fn io_error(path: &Path, error: impl std::fmt::Display) -> RustLibtestCompanionError {
191    RustLibtestCompanionError::Io {
192        path: path.to_path_buf(),
193        reason: error.to_string(),
194    }
195}
196
197#[cfg(unix)]
198fn sync_directory(path: &Path) -> Result<(), RustLibtestCompanionError> {
199    let directory = OpenOptions::new()
200        .read(true)
201        .open(path)
202        .map_err(|error| io_error(path, error))?;
203    directory.sync_all().map_err(|error| io_error(path, error))
204}
205
206#[cfg(not(unix))]
207fn sync_directory(_path: &Path) -> Result<(), RustLibtestCompanionError> {
208    Ok(())
209}
210
211fn regular_directory(path: &Path) -> Result<PathBuf, RustLibtestCompanionError> {
212    let canonical = fs::canonicalize(path).map_err(|error| io_error(path, error))?;
213    let metadata = fs::symlink_metadata(&canonical).map_err(|error| io_error(&canonical, error))?;
214    if !metadata.file_type().is_dir() {
215        return Err(RustLibtestCompanionError::UnsafeSource(canonical));
216    }
217    Ok(canonical)
218}
219
220fn safe_component(path: &Path) -> Result<String, RustLibtestCompanionError> {
221    if path
222        .components()
223        .any(|component| !matches!(component, Component::Normal(_)))
224    {
225        return Err(RustLibtestCompanionError::UnsafeSource(path.to_path_buf()));
226    }
227    path.to_str()
228        .map(|value| value.replace('\\', "/"))
229        .ok_or_else(|| RustLibtestCompanionError::NonUtf8Path(path.to_path_buf()))
230}
231
232fn collect_tree(
233    root: &Path,
234    relative: &Path,
235    entries: &mut Vec<(String, PathBuf, bool)>,
236) -> Result<(), RustLibtestCompanionError> {
237    let directory = root.join(relative);
238    let mut children = fs::read_dir(&directory)
239        .map_err(|error| io_error(&directory, error))?
240        .collect::<Result<Vec<_>, _>>()
241        .map_err(|error| io_error(&directory, error))?;
242    children.sort_by_key(std::fs::DirEntry::file_name);
243    for child in children {
244        let name = child.file_name();
245        let child_relative = relative.join(name);
246        let display = safe_component(&child_relative)?;
247        let file_type = child
248            .file_type()
249            .map_err(|error| io_error(&child.path(), error))?;
250        if file_type.is_dir() {
251            entries.push((display, child.path(), true));
252            collect_tree(root, &child_relative, entries)?;
253        } else if file_type.is_file() {
254            entries.push((display, child.path(), false));
255        } else {
256            return Err(RustLibtestCompanionError::UnsafeSource(child.path()));
257        }
258    }
259    Ok(())
260}
261
262fn source_tree_digest(root: &Path) -> Result<String, RustLibtestCompanionError> {
263    source_tree_digest_excluding(root, None)
264}
265
266fn source_tree_digest_excluding(
267    root: &Path,
268    excluded_root_file: Option<&str>,
269) -> Result<String, RustLibtestCompanionError> {
270    let mut entries = Vec::new();
271    collect_tree(root, Path::new(""), &mut entries)?;
272    let mut digest = Sha256::new();
273    for (relative, path, directory) in entries {
274        if excluded_root_file.is_some_and(|excluded| relative == excluded) {
275            continue;
276        }
277        digest.update([u8::from(directory)]);
278        digest.update((relative.len() as u64).to_le_bytes());
279        digest.update(relative.as_bytes());
280        if !directory {
281            let bytes = fs::read(&path).map_err(|error| io_error(&path, error))?;
282            digest.update((bytes.len() as u64).to_le_bytes());
283            digest.update(bytes);
284        }
285    }
286    Ok(format!("{:x}", digest.finalize()))
287}
288
289fn copy_regular_tree(source: &Path, destination: &Path) -> Result<(), RustLibtestCompanionError> {
290    let mut entries = Vec::new();
291    collect_tree(source, Path::new(""), &mut entries)?;
292    for (relative, path, directory) in entries {
293        let destination_path = destination.join(&relative);
294        if directory {
295            fs::create_dir(&destination_path)
296                .map_err(|error| io_error(&destination_path, error))?;
297        } else {
298            let bytes = fs::read(&path).map_err(|error| io_error(&path, error))?;
299            let mut options = OpenOptions::new();
300            options.write(true).create_new(true);
301            #[cfg(unix)]
302            {
303                use std::os::unix::fs::OpenOptionsExt as _;
304                options.mode(0o600);
305            }
306            let mut file = options
307                .open(&destination_path)
308                .map_err(|error| io_error(&destination_path, error))?;
309            file.write_all(&bytes)
310                .and_then(|()| file.sync_all())
311                .map_err(|error| io_error(&destination_path, error))?;
312        }
313    }
314    Ok(())
315}
316
317fn replace_once(
318    path: &Path,
319    anchor: &'static str,
320    replacement: &str,
321) -> Result<(), RustLibtestCompanionError> {
322    let source = fs::read_to_string(path).map_err(|error| io_error(path, error))?;
323    if source.matches(anchor).count() != 1 {
324        return Err(RustLibtestCompanionError::UnrecognizedSource {
325            path: path.into(),
326            anchor,
327        });
328    }
329    let bytes = source.replacen(anchor, replacement, 1).into_bytes();
330    let mut file = OpenOptions::new()
331        .write(true)
332        .truncate(true)
333        .open(path)
334        .map_err(|error| io_error(path, error))?;
335    file.write_all(&bytes)
336        .and_then(|()| file.sync_all())
337        .map_err(|error| io_error(path, error))
338}
339
340struct RemoveDirectoryOnDrop(Option<PathBuf>);
341
342impl Drop for RemoveDirectoryOnDrop {
343    fn drop(&mut self) {
344        if let Some(path) = self.0.take() {
345            let _ = fs::remove_dir_all(path);
346        }
347    }
348}
349
350struct RemoveFileOnDrop(Option<PathBuf>);
351
352impl Drop for RemoveFileOnDrop {
353    fn drop(&mut self) {
354        if let Some(path) = self.0.take() {
355            let _ = fs::remove_file(path);
356        }
357    }
358}
359
360fn acquire_kernel_lock(path: &Path) -> Result<fs::File, RustLibtestCompanionError> {
361    if let Ok(metadata) = fs::symlink_metadata(path)
362        && !metadata.file_type().is_file()
363    {
364        return Err(RustLibtestCompanionError::UnsafeSource(path.to_path_buf()));
365    }
366    let started = Instant::now();
367    loop {
368        let mut options = OpenOptions::new();
369        options.read(true).write(true).create(true);
370        #[cfg(unix)]
371        {
372            use std::os::unix::fs::OpenOptionsExt as _;
373            #[cfg(target_os = "linux")]
374            const O_NOFOLLOW: i32 = 0x2_0000;
375            #[cfg(target_os = "macos")]
376            const O_NOFOLLOW: i32 = 0x100;
377            #[cfg(any(target_os = "linux", target_os = "macos"))]
378            options.custom_flags(O_NOFOLLOW);
379            options.mode(0o600);
380        }
381        let mut file = options.open(path).map_err(|error| io_error(path, error))?;
382        if !file
383            .metadata()
384            .map_err(|error| io_error(path, error))?
385            .file_type()
386            .is_file()
387        {
388            return Err(RustLibtestCompanionError::UnsafeSource(path.to_path_buf()));
389        }
390        match file.try_lock() {
391            Ok(()) => {
392                file.set_len(0)
393                    .and_then(|()| writeln!(file, "{}", std::process::id()))
394                    .and_then(|()| file.sync_all())
395                    .map_err(|error| io_error(path, error))?;
396                return Ok(file);
397            }
398            Err(fs::TryLockError::WouldBlock) => {
399                if started.elapsed() >= BUILD_LOCK_TIMEOUT {
400                    return Err(RustLibtestCompanionError::LockTimeout(path.to_path_buf()));
401                }
402                thread::sleep(Duration::from_millis(25));
403            }
404            Err(fs::TryLockError::Error(error)) => return Err(io_error(path, error)),
405        }
406    }
407}
408
409#[cfg(unix)]
410fn inherit_lock_through_exec(command: &mut Command, lock: &fs::File) {
411    use std::os::{fd::AsRawFd as _, unix::process::CommandExt as _};
412
413    let lock_fd = lock.as_raw_fd();
414    // OpenOptions uses close-on-exec. Duplicate the already locked open-file
415    // description in the post-fork child so an abruptly killed builder cannot
416    // release publication ownership while its rustc child is still writing.
417    // `dup` is async-signal-safe and the child-only duplicate intentionally
418    // survives exec until rustc exits.
419    unsafe {
420        command.pre_exec(move || {
421            if libc::dup(lock_fd) < 0 {
422                Err(std::io::Error::last_os_error())
423            } else {
424                Ok(())
425            }
426        });
427    }
428}
429
430#[cfg(not(unix))]
431fn inherit_lock_through_exec(_command: &mut Command, _lock: &fs::File) {
432    // Windows releases a process's byte-range locks when it terminates, an
433    // inherited handle notwithstanding, so the Unix trick has no counterpart.
434    // A builder killed while its rustc still writes leaves one uniquely named
435    // .partial behind; the next builder ignores it and publishes its own.
436}
437
438fn remove_owned_path(path: &Path, expect_directory: bool) -> Result<(), RustLibtestCompanionError> {
439    let metadata = match fs::symlink_metadata(path) {
440        Ok(metadata) => metadata,
441        Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()),
442        Err(error) => return Err(io_error(path, error)),
443    };
444    if expect_directory && metadata.file_type().is_dir() {
445        fs::remove_dir_all(path).map_err(|error| io_error(path, error))
446    } else if !expect_directory && metadata.file_type().is_file() {
447        fs::remove_file(path).map_err(|error| io_error(path, error))
448    } else {
449        Err(RustLibtestCompanionError::UnsafeSource(path.to_path_buf()))
450    }
451}
452
453fn remove_owned_partials(
454    directory: &Path,
455    prefix: &str,
456    expect_directory: bool,
457) -> Result<(), RustLibtestCompanionError> {
458    for entry in fs::read_dir(directory).map_err(|error| io_error(directory, error))? {
459        let entry = entry.map_err(|error| io_error(directory, error))?;
460        let name = entry.file_name();
461        let Some(name) = name.to_str() else {
462            continue;
463        };
464        if name.starts_with(prefix) && name.ends_with(".partial") {
465            remove_owned_path(&entry.path(), expect_directory)?;
466        }
467    }
468    Ok(())
469}
470
471fn validate_prepared_libtest_source(
472    source_root: &Path,
473    destination: &Path,
474    compiler: &RustCompilerIdentity,
475) -> Result<RustLibtestCompanionSourceIdentity, RustLibtestCompanionError> {
476    let destination = regular_directory(destination)?;
477    let identity_path = destination.join(SOURCE_IDENTITY_FILE);
478    let identity: RustLibtestCompanionSourceIdentity =
479        serde_json::from_slice(&read_regular_file(&identity_path)?).map_err(|error| {
480            RustLibtestCompanionError::InvalidBundle {
481                path: identity_path.clone(),
482                reason: error.to_string(),
483            }
484        })?;
485    let runtime_sha256 = format!(
486        "{:x}",
487        Sha256::digest(rust_libtest_event_runtime_source().as_bytes())
488    );
489    if identity.event_protocol_version != RUST_LIBTEST_EVENT_PROTOCOL_VERSION
490        || identity.rustc_commit_hash != compiler.rustc_commit_hash
491        || identity.rustc_release != compiler.rustc_release
492        || identity.host_triple != compiler.host_triple
493        || identity.event_runtime_sha256 != runtime_sha256
494        || [
495            identity.original_source_sha256.as_str(),
496            identity.event_runtime_sha256.as_str(),
497            identity.patched_source_sha256.as_str(),
498        ]
499        .iter()
500        .any(|value| !canonical_lower_sha256(value))
501    {
502        return Err(RustLibtestCompanionError::BundleMismatch(
503            "prepared libtest source identity differs from the exact compiler/runtime".into(),
504        ));
505    }
506    if source_tree_digest(source_root)? != identity.original_source_sha256 {
507        return Err(RustLibtestCompanionError::BundleMismatch(
508            "exact toolchain libtest source changed after preparation".into(),
509        ));
510    }
511    if source_tree_digest_excluding(&destination, Some(SOURCE_IDENTITY_FILE))?
512        != identity.patched_source_sha256
513    {
514        return Err(RustLibtestCompanionError::BundleMismatch(
515            "prepared libtest source tree digest differs".into(),
516        ));
517    }
518    Ok(identity)
519}
520
521/// Copy and patch an exact `library/test` source tree without modifying the
522/// toolchain sysroot. `destination` becomes visible only after the complete
523/// patched tree and its identity have been validated.
524pub fn prepare_exact_libtest_source(
525    source_root: &Path,
526    destination: &Path,
527    compiler: &RustCompilerIdentity,
528) -> Result<RustLibtestCompanionSourceIdentity, RustLibtestCompanionError> {
529    let source_root = regular_directory(source_root)?;
530    let parent = destination
531        .parent()
532        .ok_or_else(|| RustLibtestCompanionError::UnsafeSource(destination.to_path_buf()))?;
533    let parent = regular_directory(parent)?;
534    let name = destination
535        .file_name()
536        .and_then(|name| name.to_str())
537        .filter(|name| !name.is_empty() && *name != "." && *name != "..")
538        .ok_or_else(|| RustLibtestCompanionError::UnsafeSource(destination.to_path_buf()))?;
539    let destination = parent.join(name);
540    let lock_path = parent.join(format!(".{name}.lock"));
541    let _lock = acquire_kernel_lock(&lock_path)?;
542    remove_owned_partials(&parent, &format!(".{name}."), true)?;
543    match fs::symlink_metadata(&destination) {
544        Ok(metadata) if metadata.file_type().is_dir() => {
545            return validate_prepared_libtest_source(&source_root, &destination, compiler);
546        }
547        Ok(_) => return Err(RustLibtestCompanionError::UnsafeSource(destination)),
548        Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
549        Err(error) => return Err(io_error(&destination, error)),
550    }
551    let nonce = SystemTime::now()
552        .duration_since(UNIX_EPOCH)
553        .map_err(|error| io_error(&destination, error))?
554        .as_nanos();
555    let partial = parent.join(format!(".{name}.{}-{nonce}.partial", std::process::id()));
556    fs::create_dir(&partial).map_err(|error| io_error(&partial, error))?;
557    let mut cleanup = RemoveDirectoryOnDrop(Some(partial.clone()));
558
559    let original_source_sha256 = source_tree_digest(&source_root)?;
560    copy_regular_tree(&source_root, &partial)?;
561    if source_tree_digest(&source_root)? != original_source_sha256 {
562        return Err(RustLibtestCompanionError::BundleMismatch(
563            "exact toolchain libtest source changed while it was copied".into(),
564        ));
565    }
566    let lib = partial.join("src/lib.rs");
567    let console = partial.join("src/console.rs");
568    replace_once(&lib, LIB_ANCHOR, LIB_REPLACEMENT)?;
569    replace_once(&lib, IN_PROCESS_ANCHOR, IN_PROCESS_REPLACEMENT)?;
570    replace_once(&lib, SPAWNED_PROCESS_ANCHOR, SPAWNED_PROCESS_REPLACEMENT)?;
571    replace_once(&lib, BENCH_ANCHOR, BENCH_REPLACEMENT)?;
572    replace_once(&console, CONSOLE_ANCHOR, CONSOLE_REPLACEMENT)?;
573    replace_once(&console, LISTING_ANCHOR, LISTING_REPLACEMENT)?;
574    let event_runtime = rust_libtest_event_runtime_source();
575    let event_path = partial.join("src/supercov_events.rs");
576    let mut options = OpenOptions::new();
577    options.write(true).create_new(true);
578    #[cfg(unix)]
579    {
580        use std::os::unix::fs::OpenOptionsExt as _;
581        options.mode(0o600);
582    }
583    let mut event_file = options
584        .open(&event_path)
585        .map_err(|error| io_error(&event_path, error))?;
586    event_file
587        .write_all(event_runtime.as_bytes())
588        .and_then(|()| event_file.sync_all())
589        .map_err(|error| io_error(&event_path, error))?;
590    drop(event_file);
591
592    let identity = RustLibtestCompanionSourceIdentity {
593        event_protocol_version: RUST_LIBTEST_EVENT_PROTOCOL_VERSION,
594        rustc_commit_hash: compiler.rustc_commit_hash.clone(),
595        rustc_release: compiler.rustc_release.clone(),
596        host_triple: compiler.host_triple.clone(),
597        original_source_sha256,
598        event_runtime_sha256: format!("{:x}", Sha256::digest(event_runtime.as_bytes())),
599        patched_source_sha256: source_tree_digest(&partial)?,
600    };
601    let identity_path = partial.join(SOURCE_IDENTITY_FILE);
602    let mut identity_bytes =
603        serde_json::to_vec_pretty(&identity).map_err(|error| io_error(&identity_path, error))?;
604    identity_bytes.push(b'\n');
605    let mut identity_file = OpenOptions::new()
606        .write(true)
607        .create_new(true)
608        .open(&identity_path)
609        .map_err(|error| io_error(&identity_path, error))?;
610    identity_file
611        .write_all(&identity_bytes)
612        .and_then(|()| identity_file.sync_all())
613        .map_err(|error| io_error(&identity_path, error))?;
614    // Windows will not rename a directory while a file inside it is open, so
615    // both writers are closed before the tree is published; the file
616    // publications below already did this.
617    drop(identity_file);
618    fs::rename(&partial, &destination).map_err(|error| io_error(&destination, error))?;
619    sync_directory(&parent)?;
620    cleanup.0 = None;
621    Ok(identity)
622}
623
624fn one_metadata(
625    directory: &Path,
626    crate_name: &'static str,
627) -> Result<PathBuf, RustLibtestCompanionError> {
628    let prefix = format!("lib{crate_name}-");
629    let mut matches = fs::read_dir(directory)
630        .map_err(|error| io_error(directory, error))?
631        .collect::<Result<Vec<_>, _>>()
632        .map_err(|error| io_error(directory, error))?
633        .into_iter()
634        .filter(|entry| {
635            let metadata = entry
636                .file_name()
637                .to_str()
638                .is_some_and(|name| name.starts_with(&prefix) && name.ends_with(".rmeta"));
639            let archive = entry.path().with_extension("rlib");
640            metadata
641                && entry.file_type().is_ok_and(|file_type| file_type.is_file())
642                && fs::symlink_metadata(archive)
643                    .is_ok_and(|metadata| metadata.file_type().is_file())
644        })
645        .map(|entry| entry.path())
646        .collect::<Vec<_>>();
647    matches.sort();
648    matches.dedup();
649    match matches.as_slice() {
650        [path] => Ok(path.clone()),
651        _ => Err(RustLibtestCompanionError::DependencyMetadata {
652            directory: directory.to_path_buf(),
653            crate_name,
654            count: matches.len(),
655        }),
656    }
657}
658
659pub fn rust_libtest_companion_build_plan(
660    patched_source: &Path,
661    target_libdir: &Path,
662    output: &Path,
663) -> Result<RustLibtestCompanionBuildPlan, RustLibtestCompanionError> {
664    let patched_source = regular_directory(patched_source)?;
665    let target_libdir = regular_directory(target_libdir)?;
666    let source = patched_source.join("src/lib.rs");
667    if !fs::symlink_metadata(&source).is_ok_and(|metadata| metadata.file_type().is_file()) {
668        return Err(RustLibtestCompanionError::UnsafeSource(source));
669    }
670    let getopts = one_metadata(&target_libdir, "getopts")?;
671    let libc = one_metadata(&target_libdir, "libc")?;
672    let identity_path = patched_source.join("supercov-libtest-source.json");
673    let identity: RustLibtestCompanionSourceIdentity =
674        serde_json::from_slice(&read_regular_file(&identity_path)?).map_err(|error| {
675            RustLibtestCompanionError::InvalidBundle {
676                path: identity_path,
677                reason: error.to_string(),
678            }
679        })?;
680    if !canonical_lower_sha256(&identity.patched_source_sha256) {
681        return Err(RustLibtestCompanionError::BundleMismatch(
682            "patched libtest source identity is noncanonical".into(),
683        ));
684    }
685    Ok(RustLibtestCompanionBuildPlan {
686        source: source.clone(),
687        output: output.to_path_buf(),
688        arguments: vec![
689            source.into_os_string(),
690            "--crate-name".into(),
691            "test".into(),
692            "--crate-type".into(),
693            "rlib".into(),
694            "--edition".into(),
695            "2024".into(),
696            "-Zcrate-attr=feature(rustc_private)".into(),
697            format!(
698                "--remap-path-prefix={}=/supercov/libtest-source",
699                patched_source.display()
700            )
701            .into(),
702            format!(
703                "-Cmetadata=supercov_{}",
704                &identity.patched_source_sha256[..16]
705            )
706            .into(),
707            "-L".into(),
708            format!("dependency={}", target_libdir.display()).into(),
709            "--extern".into(),
710            format!("getopts={}", getopts.display()).into(),
711            "--extern".into(),
712            format!("libc={}", libc.display()).into(),
713            "-o".into(),
714            output.as_os_str().to_owned(),
715        ],
716        rustc_bootstrap: "1".into(),
717    })
718}
719
720fn canonical_archive_kind(
721    kind: ReadableArchiveKind,
722    host_triple: &str,
723) -> Result<WritableArchiveKind, RustLibtestCompanionError> {
724    match kind {
725        ReadableArchiveKind::Gnu => Ok(WritableArchiveKind::Gnu),
726        ReadableArchiveKind::Gnu64 => Ok(WritableArchiveKind::Gnu64),
727        ReadableArchiveKind::Bsd if host_triple.contains("-apple-") => {
728            Ok(WritableArchiveKind::Darwin)
729        }
730        ReadableArchiveKind::Bsd => Ok(WritableArchiveKind::Bsd),
731        ReadableArchiveKind::Bsd64 => Ok(WritableArchiveKind::Darwin64),
732        ReadableArchiveKind::Coff => Ok(WritableArchiveKind::Coff),
733        ReadableArchiveKind::AixBig => Ok(WritableArchiveKind::AixBig),
734        _ => Err(RustLibtestCompanionError::InvalidArchive(format!(
735            "unsupported archive kind {kind:?} for {host_triple}"
736        ))),
737    }
738}
739
740/// Rebuild rustc's rlib container with content-derived member names.
741///
742/// rustc's object and metadata payloads are reproducible for an exact libtest
743/// source/compiler identity, but its temporary codegen archive-member suffix
744/// is deliberately per-session. The suffix has no linking semantics, yet it
745/// changes the rlib digest and its symbol table. Supercov owns release artifact
746/// identity, so it replaces only those container-local names and lets LLVM's
747/// archive writer reconstruct the target-format symbol table from the exact,
748/// unmodified payloads.
749pub fn canonicalize_rust_libtest_rlib(
750    bytes: &[u8],
751    host_triple: &str,
752) -> Result<Vec<u8>, RustLibtestCompanionError> {
753    let archive = ArchiveFile::parse(bytes).map_err(|error| {
754        RustLibtestCompanionError::InvalidArchive(format!("cannot parse archive: {error}"))
755    })?;
756    if archive.is_thin() {
757        return Err(RustLibtestCompanionError::InvalidArchive(
758            "thin archives are not self-contained".into(),
759        ));
760    }
761    let archive_kind = canonical_archive_kind(archive.kind(), host_triple)?;
762
763    let mut metadata = None;
764    let mut objects = Vec::new();
765    for member in archive.members() {
766        let member = member.map_err(|error| {
767            RustLibtestCompanionError::InvalidArchive(format!(
768                "cannot parse archive member: {error}"
769            ))
770        })?;
771        let name = std::str::from_utf8(member.name()).map_err(|_| {
772            RustLibtestCompanionError::InvalidArchive(
773                "archive contains a non-UTF-8 member name".into(),
774            )
775        })?;
776        let data = member.data(bytes).map_err(|error| {
777            RustLibtestCompanionError::InvalidArchive(format!(
778                "cannot read archive member {name}: {error}"
779            ))
780        })?;
781        if name == "lib.rmeta" {
782            if metadata.replace(data.to_vec()).is_some() {
783                return Err(RustLibtestCompanionError::InvalidArchive(
784                    "archive contains more than one lib.rmeta member".into(),
785                ));
786            }
787        } else if name.ends_with(".rcgu.o") {
788            objects.push((format!("{:x}", Sha256::digest(data)), data.to_vec()));
789        } else {
790            return Err(RustLibtestCompanionError::InvalidArchive(format!(
791                "unexpected archive member {name}"
792            )));
793        }
794    }
795    let metadata = metadata.ok_or_else(|| {
796        RustLibtestCompanionError::InvalidArchive("archive has no lib.rmeta member".into())
797    })?;
798    if objects.is_empty() {
799        return Err(RustLibtestCompanionError::InvalidArchive(
800            "archive has no codegen object members".into(),
801        ));
802    }
803    objects.sort_by(|left, right| left.0.cmp(&right.0).then_with(|| left.1.cmp(&right.1)));
804
805    let mut owned_members = Vec::with_capacity(objects.len() + 1);
806    owned_members.push(("lib.rmeta".to_owned(), metadata));
807    for (ordinal, (digest, data)) in objects.into_iter().enumerate() {
808        owned_members.push((
809            format!("supercov-libtest-{ordinal:06}-{digest}.rcgu.o"),
810            data,
811        ));
812    }
813    let members = owned_members
814        .iter()
815        .map(|(name, data)| NewArchiveMember::new(data, &DEFAULT_OBJECT_READER, name.clone()))
816        .collect::<Vec<_>>();
817    let mut output = Cursor::new(Vec::new());
818    write_archive_to_stream(
819        &mut output,
820        &members,
821        archive_kind,
822        false,
823        Some(host_triple.contains("arm64ec")),
824    )
825    .map_err(|error| {
826        RustLibtestCompanionError::InvalidArchive(format!(
827            "cannot write canonical archive: {error}"
828        ))
829    })?;
830    let output = output.into_inner();
831    let reparsed = ArchiveFile::parse(output.as_slice()).map_err(|error| {
832        RustLibtestCompanionError::InvalidArchive(format!(
833            "canonical archive did not parse: {error}"
834        ))
835    })?;
836    if reparsed.is_thin() || reparsed.members().count() != owned_members.len() {
837        return Err(RustLibtestCompanionError::InvalidArchive(
838            "canonical archive failed structural verification".into(),
839        ));
840    }
841    Ok(output)
842}
843
844pub fn rust_libtest_companion_bundle_path(compiler_companion: &Path) -> PathBuf {
845    let mut value = compiler_companion.as_os_str().to_owned();
846    value.push(".libtest.json");
847    PathBuf::from(value)
848}
849
850fn canonical_lower_sha256(value: &str) -> bool {
851    value.len() == 64
852        && value
853            .bytes()
854            .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
855}
856
857fn sha256_file(path: &Path) -> Result<String, RustLibtestCompanionError> {
858    Ok(format!("{:x}", Sha256::digest(read_regular_file(path)?)))
859}
860
861fn read_regular_file(path: &Path) -> Result<Vec<u8>, RustLibtestCompanionError> {
862    let metadata = fs::symlink_metadata(path).map_err(|error| io_error(path, error))?;
863    if !metadata.file_type().is_file() {
864        return Err(RustLibtestCompanionError::UnsafeSource(path.to_path_buf()));
865    }
866    let mut options = OpenOptions::new();
867    options.read(true);
868    #[cfg(any(target_os = "linux", target_os = "macos"))]
869    {
870        use std::os::unix::fs::OpenOptionsExt as _;
871        #[cfg(target_os = "linux")]
872        const O_NOFOLLOW: i32 = 0x2_0000;
873        #[cfg(target_os = "macos")]
874        const O_NOFOLLOW: i32 = 0x100;
875        options.custom_flags(O_NOFOLLOW);
876    }
877    let mut file = options.open(path).map_err(|error| io_error(path, error))?;
878    if !file
879        .metadata()
880        .map_err(|error| io_error(path, error))?
881        .file_type()
882        .is_file()
883    {
884        return Err(RustLibtestCompanionError::UnsafeSource(path.to_path_buf()));
885    }
886    let mut bytes = Vec::new();
887    std::io::Read::read_to_end(&mut file, &mut bytes).map_err(|error| io_error(path, error))?;
888    Ok(bytes)
889}
890
891fn safe_artifact_basename(value: &str) -> bool {
892    let path = Path::new(value);
893    !value.is_empty()
894        && value != "."
895        && value != ".."
896        && path.components().count() == 1
897        && path
898            .components()
899            .all(|component| matches!(component, Component::Normal(_)))
900}
901
902/// Authenticate the release-built libtest rlib against the already verified
903/// compiler companion. The sidecar binds both content hashes and exact rustc
904/// identity; npm/crate package integrity then authenticates the sidecar as part
905/// of the same platform artifact.
906pub fn select_rust_libtest_companion(
907    selection: &SelectedRustCompilerCompanion,
908) -> Result<SelectedRustLibtestCompanion, RustLibtestCompanionError> {
909    let bundle_path = rust_libtest_companion_bundle_path(&selection.companion_path);
910    let bundle_metadata =
911        fs::symlink_metadata(&bundle_path).map_err(|error| io_error(&bundle_path, error))?;
912    if !bundle_metadata.file_type().is_file() {
913        return Err(RustLibtestCompanionError::UnsafeSource(bundle_path));
914    }
915    let bundle: RustLibtestCompanionBundle =
916        serde_json::from_slice(&read_regular_file(&bundle_path)?).map_err(|error| {
917            RustLibtestCompanionError::InvalidBundle {
918                path: bundle_path.clone(),
919                reason: error.to_string(),
920            }
921        })?;
922    if bundle.schema_version != RUST_LIBTEST_COMPANION_BUNDLE_SCHEMA_VERSION
923        || bundle.event_protocol_version != RUST_LIBTEST_EVENT_PROTOCOL_VERSION
924        || !safe_artifact_basename(&bundle.artifact_file)
925        || [
926            bundle.compiler_companion_build_id.as_str(),
927            bundle.original_source_sha256.as_str(),
928            bundle.event_runtime_sha256.as_str(),
929            bundle.patched_source_sha256.as_str(),
930            bundle.artifact_sha256.as_str(),
931        ]
932        .iter()
933        .any(|value| !canonical_lower_sha256(value))
934    {
935        return Err(RustLibtestCompanionError::InvalidBundle {
936            path: bundle_path,
937            reason: "unsupported schema/protocol, unsafe artifact name or noncanonical digest"
938                .into(),
939        });
940    }
941    if bundle.compiler_companion_build_id != selection.handshake.companion_build_id {
942        return Err(RustLibtestCompanionError::BundleMismatch(
943            "compiler companion build ID differs".into(),
944        ));
945    }
946    if bundle.rustc_commit_hash != selection.compiler.rustc_commit_hash
947        || bundle.host_triple != selection.compiler.host_triple
948    {
949        return Err(RustLibtestCompanionError::BundleMismatch(
950            "selected rustc identity differs".into(),
951        ));
952    }
953    let directory = selection.companion_path.parent().ok_or_else(|| {
954        RustLibtestCompanionError::BundleMismatch(
955            "compiler companion has no artifact directory".into(),
956        )
957    })?;
958    let artifact_path = directory.join(&bundle.artifact_file);
959    if sha256_file(&artifact_path)? != bundle.artifact_sha256 {
960        return Err(RustLibtestCompanionError::BundleMismatch(
961            "libtest artifact digest differs".into(),
962        ));
963    }
964    Ok(SelectedRustLibtestCompanion {
965        bundle_path,
966        artifact_path,
967        bundle,
968    })
969}
970
971pub fn write_rust_libtest_companion_bundle(
972    compiler_companion: &Path,
973    source_identity: &RustLibtestCompanionSourceIdentity,
974    artifact: &Path,
975) -> Result<PathBuf, RustLibtestCompanionError> {
976    let compiler_companion = fs::canonicalize(compiler_companion)
977        .map_err(|error| io_error(compiler_companion, error))?;
978    let artifact = fs::canonicalize(artifact).map_err(|error| io_error(artifact, error))?;
979    let directory = compiler_companion.parent().ok_or_else(|| {
980        RustLibtestCompanionError::BundleMismatch(
981            "compiler companion has no artifact directory".into(),
982        )
983    })?;
984    if artifact.parent() != Some(directory) {
985        return Err(RustLibtestCompanionError::BundleMismatch(
986            "libtest artifact is not adjacent to the compiler companion".into(),
987        ));
988    }
989    let artifact_file = artifact
990        .file_name()
991        .and_then(|value| value.to_str())
992        .filter(|value| safe_artifact_basename(value))
993        .ok_or_else(|| {
994            RustLibtestCompanionError::BundleMismatch(
995                "libtest artifact has an unsafe filename".into(),
996            )
997        })?
998        .to_owned();
999    let bundle = RustLibtestCompanionBundle {
1000        schema_version: RUST_LIBTEST_COMPANION_BUNDLE_SCHEMA_VERSION,
1001        event_protocol_version: source_identity.event_protocol_version,
1002        compiler_companion_build_id: sha256_file(&compiler_companion)?,
1003        rustc_commit_hash: source_identity.rustc_commit_hash.clone(),
1004        host_triple: source_identity.host_triple.clone(),
1005        original_source_sha256: source_identity.original_source_sha256.clone(),
1006        event_runtime_sha256: source_identity.event_runtime_sha256.clone(),
1007        patched_source_sha256: source_identity.patched_source_sha256.clone(),
1008        artifact_file,
1009        artifact_sha256: sha256_file(&artifact)?,
1010    };
1011    let path = rust_libtest_companion_bundle_path(&compiler_companion);
1012    let mut bytes = serde_json::to_vec_pretty(&bundle).map_err(|error| io_error(&path, error))?;
1013    bytes.push(b'\n');
1014    let partial = path.with_file_name(format!(
1015        ".{}.{}-{}.partial",
1016        path.file_name()
1017            .and_then(|value| value.to_str())
1018            .unwrap_or("libtest"),
1019        std::process::id(),
1020        SystemTime::now()
1021            .duration_since(UNIX_EPOCH)
1022            .map_err(|error| io_error(&path, error))?
1023            .as_nanos()
1024    ));
1025    let mut cleanup = RemoveFileOnDrop(Some(partial.clone()));
1026    let mut options = OpenOptions::new();
1027    options.write(true).create_new(true);
1028    #[cfg(unix)]
1029    {
1030        use std::os::unix::fs::OpenOptionsExt as _;
1031        options.mode(0o600);
1032    }
1033    let mut file = options
1034        .open(&partial)
1035        .map_err(|error| io_error(&partial, error))?;
1036    file.write_all(&bytes)
1037        .and_then(|()| file.sync_all())
1038        .map_err(|error| io_error(&partial, error))?;
1039    drop(file);
1040    if let Ok(metadata) = fs::symlink_metadata(&path)
1041        && !metadata.file_type().is_file()
1042    {
1043        return Err(RustLibtestCompanionError::UnsafeSource(path));
1044    }
1045    fs::rename(&partial, &path).map_err(|error| io_error(&path, error))?;
1046    sync_directory(directory)?;
1047    cleanup.0 = None;
1048    Ok(path)
1049}
1050
1051fn libtest_builder_lock_path(compiler_companion: &Path) -> PathBuf {
1052    let mut value = compiler_companion.as_os_str().to_owned();
1053    value.push(".libtest.lock");
1054    PathBuf::from(value)
1055}
1056
1057fn bundle_matches_source_identity(
1058    selected: &SelectedRustLibtestCompanion,
1059    identity: &RustLibtestCompanionSourceIdentity,
1060) -> Result<(), RustLibtestCompanionError> {
1061    if selected.bundle.event_protocol_version != identity.event_protocol_version
1062        || selected.bundle.rustc_commit_hash != identity.rustc_commit_hash
1063        || selected.bundle.host_triple != identity.host_triple
1064        || selected.bundle.original_source_sha256 != identity.original_source_sha256
1065        || selected.bundle.event_runtime_sha256 != identity.event_runtime_sha256
1066        || selected.bundle.patched_source_sha256 != identity.patched_source_sha256
1067    {
1068        return Err(RustLibtestCompanionError::BundleMismatch(
1069            "published bundle differs from the authenticated prepared source".into(),
1070        ));
1071    }
1072    Ok(())
1073}
1074
1075/// Build and atomically publish the exact-toolchain libtest companion.
1076///
1077/// All mutable builder state is protected by kernel locks. A killed process
1078/// releases its lock immediately; the next builder removes only narrowly
1079/// named, regular builder-owned partials, authenticates any completed state,
1080/// and resumes. Final source trees, artifacts and bundles become visible only
1081/// after their bytes have been synced and verified.
1082pub fn build_exact_rust_libtest_companion(
1083    source_root: &Path,
1084    work_root: &Path,
1085    rustc: &Path,
1086    compiler_companion: &Path,
1087) -> Result<SelectedRustLibtestCompanion, RustLibtestCompanionError> {
1088    let rustc = fs::canonicalize(rustc).map_err(|error| io_error(rustc, error))?;
1089    let compiler = probe_rustc_identity(&rustc).map_err(|error| {
1090        RustLibtestCompanionError::BundleMismatch(format!(
1091            "could not authenticate exact rustc: {error}"
1092        ))
1093    })?;
1094    let work_root = regular_directory(work_root)?;
1095    let compiler_companion = fs::canonicalize(compiler_companion)
1096        .map_err(|error| io_error(compiler_companion, error))?;
1097    if !fs::symlink_metadata(&compiler_companion)
1098        .is_ok_and(|metadata| metadata.file_type().is_file())
1099    {
1100        return Err(RustLibtestCompanionError::UnsafeSource(compiler_companion));
1101    }
1102    let artifact_directory = compiler_companion.parent().ok_or_else(|| {
1103        RustLibtestCompanionError::BundleMismatch(
1104            "compiler companion has no artifact directory".into(),
1105        )
1106    })?;
1107    let lock_path = libtest_builder_lock_path(&compiler_companion);
1108    let builder_lock = acquire_kernel_lock(&lock_path)?;
1109    let selection =
1110        select_rust_compiler_companion(&rustc, std::slice::from_ref(&compiler_companion), false)
1111            .map_err(|error| {
1112                RustLibtestCompanionError::BundleMismatch(format!(
1113                    "could not authenticate compiler companion: {error}"
1114                ))
1115            })?;
1116
1117    let bundle_path = rust_libtest_companion_bundle_path(&compiler_companion);
1118    let bundle_name = bundle_path
1119        .file_name()
1120        .and_then(|name| name.to_str())
1121        .ok_or_else(|| RustLibtestCompanionError::UnsafeSource(bundle_path.clone()))?;
1122    remove_owned_partials(artifact_directory, &format!(".{bundle_name}."), false)?;
1123
1124    let patched_source = work_root.join("patched-libtest");
1125    let source_identity = prepare_exact_libtest_source(source_root, &patched_source, &compiler)?;
1126    match fs::symlink_metadata(&bundle_path) {
1127        Ok(metadata) if metadata.file_type().is_file() => {
1128            let selected = select_rust_libtest_companion(&selection)?;
1129            bundle_matches_source_identity(&selected, &source_identity)?;
1130            return Ok(selected);
1131        }
1132        Ok(_) => return Err(RustLibtestCompanionError::UnsafeSource(bundle_path)),
1133        Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
1134        Err(error) => return Err(io_error(&bundle_path, error)),
1135    }
1136
1137    let target_libdir_output = Command::new(&rustc)
1138        .args(["--print", "target-libdir"])
1139        .env_remove("RUSTC_WRAPPER")
1140        .env_remove("RUSTC_WORKSPACE_WRAPPER")
1141        .output()
1142        .map_err(|error| io_error(&rustc, error))?;
1143    if !target_libdir_output.status.success() || !target_libdir_output.stderr.is_empty() {
1144        return Err(RustLibtestCompanionError::BuildFailed {
1145            program: rustc,
1146            status: target_libdir_output.status.code(),
1147            stdout: String::from_utf8_lossy(&target_libdir_output.stdout).into_owned(),
1148            stderr: String::from_utf8_lossy(&target_libdir_output.stderr).into_owned(),
1149        });
1150    }
1151    let target_libdir = PathBuf::from(
1152        std::str::from_utf8(&target_libdir_output.stdout)
1153            .map_err(|_| {
1154                RustLibtestCompanionError::BundleMismatch("rustc target libdir is not UTF-8".into())
1155            })?
1156            .trim(),
1157    );
1158    let artifact_name = format!(
1159        "libtest-supercov-v{}-{}-{}.rlib",
1160        RUST_LIBTEST_COMPANION_BUNDLE_SCHEMA_VERSION,
1161        &source_identity.rustc_commit_hash[..12],
1162        &source_identity.patched_source_sha256[..12]
1163    );
1164    let artifact = artifact_directory.join(&artifact_name);
1165    remove_owned_partials(artifact_directory, &format!(".{artifact_name}."), false)?;
1166
1167    // The stable, content-derived output basename is part of rustc's codegen
1168    // identity. It is built in the dedicated work root and copied into a
1169    // unique adjacent publication partial only after canonicalization.
1170    let build_output = work_root.join(&artifact_name);
1171    remove_owned_path(&build_output, false)?;
1172    let mut build_cleanup = RemoveFileOnDrop(Some(build_output.clone()));
1173    let plan = rust_libtest_companion_build_plan(&patched_source, &target_libdir, &build_output)?;
1174    let mut command = Command::new(&rustc);
1175    command
1176        .args(&plan.arguments)
1177        .env("RUSTC_BOOTSTRAP", &plan.rustc_bootstrap)
1178        .env_remove("RUSTC_WRAPPER")
1179        .env_remove("RUSTC_WORKSPACE_WRAPPER")
1180        .env_remove(crate::rust_compiler_orchestration::RUST_COMPILER_WRAPPER_CONFIG_ENV)
1181        .env_remove(crate::rust_compiler_orchestration::RUST_COMPILER_INNER_MODE_ENV);
1182    inherit_lock_through_exec(&mut command, &builder_lock);
1183    let output = command.output().map_err(|error| io_error(&rustc, error))?;
1184    if !output.status.success() {
1185        return Err(RustLibtestCompanionError::BuildFailed {
1186            program: rustc,
1187            status: output.status.code(),
1188            stdout: String::from_utf8_lossy(&output.stdout).into_owned(),
1189            stderr: String::from_utf8_lossy(&output.stderr).into_owned(),
1190        });
1191    }
1192    let compiled = read_regular_file(&build_output)?;
1193    let canonical = canonicalize_rust_libtest_rlib(&compiled, &compiler.host_triple)?;
1194    let partial = artifact_directory.join(format!(
1195        ".{artifact_name}.{}-{}.partial",
1196        std::process::id(),
1197        SystemTime::now()
1198            .duration_since(UNIX_EPOCH)
1199            .map_err(|error| io_error(&artifact, error))?
1200            .as_nanos()
1201    ));
1202    let mut partial_cleanup = RemoveFileOnDrop(Some(partial.clone()));
1203    let mut options = OpenOptions::new();
1204    options.write(true).create_new(true);
1205    #[cfg(unix)]
1206    {
1207        use std::os::unix::fs::OpenOptionsExt as _;
1208        options.mode(0o600);
1209    }
1210    let mut partial_file = options
1211        .open(&partial)
1212        .map_err(|error| io_error(&partial, error))?;
1213    partial_file
1214        .write_all(&canonical)
1215        .and_then(|()| partial_file.sync_all())
1216        .map_err(|error| io_error(&partial, error))?;
1217    drop(partial_file);
1218
1219    match fs::symlink_metadata(&artifact) {
1220        Ok(metadata) if metadata.file_type().is_file() => {
1221            if read_regular_file(&artifact)? != canonical {
1222                return Err(RustLibtestCompanionError::BundleMismatch(
1223                    "the same exact libtest identity produced different artifact bytes".into(),
1224                ));
1225            }
1226            fs::remove_file(&partial).map_err(|error| io_error(&partial, error))?;
1227            partial_cleanup.0 = None;
1228        }
1229        Ok(_) => return Err(RustLibtestCompanionError::UnsafeSource(artifact)),
1230        Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
1231            fs::rename(&partial, &artifact).map_err(|error| io_error(&artifact, error))?;
1232            sync_directory(artifact_directory)?;
1233            partial_cleanup.0 = None;
1234        }
1235        Err(error) => return Err(io_error(&artifact, error)),
1236    }
1237    fs::remove_file(&build_output).map_err(|error| io_error(&build_output, error))?;
1238    build_cleanup.0 = None;
1239
1240    let published_bundle =
1241        write_rust_libtest_companion_bundle(&compiler_companion, &source_identity, &artifact)?;
1242    let selected = select_rust_libtest_companion(&selection)?;
1243    bundle_matches_source_identity(&selected, &source_identity)?;
1244    if selected.bundle_path != published_bundle || selected.artifact_path != artifact {
1245        return Err(RustLibtestCompanionError::BundleMismatch(
1246            "published libtest companion did not reselect exactly".into(),
1247        ));
1248    }
1249    Ok(selected)
1250}
1251
1252#[cfg(test)]
1253mod tests {
1254    use std::{
1255        sync::atomic::{AtomicU64, Ordering},
1256        time::{SystemTime, UNIX_EPOCH},
1257    };
1258
1259    use super::*;
1260    use supercov_contracts::{
1261        EVIDENCE_ARCHIVE_SCHEMA_VERSION, RUST_COMPILER_COMPANION_PROTOCOL_VERSION,
1262        RustCompilerCompanionCapabilities, RustCompilerCompanionHandshake,
1263    };
1264
1265    fn compiler() -> RustCompilerIdentity {
1266        RustCompilerIdentity {
1267            rustc_commit_hash: "a".repeat(40),
1268            rustc_release: "1.95.0".into(),
1269            host_triple: "aarch64-apple-darwin".into(),
1270            rustc_driver_sha256: "b".repeat(64),
1271        }
1272    }
1273
1274    fn selection(
1275        compiler_companion: PathBuf,
1276        compiler: RustCompilerIdentity,
1277    ) -> SelectedRustCompilerCompanion {
1278        let build_id = sha256_file(&compiler_companion).unwrap();
1279        SelectedRustCompilerCompanion {
1280            rustc_path: compiler_companion.with_file_name("rustc"),
1281            compiler_library_directory: compiler_companion.parent().unwrap().to_path_buf(),
1282            companion_path: compiler_companion,
1283            compiler: compiler.clone(),
1284            handshake: RustCompilerCompanionHandshake {
1285                protocol_version: RUST_COMPILER_COMPANION_PROTOCOL_VERSION,
1286                frontend_id: "rust".into(),
1287                coverage_model_variant: "rust-source-v1".into(),
1288                evidence_schema_version: EVIDENCE_ARCHIVE_SCHEMA_VERSION,
1289                companion_build_id: build_id,
1290                compiler,
1291                capabilities: RustCompilerCompanionCapabilities {
1292                    expanded_hir_provenance: true,
1293                    runtime_mir_probe_insertion: true,
1294                    generated_source_provenance: true,
1295                    ctfe_path_tracing: true,
1296                    rustdoc_doctest_tracing: true,
1297                    exact_test_harness_attribution: true,
1298                },
1299            },
1300        }
1301    }
1302
1303    fn fixture() -> PathBuf {
1304        static NEXT_FIXTURE: AtomicU64 = AtomicU64::new(0);
1305        let nonce = SystemTime::now()
1306            .duration_since(UNIX_EPOCH)
1307            .unwrap()
1308            .as_nanos();
1309        let root = std::env::temp_dir().join(format!(
1310            "supercov-libtest-source-{}-{nonce}-{}",
1311            std::process::id(),
1312            NEXT_FIXTURE.fetch_add(1, Ordering::Relaxed)
1313        ));
1314        fs::create_dir_all(root.join("source/src")).unwrap();
1315        fs::write(root.join("source/Cargo.toml"), b"[package]\nname='test'\n").unwrap();
1316        fs::write(root.join("source/src/lib.rs"), fixture_lib_source()).unwrap();
1317        fs::write(
1318            root.join("source/src/console.rs"),
1319            format!("use std::io;\n{CONSOLE_ANCHOR}\n    }}\n}}\n{LISTING_ANCHOR}\n}}\n"),
1320        )
1321        .unwrap();
1322        root
1323    }
1324
1325    fn fixture_lib_source() -> String {
1326        format!(
1327            "#![feature(test)]\n{LIB_ANCHOR}\n{IN_PROCESS_ANCHOR} synthetic;\n{SPAWNED_PROCESS_ANCHOR};\n{BENCH_ANCHOR}(synthetic);\n"
1328        )
1329    }
1330
1331    #[test]
1332    fn patches_atomically_with_relocation_stable_identity() {
1333        let first = fixture();
1334        let second = fixture();
1335        let first_identity = prepare_exact_libtest_source(
1336            &first.join("source"),
1337            &first.join("patched"),
1338            &compiler(),
1339        )
1340        .unwrap();
1341        let second_identity = prepare_exact_libtest_source(
1342            &second.join("source"),
1343            &second.join("patched"),
1344            &compiler(),
1345        )
1346        .unwrap();
1347        assert_eq!(first_identity, second_identity);
1348        assert!(
1349            fs::read_to_string(first.join("patched/src/lib.rs"))
1350                .unwrap()
1351                .contains("mod supercov_events;")
1352        );
1353        assert_eq!(
1354            fs::read_to_string(first.join("patched/src/lib.rs"))
1355                .unwrap()
1356                .matches("supercov_events::enter_test")
1357                .count(),
1358            3
1359        );
1360        assert!(
1361            fs::read_to_string(first.join("patched/src/console.rs"))
1362                .unwrap()
1363                .contains("crate::supercov_events::emit(event)?;")
1364        );
1365        assert!(
1366            fs::read_to_string(first.join("patched/src/console.rs"))
1367                .unwrap()
1368                .contains("crate::supercov_events::emit_listing(")
1369        );
1370        assert_eq!(
1371            fs::read_to_string(first.join("source/src/lib.rs")).unwrap(),
1372            fixture_lib_source()
1373        );
1374        assert!(first.join("patched/supercov-libtest-source.json").is_file());
1375        assert_eq!(
1376            prepare_exact_libtest_source(
1377                &first.join("source"),
1378                &first.join("patched"),
1379                &compiler()
1380            )
1381            .unwrap(),
1382            first_identity
1383        );
1384        fs::remove_dir_all(first).unwrap();
1385        fs::remove_dir_all(second).unwrap();
1386    }
1387
1388    #[test]
1389    fn prepared_source_reuse_fails_closed_on_tree_or_identity_tampering() {
1390        let root = fixture();
1391        let source = root.join("source");
1392        let patched = root.join("patched");
1393        prepare_exact_libtest_source(&source, &patched, &compiler()).unwrap();
1394        fs::write(patched.join("src/lib.rs"), b"tampered\n").unwrap();
1395        assert!(matches!(
1396            prepare_exact_libtest_source(&source, &patched, &compiler()),
1397            Err(RustLibtestCompanionError::BundleMismatch(reason))
1398                if reason.contains("tree digest")
1399        ));
1400
1401        fs::remove_dir_all(&patched).unwrap();
1402        prepare_exact_libtest_source(&source, &patched, &compiler()).unwrap();
1403        let identity_path = patched.join(SOURCE_IDENTITY_FILE);
1404        let mut identity: serde_json::Value =
1405            serde_json::from_slice(&fs::read(&identity_path).unwrap()).unwrap();
1406        identity["unknown"] = serde_json::json!(true);
1407        fs::write(&identity_path, serde_json::to_vec(&identity).unwrap()).unwrap();
1408        assert!(matches!(
1409            prepare_exact_libtest_source(&source, &patched, &compiler()),
1410            Err(RustLibtestCompanionError::InvalidBundle { .. })
1411        ));
1412        fs::remove_dir_all(root).unwrap();
1413    }
1414
1415    #[test]
1416    fn concurrent_source_preparation_converges_without_partial_debris() {
1417        let root = fixture();
1418        let source = root.join("source");
1419        let patched = root.join("patched");
1420        let workers = (0..8)
1421            .map(|_| {
1422                let source = source.clone();
1423                let patched = patched.clone();
1424                std::thread::spawn(move || {
1425                    prepare_exact_libtest_source(&source, &patched, &compiler()).unwrap()
1426                })
1427            })
1428            .collect::<Vec<_>>();
1429        let identities = workers
1430            .into_iter()
1431            .map(|worker| worker.join().unwrap())
1432            .collect::<Vec<_>>();
1433        assert!(identities.windows(2).all(|pair| pair[0] == pair[1]));
1434        assert!(fs::read_dir(&root).unwrap().all(|entry| {
1435            !entry
1436                .unwrap()
1437                .file_name()
1438                .to_string_lossy()
1439                .ends_with(".partial")
1440        }));
1441        fs::remove_dir_all(root).unwrap();
1442    }
1443
1444    #[cfg(unix)]
1445    #[test]
1446    fn libtest_builder_lock_holder_helper() {
1447        let Some(lock) = std::env::var_os("SUPERCOV_TEST_LIBTEST_LOCK") else {
1448            return;
1449        };
1450        let partial =
1451            PathBuf::from(std::env::var_os("SUPERCOV_TEST_LIBTEST_PARTIAL").expect("partial path"));
1452        let ready =
1453            PathBuf::from(std::env::var_os("SUPERCOV_TEST_LIBTEST_READY").expect("ready path"));
1454        let _lock = acquire_kernel_lock(Path::new(&lock)).unwrap();
1455        fs::write(&partial, b"incomplete\n").unwrap();
1456        fs::write(&ready, b"locked\n").unwrap();
1457        loop {
1458            std::thread::sleep(Duration::from_secs(1));
1459        }
1460    }
1461
1462    #[cfg(unix)]
1463    #[test]
1464    fn killed_builder_releases_lock_and_owned_partial_is_recoverable() {
1465        use std::process::Stdio;
1466
1467        let root = fixture();
1468        let lock = root.join("companion.libtest.lock");
1469        let partial = root.join(".artifact.123.partial");
1470        let ready = root.join("builder-ready");
1471        let mut child = Command::new(std::env::current_exe().unwrap())
1472            .args([
1473                "--exact",
1474                "rust_libtest_companion::tests::libtest_builder_lock_holder_helper",
1475                "--nocapture",
1476            ])
1477            .env("SUPERCOV_TEST_LIBTEST_LOCK", &lock)
1478            .env("SUPERCOV_TEST_LIBTEST_PARTIAL", &partial)
1479            .env("SUPERCOV_TEST_LIBTEST_READY", &ready)
1480            .stdin(Stdio::null())
1481            .stdout(Stdio::null())
1482            .stderr(Stdio::null())
1483            .spawn()
1484            .unwrap();
1485        let started = Instant::now();
1486        while !ready.is_file() {
1487            assert!(
1488                started.elapsed() < Duration::from_secs(10),
1489                "libtest builder helper did not acquire its kernel lock"
1490            );
1491            std::thread::sleep(Duration::from_millis(10));
1492        }
1493        assert_eq!(
1494            unsafe { libc::kill(child.id().try_into().unwrap(), libc::SIGKILL) },
1495            0
1496        );
1497        assert_eq!(child.wait().unwrap().code(), None);
1498        let recovery_started = Instant::now();
1499        let _lock = acquire_kernel_lock(&lock).unwrap();
1500        assert!(recovery_started.elapsed() < Duration::from_secs(5));
1501        remove_owned_partials(&root, ".artifact.", false).unwrap();
1502        assert!(!partial.exists());
1503        fs::remove_dir_all(root).unwrap();
1504    }
1505
1506    #[cfg(unix)]
1507    #[test]
1508    fn libtest_builder_child_lock_holder_helper() {
1509        let Some(lock) = std::env::var_os("SUPERCOV_TEST_LIBTEST_CHILD_LOCK") else {
1510            return;
1511        };
1512        let ready = PathBuf::from(
1513            std::env::var_os("SUPERCOV_TEST_LIBTEST_CHILD_READY").expect("ready path"),
1514        );
1515        let lock = acquire_kernel_lock(Path::new(&lock)).unwrap();
1516        let mut command = Command::new("/bin/sh");
1517        command.args(["-c", "sleep 2"]);
1518        inherit_lock_through_exec(&mut command, &lock);
1519        let child = command.spawn().unwrap();
1520        fs::write(&ready, format!("{}\n", child.id())).unwrap();
1521        // This helper is deliberately SIGKILLed by its parent test; dropping
1522        // the handle leaves the compiler-shaped child alive so it alone proves
1523        // that the inherited open-file description retains the kernel lock.
1524        drop(child);
1525        loop {
1526            std::thread::sleep(Duration::from_secs(1));
1527        }
1528    }
1529
1530    #[cfg(unix)]
1531    #[test]
1532    fn killed_builder_keeps_lock_until_its_compiler_child_exits() {
1533        use std::process::Stdio;
1534
1535        let root = fixture();
1536        let lock = root.join("companion-child.libtest.lock");
1537        let ready = root.join("compiler-ready");
1538        let mut builder = Command::new(std::env::current_exe().unwrap())
1539            .args([
1540                "--exact",
1541                "rust_libtest_companion::tests::libtest_builder_child_lock_holder_helper",
1542                "--nocapture",
1543            ])
1544            .env("SUPERCOV_TEST_LIBTEST_CHILD_LOCK", &lock)
1545            .env("SUPERCOV_TEST_LIBTEST_CHILD_READY", &ready)
1546            .stdin(Stdio::null())
1547            .stdout(Stdio::null())
1548            .stderr(Stdio::null())
1549            .spawn()
1550            .unwrap();
1551        let started = Instant::now();
1552        while !ready.is_file() {
1553            assert!(started.elapsed() < Duration::from_secs(10));
1554            std::thread::sleep(Duration::from_millis(10));
1555        }
1556        assert_eq!(
1557            unsafe { libc::kill(builder.id().try_into().unwrap(), libc::SIGKILL) },
1558            0
1559        );
1560        assert_eq!(builder.wait().unwrap().code(), None);
1561        let recovery_started = Instant::now();
1562        let _lock = acquire_kernel_lock(&lock).unwrap();
1563        assert!(
1564            recovery_started.elapsed() >= Duration::from_millis(500),
1565            "the compiler child did not retain the publication lock"
1566        );
1567        assert!(recovery_started.elapsed() < Duration::from_secs(5));
1568        fs::remove_dir_all(root).unwrap();
1569    }
1570
1571    #[test]
1572    fn rejects_unrecognized_or_unsafe_exact_source() {
1573        let root = fixture();
1574        fs::write(root.join("source/src/console.rs"), "not libtest\n").unwrap();
1575        assert!(matches!(
1576            prepare_exact_libtest_source(&root.join("source"), &root.join("patched"), &compiler()),
1577            Err(RustLibtestCompanionError::UnrecognizedSource { .. })
1578        ));
1579        assert!(!root.join("patched").exists());
1580        assert_eq!(
1581            fs::read_to_string(root.join("source/src/console.rs")).unwrap(),
1582            "not libtest\n"
1583        );
1584        fs::remove_dir_all(root).unwrap();
1585    }
1586
1587    #[cfg(unix)]
1588    #[test]
1589    fn rejects_symlinks_without_leaving_a_destination() {
1590        use std::os::unix::fs::symlink;
1591
1592        let root = fixture();
1593        symlink("lib.rs", root.join("source/src/alias.rs")).unwrap();
1594        assert!(matches!(
1595            prepare_exact_libtest_source(&root.join("source"), &root.join("patched"), &compiler()),
1596            Err(RustLibtestCompanionError::UnsafeSource(_))
1597        ));
1598        assert!(!root.join("patched").exists());
1599        fs::remove_dir_all(root).unwrap();
1600    }
1601
1602    #[test]
1603    fn build_plan_requires_exact_full_metadata() {
1604        let root = fixture();
1605        prepare_exact_libtest_source(&root.join("source"), &root.join("patched"), &compiler())
1606            .unwrap();
1607        fs::create_dir(root.join("libdir")).unwrap();
1608        fs::write(root.join("libdir/libgetopts-a.rmeta"), b"getopts").unwrap();
1609        fs::write(root.join("libdir/libgetopts-a.rlib"), b"getopts archive").unwrap();
1610        fs::write(root.join("libdir/liblibc-b.rmeta"), b"libc").unwrap();
1611        fs::write(root.join("libdir/liblibc-b.rlib"), b"libc archive").unwrap();
1612        let plan = rust_libtest_companion_build_plan(
1613            &root.join("patched"),
1614            &root.join("libdir"),
1615            &root.join("libtest-supercov.rlib"),
1616        )
1617        .unwrap();
1618        assert_eq!(
1619            plan.source,
1620            fs::canonicalize(root.join("patched/src/lib.rs")).unwrap()
1621        );
1622        assert!(
1623            plan.arguments
1624                .iter()
1625                .any(|value| value == "-Zcrate-attr=feature(rustc_private)")
1626        );
1627        assert!(
1628            !plan
1629                .arguments
1630                .iter()
1631                .any(|value| value.to_string_lossy().starts_with("-Cincremental"))
1632        );
1633        fs::write(root.join("libdir/liblibc-c.rmeta"), b"duplicate").unwrap();
1634        fs::write(root.join("libdir/liblibc-c.rlib"), b"duplicate archive").unwrap();
1635        assert!(
1636            rust_libtest_companion_build_plan(
1637                &root.join("patched"),
1638                &root.join("libdir"),
1639                &root.join("duplicate.rlib")
1640            )
1641            .is_err()
1642        );
1643        fs::remove_dir_all(root).unwrap();
1644    }
1645
1646    fn synthetic_rlib(object_suffix: &str, reverse: bool) -> Vec<u8> {
1647        let metadata = b"metadata".as_slice();
1648        let first = b"first object".as_slice();
1649        let second = b"second object".as_slice();
1650        let first_name = format!("test.alpha.{object_suffix}.rcgu.o");
1651        let second_name = format!("test.beta.{object_suffix}.rcgu.o");
1652        let mut owned = [
1653            ("lib.rmeta".to_owned(), metadata),
1654            (first_name, first),
1655            (second_name, second),
1656        ];
1657        if reverse {
1658            owned[1..].reverse();
1659        }
1660        let members = owned
1661            .iter()
1662            .map(|(name, data)| NewArchiveMember::new(data, &DEFAULT_OBJECT_READER, name.clone()))
1663            .collect::<Vec<_>>();
1664        let mut output = Cursor::new(Vec::new());
1665        write_archive_to_stream(
1666            &mut output,
1667            &members,
1668            WritableArchiveKind::Darwin,
1669            false,
1670            Some(false),
1671        )
1672        .unwrap();
1673        output.into_inner()
1674    }
1675
1676    #[test]
1677    fn canonical_rlib_ignores_session_names_and_member_order() {
1678        let first = canonicalize_rust_libtest_rlib(
1679            &synthetic_rlib("random-one", false),
1680            "aarch64-apple-darwin",
1681        )
1682        .unwrap();
1683        let second = canonicalize_rust_libtest_rlib(
1684            &synthetic_rlib("random-two", true),
1685            "aarch64-apple-darwin",
1686        )
1687        .unwrap();
1688        assert_eq!(first, second);
1689
1690        let archive = ArchiveFile::parse(first.as_slice()).unwrap();
1691        let names = archive
1692            .members()
1693            .map(|member| {
1694                std::str::from_utf8(member.unwrap().name())
1695                    .unwrap()
1696                    .to_owned()
1697            })
1698            .collect::<Vec<_>>();
1699        assert_eq!(names.first().map(String::as_str), Some("lib.rmeta"));
1700        assert!(
1701            names[1..]
1702                .iter()
1703                .all(|name| name.starts_with("supercov-libtest-") && name.ends_with(".rcgu.o"))
1704        );
1705    }
1706
1707    #[test]
1708    fn bundle_binds_exact_compiler_source_runtime_and_artifact_bytes() {
1709        let root = fixture();
1710        let compiler = compiler();
1711        let source_identity =
1712            prepare_exact_libtest_source(&root.join("source"), &root.join("patched"), &compiler)
1713                .unwrap();
1714        let compiler_companion = root.join("supercov-rustc-companion");
1715        let artifact = root.join("libtest-supercov.rlib");
1716        fs::write(&compiler_companion, b"compiler companion").unwrap();
1717        fs::write(&artifact, b"exact libtest rlib").unwrap();
1718        let selected = selection(
1719            fs::canonicalize(&compiler_companion).unwrap(),
1720            compiler.clone(),
1721        );
1722        let bundle_path =
1723            write_rust_libtest_companion_bundle(&compiler_companion, &source_identity, &artifact)
1724                .unwrap();
1725        let bound = select_rust_libtest_companion(&selected).unwrap();
1726        assert_eq!(bound.bundle_path, bundle_path);
1727        assert_eq!(bound.artifact_path, fs::canonicalize(&artifact).unwrap());
1728        assert_eq!(
1729            bound.bundle.compiler_companion_build_id,
1730            selected.handshake.companion_build_id
1731        );
1732        assert_eq!(
1733            bound.bundle.original_source_sha256,
1734            source_identity.original_source_sha256
1735        );
1736
1737        fs::write(&artifact, b"tampered").unwrap();
1738        assert!(matches!(
1739            select_rust_libtest_companion(&selected),
1740            Err(RustLibtestCompanionError::BundleMismatch(_))
1741        ));
1742        fs::remove_dir_all(root).unwrap();
1743    }
1744
1745    #[test]
1746    fn bundle_rejects_unknown_fields_unsafe_paths_and_companion_mismatch() {
1747        let root = fixture();
1748        let compiler = compiler();
1749        let source_identity =
1750            prepare_exact_libtest_source(&root.join("source"), &root.join("patched"), &compiler)
1751                .unwrap();
1752        let compiler_companion = root.join("supercov-rustc-companion");
1753        let artifact = root.join("libtest-supercov.rlib");
1754        fs::write(&compiler_companion, b"compiler companion").unwrap();
1755        fs::write(&artifact, b"exact libtest rlib").unwrap();
1756        let mut selected = selection(
1757            fs::canonicalize(&compiler_companion).unwrap(),
1758            compiler.clone(),
1759        );
1760        let bundle_path =
1761            write_rust_libtest_companion_bundle(&compiler_companion, &source_identity, &artifact)
1762                .unwrap();
1763        let mut value: serde_json::Value =
1764            serde_json::from_slice(&fs::read(&bundle_path).unwrap()).unwrap();
1765        value["unknown"] = serde_json::json!(true);
1766        fs::write(&bundle_path, serde_json::to_vec(&value).unwrap()).unwrap();
1767        assert!(matches!(
1768            select_rust_libtest_companion(&selected),
1769            Err(RustLibtestCompanionError::InvalidBundle { .. })
1770        ));
1771
1772        value.as_object_mut().unwrap().remove("unknown");
1773        value["artifactFile"] = serde_json::json!("../escaped.rlib");
1774        fs::write(&bundle_path, serde_json::to_vec(&value).unwrap()).unwrap();
1775        assert!(matches!(
1776            select_rust_libtest_companion(&selected),
1777            Err(RustLibtestCompanionError::InvalidBundle { .. })
1778        ));
1779
1780        value["artifactFile"] = serde_json::json!("libtest-supercov.rlib");
1781        fs::write(&bundle_path, serde_json::to_vec(&value).unwrap()).unwrap();
1782        selected.handshake.companion_build_id = "f".repeat(64);
1783        assert!(matches!(
1784            select_rust_libtest_companion(&selected),
1785            Err(RustLibtestCompanionError::BundleMismatch(_))
1786        ));
1787        fs::remove_dir_all(root).unwrap();
1788    }
1789}