use std::fmt;
use std::io;
use std::num::ParseIntError;
use std::str::FromStr;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum MetadataError {
#[error("Missing or empty {0}")]
MissingRequired(&'static str),
#[error("Invalid value in {field}: {source}")]
InvalidValue {
field: &'static str,
#[source]
source: ParseIntError,
},
#[error("Unknown metadata entry: {0}")]
UnknownEntry(String),
#[error("I/O error: {0}")]
Io(#[from] io::Error),
}
pub trait FileRead {
fn pkgname(&self) -> &str;
fn comment(&self) -> io::Result<String>;
fn contents(&self) -> io::Result<String>;
fn desc(&self) -> io::Result<String>;
fn build_info(&self) -> io::Result<Option<String>>;
fn build_version(&self) -> io::Result<Option<String>>;
fn deinstall(&self) -> io::Result<Option<String>>;
fn display(&self) -> io::Result<Option<String>>;
fn install(&self) -> io::Result<Option<String>>;
fn installed_info(&self) -> io::Result<Option<String>>;
fn mtree_dirs(&self) -> io::Result<Option<String>>;
fn preserve(&self) -> io::Result<Option<String>>;
fn required_by(&self) -> io::Result<Option<String>>;
fn size_all(&self) -> io::Result<Option<String>>;
fn size_pkg(&self) -> io::Result<Option<String>>;
}
#[derive(Clone, Debug, Default, Eq, Hash, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Metadata {
build_info: Option<Vec<String>>,
build_version: Option<Vec<String>>,
comment: String,
contents: String,
deinstall: Option<String>,
desc: String,
display: Option<String>,
install: Option<String>,
installed_info: Option<Vec<String>>,
mtree_dirs: Option<Vec<String>>,
preserve: Option<Vec<String>>,
required_by: Option<Vec<String>>,
size_all: Option<u64>,
size_pkg: Option<u64>,
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum Entry {
BuildInfo,
BuildVersion,
Comment,
Contents,
DeInstall,
Desc,
Display,
Install,
InstalledInfo,
MtreeDirs,
Preserve,
RequiredBy,
SizeAll,
SizePkg,
}
impl Metadata {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn build_info(&self) -> Option<&[String]> {
self.build_info.as_deref()
}
#[must_use]
pub fn build_version(&self) -> Option<&[String]> {
self.build_version.as_deref()
}
#[must_use]
pub fn comment(&self) -> &str {
&self.comment
}
#[must_use]
pub fn contents(&self) -> &str {
&self.contents
}
#[must_use]
pub fn deinstall(&self) -> Option<&str> {
self.deinstall.as_deref()
}
#[must_use]
pub fn desc(&self) -> &str {
&self.desc
}
#[must_use]
pub fn display(&self) -> Option<&str> {
self.display.as_deref()
}
#[must_use]
pub fn install(&self) -> Option<&str> {
self.install.as_deref()
}
#[must_use]
pub fn installed_info(&self) -> Option<&[String]> {
self.installed_info.as_deref()
}
#[must_use]
pub fn mtree_dirs(&self) -> Option<&[String]> {
self.mtree_dirs.as_deref()
}
#[must_use]
pub fn preserve(&self) -> Option<&[String]> {
self.preserve.as_deref()
}
#[must_use]
pub fn required_by(&self) -> Option<&[String]> {
self.required_by.as_deref()
}
#[must_use]
pub fn size_all(&self) -> Option<u64> {
self.size_all
}
#[must_use]
pub fn size_pkg(&self) -> Option<u64> {
self.size_pkg
}
pub fn read_metadata(
&mut self,
entry: Entry,
value: &str,
) -> Result<(), MetadataError> {
let make_string = || value.trim().to_string();
let make_vec = || {
value
.trim()
.lines()
.map(|s| s.to_string())
.collect::<Vec<_>>()
};
match entry {
Entry::BuildInfo => self.build_info = Some(make_vec()),
Entry::BuildVersion => self.build_version = Some(make_vec()),
Entry::Comment => self.comment.push_str(value.trim()),
Entry::Contents => self.contents.push_str(value.trim()),
Entry::DeInstall => self.deinstall = Some(make_string()),
Entry::Desc => {
self.desc.push_str(value.trim_end_matches('\n'));
}
Entry::Display => self.display = Some(make_string()),
Entry::Install => self.install = Some(make_string()),
Entry::InstalledInfo => self.installed_info = Some(make_vec()),
Entry::MtreeDirs => self.mtree_dirs = Some(make_vec()),
Entry::Preserve => self.preserve = Some(make_vec()),
Entry::RequiredBy => self.required_by = Some(make_vec()),
Entry::SizeAll => {
self.size_all =
Some(value.trim().parse::<u64>().map_err(|e| {
MetadataError::InvalidValue {
field: "+SIZE_ALL",
source: e,
}
})?);
}
Entry::SizePkg => {
self.size_pkg =
Some(value.trim().parse::<u64>().map_err(|e| {
MetadataError::InvalidValue {
field: "+SIZE_PKG",
source: e,
}
})?);
}
}
Ok(())
}
pub fn validate(&self) -> Result<(), MetadataError> {
if self.comment.is_empty() {
return Err(MetadataError::MissingRequired("+COMMENT"));
}
if self.contents.is_empty() {
return Err(MetadataError::MissingRequired("+CONTENTS"));
}
if self.desc.is_empty() {
return Err(MetadataError::MissingRequired("+DESC"));
}
Ok(())
}
#[must_use]
pub fn is_valid(&self) -> bool {
!self.comment.is_empty()
&& !self.contents.is_empty()
&& !self.desc.is_empty()
}
}
impl fmt::Display for Entry {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.to_filename())
}
}
impl FromStr for Entry {
type Err = MetadataError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::from_filename(s)
.ok_or_else(|| MetadataError::UnknownEntry(s.to_string()))
}
}
impl Entry {
#[must_use]
pub const fn to_filename(&self) -> &'static str {
match self {
Entry::BuildInfo => "+BUILD_INFO",
Entry::BuildVersion => "+BUILD_VERSION",
Entry::Comment => "+COMMENT",
Entry::Contents => "+CONTENTS",
Entry::DeInstall => "+DEINSTALL",
Entry::Desc => "+DESC",
Entry::Display => "+DISPLAY",
Entry::Install => "+INSTALL",
Entry::InstalledInfo => "+INSTALLED_INFO",
Entry::MtreeDirs => "+MTREE_DIRS",
Entry::Preserve => "+PRESERVE",
Entry::RequiredBy => "+REQUIRED_BY",
Entry::SizeAll => "+SIZE_ALL",
Entry::SizePkg => "+SIZE_PKG",
}
}
#[must_use]
pub fn from_filename(file: &str) -> Option<Entry> {
match file {
"+BUILD_INFO" => Some(Entry::BuildInfo),
"+BUILD_VERSION" => Some(Entry::BuildVersion),
"+COMMENT" => Some(Entry::Comment),
"+CONTENTS" => Some(Entry::Contents),
"+DEINSTALL" => Some(Entry::DeInstall),
"+DESC" => Some(Entry::Desc),
"+DISPLAY" => Some(Entry::Display),
"+INSTALL" => Some(Entry::Install),
"+INSTALLED_INFO" => Some(Entry::InstalledInfo),
"+MTREE_DIRS" => Some(Entry::MtreeDirs),
"+PRESERVE" => Some(Entry::Preserve),
"+REQUIRED_BY" => Some(Entry::RequiredBy),
"+SIZE_ALL" => Some(Entry::SizeAll),
"+SIZE_PKG" => Some(Entry::SizePkg),
_ => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
type Result<T> = std::result::Result<T, MetadataError>;
#[test]
fn test_metadata_new() {
let m = Metadata::new();
assert!(m.comment().is_empty());
assert!(m.contents().is_empty());
assert!(m.desc().is_empty());
assert!(m.build_info().is_none());
assert!(m.size_all().is_none());
}
#[test]
fn test_read_metadata_comment() -> Result<()> {
let mut m = Metadata::new();
m.read_metadata(Entry::Comment, " Test comment ")?;
assert_eq!(m.comment(), "Test comment");
Ok(())
}
#[test]
fn test_read_metadata_contents() -> Result<()> {
let mut m = Metadata::new();
m.read_metadata(Entry::Contents, "bin/foo\nbin/bar\n")?;
assert_eq!(m.contents(), "bin/foo\nbin/bar");
Ok(())
}
#[test]
fn test_read_metadata_desc() -> Result<()> {
let mut m = Metadata::new();
m.read_metadata(Entry::Desc, " Line 1\n Line 2\n\n")?;
assert_eq!(m.desc(), " Line 1\n Line 2");
Ok(())
}
#[test]
fn test_read_metadata_build_info() -> Result<()> {
let mut m = Metadata::new();
m.read_metadata(Entry::BuildInfo, "KEY1=val1\nKEY2=val2\n")?;
let info = m
.build_info()
.ok_or(MetadataError::MissingRequired("+BUILD_INFO"))?;
assert_eq!(info.len(), 2);
assert_eq!(info[0], "KEY1=val1");
assert_eq!(info[1], "KEY2=val2");
Ok(())
}
#[test]
fn test_read_metadata_size() -> Result<()> {
let mut m = Metadata::new();
m.read_metadata(Entry::SizeAll, " 12345 ")?;
m.read_metadata(Entry::SizePkg, "67890")?;
assert_eq!(m.size_all(), Some(12345));
assert_eq!(m.size_pkg(), Some(67890));
Ok(())
}
#[test]
fn test_read_metadata_invalid_size() {
let mut m = Metadata::new();
let result = m.read_metadata(Entry::SizeAll, "not a number");
assert!(matches!(result, Err(MetadataError::InvalidValue { .. })));
}
#[test]
fn test_read_metadata_negative_size() {
let mut m = Metadata::new();
let result = m.read_metadata(Entry::SizeAll, "-100");
assert!(matches!(result, Err(MetadataError::InvalidValue { .. })));
}
#[test]
fn test_read_metadata_optional_fields() -> Result<()> {
let mut m = Metadata::new();
m.read_metadata(Entry::DeInstall, "#!/bin/sh\nexit 0")?;
m.read_metadata(Entry::Install, "#!/bin/sh\nexit 0")?;
m.read_metadata(Entry::Display, "Important message")?;
m.read_metadata(Entry::Preserve, "yes")?;
m.read_metadata(Entry::RequiredBy, "pkg1\npkg2")?;
assert_eq!(m.deinstall(), Some("#!/bin/sh\nexit 0"));
assert_eq!(m.install(), Some("#!/bin/sh\nexit 0"));
assert_eq!(m.display(), Some("Important message"));
let preserve = m
.preserve()
.ok_or(MetadataError::MissingRequired("+PRESERVE"))?;
assert_eq!(preserve, &["yes"]);
let required_by = m
.required_by()
.ok_or(MetadataError::MissingRequired("+REQUIRED_BY"))?;
assert_eq!(required_by, &["pkg1", "pkg2"]);
Ok(())
}
#[test]
fn test_validate_empty() {
let m = Metadata::new();
assert!(matches!(
m.validate(),
Err(MetadataError::MissingRequired("+COMMENT"))
));
}
#[test]
fn test_validate_missing_contents() -> Result<()> {
let mut m = Metadata::new();
m.read_metadata(Entry::Comment, "Test")?;
assert!(matches!(
m.validate(),
Err(MetadataError::MissingRequired("+CONTENTS"))
));
Ok(())
}
#[test]
fn test_validate_missing_desc() -> Result<()> {
let mut m = Metadata::new();
m.read_metadata(Entry::Comment, "Test")?;
m.read_metadata(Entry::Contents, "bin/foo")?;
assert!(matches!(
m.validate(),
Err(MetadataError::MissingRequired("+DESC"))
));
Ok(())
}
#[test]
fn test_validate_success() -> Result<()> {
let mut m = Metadata::new();
m.read_metadata(Entry::Comment, "Test")?;
m.read_metadata(Entry::Contents, "bin/foo")?;
m.read_metadata(Entry::Desc, "Description")?;
assert!(m.validate().is_ok());
assert!(m.is_valid());
Ok(())
}
#[test]
fn test_is_valid() -> Result<()> {
let mut m = Metadata::new();
assert!(!m.is_valid());
m.read_metadata(Entry::Comment, "Test")?;
assert!(!m.is_valid());
m.read_metadata(Entry::Contents, "bin/foo")?;
assert!(!m.is_valid());
m.read_metadata(Entry::Desc, "Description")?;
assert!(m.is_valid());
Ok(())
}
#[test]
fn test_entry_display() {
assert_eq!(Entry::BuildInfo.to_string(), "+BUILD_INFO");
assert_eq!(Entry::Comment.to_string(), "+COMMENT");
assert_eq!(Entry::SizePkg.to_string(), "+SIZE_PKG");
}
#[test]
fn test_entry_from_str() -> Result<()> {
assert_eq!("+BUILD_INFO".parse::<Entry>()?, Entry::BuildInfo);
assert_eq!("+COMMENT".parse::<Entry>()?, Entry::Comment);
assert_eq!("+SIZE_PKG".parse::<Entry>()?, Entry::SizePkg);
let result = "+INVALID".parse::<Entry>();
assert!(matches!(result, Err(MetadataError::UnknownEntry(_))));
Ok(())
}
#[test]
fn test_entry_from_filename() {
assert_eq!(Entry::from_filename("+BUILD_INFO"), Some(Entry::BuildInfo));
assert_eq!(Entry::from_filename("+COMMENT"), Some(Entry::Comment));
assert_eq!(Entry::from_filename("+INVALID"), None);
assert_eq!(Entry::from_filename("COMMENT"), None);
}
#[test]
fn test_entry_to_filename() {
assert_eq!(Entry::BuildInfo.to_filename(), "+BUILD_INFO");
assert_eq!(Entry::BuildVersion.to_filename(), "+BUILD_VERSION");
assert_eq!(Entry::Comment.to_filename(), "+COMMENT");
assert_eq!(Entry::Contents.to_filename(), "+CONTENTS");
assert_eq!(Entry::DeInstall.to_filename(), "+DEINSTALL");
assert_eq!(Entry::Desc.to_filename(), "+DESC");
assert_eq!(Entry::Display.to_filename(), "+DISPLAY");
assert_eq!(Entry::Install.to_filename(), "+INSTALL");
assert_eq!(Entry::InstalledInfo.to_filename(), "+INSTALLED_INFO");
assert_eq!(Entry::MtreeDirs.to_filename(), "+MTREE_DIRS");
assert_eq!(Entry::Preserve.to_filename(), "+PRESERVE");
assert_eq!(Entry::RequiredBy.to_filename(), "+REQUIRED_BY");
assert_eq!(Entry::SizeAll.to_filename(), "+SIZE_ALL");
assert_eq!(Entry::SizePkg.to_filename(), "+SIZE_PKG");
}
#[test]
fn test_entry_roundtrip() -> Result<()> {
let entries = [
Entry::BuildInfo,
Entry::BuildVersion,
Entry::Comment,
Entry::Contents,
Entry::DeInstall,
Entry::Desc,
Entry::Display,
Entry::Install,
Entry::InstalledInfo,
Entry::MtreeDirs,
Entry::Preserve,
Entry::RequiredBy,
Entry::SizeAll,
Entry::SizePkg,
];
for entry in entries {
let filename = entry.to_filename();
let parsed = Entry::from_filename(filename)
.ok_or(MetadataError::UnknownEntry(filename.to_string()))?;
assert_eq!(entry, parsed);
}
Ok(())
}
#[test]
fn test_error_display() {
let err = MetadataError::MissingRequired("+COMMENT");
assert_eq!(err.to_string(), "Missing or empty +COMMENT");
let err = MetadataError::UnknownEntry("+BADFILE".to_string());
assert_eq!(err.to_string(), "Unknown metadata entry: +BADFILE");
}
#[test]
fn test_all_optional_getters() -> Result<()> {
let mut m = Metadata::new();
assert!(m.build_info().is_none());
assert!(m.build_version().is_none());
assert!(m.deinstall().is_none());
assert!(m.display().is_none());
assert!(m.install().is_none());
assert!(m.installed_info().is_none());
assert!(m.mtree_dirs().is_none());
assert!(m.preserve().is_none());
assert!(m.required_by().is_none());
assert!(m.size_all().is_none());
assert!(m.size_pkg().is_none());
m.read_metadata(Entry::BuildInfo, "a")?;
m.read_metadata(Entry::BuildVersion, "b")?;
m.read_metadata(Entry::DeInstall, "c")?;
m.read_metadata(Entry::Display, "d")?;
m.read_metadata(Entry::Install, "e")?;
m.read_metadata(Entry::InstalledInfo, "f")?;
m.read_metadata(Entry::MtreeDirs, "g")?;
m.read_metadata(Entry::Preserve, "h")?;
m.read_metadata(Entry::RequiredBy, "i")?;
m.read_metadata(Entry::SizeAll, "100")?;
m.read_metadata(Entry::SizePkg, "200")?;
let build_info = m
.build_info()
.ok_or(MetadataError::MissingRequired("+BUILD_INFO"))?;
assert_eq!(build_info, &["a"]);
let build_version = m
.build_version()
.ok_or(MetadataError::MissingRequired("+BUILD_VERSION"))?;
assert_eq!(build_version, &["b"]);
let deinstall = m
.deinstall()
.ok_or(MetadataError::MissingRequired("+DEINSTALL"))?;
assert_eq!(deinstall, "c");
let display = m
.display()
.ok_or(MetadataError::MissingRequired("+DISPLAY"))?;
assert_eq!(display, "d");
let install = m
.install()
.ok_or(MetadataError::MissingRequired("+INSTALL"))?;
assert_eq!(install, "e");
let installed_info = m
.installed_info()
.ok_or(MetadataError::MissingRequired("+INSTALLED_INFO"))?;
assert_eq!(installed_info, &["f"]);
let mtree_dirs = m
.mtree_dirs()
.ok_or(MetadataError::MissingRequired("+MTREE_DIRS"))?;
assert_eq!(mtree_dirs, &["g"]);
let preserve = m
.preserve()
.ok_or(MetadataError::MissingRequired("+PRESERVE"))?;
assert_eq!(preserve, &["h"]);
let required_by = m
.required_by()
.ok_or(MetadataError::MissingRequired("+REQUIRED_BY"))?;
assert_eq!(required_by, &["i"]);
let size_all = m
.size_all()
.ok_or(MetadataError::MissingRequired("+SIZE_ALL"))?;
assert_eq!(size_all, 100);
let size_pkg = m
.size_pkg()
.ok_or(MetadataError::MissingRequired("+SIZE_PKG"))?;
assert_eq!(size_pkg, 200);
Ok(())
}
}