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