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
12pub(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 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 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}