1use std::future::Future;
2use std::io::Write;
3use std::os::fd::BorrowedFd;
4use std::path::{Component, Path, PathBuf};
5use std::process::Command;
6use std::time::Duration;
7
8use anyhow::{Context, Result};
9use bootc_utils::CommandRunExt;
10use camino::Utf8Path;
11use cap_std_ext::cap_std::fs::Dir;
12use cap_std_ext::dirext::CapStdExtDirExt;
13use cap_std_ext::prelude::CapStdExtCommandExt;
14use fn_error_context::context;
15use indicatif::HumanDuration;
16use libsystemd::logging::journal_print;
17use ostree::glib;
18use ostree_ext::container::SignatureSource;
19use ostree_ext::ostree;
20
21pub(crate) fn origin_has_rpmostree_stuff(kf: &glib::KeyFile) -> bool {
24 for group in ["rpmostree", "packages", "overrides", "modules"] {
27 if kf.has_group(group) {
28 return true;
29 }
30 }
31 false
32}
33
34#[allow(unsafe_code)]
36pub(crate) fn sysroot_fd(sysroot: &ostree::Sysroot) -> BorrowedFd<'_> {
37 unsafe { BorrowedFd::borrow_raw(sysroot.fd()) }
38}
39
40pub(crate) fn sysroot_dir(sysroot: &ostree::Sysroot) -> Result<Dir> {
42 Dir::reopen_dir(&sysroot_fd(sysroot)).map_err(Into::into)
43}
44
45pub(crate) fn deployment_fd(
48 sysroot: &ostree::Sysroot,
49 deployment: &ostree::Deployment,
50) -> Result<Dir> {
51 let sysroot_dir = &Dir::reopen_dir(&sysroot_fd(sysroot))?;
52 let dirpath = sysroot.deployment_dirpath(deployment);
53 sysroot_dir.open_dir(&dirpath).map_err(Into::into)
54}
55
56pub(crate) fn find_mount_option<'a>(
60 option_string_list: &'a str,
61 optname: &'_ str,
62) -> Option<&'a str> {
63 option_string_list
64 .split(',')
65 .filter_map(|k| k.split_once('='))
66 .filter_map(|(k, v)| (k == optname).then_some(v))
67 .next()
68}
69
70pub fn have_executable(name: &str) -> Result<bool> {
71 let Some(path) = std::env::var_os("PATH") else {
72 return Ok(false);
73 };
74 for mut elt in std::env::split_paths(&path) {
75 elt.push(name);
76 if elt.try_exists()? {
77 return Ok(true);
78 }
79 }
80 Ok(false)
81}
82
83#[context("Opening {target} with writable mount")]
85pub(crate) fn open_dir_remount_rw(root: &Dir, target: &Utf8Path) -> Result<Dir> {
86 if matches!(root.is_mountpoint(target), Ok(Some(true))) {
87 tracing::debug!("Target {target} is a mountpoint, remounting rw");
88 let st = Command::new("mount")
89 .args(["-o", "remount,rw", target.as_str()])
90 .cwd_dir(root.try_clone()?)
91 .status()?;
92
93 anyhow::ensure!(st.success(), "Failed to remount: {st:?}");
94 }
95 root.open_dir(target).map_err(anyhow::Error::new)
96}
97
98#[context("Removing immutable flag from {target}")]
100pub(crate) fn remove_immutability(root: &Dir, target: &Utf8Path) -> Result<()> {
101 use anyhow::ensure;
102
103 tracing::debug!("Target {target} is a mountpoint, remounting rw");
104 let st = Command::new("chattr")
105 .args(["-i", target.as_str()])
106 .cwd_dir(root.try_clone()?)
107 .status()?;
108
109 ensure!(st.success(), "Failed to remove immutability: {st:?}");
110
111 Ok(())
112}
113
114pub(crate) fn spawn_editor(tmpf: &tempfile::NamedTempFile) -> Result<()> {
115 let editor_variables = ["EDITOR"];
116 let backup_editors = ["nano", "vim", "vi"];
118 let editor = editor_variables.into_iter().find_map(std::env::var_os);
119 let editor = if let Some(e) = editor.as_ref() {
120 e.to_str()
121 } else {
122 backup_editors
123 .into_iter()
124 .find(|v| std::path::Path::new("/usr/bin").join(v).exists())
125 };
126 let editor =
127 editor.ok_or_else(|| anyhow::anyhow!("$EDITOR is unset, and no backup editor found"))?;
128 let mut editor_args = editor.split_ascii_whitespace();
129 let argv0 = editor_args
130 .next()
131 .ok_or_else(|| anyhow::anyhow!("Invalid editor: {editor}"))?;
132 Command::new(argv0)
133 .args(editor_args)
134 .arg(tmpf.path())
135 .lifecycle_bind()
136 .run_inherited()
137 .with_context(|| format!("Invoking editor {editor} failed"))
138}
139
140pub(crate) fn sigpolicy_from_opt(enforce_container_verification: bool) -> SignatureSource {
142 match enforce_container_verification {
143 true => SignatureSource::ContainerPolicy,
144 false => SignatureSource::ContainerPolicyAllowInsecure,
145 }
146}
147
148pub(crate) fn medium_visibility_warning(s: &str) {
151 anstream::eprintln!(
152 "{}{s}{}",
153 anstyle::AnsiColor::Red.render_fg(),
154 anstyle::Reset.render()
155 );
156 std::thread::sleep(std::time::Duration::from_secs(1));
158}
159
160pub(crate) async fn async_task_with_spinner<F, T>(msg: &str, f: F) -> T
165where
166 F: Future<Output = T>,
167{
168 let start_time = std::time::Instant::now();
169 let pb = indicatif::ProgressBar::new_spinner();
170 let style = indicatif::ProgressStyle::default_bar();
171 pb.set_style(style.template("{spinner} {msg}").unwrap());
172 pb.set_message(msg.to_string());
173 pb.enable_steady_tick(Duration::from_millis(150));
174 if pb.is_hidden() {
177 eprint!("{msg}...");
178 std::io::stderr().flush().unwrap();
179 }
180 let r = f.await;
181 let elapsed = HumanDuration(start_time.elapsed());
182 let _ = journal_print(
183 libsystemd::logging::Priority::Info,
184 &format!("completed task in {elapsed}: {msg}"),
185 );
186 if pb.is_hidden() {
187 eprintln!("done ({elapsed})");
188 } else {
189 pb.finish_with_message(format!("{msg}: done ({elapsed})"));
190 }
191 r
192}
193
194#[allow(dead_code)]
198pub(crate) fn digested_pullspec(image: &str, digest: &str) -> String {
199 let image = image.rsplit_once('@').map(|v| v.0).unwrap_or(image);
200 format!("{image}@{digest}")
201}
202
203#[derive(Debug)]
204pub enum EfiError {
205 SystemNotUEFI,
206 MissingVar,
207 #[allow(dead_code)]
208 InvalidData(&'static str),
209 #[allow(dead_code)]
210 Io(std::io::Error),
211}
212
213impl From<std::io::Error> for EfiError {
214 fn from(e: std::io::Error) -> Self {
215 EfiError::Io(e)
216 }
217}
218
219pub fn read_uefi_var(var_name: &str) -> Result<String, EfiError> {
220 use crate::install::EFIVARFS;
221 use cap_std_ext::cap_std::ambient_authority;
222
223 let efivarfs = match Dir::open_ambient_dir(EFIVARFS, ambient_authority()) {
224 Ok(dir) => dir,
225 Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Err(EfiError::SystemNotUEFI),
226 Err(e) => Err(e)?,
227 };
228
229 match efivarfs.read(var_name) {
230 Ok(loader_bytes) => {
231 if loader_bytes.len() % 2 != 0 {
232 return Err(EfiError::InvalidData(
233 "EFI var length is not valid UTF-16 LE",
234 ));
235 }
236
237 let loader_u16_bytes: Vec<u16> = loader_bytes
239 .chunks_exact(2)
240 .map(|x| u16::from_le_bytes([x[0], x[1]]))
241 .collect();
242
243 let loader = String::from_utf16(&loader_u16_bytes)
244 .map_err(|_| EfiError::InvalidData("EFI var is not UTF-16"))?;
245
246 return Ok(loader);
247 }
248
249 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
250 return Err(EfiError::MissingVar);
251 }
252
253 Err(e) => Err(e)?,
254 }
255}
256
257pub(crate) fn path_relative_to(from: &Path, to: &Path) -> Result<PathBuf> {
261 if !from.is_absolute() || !to.is_absolute() {
262 anyhow::bail!("Paths must be absolute");
263 }
264
265 let from = from.components().collect::<Vec<_>>();
266 let to = to.components().collect::<Vec<_>>();
267
268 let common = from.iter().zip(&to).take_while(|(a, b)| a == b).count();
269
270 let up = std::iter::repeat(Component::ParentDir).take(from.len() - common);
271
272 let mut final_path = PathBuf::new();
273 final_path.extend(up);
274 final_path.extend(&to[common..]);
275
276 return Ok(final_path);
277}
278
279#[cfg(test)]
280mod tests {
281 use super::*;
282
283 #[test]
284 fn test_digested_pullspec() {
285 let digest = "ebe3bdccc041864e5a485f1e755e242535c3b83d110c0357fe57f110b73b143e";
286 assert_eq!(
287 digested_pullspec("quay.io/example/foo:bar", digest),
288 format!("quay.io/example/foo:bar@{digest}")
289 );
290 assert_eq!(
291 digested_pullspec("quay.io/example/foo@sha256:otherdigest", digest),
292 format!("quay.io/example/foo@{digest}")
293 );
294 assert_eq!(
295 digested_pullspec("quay.io/example/foo", digest),
296 format!("quay.io/example/foo@{digest}")
297 );
298 }
299
300 #[test]
301 fn test_find_mount_option() {
302 const V1: &str = "rw,relatime,compress=foo,subvol=blah,fast";
303 assert_eq!(find_mount_option(V1, "subvol").unwrap(), "blah");
304 assert_eq!(find_mount_option(V1, "rw"), None);
305 assert_eq!(find_mount_option(V1, "somethingelse"), None);
306 }
307
308 #[test]
309 fn test_sigpolicy_from_opts() {
310 assert_eq!(sigpolicy_from_opt(true), SignatureSource::ContainerPolicy);
311 assert_eq!(
312 sigpolicy_from_opt(false),
313 SignatureSource::ContainerPolicyAllowInsecure
314 );
315 }
316
317 #[test]
318 fn test_relative_path() {
319 let from = Path::new("/sysroot/state/deploy/image_id");
320 let to = Path::new("/sysroot/state/os/default/var");
321
322 assert_eq!(
323 path_relative_to(from, to).unwrap(),
324 PathBuf::from("../../os/default/var")
325 );
326 assert_eq!(
327 path_relative_to(&Path::new("state/deploy"), to)
328 .unwrap_err()
329 .to_string(),
330 "Paths must be absolute"
331 );
332 }
333
334 #[test]
335 fn test_have_executable() {
336 assert!(have_executable("true").unwrap());
337 assert!(!have_executable("someexethatdoesnotexist").unwrap());
338 }
339}