diff --git a/src/bios.rs b/src/bios.rs index 7dc7099c..e2a25312 100644 --- a/src/bios.rs +++ b/src/bios.rs @@ -4,6 +4,7 @@ use std::process::Command; use crate::component::*; use crate::model::*; +use crate::packagesystem; use anyhow::{bail, Result}; use crate::util; @@ -114,14 +115,7 @@ impl Component for Bios { } // Query the rpm database and list the package and build times for /usr/sbin/grub2-install - let mut rpmout = util::rpm_query(sysroot_path, &grub_install)?; - let rpmout = rpmout.output()?; - if !rpmout.status.success() { - std::io::stderr().write_all(&rpmout.stderr)?; - bail!("Failed to invoke rpm -qf"); - } - - let meta = util::parse_rpm_metadata(rpmout.stdout)?; + let meta = packagesystem::query_files(sysroot_path, [&grub_install])?; write_update_metadata(sysroot_path, self, &meta)?; Ok(meta) } diff --git a/src/efi.rs b/src/efi.rs index ddabd986..b6d04ba4 100644 --- a/src/efi.rs +++ b/src/efi.rs @@ -5,7 +5,6 @@ */ use std::cell::RefCell; -use std::io::prelude::*; use std::os::unix::io::AsRawFd; use std::path::{Path, PathBuf}; use std::process::Command; @@ -14,12 +13,11 @@ use anyhow::{bail, Context, Result}; use openat_ext::OpenatDirExt; use widestring::U16CString; -use crate::component::*; use crate::filetree; use crate::model::*; use crate::ostreeutil; -use crate::util; use crate::util::CommandRunExt; +use crate::{component::*, packagesystem}; /// Well-known paths to the ESP that may have been mounted external to us. pub(crate) const ESP_MOUNTS: &[&str] = &["boot", "boot/efi", "efi"]; @@ -341,17 +339,13 @@ impl Component for Efi { Command::new("mv").args([&efisrc, &dest_efidir]).run()?; } - // Query the rpm database and list the package and build times for all the - // files in the EFI system partition. If any files are not owned it is considered - // and error condition. - let mut rpmout = util::rpm_query(sysroot_path, &dest_efidir)?; - let rpmout = rpmout.output()?; - if !rpmout.status.success() { - std::io::stderr().write_all(&rpmout.stderr)?; - bail!("Failed to invoke rpm -qf"); - } + let efidir = openat::Dir::open(&dest_efidir)?; + let files = crate::util::filenames(&efidir)?.into_iter().map(|mut f| { + f.insert_str(0, "/boot/efi/EFI/"); + f + }); - let meta = util::parse_rpm_metadata(rpmout.stdout)?; + let meta = packagesystem::query_files(sysroot_path, files)?; write_update_metadata(sysroot_path, self, &meta)?; Ok(meta) } diff --git a/src/main.rs b/src/main.rs index 25706080..46d676a8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -28,6 +28,7 @@ mod ipc; mod model; mod model_legacy; mod ostreeutil; +mod packagesystem; mod sha512string; mod util; diff --git a/src/packagesystem.rs b/src/packagesystem.rs new file mode 100644 index 00000000..2536a93c --- /dev/null +++ b/src/packagesystem.rs @@ -0,0 +1,78 @@ +use std::collections::{BTreeMap, BTreeSet}; +use std::io::Write; +use std::path::Path; + +use anyhow::{bail, Context, Result}; +use chrono::prelude::*; + +use crate::model::*; +use crate::ostreeutil; + +/// Parse the output of `rpm -q` +fn rpm_parse_metadata(stdout: &[u8]) -> Result { + let pkgs = std::str::from_utf8(stdout)? + .split_whitespace() + .map(|s| -> Result<_> { + let parts: Vec<_> = s.splitn(2, ',').collect(); + let name = parts[0]; + if let Some(ts) = parts.get(1) { + let nt = DateTime::parse_from_str(ts, "%s") + .context("Failed to parse rpm buildtime")? + .with_timezone(&chrono::Utc); + Ok((name, nt)) + } else { + bail!("Failed to parse: {}", s); + } + }) + .collect::>>>()?; + if pkgs.is_empty() { + bail!("Failed to find any RPM packages matching files in source efidir"); + } + let timestamps: BTreeSet<&DateTime> = pkgs.values().collect(); + // Unwrap safety: We validated pkgs has at least one value above + let largest_timestamp = timestamps.iter().last().unwrap(); + let version = pkgs.keys().fold("".to_string(), |mut s, n| { + if !s.is_empty() { + s.push(','); + } + s.push_str(n); + s + }); + Ok(ContentMetadata { + timestamp: **largest_timestamp, + version, + }) +} + +/// Query the rpm database and list the package and build times. +pub(crate) fn query_files( + sysroot_path: &str, + paths: impl IntoIterator, +) -> Result +where + T: AsRef, +{ + let mut c = ostreeutil::rpm_cmd(sysroot_path); + c.args(["-q", "--queryformat", "%{nevra},%{buildtime} ", "-f"]); + for arg in paths { + c.arg(arg.as_ref()); + } + + let rpmout = c.output()?; + if !rpmout.status.success() { + std::io::stderr().write_all(&rpmout.stderr)?; + bail!("Failed to invoke rpm -qf"); + } + + rpm_parse_metadata(&rpmout.stdout) +} + +#[test] +fn test_parse_rpmout() { + let testdata = "grub2-efi-x64-1:2.06-95.fc38.x86_64,1681321788 grub2-efi-x64-1:2.06-95.fc38.x86_64,1681321788 shim-x64-15.6-2.x86_64,1657222566 shim-x64-15.6-2.x86_64,1657222566 shim-x64-15.6-2.x86_64,1657222566"; + let parsed = rpm_parse_metadata(testdata.as_bytes()).unwrap(); + assert_eq!( + parsed.version, + "grub2-efi-x64-1:2.06-95.fc38.x86_64,shim-x64-15.6-2.x86_64" + ); +} diff --git a/src/util.rs b/src/util.rs index 90eed324..c88d173e 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,15 +1,9 @@ use std::collections::HashSet; - -use anyhow::{bail, Context, Result}; -use openat_ext::OpenatDirExt; - use std::path::Path; use std::process::Command; -use crate::model::*; -use crate::ostreeutil; -use chrono::prelude::*; -use std::collections::{BTreeMap, BTreeSet}; +use anyhow::{bail, Context, Result}; +use openat_ext::OpenatDirExt; pub(crate) trait CommandRunExt { fn run(&mut self) -> Result<()>; @@ -89,66 +83,6 @@ pub(crate) fn ensure_writable_mount>(p: P) -> Result<()> { Ok(()) } -/// Parse the output of `rpm -q` -pub(crate) fn parse_rpm_metadata(stdout: Vec) -> Result { - let pkgs = std::str::from_utf8(&stdout)? - .split_whitespace() - .map(|s| -> Result<_> { - let parts: Vec<_> = s.splitn(2, ',').collect(); - let name = parts[0]; - if let Some(ts) = parts.get(1) { - let nt = DateTime::parse_from_str(ts, "%s") - .context("Failed to parse rpm buildtime")? - .with_timezone(&chrono::Utc); - Ok((name, nt)) - } else { - bail!("Failed to parse: {}", s); - } - }) - .collect::>>>()?; - if pkgs.is_empty() { - bail!("Failed to find any RPM packages matching files in source efidir"); - } - let timestamps: BTreeSet<&DateTime> = pkgs.values().collect(); - // Unwrap safety: We validated pkgs has at least one value above - let largest_timestamp = timestamps.iter().last().unwrap(); - let version = pkgs.keys().fold("".to_string(), |mut s, n| { - if !s.is_empty() { - s.push(','); - } - s.push_str(n); - s - }); - Ok(ContentMetadata { - timestamp: **largest_timestamp, - version, - }) -} - -/// Query the rpm database and list the package and build times, for all the -/// files in the EFI system partition, or for grub2-install file -pub(crate) fn rpm_query(sysroot_path: &str, path: &Path) -> Result { - let mut c = ostreeutil::rpm_cmd(sysroot_path); - c.args(["-q", "--queryformat", "%{nevra},%{buildtime} ", "-f"]); - - match path.file_name().expect("filename").to_str() { - Some("EFI") => { - let efidir = openat::Dir::open(path)?; - c.args(filenames(&efidir)?.drain().map(|mut f| { - f.insert_str(0, "/boot/efi/EFI/"); - f - })); - } - Some("grub2-install") => { - c.arg(path); - } - _ => { - bail!("Unsupported file/directory {:?}", path) - } - } - Ok(c) -} - /// Runs the provided Command object, captures its stdout, and swallows its stderr except on /// failure. Returns a Result describing whether the command failed, and if not, its /// standard output. Output is assumed to be UTF-8. Errors are adequately prefixed with the full