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