From dbd1eb7b83aa62e7e51fa17b7b12099d26547649 Mon Sep 17 00:00:00 2001 From: Daniel Alley Date: Wed, 22 May 2024 09:11:03 -0400 Subject: [PATCH 1/3] Users should be able to read files from the RPM payload closes #222 --- CHANGELOG.md | 1 + Cargo.toml | 2 +- src/rpm/compressor.rs | 36 +++++++++++++++++++++-- src/rpm/filecaps.rs | 8 +++++ src/rpm/package.rs | 68 ++++++++++++++++++++++++++++++++++++++++++- 5 files changed, 111 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ba8202..2321663 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Added support for ecdsa signatures +- Added `Package::files()` for iterating over the files of an RPM package (metadata & contents). ## 0.16.0 diff --git a/Cargo.toml b/Cargo.toml index e81a559..535758a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -53,7 +53,7 @@ itertools = "0.13" hex = { version = "0.4", features = ["std"] } zstd = { version = "0.13", optional = true } xz2 = { version = "0.1", optional = true } -bzip2 = { version = "0.4.4", optional = true } +bzip2 = { version = "0.5.0", optional = true } [dev-dependencies] env_logger = "0.11" diff --git a/src/rpm/compressor.rs b/src/rpm/compressor.rs index 5d8e1fc..befebbe 100644 --- a/src/rpm/compressor.rs +++ b/src/rpm/compressor.rs @@ -1,4 +1,4 @@ -use std::io::Write; +use std::io; use crate::errors::*; @@ -13,6 +13,18 @@ pub enum CompressionType { Bzip2, } +impl std::fmt::Display for CompressionType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::None => write!(f, "none"), + Self::Gzip => write!(f, "gzip"), + Self::Zstd => write!(f, "zstd"), + Self::Xz => write!(f, "xz"), + Self::Bzip2 => write!(f, "bzip2"), + } + } +} + impl std::str::FromStr for CompressionType { type Err = Error; fn from_str(raw: &str) -> Result { @@ -80,7 +92,7 @@ impl TryFrom for Compressor { } } -impl Write for Compressor { +impl io::Write for Compressor { fn write(&mut self, content: &[u8]) -> Result { match self { Compressor::None(data) => data.write(content), @@ -192,3 +204,23 @@ impl std::fmt::Display for CompressionWithLevel { } } } + +pub(crate) fn decompress_stream( + value: CompressionType, + reader: impl io::BufRead + 'static, +) -> Result, Error> { + match value { + CompressionType::None => Ok(Box::new(reader)), + #[cfg(feature = "gzip-compression")] + CompressionType::Gzip => Ok(Box::new(flate2::bufread::GzDecoder::new(reader))), + #[cfg(feature = "zstd-compression")] + CompressionType::Zstd => Ok(Box::new(zstd::stream::Decoder::new(reader)?)), + #[cfg(feature = "xz-compression")] + CompressionType::Xz => Ok(Box::new(xz2::bufread::XzDecoder::new(reader))), + #[cfg(feature = "bzip2-compression")] + CompressionType::Bzip2 => Ok(Box::new(bzip2::read::BzipDecoder::new(reader))), + // This is an issue when building with all compression types enabled + #[allow(unreachable_patterns)] + _ => Err(Error::UnsupportedCompressorType(value.to_string())), + } +} diff --git a/src/rpm/filecaps.rs b/src/rpm/filecaps.rs index cb493bd..c65d81c 100644 --- a/src/rpm/filecaps.rs +++ b/src/rpm/filecaps.rs @@ -50,6 +50,14 @@ const CAPS: &[&str; 41] = &[ #[derive(Debug, Clone)] pub struct FileCaps(String); +impl FileCaps { + pub fn new(input: String) -> Result { + validate_caps_text(&input)?; + + Ok(Self(input)) + } +} + impl Display for FileCaps { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.0) diff --git a/src/rpm/package.rs b/src/rpm/package.rs index 78424e7..dc0368c 100644 --- a/src/rpm/package.rs +++ b/src/rpm/package.rs @@ -1,5 +1,6 @@ use std::{ fs, io, + io::Read, path::{Path, PathBuf}, str::FromStr, }; @@ -7,7 +8,7 @@ use std::{ use digest::Digest; use num_traits::FromPrimitive; -use crate::{constants::*, errors::*, CompressionType}; +use crate::{constants::*, decompress_stream, errors::*, CompressionType}; #[cfg(feature = "signature-pgp")] use crate::signature::pgp::Verifier; @@ -61,6 +62,21 @@ impl Package { self.write(&mut io::BufWriter::new(fs::File::create(path)?)) } + /// Iterate over the file contents of the package payload + pub fn files(&self) -> Result { + let file_entries = self.metadata.get_file_entries()?; + let archive = decompress_stream( + self.metadata.get_payload_compressor()?, + io::Cursor::new(self.content.clone()), + )?; + + Ok(FileIterator { + file_entries, + archive, + count: 0, + }) + } + /// Create package signatures using an external key and add them to the signature header #[cfg(feature = "signature-meta")] pub fn sign(&mut self, signer: S) -> Result<(), Error> @@ -1019,3 +1035,53 @@ impl PackageMetadata { } } } + +pub struct FileIterator<'a> { + file_entries: Vec, + archive: Box, + count: usize, +} + +#[derive(Debug)] +pub struct RpmFile { + pub file_entry: FileEntry, + pub content: Vec, +} + +impl Iterator for FileIterator<'_> { + type Item = Result; + + fn next(&mut self) -> Option { + if self.count >= self.file_entries.len() { + return None; + } + + let file_entry = self.file_entries[self.count].clone(); + self.count += 1; + + let reader = cpio::NewcReader::new(&mut self.archive); + + match reader { + Ok(mut entry_reader) => { + if entry_reader.entry().is_trailer() { + return None; + } + + let mut content = Vec::new(); + + if let Err(e) = entry_reader.read_to_end(&mut content) { + return Some(Err(Error::Io(e))); + } + if let Err(e) = entry_reader.finish() { + return Some(Err(Error::Io(e))); + } + + Some(Ok(RpmFile { + file_entry, + content, + })) + } + Err(e) => Some(Err(Error::Io(e))), + } + } +} From 9d8418e495c6f54254dae409a99c68e82067952f Mon Sep 17 00:00:00 2001 From: Daniel Alley Date: Fri, 17 Jan 2025 17:01:17 -0500 Subject: [PATCH 2/3] Add a function for extracting the contents of an RPM to disk --- CHANGELOG.md | 1 + src/rpm/package.rs | 62 +++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 59 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2321663..9b216c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added support for ecdsa signatures - Added `Package::files()` for iterating over the files of an RPM package (metadata & contents). +- Added `Package::extract()` for extracting the archive contents of an RPM package to a directory on disk ## 0.16.0 diff --git a/src/rpm/package.rs b/src/rpm/package.rs index dc0368c..e361c54 100644 --- a/src/rpm/package.rs +++ b/src/rpm/package.rs @@ -1,6 +1,7 @@ use std::{ - fs, io, - io::Read, + fs, + io::{self, Read, Write}, + os::unix::fs::PermissionsExt, path::{Path, PathBuf}, str::FromStr, }; @@ -77,6 +78,59 @@ impl Package { }) } + /// Extract all contents of the package payload to a given directory + pub fn extract(&self, dest: impl AsRef) -> Result<(), Error> { + fs::create_dir_all(&dest)?; + + let dirs = self + .metadata + .header + .get_entry_data_as_string_array(IndexTag::RPMTAG_DIRNAMES)?; + + // pull every base directory name in the package and create the directory in advancec + for dir in dirs { + let dir_path = dest + .as_ref() + .join(Path::new(dir).strip_prefix("/").unwrap_or(dest.as_ref())); + fs::create_dir_all(&dir_path)?; + } + + // TODO: reduce memory by replacing this with an impl that writes the files immediately after reading them from the archive + // instead of reading each file entirely into memory (while the archive is also entirely in memory) before writing them + for file in self.files()? { + let file = file?; + let file_path = dest.as_ref().join( + file.metadata + .path + .strip_prefix("/") + .unwrap_or(dest.as_ref()), + ); + + let perms = fs::Permissions::from_mode(file.metadata.mode.permissions().into()); + match file.metadata.mode { + FileMode::Dir { .. } => { + fs::create_dir_all(&file_path)?; + fs::set_permissions(&file_path, perms)?; + } + FileMode::Regular { .. } => { + let mut f = fs::File::create(&file_path)?; + f.write_all(&file.content)?; + fs::set_permissions(&file_path, perms)?; + } + FileMode::SymbolicLink { .. } => { + // broken symlinks (common for debuginfo handling) are perceived as not existing by "exists()" + if file_path.exists() || file_path.symlink_metadata().is_ok() { + fs::remove_file(&file_path)?; + } + std::os::unix::fs::symlink(&file.metadata.linkto, &file_path)?; + } + _ => unreachable!("Encountered an unknown or invalid FileMode"), + } + } + + Ok(()) + } + /// Create package signatures using an external key and add them to the signature header #[cfg(feature = "signature-meta")] pub fn sign(&mut self, signer: S) -> Result<(), Error> @@ -1044,7 +1098,7 @@ pub struct FileIterator<'a> { #[derive(Debug)] pub struct RpmFile { - pub file_entry: FileEntry, + pub metadata: FileEntry, pub content: Vec, } @@ -1077,7 +1131,7 @@ impl Iterator for FileIterator<'_> { } Some(Ok(RpmFile { - file_entry, + metadata: file_entry, content, })) } From 1da435c9dd338880fec287db214ba734b40d54a8 Mon Sep 17 00:00:00 2001 From: Daniel Alley Date: Thu, 16 Jan 2025 22:44:05 -0500 Subject: [PATCH 3/3] Apply cargo clippy fixes --- src/rpm/builder.rs | 1 - src/rpm/compressor.rs | 2 +- src/rpm/package.rs | 60 +++++++++++++++++++++++++++++++++++++++---- src/version.rs | 14 +++++----- 4 files changed, 63 insertions(+), 14 deletions(-) diff --git a/src/rpm/builder.rs b/src/rpm/builder.rs index 915a50c..d007a55 100644 --- a/src/rpm/builder.rs +++ b/src/rpm/builder.rs @@ -286,7 +286,6 @@ impl PackageBuilder { /// /// The a changelog entry consists of an entry name (which includes author, email followed by /// a dash followed by a version number), description, and the date and time of the change. - /// ``` /// # #[cfg(feature = "chrono")] /// # || -> Result<(), Box> { diff --git a/src/rpm/compressor.rs b/src/rpm/compressor.rs index befebbe..aa546d4 100644 --- a/src/rpm/compressor.rs +++ b/src/rpm/compressor.rs @@ -218,7 +218,7 @@ pub(crate) fn decompress_stream( #[cfg(feature = "xz-compression")] CompressionType::Xz => Ok(Box::new(xz2::bufread::XzDecoder::new(reader))), #[cfg(feature = "bzip2-compression")] - CompressionType::Bzip2 => Ok(Box::new(bzip2::read::BzipDecoder::new(reader))), + CompressionType::Bzip2 => Ok(Box::new(bzip2::bufread::BzDecoder::new(reader))), // This is an issue when building with all compression types enabled #[allow(unreachable_patterns)] _ => Err(Error::UnsupportedCompressorType(value.to_string())), diff --git a/src/rpm/package.rs b/src/rpm/package.rs index e361c54..6c61895 100644 --- a/src/rpm/package.rs +++ b/src/rpm/package.rs @@ -64,6 +64,19 @@ impl Package { } /// Iterate over the file contents of the package payload + /// + /// # Examples + /// + /// ```ignore + /// # fn main() -> Result<(), Box> { + /// let package = rpm::Package::open("test_assets/freesrp-udev-0.3.0-1.25.x86_64.rpm")?; + /// for entry in package.files()? { + /// let file = entry?; + /// // do something with file.content + /// println!("{} is {} bytes", file.metadata.path.display(), file.content.len()); + /// } + /// # Ok(()) } + /// ``` pub fn files(&self) -> Result { let file_entries = self.metadata.get_file_entries()?; let archive = decompress_stream( @@ -78,16 +91,31 @@ impl Package { }) } - /// Extract all contents of the package payload to a given directory + /// Extract all contents of the package payload to a given directory. + /// + /// # Implementation + /// + /// The if the directory is nested, its parent directories must already exist. If the + /// directory itself already exists, the operation will fail. All extracted files will be + /// dropped relative to the provided directory (it will not install any files). + /// + /// # Examples + /// + /// ```ignore + /// # fn main() -> Result<(), Box> { + /// let package = rpm::Package::open("test_assets/ima_signed.rpm")?; + /// package.extract(&package.metadata.get_name()?)?; + /// # Ok(()) } + /// ``` pub fn extract(&self, dest: impl AsRef) -> Result<(), Error> { - fs::create_dir_all(&dest)?; + fs::create_dir(&dest)?; let dirs = self .metadata .header .get_entry_data_as_string_array(IndexTag::RPMTAG_DIRNAMES)?; - // pull every base directory name in the package and create the directory in advancec + // pull every base directory name in the package and create the directory in advance for dir in dirs { let dir_path = dest .as_ref() @@ -147,6 +175,7 @@ impl Package { /// prefer using the [`sign`][Package::sign] method instead. /// /// # Examples + /// /// ``` /// # fn main() -> Result<(), Box> { /// let mut package = rpm::Package::open("test_assets/ima_signed.rpm")?; @@ -270,7 +299,6 @@ impl Package { "signature_header(header and content)", signature_header_and_content, ); - use io::Read; let header_and_content_cursor = io::Cursor::new(&header_bytes).chain(io::Cursor::new(&self.content)); verifier.verify(header_and_content_cursor, signature_header_and_content)?; @@ -813,6 +841,17 @@ impl PackageMetadata { } /// Extract a the set of contained file names. + /// + /// # Examples + /// + /// ``` + /// # fn main() -> Result<(), Box> { + /// let package = rpm::Package::open("test_assets/ima_signed.rpm")?; + /// for path in package.metadata.get_file_paths()? { + /// println!("{}", path.display()); + /// } + /// # Ok(()) } + /// ``` pub fn get_file_paths(&self) -> Result, Error> { // reconstruct the messy de-constructed paths let basenames = self @@ -880,7 +919,18 @@ impl PackageMetadata { }) } - /// Extract a the set of contained file names including the additional metadata. + /// Get a list of metadata about the files in the RPM, without the file contents. + /// + /// # Examples + /// + /// ``` + /// # fn main() -> Result<(), Box> { + /// let package = rpm::Package::open("test_assets/ima_signed.rpm")?; + /// for entry in package.metadata.get_file_entries()? { + /// println!("{} is {} bytes", entry.path.display(), entry.size); + /// } + /// # Ok(()) } + /// ``` pub fn get_file_entries(&self) -> Result, Error> { // rpm does not encode it, if it is the default md5 let algorithm = self diff --git a/src/version.rs b/src/version.rs index ffd762f..cd5ae1c 100644 --- a/src/version.rs +++ b/src/version.rs @@ -127,19 +127,19 @@ impl<'a> Nevra<'a> { } } -impl<'a> fmt::Display for Nevra<'a> { +impl fmt::Display for Nevra<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}-{}.{}", self.name, self.evr, self.arch) } } -impl<'a> PartialOrd for Nevra<'a> { +impl PartialOrd for Nevra<'_> { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } -impl<'a> Ord for Nevra<'a> { +impl Ord for Nevra<'_> { fn cmp(&self, other: &Self) -> Ordering { let name_cmp = compare_version_string(&self.name, &other.name); if name_cmp != Ordering::Equal { @@ -248,7 +248,7 @@ impl<'a> From<(&'a str, &'a str, &'a str)> for Evr<'a> { } } -impl<'a> PartialEq for Evr<'a> { +impl PartialEq for Evr<'_> { fn eq(&self, other: &Self) -> bool { ((self.epoch == other.epoch) || (self.epoch == "" && other.epoch == "0") @@ -258,7 +258,7 @@ impl<'a> PartialEq for Evr<'a> { } } -impl<'a> fmt::Display for Evr<'a> { +impl fmt::Display for Evr<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { if !self.epoch.is_empty() { write!(f, "{}:", self.epoch)?; @@ -268,13 +268,13 @@ impl<'a> fmt::Display for Evr<'a> { } } -impl<'a> PartialOrd for Evr<'a> { +impl PartialOrd for Evr<'_> { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } -impl<'a> Ord for Evr<'a> { +impl Ord for Evr<'_> { fn cmp(&self, other: &Self) -> Ordering { let epoch_1 = if self.epoch.is_empty() { "0"