Skip to main content

snix_build/oci/
mod.rs

1mod bundle;
2mod spec;
3pub(crate) mod subuid;
4
5pub(crate) use bundle::make_bundle;
6pub(crate) use spec::make_spec;
7
8use std::path::{Path, PathBuf};
9
10use crate::sandbox::SandboxOutputs;
11
12/// For a given scratch path, return the scratch_name that's allocated.
13// We currently use use lower hex encoding of the b3 digest of the scratch
14// path, so we don't need to globally allocate and pass down some uuids.
15pub(crate) fn scratch_name(scratch_path: impl AsRef<[u8]>) -> String {
16    data_encoding::BASE32.encode(blake3::hash(scratch_path.as_ref()).as_bytes())
17}
18
19#[derive(Debug, Clone)]
20pub struct OciOutputs {
21    /// Pre-sorted scratch mappings: `(guest_mountpoint, host_scratch_dir)`
22    /// Sorted descending by component count so longer, more specific prefixes match first.
23    scratch_mappings: Vec<(String, PathBuf)>,
24}
25
26impl OciOutputs {
27    pub fn new<S>(bundle_path: &Path, scratches: &[S]) -> anyhow::Result<Self>
28    where
29        S: AsRef<str>,
30    {
31        let scratch_root = bundle_path.join("scratch");
32
33        let mut scratch_mappings: Vec<(String, PathBuf)> = Vec::with_capacity(scratches.len());
34        for mp in scratches {
35            let mp = mp.as_ref();
36            if !Path::new(mp).is_relative() || mp.is_empty() {
37                anyhow::bail!("scratch path must be relative and non-empty: {mp:?}");
38            }
39            let host_dir = scratch_root.join(scratch_name(mp));
40            scratch_mappings.push((mp.to_owned(), host_dir));
41        }
42
43        // Sort descending by number of path components so more specific prefixes match first
44        scratch_mappings
45            .sort_by_key(|a| std::cmp::Reverse(std::path::Path::new(&a.0).components().count()));
46
47        Ok(Self { scratch_mappings })
48    }
49}
50
51impl SandboxOutputs for OciOutputs {
52    async fn find_path(&self, path: impl AsRef<Path>) -> Option<PathBuf> {
53        let path = path.as_ref();
54        for (mp, host_dir) in &self.scratch_mappings {
55            if let Ok(relpath) = path.strip_prefix(mp) {
56                let host_path = host_dir.join(relpath);
57                if let Ok(metadata) = tokio::fs::symlink_metadata(&host_path).await
58                    && (metadata.is_symlink() || metadata.is_dir() || metadata.is_file())
59                {
60                    return Some(host_path);
61                }
62                return None;
63            }
64        }
65        None
66    }
67}
68
69#[cfg(test)]
70mod tests {
71    use std::fs;
72    use std::path::PathBuf;
73
74    use rstest::rstest;
75    use tempfile::TempDir;
76
77    use super::{OciOutputs, scratch_name};
78    use crate::sandbox::SandboxOutputs;
79
80    #[rstest]
81    #[tokio::test]
82    #[case::simple("nix/store/aaaa", &["nix/store".into()], Some(("nix/store", "aaaa")))]
83    #[case::prefix_no_sep("nix/store/aaaa", &["nix/sto".into()], None)]
84    #[case::not_found("nix/store/aaaa", &["build".into()], None)]
85    async fn test_find_path_in_scratches(
86        #[case] search_path: &str,
87        #[case] mountpoints: &[String],
88        #[case] expected: Option<(&str, &str)>,
89    ) {
90        let temp_dir = TempDir::new().unwrap();
91        let bundle_path = temp_dir.path();
92
93        let expected_path = expected.map(|(mp, rel)| {
94            let p = bundle_path.join("scratch").join(scratch_name(mp)).join(rel);
95            fs::create_dir_all(p.parent().unwrap()).unwrap();
96            fs::write(&p, b"").unwrap();
97            p
98        });
99
100        let outputs = OciOutputs::new(bundle_path, mountpoints).expect("must succeed");
101
102        assert_eq!(outputs.find_path(search_path).await, expected_path);
103    }
104
105    #[tokio::test]
106    async fn test_get_host_output_paths_simple() {
107        let temp_dir = TempDir::new().unwrap();
108        let bundle_path = temp_dir.path();
109        let scratch_paths = vec!["build", "nix/store"];
110
111        let mut expected_path = PathBuf::new();
112        expected_path.push(bundle_path);
113        expected_path.push("scratch");
114        expected_path.push(scratch_name("nix/store"));
115        expected_path.push("fhaj6gmwns62s6ypkcldbaj2ybvkhx3p-foo");
116
117        fs::create_dir_all(expected_path.parent().unwrap()).unwrap();
118        fs::write(&expected_path, b"").unwrap();
119
120        let outputs = OciOutputs::new(bundle_path, &scratch_paths).expect("must succeed");
121
122        assert_eq!(
123            Some(expected_path),
124            outputs
125                .find_path("nix/store/fhaj6gmwns62s6ypkcldbaj2ybvkhx3p-foo")
126                .await
127        );
128    }
129
130    #[test]
131    fn test_oci_outputs_invalid_scratch() {
132        let temp_dir = TempDir::new().unwrap();
133        let bundle_path = temp_dir.path();
134
135        let err = OciOutputs::new(bundle_path, &["/absolute"]).unwrap_err();
136        assert!(err.to_string().contains("scratch path must be relative"));
137
138        let err = OciOutputs::new(bundle_path, &[""]).unwrap_err();
139        assert!(err.to_string().contains("scratch path must be relative"));
140    }
141}