Skip to main content

rucc_driver/
install.rs

1//! Checking a sysroot artifact and putting it in the cache.
2//!
3//! Design: `spec/cross-compile/13-distribution.md` section 13.8, which divides a fetch in two. A
4//! downloader the machine already has moves the bytes, and everything that decides whether the
5//! result is correct is here. That division is why this file has no URL in it and runs no network
6//! code: what it is handed is a file on disk and the hash that file is supposed to have.
7//!
8//! # The order, which is the whole of the argument
9//!
10//! The hash of the archive is checked before anything is unpacked. So what `tar` is pointed at is
11//! always a file we have already identified, and an unpacker's behaviour on a file somebody else
12//! chose is not a question this has to have an answer to.
13//!
14//! Then the files that came out are checked against the manifest the producer put in the archive,
15//! in both directions. Every line has to name a file with the sha256 it recorded, and every file
16//! has to be named by a line. The second direction is the one that is easy to leave out and it is
17//! the one that matters: [`rucc_sysroot::Manifest::digest`] is a claim about what is under a
18//! directory, and a file nobody recorded makes it a claim about less than what is there.
19//!
20//! The manifest is the producer's rather than ours because of what is in it. An input carries a
21//! source, a URL and a licence, and a walk of a directory tree knows none of the three. The
22//! producer in `tamnd/rucc-cross` knows all of them, so it writes the record and this checks it
23//! against the bytes, which is the same shape as section 13.6's rule about a generated artifact
24//! being checked against its generator.
25//!
26//! Only then is the result renamed into place, which is section 13.2's concurrency rule. The
27//! staging directory is inside the cache so that the rename is a rename and not a copy, because
28//! the two paths are on one filesystem by construction.
29
30use std::fs;
31use std::io;
32use std::path::{Component, Path, PathBuf};
33use std::process::Command;
34use std::time::{SystemTime, UNIX_EPOCH};
35
36use rucc_sysroot::{Manifest, Sysroot, sha256};
37use rucc_tuple::TargetTuple;
38
39use crate::{CliError, err};
40
41/// What was at the destination before an install put something there.
42#[derive(Debug, Clone, PartialEq, Eq)]
43pub enum Before {
44    /// Nothing, which is the first install of this target on this machine.
45    Nothing,
46    /// A sysroot whose manifest has the same digest, so the install was not needed and nothing was
47    /// moved. Two rucc versions share a directory for the reason section 13.2 gives, and a second
48    /// fetch of the same artifact is the ordinary way this happens.
49    TheSame,
50    /// A sysroot with a different digest, which was replaced. The string is the digest that was
51    /// there, so that whatever reports the install can say what it stood on.
52    Different(String),
53}
54
55/// What an install left behind.
56#[derive(Debug, Clone, PartialEq, Eq)]
57pub struct Installed {
58    /// Where it is, which is `sysroots/<tuple>` under the cache.
59    pub root: PathBuf,
60    /// The digest of its manifest, which is what `rucc -print-sysroot-digest` prints for it.
61    pub digest: String,
62    /// How many files the manifest records, all of which were checked.
63    pub files: usize,
64    /// What the destination held before this.
65    pub before: Before,
66}
67
68/// Check a file against the hash it is supposed to have.
69///
70/// The hash comes from the rucc release rather than from the artifact or from the server that
71/// served it, which is the only arrangement where a check means anything. Section 13.2 says a
72/// mismatch is a hard failure with no override flag, so there is no argument here that could turn
73/// one into a warning.
74///
75/// # Errors
76///
77/// A file that cannot be read, and a hash that does not match. The message names both hashes,
78/// because the first thing anybody does with a mismatch is ask whether they downloaded the wrong
79/// thing or the right thing badly, and the two look different.
80pub fn verify(archive: &Path, expected: &str) -> Result<(), CliError> {
81    let bytes = fs::read(archive).map_err(|why| err(format!("{}: {why}", archive.display())))?;
82    let found = sha256::hex(&bytes);
83    if found == expected {
84        return Ok(());
85    }
86    Err(err(format!(
87        "{} has sha256 {found} where this release pins {expected}, so it is not the artifact this \
88         build knows about",
89        archive.display()
90    )))
91}
92
93/// Check an artifact and install it as the sysroot for a target.
94///
95/// The steps are ยง13.8's, in order: the hash, the unpack, the manifest against the tree and the
96/// tree against the manifest, the rename. Nothing is written outside the cache and nothing outside
97/// the cache is read except the archive.
98///
99/// # Errors
100///
101/// Every step, and each message says which step. An artifact for the wrong target, a missing
102/// manifest, a file whose hash disagrees with the record, a file no line names, and anything the
103/// filesystem or `tar` refuses. A failure leaves the destination as it was: the work happens in a
104/// staging directory and the last thing that happens is the rename.
105pub fn install(
106    archive: &Path,
107    expected: &str,
108    target: TargetTuple,
109    cache: &Path,
110) -> Result<Installed, CliError> {
111    verify(archive, expected)?;
112
113    let staging = staging_dir(cache, target);
114    fs::create_dir_all(&staging).map_err(|why| err(format!("{}: {why}", staging.display())))?;
115    // Every exit from here on has to take the staging directory with it, including the ones that
116    // are somebody else's fault, or a machine that fetches a broken artifact twice a day fills its
117    // cache with half unpacked trees.
118    let outcome = install_staged(archive, target, cache, &staging);
119    if outcome.is_err() {
120        let _ = fs::remove_dir_all(&staging);
121    }
122    outcome
123}
124
125/// The install, with the staging directory already made and cleaned up by the caller.
126fn install_staged(
127    archive: &Path,
128    target: TargetTuple,
129    cache: &Path,
130    staging: &Path,
131) -> Result<Installed, CliError> {
132    unpack(archive, staging)?;
133
134    let record = staging.join("manifest");
135    let text = fs::read_to_string(&record).map_err(|why| {
136        if why.kind() == io::ErrorKind::NotFound {
137            err(format!(
138                "{} has no manifest in it, so there is nothing to check its files against",
139                archive.display()
140            ))
141        } else {
142            err(format!("{}: {why}", record.display()))
143        }
144    })?;
145    let manifest =
146        Manifest::parse(&text).map_err(|why| err(format!("{}: {why}", archive.display())))?;
147
148    if manifest.target() != target {
149        return Err(err(format!(
150            "{} is a sysroot for {}, which is not {}",
151            archive.display(),
152            manifest.target().to_canonical_string(),
153            target.to_canonical_string()
154        )));
155    }
156
157    check(staging, &manifest).map_err(|why| err(format!("{}: {why}", archive.display())))?;
158
159    let digest = manifest.digest();
160    let root = Sysroot::in_cache(cache, target).root().to_path_buf();
161    let before = swap(staging, &root, &digest)?;
162    Ok(Installed { root, digest, files: manifest.inputs().len(), before })
163}
164
165/// Where this install does its work.
166///
167/// Inside the cache, because the rename at the end is only atomic if the two paths are on one
168/// filesystem, and a name nothing else will pick, because two builds fetching the same target at
169/// the same time is the ordinary case rather than the unlucky one. The process id is not enough on
170/// its own: one process can install the same target twice.
171fn staging_dir(cache: &Path, target: TargetTuple) -> PathBuf {
172    let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or_default();
173    let unique =
174        format!("{}-{}-{}", target.to_canonical_string(), std::process::id(), now.as_nanos());
175    cache.join("staging").join(unique)
176}
177
178/// Unpack a verified archive with the platform's own `tar`.
179///
180/// Section 13.8's decision keeps an archive reader out of the compiler for the same reason it keeps
181/// a TLS stack out, and `tar` is on every host in the support table, including Windows since 1803.
182/// The members are unpacked as they are, with no component stripped, because the staging directory
183/// is one we made for this and a single directory inside the archive would only be a name to
184/// disagree about.
185fn unpack(archive: &Path, into: &Path) -> Result<(), CliError> {
186    let output =
187        Command::new("tar").arg("-xzf").arg(archive).arg("-C").arg(into).output().map_err(
188            |why| err(format!("could not run `tar`, which is how an artifact is unpacked: {why}")),
189        )?;
190    if output.status.success() {
191        return Ok(());
192    }
193    let said = String::from_utf8_lossy(&output.stderr);
194    let said = said.trim();
195    let detail = if said.is_empty() { String::new() } else { format!(": {said}") };
196    Err(err(format!("`tar` could not unpack {}{detail}", archive.display())))
197}
198
199/// Check a tree against a manifest and the manifest against the tree.
200///
201/// The errors are collected rather than returned one at a time. An artifact that fails this is
202/// either the wrong artifact or a broken producer, and which of the two it is shows in how many
203/// files disagree, so a report that stopped at the first one would hide the thing that tells them
204/// apart.
205fn check(tree: &Path, manifest: &Manifest) -> Result<(), String> {
206    let mut problems: Vec<String> = Vec::new();
207    let mut recorded: Vec<&str> = Vec::new();
208
209    for input in manifest.inputs() {
210        recorded.push(&input.path);
211        let at = match relative(tree, &input.path) {
212            Ok(at) => at,
213            Err(why) => {
214                problems.push(why);
215                continue;
216            }
217        };
218        match fs::read(&at) {
219            Ok(bytes) => {
220                let found = sha256::hex(&bytes);
221                if found != input.sha256 {
222                    problems.push(format!(
223                        "{} has sha256 {found} where the record says {}",
224                        input.path, input.sha256
225                    ));
226                }
227            }
228            Err(why) if why.kind() == io::ErrorKind::NotFound => {
229                problems.push(format!("{} is in the record and not in the archive", input.path));
230            }
231            Err(why) => problems.push(format!("{}: {why}", input.path)),
232        }
233    }
234
235    let mut found = Vec::new();
236    walk(tree, String::new(), &mut found).map_err(|why| format!("{}: {why}", tree.display()))?;
237    recorded.sort_unstable();
238    for path in &found {
239        // The manifest is the record and is not a line in itself, which is the one file in a
240        // sysroot that is allowed to be there without being recorded.
241        if path == "manifest" {
242            continue;
243        }
244        if recorded.binary_search(&path.as_str()).is_err() {
245            problems.push(format!("{path} is in the archive and not in the record"));
246        }
247    }
248
249    if problems.is_empty() {
250        return Ok(());
251    }
252    problems.sort();
253    let first = &problems[0];
254    if problems.len() == 1 {
255        return Err(format!("the archive does not match its own manifest: {first}"));
256    }
257    Err(format!(
258        "the archive does not match its own manifest: {first}, and {} more files disagree",
259        problems.len() - 1
260    ))
261}
262
263/// The path a recorded input names, refusing one that is not under the tree.
264///
265/// A manifest is checked before it is trusted and a path is part of a manifest. `..` in a recorded
266/// path, or a path that starts at the root of the filesystem, would write the check against a file
267/// the archive never carried, so neither is a path this reads.
268fn relative(tree: &Path, path: &str) -> Result<PathBuf, String> {
269    let candidate = Path::new(path);
270    let ordinary = candidate.components().all(|part| matches!(part, Component::Normal(_)));
271    if !ordinary {
272        return Err(format!("{path} is not a path inside a sysroot"));
273    }
274    Ok(tree.join(candidate))
275}
276
277/// Every file under a directory, as paths relative to it with `/` between the parts.
278///
279/// `/` whatever the host separator is, because that is the spelling a manifest uses and the
280/// comparison is against a manifest. A symlink is a leaf rather than something to follow: following
281/// one would count a file twice, or walk forever, and what a recorded hash is about is the bytes a
282/// compiler reads through that name, which `fs::read` gets by following it once.
283fn walk(dir: &Path, prefix: String, out: &mut Vec<String>) -> io::Result<()> {
284    for entry in fs::read_dir(dir)? {
285        let entry = entry?;
286        let name = entry.file_name().to_string_lossy().into_owned();
287        let path = if prefix.is_empty() { name } else { format!("{prefix}/{name}") };
288        if entry.file_type()?.is_dir() {
289            walk(&entry.path(), path, out)?;
290        } else {
291            out.push(path);
292        }
293    }
294    Ok(())
295}
296
297/// Put the staged tree where a compiler will look for it.
298///
299/// Section 13.2 asks for an atomic rename and never an in place mutation, which is a rule about
300/// what a parallel build sees. A compile that is reading the old sysroot while this happens keeps
301/// reading the files it has open, and one that starts during the swap finds either the old tree or
302/// the new one.
303///
304/// An existing tree with the same digest is left alone. That is not an optimization: a second fetch
305/// of the same artifact is the ordinary case, and replacing a directory that is already correct
306/// would move files under a build for no reason at all.
307fn swap(staging: &Path, root: &Path, digest: &str) -> Result<Before, CliError> {
308    let before = match existing(root) {
309        Some(found) if found == digest => {
310            let _ = fs::remove_dir_all(staging);
311            return Ok(Before::TheSame);
312        }
313        Some(found) => Before::Different(found),
314        None => Before::Nothing,
315    };
316
317    if let Some(parent) = root.parent() {
318        fs::create_dir_all(parent).map_err(|why| err(format!("{}: {why}", parent.display())))?;
319    }
320
321    // The old tree is moved aside rather than deleted first. Deleting it first would leave the path
322    // missing for as long as the delete takes, which on a sysroot is thousands of files, and the
323    // window this way is one rename wide.
324    let aside = root.with_extension(format!("old.{}", std::process::id()));
325    if before != Before::Nothing {
326        let _ = fs::remove_dir_all(&aside);
327        fs::rename(root, &aside)
328            .map_err(|why| err(format!("could not move {} aside: {why}", root.display())))?;
329    }
330    let renamed = fs::rename(staging, root);
331    if let Err(why) = renamed {
332        // Put back what was there. A failed install that also took the working sysroot away would
333        // be worse than the failure it started as.
334        if before != Before::Nothing {
335            let _ = fs::rename(&aside, root);
336        }
337        return Err(err(format!("could not put {} in place: {why}", root.display())));
338    }
339    if before != Before::Nothing {
340        let _ = fs::remove_dir_all(&aside);
341    }
342    Ok(before)
343}
344
345/// The digest of the sysroot that is already at this path, if there is one with a manifest.
346///
347/// A directory with no manifest in it answers [`None`] and is replaced like anything else. It is
348/// either a tree somebody assembled by hand, which `--sysroot` is the flag for and the cache is not
349/// the place for, or the wreckage of an install from before this code existed.
350fn existing(root: &Path) -> Option<String> {
351    let text = fs::read_to_string(root.join("manifest")).ok()?;
352    Manifest::parse(&text).ok().map(|manifest| manifest.digest())
353}
354
355#[cfg(test)]
356mod tests {
357    use super::{Before, Installed, check, install, relative, verify, walk};
358    use rucc_sysroot::{Input, Licence, Manifest, Provenance, sha256};
359    use rucc_tuple::TargetTuple;
360    use std::path::{Path, PathBuf};
361    use std::process::Command;
362
363    /// A directory that goes away with the test, and the files in it.
364    struct Tree(PathBuf);
365
366    impl Drop for Tree {
367        fn drop(&mut self) {
368            let _ = std::fs::remove_dir_all(&self.0);
369        }
370    }
371
372    impl Tree {
373        fn new(name: &str) -> Tree {
374            let dir =
375                std::env::temp_dir().join(format!("rucc-install-{}-{name}", std::process::id()));
376            let _ = std::fs::remove_dir_all(&dir);
377            std::fs::create_dir_all(&dir).expect("a temporary directory should be writable");
378            Tree(dir)
379        }
380
381        fn write(&self, path: &str, text: &str) {
382            let at = self.0.join(path);
383            if let Some(parent) = at.parent() {
384                std::fs::create_dir_all(parent).expect("a subdirectory should be creatable");
385            }
386            std::fs::write(&at, text).expect("a temporary file should be writable");
387        }
388    }
389
390    /// The target every test here uses, and a musl one so that nothing depends on the host.
391    fn target() -> TargetTuple {
392        "x86_64-linux-musl".parse().expect("a tuple the table knows")
393    }
394
395    /// A manifest for these files, with their real hashes in it.
396    fn manifest_for(files: &[(&str, &str)]) -> Manifest {
397        let mut manifest = Manifest::new(target());
398        for (path, text) in files {
399            manifest.push(Input {
400                path: (*path).to_owned(),
401                source: "musl-1.2.5".to_owned(),
402                url: "https://musl.libc.org/releases/musl-1.2.5.tar.gz".to_owned(),
403                sha256: sha256::hex(text.as_bytes()),
404                licence: Licence::Mit,
405                provenance: Provenance::Bundled,
406            });
407        }
408        manifest
409    }
410
411    /// An artifact holding these files and a manifest that describes them, and its sha256.
412    ///
413    /// Built with the same `tar` the install unpacks with, which is the point: a test that wrote its
414    /// own archive format would be testing a reader nobody uses.
415    fn artifact(tree: &Tree, files: &[(&str, &str)], manifest: &Manifest) -> (PathBuf, String) {
416        let staged = tree.0.join("staged");
417        std::fs::create_dir_all(&staged).expect("a staging directory should be creatable");
418        for (path, text) in files {
419            let at = staged.join(path);
420            if let Some(parent) = at.parent() {
421                std::fs::create_dir_all(parent).expect("a subdirectory should be creatable");
422            }
423            std::fs::write(&at, text).expect("a file should be writable");
424        }
425        std::fs::write(staged.join("manifest"), manifest.render())
426            .expect("the manifest should be writable");
427
428        let archive = tree.0.join("artifact.tar.gz");
429        let status = Command::new("tar")
430            .arg("-czf")
431            .arg(&archive)
432            .arg("-C")
433            .arg(&staged)
434            .arg(".")
435            .status()
436            .expect("tar should be on a machine that runs these tests");
437        assert!(status.success(), "tar should be able to write an archive");
438        std::fs::remove_dir_all(&staged).expect("the staged tree should be removable");
439
440        let bytes = std::fs::read(&archive).expect("the archive should be readable");
441        let hash = sha256::hex(&bytes);
442        (archive, hash)
443    }
444
445    const FILES: &[(&str, &str)] =
446        &[("include/stdio.h", "int puts(const char *);\n"), ("lib/libc.so", "not really\n")];
447
448    #[test]
449    fn an_artifact_that_matches_its_record_is_installed() {
450        let tree = Tree::new("good");
451        let manifest = manifest_for(FILES);
452        let (archive, hash) = artifact(&tree, FILES, &manifest);
453        let cache = tree.0.join("cache");
454
455        let done = install(&archive, &hash, target(), &cache).expect("this one should install");
456        assert_eq!(done.before, Before::Nothing);
457        assert_eq!(done.files, 2);
458        assert_eq!(done.digest, manifest.digest());
459        assert_eq!(done.root, cache.join("sysroots").join("x86_64-linux-musl"));
460
461        // The files are where a compiler looks for them, and the record is beside them, which is
462        // what `-print-sysroot-provenance` reads.
463        assert!(done.root.join("include/stdio.h").is_file());
464        assert_eq!(
465            std::fs::read_to_string(done.root.join("manifest")).expect("a manifest"),
466            manifest.render()
467        );
468        // And nothing is left in the staging area, which is a cache that would otherwise grow a
469        // copy of every sysroot it ever installed.
470        let left: Vec<PathBuf> = std::fs::read_dir(cache.join("staging"))
471            .expect("the staging directory")
472            .map(|entry| entry.expect("an entry").path())
473            .collect();
474        assert_eq!(left, Vec::<PathBuf>::new(), "a staging tree was left behind");
475    }
476
477    /// `--fetch` from end to end, with the artifact already where a downloader would have put it.
478    ///
479    /// No downloader runs, and that is the case rather than a way around one: section 13.8 says a
480    /// machine with none of the three is told the path to put a file at and a second run carries on
481    /// from the check, so this is that machine. What it tests is that the table, the check, the
482    /// unpack and the rename are wired to each other, which is the one thing neither module's own
483    /// tests can see.
484    ///
485    /// It is here rather than beside the parser because the fixtures for an artifact are here, and a
486    /// second copy of them next door is the thing most likely to drift away from this one.
487    #[test]
488    fn a_fetch_of_an_artifact_that_is_already_on_the_machine_installs_it() {
489        let tree = Tree::new("fetch");
490        let manifest = manifest_for(FILES);
491        let (built, hash) = artifact(&tree, FILES, &manifest);
492        let cache = tree.0.join("cache");
493
494        // A table row names an artifact by a URL and a hash, and both are static strings there
495        // because a release is what writes them. A test computes the hash as it goes, so it leaks
496        // two strings into a process that is about to end.
497        let pinned = rucc_sysroot::Pinned {
498            tuple: "x86_64-linux-musl",
499            url: "https://example.invalid/rucc-sysroot-x86_64-linux-musl.tar.gz",
500            sha256: String::leak(hash),
501        };
502        // Where a downloader would have written it, which is what the fetch looks at first.
503        let at = pinned.archive_in(&cache);
504        std::fs::create_dir_all(at.parent().expect("a parent")).expect("a downloads directory");
505        std::fs::copy(&built, &at).expect("the artifact should be placeable");
506
507        assert_eq!(crate::fetch_sysroot(&pinned, target(), &cache), 0);
508        let root = cache.join("sysroots").join("x86_64-linux-musl");
509        assert!(root.join("include/stdio.h").is_file());
510        assert_eq!(
511            std::fs::read_to_string(root.join("manifest")).expect("a manifest"),
512            manifest.render()
513        );
514        // And again, which is the ordinary second run: the archive is still there, it still matches,
515        // and the tree it would install is the tree that is already installed.
516        assert_eq!(crate::fetch_sysroot(&pinned, target(), &cache), 0);
517    }
518
519    #[test]
520    fn the_same_artifact_twice_does_not_move_anything() {
521        let tree = Tree::new("again");
522        let manifest = manifest_for(FILES);
523        let (archive, hash) = artifact(&tree, FILES, &manifest);
524        let cache = tree.0.join("cache");
525
526        let first = install(&archive, &hash, target(), &cache).expect("the first install");
527        let second = install(&archive, &hash, target(), &cache).expect("the second install");
528        assert_eq!(second.before, Before::TheSame);
529        assert_eq!(second.root, first.root);
530        assert_eq!(second.digest, first.digest);
531    }
532
533    #[test]
534    fn a_hash_that_does_not_match_is_refused_before_anything_is_unpacked() {
535        let tree = Tree::new("hash");
536        let manifest = manifest_for(FILES);
537        let (archive, _) = artifact(&tree, FILES, &manifest);
538        let cache = tree.0.join("cache");
539
540        let wrong = "0".repeat(64);
541        let why =
542            install(&archive, &wrong, target(), &cache).expect_err("this is not the artifact");
543        assert!(why.message.contains("where this release pins"), "{}", why.message);
544        // Nothing was unpacked, which is the order section 13.8 asks for: the cache does not even
545        // have the directories in it.
546        assert!(!cache.exists(), "a refused artifact should not have reached the cache");
547    }
548
549    #[test]
550    fn an_artifact_for_another_target_is_refused() {
551        let tree = Tree::new("target");
552        let mut manifest = Manifest::new("aarch64-linux-musl".parse().expect("a tuple"));
553        for (path, text) in FILES {
554            manifest.push(Input {
555                path: (*path).to_owned(),
556                source: "musl-1.2.5".to_owned(),
557                url: "https://musl.libc.org/releases/musl-1.2.5.tar.gz".to_owned(),
558                sha256: sha256::hex(text.as_bytes()),
559                licence: Licence::Mit,
560                provenance: Provenance::Bundled,
561            });
562        }
563        let (archive, hash) = artifact(&tree, FILES, &manifest);
564        let cache = tree.0.join("cache");
565
566        let why = install(&archive, &hash, target(), &cache).expect_err("the wrong target");
567        assert!(why.message.contains("aarch64-linux-musl"), "{}", why.message);
568        assert!(why.message.contains("x86_64-linux-musl"), "{}", why.message);
569        assert!(!cache.join("sysroots").exists(), "nothing should have been installed");
570    }
571
572    #[test]
573    fn an_archive_with_no_record_in_it_is_refused() {
574        let tree = Tree::new("bare");
575        let staged = tree.0.join("staged");
576        std::fs::create_dir_all(&staged).expect("a directory");
577        std::fs::write(staged.join("include.h"), "int x;\n").expect("a file");
578        let archive = tree.0.join("bare.tar.gz");
579        let status = Command::new("tar")
580            .arg("-czf")
581            .arg(&archive)
582            .arg("-C")
583            .arg(&staged)
584            .arg(".")
585            .status()
586            .expect("tar should run");
587        assert!(status.success());
588        let hash = sha256::hex(&std::fs::read(&archive).expect("readable"));
589        let cache = tree.0.join("cache");
590
591        let why = install(&archive, &hash, target(), &cache).expect_err("no manifest");
592        assert!(why.message.contains("has no manifest in it"), "{}", why.message);
593    }
594
595    #[test]
596    fn a_file_the_record_does_not_name_is_refused() {
597        // The direction that is easy to leave out. A digest is a claim about what is under a
598        // directory, so an unrecorded file makes it a claim about less than what is there.
599        let tree = Tree::new("extra");
600        let manifest = manifest_for(FILES);
601        let mut with_extra: Vec<(&str, &str)> = FILES.to_vec();
602        with_extra.push(("lib/surprise.o", "nobody wrote this down\n"));
603        let (archive, hash) = artifact(&tree, &with_extra, &manifest);
604        let cache = tree.0.join("cache");
605
606        let why = install(&archive, &hash, target(), &cache).expect_err("an unrecorded file");
607        assert!(why.message.contains("lib/surprise.o"), "{}", why.message);
608        assert!(why.message.contains("not in the record"), "{}", why.message);
609    }
610
611    #[test]
612    fn a_file_whose_bytes_changed_is_refused_and_so_is_one_that_is_missing() {
613        let tree = Tree::new("bytes");
614        // The manifest describes what the files were supposed to be and the archive holds something
615        // else, which is what a producer with a bug in it looks like from here.
616        let manifest = manifest_for(&[("include/stdio.h", "what the record says\n")]);
617        let (archive, hash) = artifact(&tree, &[("include/stdio.h", "what is there\n")], &manifest);
618        let cache = tree.0.join("cache");
619        let why = install(&archive, &hash, target(), &cache).expect_err("changed bytes");
620        assert!(why.message.contains("include/stdio.h has sha256"), "{}", why.message);
621        assert!(why.message.contains("where the record says"), "{}", why.message);
622
623        let gone = Tree::new("gone");
624        let manifest = manifest_for(FILES);
625        let (archive, hash) = artifact(&gone, &FILES[..1], &manifest);
626        let cache = gone.0.join("cache");
627        let why = install(&archive, &hash, target(), &cache).expect_err("a missing file");
628        assert!(why.message.contains("lib/libc.so"), "{}", why.message);
629        assert!(why.message.contains("not in the archive"), "{}", why.message);
630    }
631
632    #[test]
633    fn more_than_one_disagreement_says_how_many() {
634        // Which of the two failures this is shows in the count, so the count is in the message.
635        let tree = Tree::new("count");
636        let manifest = manifest_for(&[("a.h", "one\n"), ("b.h", "two\n"), ("c.h", "three\n")]);
637        let (archive, hash) = artifact(
638            &tree,
639            &[("a.h", "not one\n"), ("b.h", "not two\n"), ("c.h", "three\n")],
640            &manifest,
641        );
642        let cache = tree.0.join("cache");
643
644        let why = install(&archive, &hash, target(), &cache).expect_err("two files disagree");
645        assert!(why.message.contains("and 1 more files disagree"), "{}", why.message);
646    }
647
648    #[test]
649    fn an_install_over_a_different_sysroot_says_what_it_replaced() {
650        let tree = Tree::new("replace");
651        let cache = tree.0.join("cache");
652        let first = manifest_for(&[("include/stdio.h", "the old one\n")]);
653        let (archive, hash) = artifact(&tree, &[("include/stdio.h", "the old one\n")], &first);
654        let done = install(&archive, &hash, target(), &cache).expect("the first install");
655        let was = done.digest.clone();
656
657        let second = Tree::new("replace-second");
658        let manifest = manifest_for(&[("include/stdio.h", "the new one\n")]);
659        let (archive, hash) = artifact(&second, &[("include/stdio.h", "the new one\n")], &manifest);
660        let done = install(&archive, &hash, target(), &cache).expect("the second install");
661
662        assert_eq!(done.before, Before::Different(was));
663        assert_eq!(
664            std::fs::read_to_string(done.root.join("include/stdio.h")).expect("the new file"),
665            "the new one\n"
666        );
667        // And the tree that was moved aside is gone rather than left beside the one that replaced
668        // it, since a cache that keeps every version it ever had is a cache nobody can size.
669        let kept: Vec<String> = std::fs::read_dir(cache.join("sysroots"))
670            .expect("the sysroots directory")
671            .map(|entry| entry.expect("an entry").file_name().to_string_lossy().into_owned())
672            .collect();
673        assert_eq!(kept, vec!["x86_64-linux-musl".to_owned()]);
674    }
675
676    #[test]
677    fn a_record_that_names_a_path_outside_the_tree_is_refused() {
678        // Not a likely producer bug, and the check is here because a manifest is data that has not
679        // been checked yet at the moment its paths are read.
680        assert!(relative(Path::new("/cache/sysroots/t"), "include/stdio.h").is_ok());
681        for path in ["../outside.h", "/etc/passwd", "include/../../outside.h"] {
682            let why = relative(Path::new("/cache/sysroots/t"), path)
683                .expect_err("this is not a path inside a sysroot");
684            assert!(why.contains(path), "{why}");
685        }
686    }
687
688    #[test]
689    fn the_walk_names_files_the_way_a_manifest_does() {
690        // With `/` between the parts whatever the host separator is, because the comparison is
691        // against a manifest and a manifest has one spelling.
692        let tree = Tree::new("walk");
693        tree.write("include/sys/types.h", "typedef int t;\n");
694        tree.write("manifest", "rucc sysroot manifest 3\n");
695        let mut found = Vec::new();
696        walk(&tree.0, String::new(), &mut found).expect("the walk should work");
697        found.sort();
698        assert_eq!(found, vec!["include/sys/types.h".to_owned(), "manifest".to_owned()]);
699    }
700
701    #[test]
702    fn verify_is_the_hash_of_the_file_and_says_both_when_it_is_not() {
703        let tree = Tree::new("verify");
704        tree.write("thing", "bytes\n");
705        let at = tree.0.join("thing");
706        let hash = sha256::hex(b"bytes\n");
707        assert!(verify(&at, &hash).is_ok());
708        let why = verify(&at, &"f".repeat(64)).expect_err("a mismatch");
709        assert!(why.message.contains(&hash), "{}", why.message);
710        assert!(why.message.contains(&"f".repeat(64)), "{}", why.message);
711
712        // A file that is not there is not a mismatch and does not read as one.
713        let why = verify(&tree.0.join("absent"), &hash).expect_err("nothing to hash");
714        assert!(!why.message.contains("where this release pins"), "{}", why.message);
715    }
716
717    #[test]
718    fn the_check_passes_a_tree_that_matches() {
719        // The unit underneath the install, so that a failure in the install tells you which half.
720        let tree = Tree::new("check");
721        for (path, text) in FILES {
722            tree.write(path, text);
723        }
724        tree.write("manifest", "rucc sysroot manifest 3\n");
725        let manifest = manifest_for(FILES);
726        assert_eq!(check(&tree.0, &manifest), Ok(()));
727        // An empty directory is not a file and is not a disagreement, which is what a tree that
728        // went through `tar` on one host and not another looks like.
729        std::fs::create_dir_all(tree.0.join("lib/empty")).expect("a directory");
730        assert_eq!(check(&tree.0, &manifest), Ok(()));
731    }
732
733    #[test]
734    fn installed_says_where_and_what() {
735        // The type the caller reports from, asserted once so that a change to it is a change to a
736        // test rather than to a message nobody reads.
737        let made = Installed {
738            root: PathBuf::from("/cache/sysroots/x86_64-linux-musl"),
739            digest: "0".repeat(64),
740            files: 3,
741            before: Before::Nothing,
742        };
743        assert_eq!(made.files, 3);
744        assert_eq!(made.before, Before::Nothing);
745    }
746}