bootc_lib/bootc_composefs/
digest.rs1use std::fs::File;
4use std::io::BufWriter;
5use std::sync::Arc;
6
7use anyhow::{Context, Result};
8use camino::Utf8Path;
9use cap_std_ext::cap_std;
10use cap_std_ext::cap_std::fs::Dir;
11use cfsctl::composefs;
12use cfsctl::composefs_boot;
13use composefs::dumpfile;
14use composefs::fsverity::FsVerityHashValue;
15use composefs_boot::BootOps as _;
16use tempfile::TempDir;
17
18use crate::store::ComposefsRepository;
19
20#[fn_error_context::context("Creating new temp composefs repo")]
25pub(crate) fn new_temp_composefs_repo() -> Result<(TempDir, Arc<ComposefsRepository>)> {
26 let td_guard = tempfile::tempdir_in("/var/tmp")?;
27 let td_path = td_guard.path();
28 let td_dir = Dir::open_ambient_dir(td_path, cap_std::ambient_authority())?;
29
30 td_dir.create_dir("repo")?;
31 let repo_dir = td_dir.open_dir("repo")?;
32 let mut repo = ComposefsRepository::open_path(&repo_dir, ".").context("Init cfs repo")?;
33 repo.set_insecure(true);
35 Ok((td_guard, Arc::new(repo)))
36}
37
38#[fn_error_context::context("Computing composefs digest")]
56pub(crate) fn compute_composefs_digest(
57 path: &Utf8Path,
58 write_dumpfile_to: Option<&Utf8Path>,
59) -> Result<String> {
60 if path.as_str() == "/" {
61 anyhow::bail!("Cannot operate on active root filesystem; mount separate target instead");
62 }
63
64 let (_td_guard, repo) = new_temp_composefs_repo()?;
65
66 let mut fs =
68 composefs::fs::read_container_root(rustix::fs::CWD, path.as_std_path(), Some(&repo))
69 .context("Reading container root")?;
70 fs.transform_for_boot(&repo).context("Preparing for boot")?;
71 let id = fs.compute_image_id();
72 let digest = id.to_hex();
73
74 if let Some(dumpfile_path) = write_dumpfile_to {
75 let mut w = File::create(dumpfile_path)
76 .with_context(|| format!("Opening {dumpfile_path}"))
77 .map(BufWriter::new)?;
78 dumpfile::write_dumpfile(&mut w, &fs).context("Writing dumpfile")?;
79 }
80
81 Ok(digest)
82}
83
84#[cfg(test)]
85mod tests {
86 use super::*;
87 use std::fs::{self, Permissions};
88 use std::os::unix::fs::PermissionsExt;
89
90 fn create_test_filesystem(root: &std::path::Path) -> Result<()> {
92 fs::create_dir_all(root.join("boot"))?;
94 fs::create_dir_all(root.join("sysroot"))?;
95
96 let usr_bin = root.join("usr/bin");
98 fs::create_dir_all(&usr_bin)?;
99
100 let hello_path = usr_bin.join("hello");
102 fs::write(&hello_path, "test\n")?;
103 fs::set_permissions(&hello_path, Permissions::from_mode(0o755))?;
104
105 let etc = root.join("etc");
107 fs::create_dir_all(&etc)?;
108
109 let config_path = etc.join("config");
111 fs::write(&config_path, "test\n")?;
112 fs::set_permissions(&config_path, Permissions::from_mode(0o644))?;
113
114 Ok(())
115 }
116
117 #[test]
118 fn test_compute_composefs_digest() {
119 let td = tempfile::tempdir().unwrap();
121 create_test_filesystem(td.path()).unwrap();
122
123 let path = Utf8Path::from_path(td.path()).unwrap();
125 let digest = compute_composefs_digest(path, None).unwrap();
126
127 assert_eq!(
129 digest.len(),
130 128,
131 "Expected 512-bit hex digest, got length {}",
132 digest.len()
133 );
134 assert!(
135 digest.chars().all(|c| c.is_ascii_hexdigit()),
136 "Digest contains non-hex characters: {digest}"
137 );
138
139 let digest2 = compute_composefs_digest(path, None).unwrap();
141 assert_eq!(
142 digest, digest2,
143 "Digest should be consistent across multiple computations"
144 );
145 }
146
147 #[test]
148 fn test_compute_composefs_digest_rejects_root() {
149 let result = compute_composefs_digest(Utf8Path::new("/"), None);
150 assert!(result.is_err());
151 let err = result.unwrap_err();
152 let found = err.chain().any(|e| {
153 e.to_string()
154 .contains("Cannot operate on active root filesystem")
155 });
156
157 assert!(found, "Unexpected error chain: {err:?}");
158 }
159}