use std::fmt;
use std::io::{self, BufRead};
use std::num::ParseIntError;
use std::str::FromStr;
use crate::PkgName;
use crate::kv::Kv;
pub use crate::kv::Span;
#[derive(Clone, Debug, Default, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ErrorContext {
entry: Option<usize>,
span: Option<Span>,
}
impl ErrorContext {
#[must_use]
pub fn new(span: Span) -> Self {
Self {
entry: None,
span: Some(span),
}
}
#[must_use]
pub fn with_entry(mut self, entry: usize) -> Self {
self.entry = Some(entry);
self
}
#[must_use]
pub fn adjust_offset(mut self, adjustment: usize) -> Self {
if let Some(ref mut span) = self.span {
span.offset += adjustment;
}
self
}
#[must_use]
pub fn with_span_if_none(mut self, span: Span) -> Self {
if self.span.is_none() {
self.span = Some(span);
}
self
}
#[must_use]
pub const fn entry(&self) -> Option<usize> {
self.entry
}
#[must_use]
pub const fn span(&self) -> Option<Span> {
self.span
}
}
#[cfg(test)]
use indoc::indoc;
pub type Result<T> = std::result::Result<T, SummaryError>;
impl fmt::Display for Summary {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
macro_rules! write_required_field {
($field:expr, $name:expr) => {
writeln!(f, "{}={}", $name, $field)?;
};
}
macro_rules! write_optional_field {
($field:expr, $name:expr) => {
if let Some(val) = &$field {
writeln!(f, "{}={}", $name, val)?;
}
};
}
macro_rules! write_required_array_field {
($field:expr, $name:expr) => {
for val in $field {
writeln!(f, "{}={}", $name, val)?;
}
};
}
macro_rules! write_optional_array_field {
($field:expr, $name:expr) => {
if let Some(arr) = &$field {
for val in arr {
writeln!(f, "{}={}", $name, val)?;
}
}
};
}
write_optional_array_field!(self.conflicts, "CONFLICTS");
write_required_field!(self.pkgname.pkgname(), "PKGNAME");
write_optional_array_field!(self.depends, "DEPENDS");
write_required_field!(&self.comment, "COMMENT");
write_required_field!(self.size_pkg, "SIZE_PKG");
write_required_field!(&self.build_date, "BUILD_DATE");
writeln!(f, "CATEGORIES={}", self.categories.join(" "))?;
write_optional_field!(self.homepage, "HOMEPAGE");
write_optional_field!(self.license, "LICENSE");
write_required_field!(&self.machine_arch, "MACHINE_ARCH");
write_required_field!(&self.opsys, "OPSYS");
write_required_field!(&self.os_version, "OS_VERSION");
write_required_field!(&self.pkgpath, "PKGPATH");
write_required_field!(&self.pkgtools_version, "PKGTOOLS_VERSION");
write_optional_field!(self.pkg_options, "PKG_OPTIONS");
write_optional_field!(self.prev_pkgpath, "PREV_PKGPATH");
write_optional_array_field!(self.provides, "PROVIDES");
write_optional_array_field!(self.requires, "REQUIRES");
write_optional_array_field!(self.supersedes, "SUPERSEDES");
write_optional_field!(self.file_name, "FILE_NAME");
write_optional_field!(self.file_size, "FILE_SIZE");
write_optional_field!(self.file_cksum, "FILE_CKSUM");
if self.description.is_empty() {
writeln!(f, "DESCRIPTION=")?;
} else {
write_required_array_field!(&self.description, "DESCRIPTION");
}
Ok(())
}
}
#[derive(Clone, Debug, Eq, Hash, Kv, PartialEq)]
pub struct Summary {
#[kv(variable = "BUILD_DATE")]
build_date: String,
#[kv(variable = "CATEGORIES")]
categories: Vec<String>,
#[kv(variable = "COMMENT")]
comment: String,
#[kv(variable = "CONFLICTS", multiline)]
conflicts: Option<Vec<String>>,
#[kv(variable = "DEPENDS", multiline)]
depends: Option<Vec<String>>,
#[kv(variable = "DESCRIPTION", multiline)]
description: Vec<String>,
#[kv(variable = "FILE_CKSUM")]
file_cksum: Option<String>,
#[kv(variable = "FILE_NAME")]
file_name: Option<String>,
#[kv(variable = "FILE_SIZE")]
file_size: Option<u64>,
#[kv(variable = "HOMEPAGE")]
homepage: Option<String>,
#[kv(variable = "LICENSE")]
license: Option<String>,
#[kv(variable = "MACHINE_ARCH")]
machine_arch: String,
#[kv(variable = "OPSYS")]
opsys: String,
#[kv(variable = "OS_VERSION")]
os_version: String,
#[kv(variable = "PKGNAME")]
pkgname: PkgName,
#[kv(variable = "PKGPATH")]
pkgpath: String,
#[kv(variable = "PKGTOOLS_VERSION")]
pkgtools_version: String,
#[kv(variable = "PKG_OPTIONS")]
pkg_options: Option<String>,
#[kv(variable = "PREV_PKGPATH")]
prev_pkgpath: Option<String>,
#[kv(variable = "PROVIDES", multiline)]
provides: Option<Vec<String>>,
#[kv(variable = "REQUIRES", multiline)]
requires: Option<Vec<String>>,
#[kv(variable = "SIZE_PKG")]
size_pkg: u64,
#[kv(variable = "SUPERSEDES", multiline)]
supersedes: Option<Vec<String>>,
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct SummaryBuilder {
lines: Vec<String>,
allow_unknown: bool,
allow_incomplete: bool,
}
impl SummaryBuilder {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn var(mut self, line: impl AsRef<str>) -> Self {
self.lines.push(line.as_ref().to_string());
self
}
#[must_use]
pub fn vars<I, S>(mut self, lines: I) -> Self
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
for line in lines {
self.lines.push(line.as_ref().to_string());
}
self
}
#[must_use]
pub fn allow_unknown(mut self, yes: bool) -> Self {
self.allow_unknown = yes;
self
}
#[must_use]
pub fn allow_incomplete(mut self, yes: bool) -> Self {
self.allow_incomplete = yes;
self
}
pub fn build(self) -> Result<Summary> {
let input = self.lines.join("\n");
parse_summary(&input, self.allow_unknown, self.allow_incomplete)
}
}
impl Summary {
#[allow(clippy::too_many_arguments)]
#[must_use]
pub(crate) fn new(
pkgname: PkgName,
comment: String,
size_pkg: u64,
build_date: String,
categories: Vec<String>,
machine_arch: String,
opsys: String,
os_version: String,
pkgpath: String,
pkgtools_version: String,
description: Vec<String>,
conflicts: Option<Vec<String>>,
depends: Option<Vec<String>>,
homepage: Option<String>,
license: Option<String>,
pkg_options: Option<String>,
prev_pkgpath: Option<String>,
provides: Option<Vec<String>>,
requires: Option<Vec<String>>,
supersedes: Option<Vec<String>>,
file_name: Option<String>,
file_size: Option<u64>,
file_cksum: Option<String>,
) -> Self {
Self {
build_date,
categories,
comment,
conflicts,
depends,
description,
file_cksum,
file_name,
file_size,
homepage,
license,
machine_arch,
opsys,
os_version,
pkgname,
pkgpath,
pkgtools_version,
pkg_options,
prev_pkgpath,
provides,
requires,
size_pkg,
supersedes,
}
}
pub fn from_reader<R: BufRead>(reader: R) -> SummaryIter<R> {
SummaryIter {
reader,
line_buf: String::new(),
buffer: String::new(),
record_number: 0,
byte_offset: 0,
entry_start: 0,
allow_unknown: false,
allow_incomplete: false,
}
}
pub fn build_date(&self) -> &str {
&self.build_date
}
pub fn categories(&self) -> &[String] {
&self.categories
}
pub fn comment(&self) -> &str {
&self.comment
}
pub fn conflicts(&self) -> Option<&[String]> {
self.conflicts.as_deref()
}
pub fn depends(&self) -> Option<&[String]> {
self.depends.as_deref()
}
pub fn description(&self) -> &[String] {
self.description.as_slice()
}
pub fn file_cksum(&self) -> Option<&str> {
self.file_cksum.as_deref()
}
pub fn file_name(&self) -> Option<&str> {
self.file_name.as_deref()
}
pub fn file_size(&self) -> Option<u64> {
self.file_size
}
pub fn homepage(&self) -> Option<&str> {
self.homepage.as_deref()
}
pub fn license(&self) -> Option<&str> {
self.license.as_deref()
}
pub fn machine_arch(&self) -> &str {
&self.machine_arch
}
pub fn opsys(&self) -> &str {
&self.opsys
}
pub fn os_version(&self) -> &str {
&self.os_version
}
pub fn pkg_options(&self) -> Option<&str> {
self.pkg_options.as_deref()
}
pub fn pkgname(&self) -> &PkgName {
&self.pkgname
}
pub fn pkgpath(&self) -> &str {
&self.pkgpath
}
pub fn pkgtools_version(&self) -> &str {
&self.pkgtools_version
}
pub fn prev_pkgpath(&self) -> Option<&str> {
self.prev_pkgpath.as_deref()
}
pub fn provides(&self) -> Option<&[String]> {
self.provides.as_deref()
}
pub fn requires(&self) -> Option<&[String]> {
self.requires.as_deref()
}
pub fn size_pkg(&self) -> u64 {
self.size_pkg
}
pub fn supersedes(&self) -> Option<&[String]> {
self.supersedes.as_deref()
}
}
impl FromStr for Summary {
type Err = SummaryError;
fn from_str(s: &str) -> Result<Self> {
Summary::parse(s).map_err(SummaryError::from)
}
}
fn parse_summary(
s: &str,
allow_unknown: bool,
allow_incomplete: bool,
) -> Result<Summary> {
if allow_unknown || allow_incomplete {
parse_summary_lenient(s, allow_unknown, allow_incomplete)
} else {
Summary::parse(s).map_err(SummaryError::from)
}
}
fn parse_summary_lenient(
s: &str,
allow_unknown: bool,
allow_incomplete: bool,
) -> Result<Summary> {
use crate::kv::FromKv;
let mut build_date: Option<String> = None;
let mut categories: Option<Vec<String>> = None;
let mut comment: Option<String> = None;
let mut conflicts: Option<Vec<String>> = None;
let mut depends: Option<Vec<String>> = None;
let mut description: Option<Vec<String>> = None;
let mut file_cksum: Option<String> = None;
let mut file_name: Option<String> = None;
let mut file_size: Option<u64> = None;
let mut homepage: Option<String> = None;
let mut license: Option<String> = None;
let mut machine_arch: Option<String> = None;
let mut opsys: Option<String> = None;
let mut os_version: Option<String> = None;
let mut pkgname: Option<PkgName> = None;
let mut pkgpath: Option<String> = None;
let mut pkgtools_version: Option<String> = None;
let mut pkg_options: Option<String> = None;
let mut prev_pkgpath: Option<String> = None;
let mut provides: Option<Vec<String>> = None;
let mut requires: Option<Vec<String>> = None;
let mut size_pkg: Option<u64> = None;
let mut supersedes: Option<Vec<String>> = None;
for line in s.lines() {
if line.is_empty() {
continue;
}
let line_offset = line.as_ptr() as usize - s.as_ptr() as usize;
let (key, value) =
line.split_once('=')
.ok_or_else(|| SummaryError::ParseLine {
context: ErrorContext::new(Span {
offset: line_offset,
len: line.len(),
}),
})?;
let value_offset = line_offset + key.len() + 1;
let value_span = Span {
offset: value_offset,
len: value.len(),
};
match key {
"BUILD_DATE" => {
build_date = Some(
<String as FromKv>::from_kv(value, value_span)
.map_err(kv_to_summary_error)?,
);
}
"CATEGORIES" => {
let items: Vec<String> =
value.split_whitespace().map(String::from).collect();
categories = Some(items);
}
"COMMENT" => {
comment = Some(
<String as FromKv>::from_kv(value, value_span)
.map_err(kv_to_summary_error)?,
);
}
"CONFLICTS" => {
let mut vec = conflicts.unwrap_or_default();
vec.push(
<String as FromKv>::from_kv(value, value_span)
.map_err(kv_to_summary_error)?,
);
conflicts = Some(vec);
}
"DEPENDS" => {
let mut vec = depends.unwrap_or_default();
vec.push(
<String as FromKv>::from_kv(value, value_span)
.map_err(kv_to_summary_error)?,
);
depends = Some(vec);
}
"DESCRIPTION" => {
let mut vec = description.unwrap_or_default();
vec.push(
<String as FromKv>::from_kv(value, value_span)
.map_err(kv_to_summary_error)?,
);
description = Some(vec);
}
"FILE_CKSUM" => {
file_cksum = Some(
<String as FromKv>::from_kv(value, value_span)
.map_err(kv_to_summary_error)?,
);
}
"FILE_NAME" => {
file_name = Some(
<String as FromKv>::from_kv(value, value_span)
.map_err(kv_to_summary_error)?,
);
}
"FILE_SIZE" => {
file_size = Some(
<u64 as FromKv>::from_kv(value, value_span)
.map_err(kv_to_summary_error)?,
);
}
"HOMEPAGE" => {
homepage = Some(
<String as FromKv>::from_kv(value, value_span)
.map_err(kv_to_summary_error)?,
);
}
"LICENSE" => {
license = Some(
<String as FromKv>::from_kv(value, value_span)
.map_err(kv_to_summary_error)?,
);
}
"MACHINE_ARCH" => {
machine_arch = Some(
<String as FromKv>::from_kv(value, value_span)
.map_err(kv_to_summary_error)?,
);
}
"OPSYS" => {
opsys = Some(
<String as FromKv>::from_kv(value, value_span)
.map_err(kv_to_summary_error)?,
);
}
"OS_VERSION" => {
os_version = Some(
<String as FromKv>::from_kv(value, value_span)
.map_err(kv_to_summary_error)?,
);
}
"PKGNAME" => {
pkgname = Some(
<PkgName as FromKv>::from_kv(value, value_span)
.map_err(kv_to_summary_error)?,
);
}
"PKGPATH" => {
pkgpath = Some(
<String as FromKv>::from_kv(value, value_span)
.map_err(kv_to_summary_error)?,
);
}
"PKGTOOLS_VERSION" => {
pkgtools_version = Some(
<String as FromKv>::from_kv(value, value_span)
.map_err(kv_to_summary_error)?,
);
}
"PKG_OPTIONS" => {
pkg_options = Some(
<String as FromKv>::from_kv(value, value_span)
.map_err(kv_to_summary_error)?,
);
}
"PREV_PKGPATH" => {
prev_pkgpath = Some(
<String as FromKv>::from_kv(value, value_span)
.map_err(kv_to_summary_error)?,
);
}
"PROVIDES" => {
let mut vec = provides.unwrap_or_default();
vec.push(
<String as FromKv>::from_kv(value, value_span)
.map_err(kv_to_summary_error)?,
);
provides = Some(vec);
}
"REQUIRES" => {
let mut vec = requires.unwrap_or_default();
vec.push(
<String as FromKv>::from_kv(value, value_span)
.map_err(kv_to_summary_error)?,
);
requires = Some(vec);
}
"SIZE_PKG" => {
size_pkg = Some(
<u64 as FromKv>::from_kv(value, value_span)
.map_err(kv_to_summary_error)?,
);
}
"SUPERSEDES" => {
let mut vec = supersedes.unwrap_or_default();
vec.push(
<String as FromKv>::from_kv(value, value_span)
.map_err(kv_to_summary_error)?,
);
supersedes = Some(vec);
}
unknown => {
if !allow_unknown {
return Err(SummaryError::UnknownVariable {
variable: unknown.to_string(),
context: ErrorContext::new(Span {
offset: line_offset,
len: key.len(),
}),
});
}
}
}
}
let build_date = if allow_incomplete {
build_date.unwrap_or_default()
} else {
build_date.ok_or_else(|| SummaryError::Incomplete {
field: "BUILD_DATE".to_string(),
context: ErrorContext::default(),
})?
};
let categories = if allow_incomplete {
categories.unwrap_or_default()
} else {
categories.ok_or_else(|| SummaryError::Incomplete {
field: "CATEGORIES".to_string(),
context: ErrorContext::default(),
})?
};
let comment = if allow_incomplete {
comment.unwrap_or_default()
} else {
comment.ok_or_else(|| SummaryError::Incomplete {
field: "COMMENT".to_string(),
context: ErrorContext::default(),
})?
};
let description = if allow_incomplete {
description.unwrap_or_default()
} else {
description.ok_or_else(|| SummaryError::Incomplete {
field: "DESCRIPTION".to_string(),
context: ErrorContext::default(),
})?
};
let machine_arch = if allow_incomplete {
machine_arch.unwrap_or_default()
} else {
machine_arch.ok_or_else(|| SummaryError::Incomplete {
field: "MACHINE_ARCH".to_string(),
context: ErrorContext::default(),
})?
};
let opsys = if allow_incomplete {
opsys.unwrap_or_default()
} else {
opsys.ok_or_else(|| SummaryError::Incomplete {
field: "OPSYS".to_string(),
context: ErrorContext::default(),
})?
};
let os_version = if allow_incomplete {
os_version.unwrap_or_default()
} else {
os_version.ok_or_else(|| SummaryError::Incomplete {
field: "OS_VERSION".to_string(),
context: ErrorContext::default(),
})?
};
let pkgname = if allow_incomplete {
pkgname.unwrap_or_else(|| PkgName::new("unknown-0"))
} else {
pkgname.ok_or_else(|| SummaryError::Incomplete {
field: "PKGNAME".to_string(),
context: ErrorContext::default(),
})?
};
let pkgpath = if allow_incomplete {
pkgpath.unwrap_or_default()
} else {
pkgpath.ok_or_else(|| SummaryError::Incomplete {
field: "PKGPATH".to_string(),
context: ErrorContext::default(),
})?
};
let pkgtools_version = if allow_incomplete {
pkgtools_version.unwrap_or_default()
} else {
pkgtools_version.ok_or_else(|| SummaryError::Incomplete {
field: "PKGTOOLS_VERSION".to_string(),
context: ErrorContext::default(),
})?
};
let size_pkg = if allow_incomplete {
size_pkg.unwrap_or(0)
} else {
size_pkg.ok_or_else(|| SummaryError::Incomplete {
field: "SIZE_PKG".to_string(),
context: ErrorContext::default(),
})?
};
Ok(Summary {
build_date,
categories,
comment,
conflicts,
depends,
description,
file_cksum,
file_name,
file_size,
homepage,
license,
machine_arch,
opsys,
os_version,
pkgname,
pkgpath,
pkgtools_version,
pkg_options,
prev_pkgpath,
provides,
requires,
size_pkg,
supersedes,
})
}
fn kv_to_summary_error(e: crate::kv::KvError) -> SummaryError {
SummaryError::from(e)
}
pub struct SummaryIter<R: BufRead> {
reader: R,
line_buf: String,
buffer: String,
record_number: usize,
byte_offset: usize,
entry_start: usize,
allow_unknown: bool,
allow_incomplete: bool,
}
impl<R: BufRead> Iterator for SummaryIter<R> {
type Item = Result<Summary>;
fn next(&mut self) -> Option<Self::Item> {
self.buffer.clear();
self.entry_start = self.byte_offset;
loop {
self.line_buf.clear();
match self.reader.read_line(&mut self.line_buf) {
Ok(0) => {
return if self.buffer.is_empty() {
None
} else {
let entry = self.record_number;
let entry_start = self.entry_start;
let entry_len = self.buffer.len();
self.record_number += 1;
Some(
parse_summary(
&self.buffer,
self.allow_unknown,
self.allow_incomplete,
)
.map_err(
|e: SummaryError| {
e.with_entry_span(Span {
offset: 0,
len: entry_len,
})
.with_entry(entry)
.adjust_offset(entry_start)
},
),
)
};
}
Ok(line_bytes) => {
let is_blank =
self.line_buf.trim_end_matches(['\r', '\n']).is_empty();
if is_blank {
self.byte_offset += line_bytes;
if !self.buffer.is_empty() {
let entry = self.record_number;
let entry_start = self.entry_start;
let to_parse =
self.buffer.trim_end_matches(['\r', '\n']);
let entry_len = to_parse.len();
self.record_number += 1;
self.entry_start = self.byte_offset;
return Some(
parse_summary(
to_parse,
self.allow_unknown,
self.allow_incomplete,
)
.map_err(
|e: SummaryError| {
e.with_entry_span(Span {
offset: 0,
len: entry_len,
})
.with_entry(entry)
.adjust_offset(entry_start)
},
),
);
}
} else {
self.buffer.push_str(&self.line_buf);
self.byte_offset += line_bytes;
}
}
Err(e) => return Some(Err(SummaryError::Io(e))),
}
}
}
}
impl<R: BufRead> SummaryIter<R> {
#[must_use]
pub fn allow_unknown(mut self, yes: bool) -> Self {
self.allow_unknown = yes;
self
}
#[must_use]
pub fn allow_incomplete(mut self, yes: bool) -> Self {
self.allow_incomplete = yes;
self
}
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum SummaryError {
#[error("missing required field '{field}'")]
Incomplete {
field: String,
context: ErrorContext,
},
#[error(transparent)]
Io(#[from] io::Error),
#[error("line is not in VARIABLE=VALUE format")]
ParseLine {
context: ErrorContext,
},
#[error("'{variable}' is not a valid pkg_summary variable")]
UnknownVariable {
variable: String,
context: ErrorContext,
},
#[error("failed to parse integer")]
ParseInt {
#[source]
source: ParseIntError,
context: ErrorContext,
},
#[error("duplicate value for '{variable}'")]
Duplicate {
variable: String,
context: ErrorContext,
},
#[error("{message}")]
Parse {
message: String,
context: ErrorContext,
},
}
impl From<crate::kv::KvError> for SummaryError {
fn from(e: crate::kv::KvError) -> Self {
match e {
crate::kv::KvError::ParseLine(span) => Self::ParseLine {
context: ErrorContext::new(span),
},
crate::kv::KvError::Incomplete(field) => Self::Incomplete {
field,
context: ErrorContext::default(),
},
crate::kv::KvError::UnknownVariable { variable, span } => {
Self::UnknownVariable {
variable,
context: ErrorContext::new(span),
}
}
crate::kv::KvError::ParseInt { source, span } => Self::ParseInt {
source,
context: ErrorContext::new(span),
},
crate::kv::KvError::Parse { message, span } => Self::Parse {
message,
context: ErrorContext::new(span),
},
}
}
}
impl SummaryError {
pub fn entry(&self) -> Option<usize> {
match self {
Self::Incomplete { context, .. }
| Self::ParseLine { context, .. }
| Self::UnknownVariable { context, .. }
| Self::ParseInt { context, .. }
| Self::Duplicate { context, .. }
| Self::Parse { context, .. } => context.entry(),
Self::Io(_) => None,
}
}
pub fn span(&self) -> Option<Span> {
match self {
Self::Incomplete { context, .. }
| Self::ParseLine { context, .. }
| Self::UnknownVariable { context, .. }
| Self::ParseInt { context, .. }
| Self::Duplicate { context, .. }
| Self::Parse { context, .. } => context.span(),
Self::Io(_) => None,
}
}
fn with_entry(self, entry: usize) -> Self {
match self {
Self::Incomplete { field, context } => Self::Incomplete {
field,
context: context.with_entry(entry),
},
Self::ParseLine { context } => Self::ParseLine {
context: context.with_entry(entry),
},
Self::UnknownVariable { variable, context } => {
Self::UnknownVariable {
variable,
context: context.with_entry(entry),
}
}
Self::ParseInt { source, context } => Self::ParseInt {
source,
context: context.with_entry(entry),
},
Self::Duplicate { variable, context } => Self::Duplicate {
variable,
context: context.with_entry(entry),
},
Self::Parse { message, context } => Self::Parse {
message,
context: context.with_entry(entry),
},
Self::Io(e) => Self::Io(e),
}
}
fn adjust_offset(self, base: usize) -> Self {
match self {
Self::Incomplete { field, context } => Self::Incomplete {
field,
context: context.adjust_offset(base),
},
Self::ParseLine { context } => Self::ParseLine {
context: context.adjust_offset(base),
},
Self::UnknownVariable { variable, context } => {
Self::UnknownVariable {
variable,
context: context.adjust_offset(base),
}
}
Self::ParseInt { source, context } => Self::ParseInt {
source,
context: context.adjust_offset(base),
},
Self::Duplicate { variable, context } => Self::Duplicate {
variable,
context: context.adjust_offset(base),
},
Self::Parse { message, context } => Self::Parse {
message,
context: context.adjust_offset(base),
},
Self::Io(e) => Self::Io(e),
}
}
fn with_entry_span(self, span: Span) -> Self {
match self {
Self::Incomplete { field, context } => Self::Incomplete {
field,
context: context.with_span_if_none(span),
},
other => other,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_err() -> std::result::Result<(), &'static str> {
let err = Summary::from_str("BUILD_DATE")
.err()
.ok_or("expected error")?;
assert!(matches!(err, SummaryError::ParseLine { .. }));
let err = Summary::from_str("BILD_DATE=")
.err()
.ok_or("expected error")?;
assert!(matches!(err, SummaryError::UnknownVariable { .. }));
let input = indoc! {"
BUILD_DATE=2019-08-12
CATEGORIES=devel
COMMENT=test
DESCRIPTION=test
MACHINE_ARCH=x86_64
OPSYS=NetBSD
OS_VERSION=9.0
PKGNAME=test-1.0
PKGPATH=devel/test
PKGTOOLS_VERSION=20091115
SIZE_PKG=1234
FILE_SIZE=NaN
"};
let err = Summary::from_str(input).err().ok_or("expected error")?;
assert!(matches!(err, SummaryError::ParseInt { .. }));
let err = Summary::from_str("FILE_SIZE=1234")
.err()
.ok_or("expected error")?;
assert!(matches!(err, SummaryError::Incomplete { .. }));
Ok(())
}
#[test]
fn test_error_context() -> std::result::Result<(), &'static str> {
let err = Summary::from_str("BUILD_DATE=2019-08-12\nBAD LINE\n")
.err()
.ok_or("expected error")?;
assert!(matches!(err, SummaryError::ParseLine { .. }));
let span = err.span().ok_or("should have span")?;
assert_eq!(span.offset, 22); assert_eq!(span.len, 8); assert!(err.entry().is_none());
let err = Summary::from_str("INVALID_KEY=value\n")
.err()
.ok_or("expected error")?;
assert!(
matches!(err, SummaryError::UnknownVariable { variable, .. } if variable == "INVALID_KEY")
);
let input = indoc! {"
PKGNAME=good-1.0
COMMENT=test
SIZE_PKG=100
BUILD_DATE=2019-08-12
CATEGORIES=test
DESCRIPTION=test
MACHINE_ARCH=x86_64
OPSYS=Darwin
OS_VERSION=18.7.0
PKGPATH=test/good
PKGTOOLS_VERSION=20091115
PKGNAME=bad-1.0
COMMENT=test
SIZE_PKG=100
BUILD_DATEFOO=2019-08-12
CATEGORIES=test
"};
let mut iter = Summary::from_reader(input.trim().as_bytes());
let first = iter.next().ok_or("expected first entry")?;
assert!(first.is_ok());
let second = iter.next().ok_or("expected second entry")?;
assert!(second.is_err());
let err = second.err().ok_or("expected error")?;
assert_eq!(err.entry(), Some(1)); Ok(())
}
#[test]
fn test_lenient_parse_mode() -> Result<()> {
let input = indoc! {"
PKGNAME=testpkg-1.0
UNKNOWN_FIELD=value
COMMENT=Test package
BUILD_DATE=2019-08-12 15:58:02 +0100
CATEGORIES=test
DESCRIPTION=Test description
MACHINE_ARCH=x86_64
OPSYS=Darwin
OS_VERSION=18.7.0
PKGPATH=test/pkg
PKGTOOLS_VERSION=20091115
SIZE_PKG=100
"};
let trimmed = input.trim();
let err = Summary::from_str(trimmed).expect_err("expected error");
assert!(
matches!(err, SummaryError::UnknownVariable { variable, .. } if variable == "UNKNOWN_FIELD")
);
let pkg = parse_summary(trimmed, true, false)?;
assert_eq!(pkg.pkgname().pkgname(), "testpkg-1.0");
let pkg = SummaryBuilder::new()
.allow_unknown(true)
.vars(trimmed.lines())
.build()?;
assert_eq!(pkg.pkgname().pkgname(), "testpkg-1.0");
Ok(())
}
#[test]
fn test_iter_with_options_allow_unknown() -> Result<()> {
let input = indoc! {"
PKGNAME=iterpkg-1.0
COMMENT=Iterator test
UNKNOWN=value
BUILD_DATE=2019-08-12 15:58:02 +0100
CATEGORIES=test
DESCRIPTION=Iterator description
MACHINE_ARCH=x86_64
OPSYS=Darwin
OS_VERSION=18.7.0
PKGPATH=test/iterpkg
PKGTOOLS_VERSION=20091115
SIZE_PKG=100
"};
let mut iter = Summary::from_reader(input.trim().as_bytes());
let result = iter.next().expect("expected entry");
assert!(result.is_err());
let mut iter =
Summary::from_reader(input.trim().as_bytes()).allow_unknown(true);
let pkg = iter.next().expect("expected entry")?;
assert_eq!(pkg.pkgname().pkgname(), "iterpkg-1.0");
Ok(())
}
#[test]
fn test_iter_with_options_allow_incomplete() -> Result<()> {
let input = indoc! {"
PKGNAME=incomplete-1.0
COMMENT=Incomplete test
"};
let mut iter = Summary::from_reader(input.trim().as_bytes());
let result = iter.next().expect("expected entry");
assert!(result.is_err());
let mut iter = Summary::from_reader(input.trim().as_bytes())
.allow_incomplete(true);
let pkg = iter.next().expect("expected entry")?;
assert_eq!(pkg.pkgname().pkgname(), "incomplete-1.0");
assert_eq!(pkg.comment(), "Incomplete test");
assert!(pkg.categories().is_empty());
assert!(pkg.description().is_empty());
Ok(())
}
#[test]
fn test_from_reader_all_fields() -> Result<()> {
use flate2::read::GzDecoder;
use std::fs::File;
use std::io::BufReader;
let path = concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/data/summary/pkg_summary.gz"
);
let file = File::open(path)?;
let reader = BufReader::new(GzDecoder::new(file));
let mut blackbox: Option<Summary> = None;
let mut mktool: Option<Summary> = None;
for pkg in Summary::from_reader(reader) {
let pkg = pkg?;
match pkg.pkgname().pkgname() {
"blackbox-0.77nb4" => blackbox = Some(pkg),
"mktool-1.4.2" => mktool = Some(pkg),
_ => {}
}
if blackbox.is_some() && mktool.is_some() {
break;
}
}
let bb = blackbox.expect("blackbox not found");
assert_eq!(bb.pkgname().pkgname(), "blackbox-0.77nb4");
assert_eq!(bb.pkgname().pkgbase(), "blackbox");
assert_eq!(bb.comment(), "Small and fast X11 window manager");
assert_eq!(bb.size_pkg(), 1464205);
assert_eq!(bb.build_date(), "2025-11-17 04:04:52 +0000");
assert_eq!(bb.categories(), &["wm", "x11"]);
assert_eq!(
bb.homepage(),
Some("https://github.com/bbidulock/blackboxwm")
);
assert_eq!(bb.license(), Some("mit"));
assert_eq!(bb.machine_arch(), "aarch64");
assert_eq!(bb.opsys(), "Darwin");
assert_eq!(bb.os_version(), "23.6.0");
assert_eq!(bb.pkgpath(), "wm/blackbox");
assert_eq!(bb.pkgtools_version(), "20091115");
assert_eq!(bb.pkg_options(), Some("nls xft2"));
assert_eq!(bb.prev_pkgpath(), Some("wm/blackbox70"));
assert_eq!(bb.file_name(), Some("blackbox-0.77nb4.tgz"));
assert_eq!(bb.file_size(), Some(477526));
assert_eq!(bb.file_cksum(), None);
assert!(bb.description().len() > 1);
assert!(bb.description()[0].starts_with("Blackbox is yet"));
let conflicts = bb.conflicts().expect("blackbox CONFLICTS");
assert!(conflicts.contains(&"bsetroot-[0-9]*".to_string()));
assert!(conflicts.contains(&"blackbox70-[0-9]*".to_string()));
let depends = bb.depends().expect("blackbox DEPENDS");
assert!(depends.contains(&"gettext-lib>=0.22".to_string()));
let provides = bb.provides().expect("blackbox PROVIDES");
assert!(provides.contains(&"/opt/pkg/lib/libbt.0.dylib".to_string()));
let requires = bb.requires().expect("blackbox REQUIRES");
assert!(requires.contains(&"/opt/pkg/lib/libX11.6.dylib".to_string()));
let supersedes = bb.supersedes().expect("blackbox SUPERSEDES");
assert!(supersedes.contains(&"blackbox70-[0-9]*".to_string()));
let output = bb.to_string();
assert!(output.contains("CONFLICTS=bsetroot-[0-9]*"));
assert!(output.contains("PREV_PKGPATH=wm/blackbox70"));
assert!(output.contains("PKG_OPTIONS=nls xft2"));
assert!(output.contains("PROVIDES=/opt/pkg/lib/libbt.0.dylib"));
assert!(output.contains("REQUIRES=/opt/pkg/lib/libX11.6.dylib"));
assert!(output.contains("SUPERSEDES=blackbox70-[0-9]*"));
let mk = mktool.expect("mktool not found");
assert_eq!(mk.pkgname().pkgname(), "mktool-1.4.2");
assert_eq!(mk.conflicts(), None);
assert_eq!(mk.depends(), None);
assert_eq!(mk.pkg_options(), None);
assert_eq!(mk.prev_pkgpath(), None);
assert_eq!(mk.provides(), None);
assert_eq!(mk.requires(), None);
assert_eq!(mk.supersedes(), None);
assert_eq!(mk.file_cksum(), None);
assert_eq!(mk.homepage(), Some("https://github.com/jperkin/mktool/"));
assert_eq!(mk.license(), Some("isc"));
assert_eq!(mk.file_name(), Some("mktool-1.4.2.tgz"));
assert_eq!(mk.file_size(), Some(2871260));
Ok(())
}
#[test]
fn test_builder_var() -> Result<()> {
let pkg = SummaryBuilder::new()
.var("CONFLICTS=old-pkg-[0-9]*")
.var("PKGNAME=built-1.0")
.var("DEPENDS=dep1>=1.0")
.var("DEPENDS=dep2-[0-9]*")
.var("COMMENT=Built package")
.var("SIZE_PKG=100")
.var("BUILD_DATE=2025-01-01")
.var("CATEGORIES=test")
.var("HOMEPAGE=https://example.com/")
.var("LICENSE=isc")
.var("MACHINE_ARCH=x86_64")
.var("OPSYS=Darwin")
.var("OS_VERSION=23.0")
.var("PKGPATH=test/built")
.var("PKGTOOLS_VERSION=20091115")
.var("PKG_OPTIONS=opt1 opt2")
.var("PREV_PKGPATH=test/old-built")
.var("PROVIDES=/opt/pkg/lib/libfoo.dylib")
.var("REQUIRES=/opt/pkg/lib/libbar.dylib")
.var("SUPERSEDES=old-pkg-[0-9]*")
.var("FILE_NAME=built-1.0.tgz")
.var("FILE_SIZE=5000")
.var("FILE_CKSUM=SHA256:abc123")
.var("DESCRIPTION=Test description")
.build()?;
assert_eq!(pkg.pkgname().pkgname(), "built-1.0");
assert_eq!(pkg.comment(), "Built package");
assert_eq!(
pkg.conflicts(),
Some(["old-pkg-[0-9]*".to_string()].as_slice())
);
assert_eq!(
pkg.depends(),
Some(
["dep1>=1.0".to_string(), "dep2-[0-9]*".to_string()].as_slice()
)
);
assert_eq!(pkg.homepage(), Some("https://example.com/"));
assert_eq!(pkg.license(), Some("isc"));
assert_eq!(pkg.pkg_options(), Some("opt1 opt2"));
assert_eq!(pkg.prev_pkgpath(), Some("test/old-built"));
assert_eq!(
pkg.provides(),
Some(["/opt/pkg/lib/libfoo.dylib".to_string()].as_slice())
);
assert_eq!(
pkg.requires(),
Some(["/opt/pkg/lib/libbar.dylib".to_string()].as_slice())
);
assert_eq!(
pkg.supersedes(),
Some(["old-pkg-[0-9]*".to_string()].as_slice())
);
assert_eq!(pkg.file_name(), Some("built-1.0.tgz"));
assert_eq!(pkg.file_size(), Some(5000));
assert_eq!(pkg.file_cksum(), Some("SHA256:abc123"));
Ok(())
}
#[test]
fn test_display_empty_description() -> Result<()> {
let pkg = SummaryBuilder::new()
.allow_incomplete(true)
.var("PKGNAME=nodesc-1.0")
.var("COMMENT=No description")
.var("SIZE_PKG=100")
.var("BUILD_DATE=2025-01-01")
.var("CATEGORIES=test")
.var("MACHINE_ARCH=x86_64")
.var("OPSYS=Darwin")
.var("OS_VERSION=23.0")
.var("PKGPATH=test/nodesc")
.var("PKGTOOLS_VERSION=20091115")
.build()?;
let output = pkg.to_string();
assert!(output.contains("DESCRIPTION=\n"));
assert!(!output.contains("DESCRIPTION=\nDESCRIPTION="));
Ok(())
}
#[test]
fn test_display() -> Result<()> {
let input = indoc! {"
PKGNAME=testpkg-1.0
COMMENT=Test package
BUILD_DATE=2019-08-12 15:58:02 +0100
CATEGORIES=test cat2
DESCRIPTION=Line 1
DESCRIPTION=Line 2
MACHINE_ARCH=x86_64
OPSYS=Darwin
OS_VERSION=18.7.0
PKGPATH=test/pkg
PKGTOOLS_VERSION=20091115
SIZE_PKG=100
DEPENDS=dep1-[0-9]*
DEPENDS=dep2>=1.0
"};
let pkg: Summary = input.trim().parse()?;
let output = pkg.to_string();
assert!(output.contains("PKGNAME=testpkg-1.0"));
assert!(output.contains("COMMENT=Test package"));
assert!(output.contains("CATEGORIES=test cat2"));
assert!(output.contains("DESCRIPTION=Line 1"));
assert!(output.contains("DESCRIPTION=Line 2"));
assert!(output.contains("DEPENDS=dep1-[0-9]*"));
assert!(output.contains("DEPENDS=dep2>=1.0"));
Ok(())
}
#[test]
fn test_lenient_all_fields() -> Result<()> {
let input = indoc! {"
BUILD_DATE=2025-01-01
CATEGORIES=test
COMMENT=Lenient test
CONFLICTS=old-pkg-[0-9]*
DEPENDS=dep1>=1.0
DEPENDS=dep2-[0-9]*
DESCRIPTION=Line 1
DESCRIPTION=Line 2
FILE_CKSUM=SHA256:abc123
FILE_NAME=lenient-1.0.tgz
FILE_SIZE=5000
HOMEPAGE=https://example.com/
LICENSE=isc
MACHINE_ARCH=x86_64
OPSYS=Darwin
OS_VERSION=23.0
PKGNAME=lenient-1.0
PKGPATH=test/lenient
PKGTOOLS_VERSION=20091115
PKG_OPTIONS=opt1 opt2
PREV_PKGPATH=test/old-lenient
PROVIDES=/opt/pkg/lib/libfoo.dylib
REQUIRES=/opt/pkg/lib/libbar.dylib
SIZE_PKG=100
SUPERSEDES=old-pkg-[0-9]*
UNKNOWN_FIELD=ignored
"};
let pkg = parse_summary(input.trim(), true, false)?;
assert_eq!(pkg.pkgname().pkgname(), "lenient-1.0");
assert_eq!(pkg.comment(), "Lenient test");
assert_eq!(pkg.build_date(), "2025-01-01");
assert_eq!(pkg.categories(), &["test"]);
assert_eq!(pkg.machine_arch(), "x86_64");
assert_eq!(pkg.opsys(), "Darwin");
assert_eq!(pkg.os_version(), "23.0");
assert_eq!(pkg.pkgpath(), "test/lenient");
assert_eq!(pkg.pkgtools_version(), "20091115");
assert_eq!(pkg.size_pkg(), 100);
assert_eq!(pkg.description(), &["Line 1", "Line 2"]);
assert_eq!(pkg.homepage(), Some("https://example.com/"));
assert_eq!(pkg.license(), Some("isc"));
assert_eq!(pkg.pkg_options(), Some("opt1 opt2"));
assert_eq!(pkg.prev_pkgpath(), Some("test/old-lenient"));
assert_eq!(pkg.file_name(), Some("lenient-1.0.tgz"));
assert_eq!(pkg.file_size(), Some(5000));
assert_eq!(pkg.file_cksum(), Some("SHA256:abc123"));
assert_eq!(
pkg.conflicts(),
Some(["old-pkg-[0-9]*".to_string()].as_slice())
);
assert_eq!(
pkg.depends(),
Some(
["dep1>=1.0".to_string(), "dep2-[0-9]*".to_string()].as_slice()
)
);
assert_eq!(
pkg.provides(),
Some(["/opt/pkg/lib/libfoo.dylib".to_string()].as_slice())
);
assert_eq!(
pkg.requires(),
Some(["/opt/pkg/lib/libbar.dylib".to_string()].as_slice())
);
assert_eq!(
pkg.supersedes(),
Some(["old-pkg-[0-9]*".to_string()].as_slice())
);
Ok(())
}
}