1use std::future::Future;
2use std::io::{Read, 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 for elt in executable_candidates(name) {
72 if elt.try_exists()? {
73 return Ok(true);
74 }
75 }
76 Ok(false)
77}
78
79fn executable_candidates(name: &str) -> Vec<PathBuf> {
80 let Some(path) = std::env::var_os("PATH") else {
81 return Vec::new();
82 };
83 std::env::split_paths(&path)
84 .map(|mut path| {
85 path.push(name);
86 path
87 })
88 .collect()
89}
90
91pub fn have_executable_in_root(root: &Dir, name: &str) -> Result<bool> {
94 for candidate in executable_candidates(name) {
95 let candidate = candidate.strip_prefix("/").unwrap_or(&candidate);
96 if root
97 .try_exists(candidate)
98 .with_context(|| format!("checking whether {} exists", candidate.display()))?
99 {
100 return Ok(true);
101 }
102 }
103 Ok(false)
104}
105
106#[context("Opening {target} with writable mount")]
108pub(crate) fn open_dir_remount_rw(root: &Dir, target: &Utf8Path) -> Result<Dir> {
109 if matches!(root.is_mountpoint(target), Ok(Some(true))) {
110 tracing::debug!("Target {target} is a mountpoint, remounting rw");
111 let st = Command::new("mount")
112 .args(["-o", "remount,rw", target.as_str()])
113 .cwd_dir(root.try_clone()?)
114 .status()?;
115
116 anyhow::ensure!(st.success(), "Failed to remount: {st:?}");
117 }
118 root.open_dir(target).map_err(anyhow::Error::new)
119}
120
121#[context("Removing immutable flag from {target}")]
123pub(crate) fn remove_immutability(root: &Dir, target: &Utf8Path) -> Result<()> {
124 use anyhow::ensure;
125
126 tracing::debug!("Target {target} is a mountpoint, remounting rw");
127 let st = Command::new("chattr")
128 .args(["-i", target.as_str()])
129 .cwd_dir(root.try_clone()?)
130 .status()?;
131
132 ensure!(st.success(), "Failed to remove immutability: {st:?}");
133
134 Ok(())
135}
136
137pub(crate) fn spawn_editor(tmpf: &tempfile::NamedTempFile) -> Result<()> {
138 let editor_variables = ["EDITOR"];
139 let backup_editors = ["nano", "vim", "vi"];
141 let editor = editor_variables.into_iter().find_map(std::env::var_os);
142 let editor = if let Some(e) = editor.as_ref() {
143 e.to_str()
144 } else {
145 backup_editors
146 .into_iter()
147 .find(|v| std::path::Path::new("/usr/bin").join(v).exists())
148 };
149 let editor =
150 editor.ok_or_else(|| anyhow::anyhow!("$EDITOR is unset, and no backup editor found"))?;
151 let mut editor_args = editor.split_ascii_whitespace();
152 let argv0 = editor_args
153 .next()
154 .ok_or_else(|| anyhow::anyhow!("Invalid editor: {editor}"))?;
155 Command::new(argv0)
156 .args(editor_args)
157 .arg(tmpf.path())
158 .lifecycle_bind()
159 .run_inherited()
160 .with_context(|| format!("Invoking editor {editor} failed"))
161}
162
163pub(crate) fn sigpolicy_from_opt(enforce_container_verification: bool) -> SignatureSource {
165 match enforce_container_verification {
166 true => SignatureSource::ContainerPolicy,
167 false => SignatureSource::ContainerPolicyAllowInsecure,
168 }
169}
170
171pub(crate) fn medium_visibility_warning(s: &str) {
174 anstream::eprintln!(
175 "{}{s}{}",
176 anstyle::AnsiColor::Red.render_fg(),
177 anstyle::Reset.render()
178 );
179 std::thread::sleep(std::time::Duration::from_secs(1));
181}
182
183pub(crate) async fn async_task_with_spinner<F, T>(msg: &str, f: F) -> T
188where
189 F: Future<Output = T>,
190{
191 let start_time = std::time::Instant::now();
192 let pb = indicatif::ProgressBar::new_spinner();
193 let style = indicatif::ProgressStyle::default_bar();
194 pb.set_style(style.template("{spinner} {msg}").unwrap());
195 pb.set_message(msg.to_string());
196 pb.enable_steady_tick(Duration::from_millis(150));
197 if pb.is_hidden() {
200 eprint!("{msg}...");
201 std::io::stderr().flush().unwrap();
202 }
203 let r = f.await;
204 let elapsed = HumanDuration(start_time.elapsed());
205 let _ = journal_print(
206 libsystemd::logging::Priority::Info,
207 &format!("completed task in {elapsed}: {msg}"),
208 );
209 if pb.is_hidden() {
210 eprintln!("done ({elapsed})");
211 } else {
212 pb.finish_with_message(format!("{msg}: done ({elapsed})"));
213 }
214 r
215}
216
217#[allow(dead_code)]
221pub(crate) fn digested_pullspec(image: &str, digest: &str) -> String {
222 let image = image.rsplit_once('@').map(|v| v.0).unwrap_or(image);
223 format!("{image}@{digest}")
224}
225
226#[derive(Debug)]
227pub enum EfiError {
228 SystemNotUEFI,
229 MissingVar,
230 #[allow(dead_code)]
231 InvalidData(&'static str),
232 #[allow(dead_code)]
233 Io(std::io::Error),
234}
235
236impl From<std::io::Error> for EfiError {
237 fn from(e: std::io::Error) -> Self {
238 EfiError::Io(e)
239 }
240}
241
242pub fn read_uefi_var(var_name: &str) -> Result<String, EfiError> {
243 use crate::install::EFIVARFS;
244 use cap_std_ext::cap_std::ambient_authority;
245
246 let efivarfs = match Dir::open_ambient_dir(EFIVARFS, ambient_authority()) {
247 Ok(dir) => dir,
248 Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Err(EfiError::SystemNotUEFI),
249 Err(e) => Err(e)?,
250 };
251
252 match efivarfs.open(var_name) {
253 Ok(f) => parse_efi_var(f),
254 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Err(EfiError::MissingVar),
255 Err(e) => Err(e)?,
256 }
257}
258
259fn parse_efi_var(mut reader: impl Read) -> Result<String, EfiError> {
271 let mut bytes = Vec::new();
272 reader.read_to_end(&mut bytes)?;
273
274 let Some((_, data)) = bytes.split_at_checked(4) else {
275 return Err(EfiError::InvalidData("EFI variable is too short"));
276 };
277
278 if data.len() % 2 != 0 {
279 return Err(EfiError::InvalidData(
280 "EFI var length is not valid UTF-16 LE",
281 ));
282 }
283
284 let data_u16_bytes: Vec<u16> = data
286 .chunks_exact(2)
287 .map(|x| u16::from_le_bytes([x[0], x[1]]))
288 .collect();
289
290 let var_string = String::from_utf16(&data_u16_bytes)
291 .map_err(|_| EfiError::InvalidData("EFI var is not UTF-16"))?;
292
293 Ok(var_string.trim_matches(|c| c == '\0').trim().to_string())
297}
298
299pub(crate) fn path_relative_to(from: &Path, to: &Path) -> Result<PathBuf> {
303 if !from.is_absolute() || !to.is_absolute() {
304 anyhow::bail!("Paths must be absolute");
305 }
306
307 let from = from.components().collect::<Vec<_>>();
308 let to = to.components().collect::<Vec<_>>();
309
310 let common = from.iter().zip(&to).take_while(|(a, b)| a == b).count();
311
312 let up = std::iter::repeat(Component::ParentDir).take(from.len() - common);
313
314 let mut final_path = PathBuf::new();
315 final_path.extend(up);
316 final_path.extend(&to[common..]);
317
318 return Ok(final_path);
319}
320
321#[cfg(test)]
322mod tests {
323 use super::*;
324
325 #[test]
326 fn test_digested_pullspec() {
327 let digest = "ebe3bdccc041864e5a485f1e755e242535c3b83d110c0357fe57f110b73b143e";
328 assert_eq!(
329 digested_pullspec("quay.io/example/foo:bar", digest),
330 format!("quay.io/example/foo:bar@{digest}")
331 );
332 assert_eq!(
333 digested_pullspec("quay.io/example/foo@sha256:otherdigest", digest),
334 format!("quay.io/example/foo@{digest}")
335 );
336 assert_eq!(
337 digested_pullspec("quay.io/example/foo", digest),
338 format!("quay.io/example/foo@{digest}")
339 );
340 }
341
342 fn efi_var_bytes(s: &str) -> Vec<u8> {
345 let mut bytes = vec![0x07, 0x00, 0x00, 0x00];
346 for unit in s.encode_utf16() {
347 bytes.extend_from_slice(&unit.to_le_bytes());
348 }
349 bytes
350 }
351
352 #[test]
353 fn test_parse_efi_var() {
354 let bytes = efi_var_bytes("abcd-1234\0");
356 assert_eq!(parse_efi_var(&bytes[..]).unwrap(), "abcd-1234");
357
358 let bytes = efi_var_bytes(" value \0\0");
360 assert_eq!(parse_efi_var(&bytes[..]).unwrap(), "value");
361
362 let bytes = efi_var_bytes("");
364 assert_eq!(parse_efi_var(&bytes[..]).unwrap(), "");
365 }
366
367 #[test]
368 fn test_parse_efi_var_errors() {
369 assert!(matches!(
371 parse_efi_var(&[0x07, 0x00][..]),
372 Err(EfiError::InvalidData(_))
373 ));
374
375 let bytes = [0x07, 0x00, 0x00, 0x00, 0x41];
377 assert!(matches!(
378 parse_efi_var(&bytes[..]),
379 Err(EfiError::InvalidData(_))
380 ));
381
382 let bytes = [0x07, 0x00, 0x00, 0x00, 0x00, 0xD8];
384 assert!(matches!(
385 parse_efi_var(&bytes[..]),
386 Err(EfiError::InvalidData(_))
387 ));
388 }
389
390 #[test]
391 fn test_find_mount_option() {
392 const V1: &str = "rw,relatime,compress=foo,subvol=blah,fast";
393 assert_eq!(find_mount_option(V1, "subvol").unwrap(), "blah");
394 assert_eq!(find_mount_option(V1, "rw"), None);
395 assert_eq!(find_mount_option(V1, "somethingelse"), None);
396 }
397
398 #[test]
399 fn test_sigpolicy_from_opts() {
400 assert_eq!(sigpolicy_from_opt(true), SignatureSource::ContainerPolicy);
401 assert_eq!(
402 sigpolicy_from_opt(false),
403 SignatureSource::ContainerPolicyAllowInsecure
404 );
405 }
406
407 #[test]
408 fn test_relative_path() {
409 let from = Path::new("/sysroot/state/deploy/image_id");
410 let to = Path::new("/sysroot/state/os/default/var");
411
412 assert_eq!(
413 path_relative_to(from, to).unwrap(),
414 PathBuf::from("../../os/default/var")
415 );
416 assert_eq!(
417 path_relative_to(&Path::new("state/deploy"), to)
418 .unwrap_err()
419 .to_string(),
420 "Paths must be absolute"
421 );
422 }
423
424 #[test]
425 fn test_have_executable() {
426 assert!(have_executable("true").unwrap());
427 assert!(!have_executable("someexethatdoesnotexist").unwrap());
428 }
429
430 #[test]
431 fn test_have_executable_in_root() -> anyhow::Result<()> {
432 use cap_std_ext::cap_std;
433
434 let root = cap_std_ext::cap_tempfile::tempdir(cap_std::ambient_authority())?;
435 assert!(!have_executable_in_root(&root, "podman")?);
436
437 root.create_dir_all("usr/bin")?;
438 root.write("usr/bin/podman", "")?;
439 assert!(have_executable_in_root(&root, "podman")?);
440 assert!(!have_executable_in_root(&root, "missing")?);
441
442 const PROBE: &str = "/usr/local/bin/probe";
445 let cases = [
446 ("present", Some(true)),
447 ("absent", Some(false)),
448 ("escapes", None),
449 ("contained-symlink", Some(true)),
450 ];
451 for (label, expected) in cases {
452 let root = cap_std_ext::cap_tempfile::tempdir(cap_std::ambient_authority())?;
453 match label {
454 "present" => {
455 root.create_dir_all("usr/local/bin")?;
456 root.write("usr/local/bin/probe", "")?;
457 }
458 "absent" => {}
459 "escapes" => {
460 root.create_dir("usr")?;
461 root.symlink_contents("../../var/usrlocal", "usr/local")?;
462 }
463 "contained-symlink" => {
464 root.create_dir_all("var/usrlocal/bin")?;
465 root.write("var/usrlocal/bin/probe", "")?;
466 root.create_dir("usr")?;
467 root.symlink_contents("../var/usrlocal", "usr/local")?;
468 }
469 _ => unreachable!("{label}"),
470 }
471 let found = have_executable_in_root(&root, PROBE);
472 match expected {
473 Some(expected) => assert_eq!(found?, expected, "{label}"),
474 None => {
475 let err = found.expect_err(label);
476 assert!(
477 err.chain()
478 .any(|source| source.to_string().contains("usr/local/bin/probe")),
479 "{label}: {err:?}"
480 );
481 assert!(
482 err.chain().any(|source| {
483 source
484 .downcast_ref::<std::io::Error>()
485 .is_some_and(|io_err| {
486 io_err
487 .to_string()
488 .contains("a path led outside of the filesystem")
489 })
490 }),
491 "{label}: {err:?}"
492 );
493 }
494 }
495 }
496 Ok(())
497 }
498}