From 512c177b0ef12e41b1bbea8758e714c89af4a55d Mon Sep 17 00:00:00 2001 From: Colin Walters Date: Fri, 29 Sep 2023 16:14:42 -0400 Subject: [PATCH 1/5] Refactor rpm bits into packagesystem module Prep for cleaning this up and making it more generic to support non-rpm systems in a pluggable way. --- src/bios.rs | 5 ++-- src/efi.rs | 7 ++--- src/main.rs | 1 + src/packagesystem.rs | 69 +++++++++++++++++++++++++++++++++++++++++++ src/util.rs | 70 ++------------------------------------------ 5 files changed, 78 insertions(+), 74 deletions(-) create mode 100644 src/packagesystem.rs diff --git a/src/bios.rs b/src/bios.rs index 7dc7099c..de7533d5 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,14 @@ 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 mut rpmout = packagesystem::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::parse_metadata(rpmout.stdout)?; write_update_metadata(sysroot_path, self, &meta)?; Ok(meta) } diff --git a/src/efi.rs b/src/efi.rs index ddabd986..7021f3ef 100644 --- a/src/efi.rs +++ b/src/efi.rs @@ -14,12 +14,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"]; @@ -344,14 +343,14 @@ impl Component for Efi { // 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 mut rpmout = packagesystem::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 meta = util::parse_rpm_metadata(rpmout.stdout)?; + let meta = packagesystem::parse_metadata(rpmout.stdout)?; 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..46648cca --- /dev/null +++ b/src/packagesystem.rs @@ -0,0 +1,69 @@ +use std::collections::{BTreeMap, BTreeSet}; +use std::path::Path; +use std::process::Command; + +use anyhow::{bail, Context, Result}; +use chrono::prelude::*; + +use crate::model::*; +use crate::ostreeutil; + +/// Parse the output of `rpm -q` +pub(crate) fn parse_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 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(crate::util::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) +} 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 From a37b1e64b6e68db21eb9455868dfd6562c98ed5b Mon Sep 17 00:00:00 2001 From: Colin Walters Date: Fri, 29 Sep 2023 16:17:49 -0400 Subject: [PATCH 2/5] De-duplicate package querying There's no reason to have two separate functions here. --- src/bios.rs | 9 +-------- src/efi.rs | 13 +------------ src/packagesystem.rs | 15 +++++++++++---- 3 files changed, 13 insertions(+), 24 deletions(-) diff --git a/src/bios.rs b/src/bios.rs index de7533d5..ba6ac7b1 100644 --- a/src/bios.rs +++ b/src/bios.rs @@ -115,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 = packagesystem::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 = packagesystem::parse_metadata(rpmout.stdout)?; + let meta = packagesystem::query(sysroot_path, &grub_install)?; write_update_metadata(sysroot_path, self, &meta)?; Ok(meta) } diff --git a/src/efi.rs b/src/efi.rs index 7021f3ef..5fdba396 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; @@ -340,17 +339,7 @@ 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 = packagesystem::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 meta = packagesystem::parse_metadata(rpmout.stdout)?; + let meta = packagesystem::query(sysroot_path, &dest_efidir)?; write_update_metadata(sysroot_path, self, &meta)?; Ok(meta) } diff --git a/src/packagesystem.rs b/src/packagesystem.rs index 46648cca..d64add1d 100644 --- a/src/packagesystem.rs +++ b/src/packagesystem.rs @@ -1,6 +1,6 @@ use std::collections::{BTreeMap, BTreeSet}; +use std::io::Write; use std::path::Path; -use std::process::Command; use anyhow::{bail, Context, Result}; use chrono::prelude::*; @@ -9,7 +9,7 @@ use crate::model::*; use crate::ostreeutil; /// Parse the output of `rpm -q` -pub(crate) fn parse_metadata(stdout: Vec) -> Result { +fn rpm_parse_metadata(stdout: Vec) -> Result { let pkgs = std::str::from_utf8(&stdout)? .split_whitespace() .map(|s| -> Result<_> { @@ -46,7 +46,7 @@ pub(crate) fn parse_metadata(stdout: Vec) -> Result { /// 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 query(sysroot_path: &str, path: &Path) -> Result { +pub(crate) fn query(sysroot_path: &str, path: &Path) -> Result { let mut c = ostreeutil::rpm_cmd(sysroot_path); c.args(["-q", "--queryformat", "%{nevra},%{buildtime} ", "-f"]); @@ -65,5 +65,12 @@ pub(crate) fn query(sysroot_path: &str, path: &Path) -> Result { bail!("Unsupported file/directory {:?}", path) } } - Ok(c) + + 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) } From 981aa0ab814c8298bcdead6280015a368c077480 Mon Sep 17 00:00:00 2001 From: Colin Walters Date: Fri, 29 Sep 2023 16:33:06 -0400 Subject: [PATCH 3/5] packagesystem: Add a unit test On general principle. --- src/packagesystem.rs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/packagesystem.rs b/src/packagesystem.rs index d64add1d..4ddde0ed 100644 --- a/src/packagesystem.rs +++ b/src/packagesystem.rs @@ -9,7 +9,7 @@ use crate::model::*; use crate::ostreeutil; /// Parse the output of `rpm -q` -fn rpm_parse_metadata(stdout: Vec) -> Result { +fn rpm_parse_metadata(stdout: &[u8]) -> Result { let pkgs = std::str::from_utf8(&stdout)? .split_whitespace() .map(|s| -> Result<_> { @@ -72,5 +72,15 @@ pub(crate) fn query(sysroot_path: &str, path: &Path) -> Result bail!("Failed to invoke rpm -qf"); } - rpm_parse_metadata(rpmout.stdout) + 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" + ); } From 14cdf1bb4dddb90105c19bc8f9c65dfec9b67d57 Mon Sep 17 00:00:00 2001 From: Colin Walters Date: Fri, 29 Sep 2023 17:24:11 -0400 Subject: [PATCH 4/5] Fix clippy lint --- src/packagesystem.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/packagesystem.rs b/src/packagesystem.rs index 4ddde0ed..15ffdff5 100644 --- a/src/packagesystem.rs +++ b/src/packagesystem.rs @@ -10,7 +10,7 @@ use crate::ostreeutil; /// Parse the output of `rpm -q` fn rpm_parse_metadata(stdout: &[u8]) -> Result { - let pkgs = std::str::from_utf8(&stdout)? + let pkgs = std::str::from_utf8(stdout)? .split_whitespace() .map(|s| -> Result<_> { let parts: Vec<_> = s.splitn(2, ',').collect(); From 5b5d0e6c95bcf2aad06efc32761ad50056ab860a Mon Sep 17 00:00:00 2001 From: Colin Walters Date: Sat, 30 Sep 2023 10:26:43 -0400 Subject: [PATCH 5/5] packagesystem: More API cleanup It doesn't make sense to hardcode the file list inside the query function; just have the caller pass it the files it needs. Now the packagesystem module is independent of the content. --- src/bios.rs | 2 +- src/efi.rs | 8 +++++++- src/packagesystem.rs | 28 ++++++++++------------------ 3 files changed, 18 insertions(+), 20 deletions(-) diff --git a/src/bios.rs b/src/bios.rs index ba6ac7b1..e2a25312 100644 --- a/src/bios.rs +++ b/src/bios.rs @@ -115,7 +115,7 @@ impl Component for Bios { } // Query the rpm database and list the package and build times for /usr/sbin/grub2-install - let meta = packagesystem::query(sysroot_path, &grub_install)?; + 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 5fdba396..b6d04ba4 100644 --- a/src/efi.rs +++ b/src/efi.rs @@ -339,7 +339,13 @@ impl Component for Efi { Command::new("mv").args([&efisrc, &dest_efidir]).run()?; } - let meta = packagesystem::query(sysroot_path, &dest_efidir)?; + 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 = packagesystem::query_files(sysroot_path, files)?; write_update_metadata(sysroot_path, self, &meta)?; Ok(meta) } diff --git a/src/packagesystem.rs b/src/packagesystem.rs index 15ffdff5..2536a93c 100644 --- a/src/packagesystem.rs +++ b/src/packagesystem.rs @@ -44,26 +44,18 @@ fn rpm_parse_metadata(stdout: &[u8]) -> Result { }) } -/// 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 query(sysroot_path: &str, path: &Path) -> Result { +/// 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"]); - - match path.file_name().expect("filename").to_str() { - Some("EFI") => { - let efidir = openat::Dir::open(path)?; - c.args(crate::util::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) - } + for arg in paths { + c.arg(arg.as_ref()); } let rpmout = c.output()?;