Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add an API for reading files from the RPM payload #251

Merged
merged 3 commits into from
Jan 21, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ 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).
- Added `Package::extract()` for extracting the archive contents of an RPM package to a directory on disk

## 0.16.0

Expand Down
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
1 change: 0 additions & 1 deletion src/rpm/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<dyn std::error::Error>> {
Expand Down
36 changes: 34 additions & 2 deletions src/rpm/compressor.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use std::io::Write;
use std::io;

use crate::errors::*;

Expand All @@ -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<Self, Self::Err> {
Expand Down Expand Up @@ -80,7 +92,7 @@ impl TryFrom<CompressionWithLevel> for Compressor {
}
}

impl Write for Compressor {
impl io::Write for Compressor {
fn write(&mut self, content: &[u8]) -> Result<usize, std::io::Error> {
match self {
Compressor::None(data) => data.write(content),
Expand Down Expand Up @@ -192,3 +204,23 @@ impl std::fmt::Display for CompressionWithLevel {
}
}
}

pub(crate) fn decompress_stream(
value: CompressionType,
reader: impl io::BufRead + 'static,
) -> Result<Box<dyn io::Read>, 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::bufread::BzDecoder::new(reader))),
// This is an issue when building with all compression types enabled
#[allow(unreachable_patterns)]
_ => Err(Error::UnsupportedCompressorType(value.to_string())),
}
}
8 changes: 8 additions & 0 deletions src/rpm/filecaps.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,14 @@ const CAPS: &[&str; 41] = &[
#[derive(Debug, Clone)]
pub struct FileCaps(String);

impl FileCaps {
pub fn new(input: String) -> Result<Self, Error> {
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)
Expand Down
178 changes: 174 additions & 4 deletions src/rpm/package.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
use std::{
fs, io,
fs,
io::{self, Read, Write},
os::unix::fs::PermissionsExt,
path::{Path, PathBuf},
str::FromStr,
};

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;
Expand Down Expand Up @@ -61,6 +63,102 @@ impl Package {
self.write(&mut io::BufWriter::new(fs::File::create(path)?))
}

/// Iterate over the file contents of the package payload
///
/// # Examples
///
/// ```ignore
dralley marked this conversation as resolved.
Show resolved Hide resolved
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// 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<FileIterator, Error> {
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,
})
}

/// 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).
///
dralley marked this conversation as resolved.
Show resolved Hide resolved
dralley marked this conversation as resolved.
Show resolved Hide resolved
/// # Examples
///
/// ```ignore
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let package = rpm::Package::open("test_assets/ima_signed.rpm")?;
/// package.extract(&package.metadata.get_name()?)?;
/// # Ok(()) }
/// ```
pub fn extract(&self, dest: impl AsRef<Path>) -> Result<(), Error> {
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 advance
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<S>(&mut self, signer: S) -> Result<(), Error>
Expand All @@ -77,6 +175,7 @@ impl Package {
/// prefer using the [`sign`][Package::sign] method instead.
///
/// # Examples
///
/// ```
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let mut package = rpm::Package::open("test_assets/ima_signed.rpm")?;
Expand Down Expand Up @@ -200,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)?;
Expand Down Expand Up @@ -743,6 +841,17 @@ impl PackageMetadata {
}

/// Extract a the set of contained file names.
///
/// # Examples
///
/// ```
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// 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<Vec<PathBuf>, Error> {
// reconstruct the messy de-constructed paths
let basenames = self
Expand Down Expand Up @@ -810,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<dyn std::error::Error>> {
/// 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<Vec<FileEntry>, Error> {
// rpm does not encode it, if it is the default md5
let algorithm = self
Expand Down Expand Up @@ -1019,3 +1139,53 @@ impl PackageMetadata {
}
}
}

pub struct FileIterator<'a> {
file_entries: Vec<FileEntry>,
archive: Box<dyn io::Read + 'a>,
count: usize,
}

#[derive(Debug)]
pub struct RpmFile {
pub metadata: FileEntry,
pub content: Vec<u8>,
drahnr marked this conversation as resolved.
Show resolved Hide resolved
}

impl Iterator for FileIterator<'_> {
type Item = Result<RpmFile, Error>;

fn next(&mut self) -> Option<Self::Item> {
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 {
metadata: file_entry,
content,
}))
}
Err(e) => Some(Err(Error::Io(e))),
}
}
}
14 changes: 7 additions & 7 deletions src/version.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Ordering> {
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 {
Expand Down Expand Up @@ -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")
Expand All @@ -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)?;
Expand All @@ -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<Ordering> {
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"
Expand Down
Loading