use crate::kv::Kv;
use crate::{Depend, DependError, PkgName, PkgPath};
use std::fmt;
use std::io::{self, BufRead};
use std::path::Path;
use std::str::FromStr;
#[derive(Clone, Debug, Default, Eq, Hash, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(transparent))]
pub struct ScanDepends(String);
impl ScanDepends {
fn items(&self) -> std::str::SplitAsciiWhitespace<'_> {
self.0.split_ascii_whitespace()
}
pub fn iter(&self) -> impl Iterator<Item = &Path> {
self.items().map(Path::new)
}
pub fn len(&self) -> usize {
self.items().count()
}
pub fn is_empty(&self) -> bool {
self.items().next().is_none()
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl AsRef<str> for ScanDepends {
fn as_ref(&self) -> &str {
&self.0
}
}
impl fmt::Display for ScanDepends {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl From<&str> for ScanDepends {
fn from(s: &str) -> Self {
ScanDepends(s.to_string())
}
}
impl crate::kv::FromKv for ScanDepends {
fn from_kv(value: &str, _span: crate::kv::Span) -> crate::kv::Result<Self> {
Ok(ScanDepends(value.to_string()))
}
}
#[derive(Clone, Debug, Default, Eq, Hash, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(transparent))]
pub struct AllDepends(String);
impl AllDepends {
fn items(&self) -> std::str::SplitAsciiWhitespace<'_> {
self.0.split_ascii_whitespace()
}
pub fn iter(&self) -> AllDependsIter<'_> {
AllDependsIter(self.items())
}
pub fn depends(
&self,
) -> impl Iterator<Item = Result<Depend, DependError>> + '_ {
self.items().map(Depend::new)
}
pub fn len(&self) -> usize {
self.items().count()
}
pub fn is_empty(&self) -> bool {
self.items().next().is_none()
}
pub fn as_str(&self) -> &str {
&self.0
}
}
pub struct AllDependsIter<'a>(std::str::SplitAsciiWhitespace<'a>);
impl<'a> Iterator for AllDependsIter<'a> {
type Item = Result<RawDepend<'a>, DependError>;
fn next(&mut self) -> Option<Self::Item> {
self.0.next().map(RawDepend::new)
}
}
impl<'a> IntoIterator for &'a AllDepends {
type Item = Result<RawDepend<'a>, DependError>;
type IntoIter = AllDependsIter<'a>;
fn into_iter(self) -> Self::IntoIter {
self.iter()
}
}
impl AsRef<str> for AllDepends {
fn as_ref(&self) -> &str {
&self.0
}
}
impl fmt::Display for AllDepends {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl From<&str> for AllDepends {
fn from(s: &str) -> Self {
AllDepends(s.to_string())
}
}
impl<S: AsRef<str>> FromIterator<S> for AllDepends {
fn from_iter<I: IntoIterator<Item = S>>(iter: I) -> Self {
let mut out = String::new();
for item in iter {
if !out.is_empty() {
out.push(' ');
}
out.push_str(item.as_ref());
}
AllDepends(out)
}
}
impl crate::kv::FromKv for AllDepends {
fn from_kv(value: &str, _span: crate::kv::Span) -> crate::kv::Result<Self> {
Ok(AllDepends(value.to_string()))
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct RawDepend<'a> {
raw: &'a str,
colon: usize,
}
impl<'a> RawDepend<'a> {
fn new(raw: &'a str) -> Result<Self, DependError> {
let colon = raw.find(':').ok_or(DependError::Invalid)?;
if raw[colon + 1..].contains(':') {
return Err(DependError::Invalid);
}
Ok(RawDepend { raw, colon })
}
pub fn pattern(&self) -> &'a str {
&self.raw[..self.colon]
}
pub fn pkgpath(&self) -> &'a str {
&self.raw[self.colon + 1..]
}
pub fn as_str(&self) -> &'a str {
self.raw
}
}
impl fmt::Display for RawDepend<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.raw)
}
}
#[derive(Clone, Debug, Default, Eq, Hash, Kv, PartialEq)]
pub struct ScanIndex {
pub pkgname: PkgName,
pub pkg_location: Option<PkgPath>,
pub all_depends: Option<AllDepends>,
pub pkg_skip_reason: Option<String>,
pub pkg_fail_reason: Option<String>,
pub no_bin_on_ftp: Option<String>,
pub restricted: Option<String>,
pub categories: Option<String>,
pub maintainer: Option<String>,
pub use_destdir: Option<String>,
pub bootstrap_pkg: Option<String>,
pub usergroup_phase: Option<String>,
pub scan_depends: Option<ScanDepends>,
pub make_jobs_safe: Option<String>,
pub pbulk_weight: Option<String>,
pub multi_version: Option<Vec<String>>,
#[kv(variable = "DEPENDS")]
pub resolved_depends: Option<Vec<PkgName>>,
}
impl FromStr for ScanIndex {
type Err = crate::kv::KvError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::parse(s)
}
}
#[derive(Clone, Copy)]
enum FormatMode {
Pscan,
Presolve,
Report,
}
impl ScanIndex {
#[must_use]
pub fn pscan(&self) -> Pscan<'_> {
Pscan(self)
}
#[must_use]
pub fn presolve(&self) -> Presolve<'_> {
Presolve(self)
}
#[must_use]
pub fn report(&self) -> Report<'_> {
Report(self)
}
#[must_use]
pub fn depends(&self) -> &[PkgName] {
self.resolved_depends.as_deref().unwrap_or(&[])
}
fn fmt_record(
&self,
f: &mut fmt::Formatter<'_>,
mode: FormatMode,
) -> fmt::Result {
writeln!(f, "PKGNAME={}", self.pkgname)?;
if let Some(ref v) = self.pkg_location {
writeln!(f, "PKG_LOCATION={v}")?;
}
write!(f, "ALL_DEPENDS=")?;
if let Some(ref deps) = self.all_depends {
write!(f, "{deps}")?;
}
writeln!(f)?;
writeln!(f, "PKG_SKIP_REASON={}", opt_str(&self.pkg_skip_reason))?;
writeln!(f, "PKG_FAIL_REASON={}", opt_str(&self.pkg_fail_reason))?;
writeln!(f, "NO_BIN_ON_FTP={}", opt_str(&self.no_bin_on_ftp))?;
writeln!(f, "RESTRICTED={}", opt_str(&self.restricted))?;
writeln!(f, "CATEGORIES={}", opt_str(&self.categories))?;
writeln!(f, "MAINTAINER={}", opt_str(&self.maintainer))?;
writeln!(f, "USE_DESTDIR={}", opt_str(&self.use_destdir))?;
writeln!(f, "BOOTSTRAP_PKG={}", opt_str(&self.bootstrap_pkg))?;
writeln!(f, "USERGROUP_PHASE={}", opt_str(&self.usergroup_phase))?;
write!(f, "SCAN_DEPENDS=")?;
if let Some(ref deps) = self.scan_depends {
write!(f, "{deps}")?;
}
writeln!(f)?;
if let Some(ref v) = self.make_jobs_safe {
writeln!(f, "MAKE_JOBS_SAFE={v}")?;
}
if !matches!(mode, FormatMode::Report) {
if let Some(ref v) = self.pbulk_weight {
writeln!(f, "PBULK_WEIGHT={v}")?;
}
}
if let Some(ref vars) = self.multi_version {
if !vars.is_empty() {
write!(f, "MULTI_VERSION=")?;
for v in vars {
write!(f, " {v}")?;
}
writeln!(f)?;
}
}
if !matches!(mode, FormatMode::Pscan) {
let deps = self.depends();
if !deps.is_empty() {
write!(f, "DEPENDS=")?;
for (i, d) in deps.iter().enumerate() {
if i > 0 {
write!(f, " ")?;
}
write!(f, "{d}")?;
}
writeln!(f)?;
}
}
Ok(())
}
}
pub struct Pscan<'a>(&'a ScanIndex);
impl fmt::Display for Pscan<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt_record(f, FormatMode::Pscan)
}
}
pub struct Presolve<'a>(&'a ScanIndex);
impl fmt::Display for Presolve<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt_record(f, FormatMode::Presolve)
}
}
pub struct Report<'a>(&'a ScanIndex);
impl fmt::Display for Report<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt_record(f, FormatMode::Report)
}
}
fn opt_str(o: &Option<String>) -> &str {
o.as_deref().unwrap_or("")
}
impl ScanIndex {
pub fn from_reader<R: BufRead>(reader: R) -> ScanIndexIter<R> {
ScanIndexIter {
reader,
line_buf: String::new(),
buffer: String::new(),
done: false,
}
}
}
pub struct ScanIndexIter<R> {
reader: R,
line_buf: String,
buffer: String,
done: bool,
}
impl<R: BufRead> Iterator for ScanIndexIter<R> {
type Item = io::Result<ScanIndex>;
fn next(&mut self) -> Option<Self::Item> {
if self.done {
return None;
}
loop {
self.line_buf.clear();
match self.reader.read_line(&mut self.line_buf) {
Ok(0) => {
self.done = true;
if self.buffer.is_empty() {
return None;
}
return Some(parse_record(&std::mem::take(
&mut self.buffer,
)));
}
Ok(_) => {
if self.line_buf.starts_with("PKGNAME=")
&& !self.buffer.is_empty()
{
let record = std::mem::take(&mut self.buffer);
self.buffer.push_str(&self.line_buf);
return Some(parse_record(&record));
}
self.buffer.push_str(&self.line_buf);
}
Err(e) => return Some(Err(e)),
}
}
}
}
fn parse_record(s: &str) -> io::Result<ScanIndex> {
s.parse()
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::kv::KvError;
use anyhow::Context;
use std::fs::File;
use std::io::BufReader;
use std::path::PathBuf;
#[test]
fn multi_input() -> anyhow::Result<()> {
let mut scanfile = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
scanfile.push("tests/data/scanindex/pbulk-index.txt");
let file = File::open(&scanfile)?;
let reader = BufReader::new(file);
let index: Vec<_> =
ScanIndex::from_reader(reader).collect::<Result<_, _>>()?;
assert_eq!(index.len(), 40);
let all_depends = index[0]
.all_depends
.as_ref()
.context("missing all_depends")?;
assert_eq!(all_depends.len(), 11);
let scan_depends = index[0]
.scan_depends
.as_ref()
.context("missing scan_depends")?;
assert_eq!(scan_depends.len(), 155);
let multi_version = index[0]
.multi_version
.as_ref()
.context("missing multi_version")?;
assert_eq!(multi_version.len(), 2);
Ok(())
}
#[test]
fn duplicate_pkgname() -> Result<(), io::Error> {
let input = "PKGNAME=foo\nPKGNAME=foo\n";
let index: Vec<_> = ScanIndex::from_reader(input.as_bytes())
.collect::<Result<_, _>>()?;
assert_eq!(index.len(), 2);
Ok(())
}
#[test]
fn no_input() -> Result<(), io::Error> {
let input = "";
let index: Vec<_> = ScanIndex::from_reader(input.as_bytes())
.collect::<Result<_, _>>()?;
assert_eq!(index.len(), 0);
Ok(())
}
#[test]
fn empty_input() -> Result<(), io::Error> {
let input = "PKGNAME=";
let index: Vec<_> = ScanIndex::from_reader(input.as_bytes())
.collect::<Result<_, _>>()?;
assert_eq!(index.len(), 1);
assert_eq!(index[0].pkgname.pkgname(), "");
Ok(())
}
#[test]
fn input_error() {
let input = "ALL_DEPENDS=";
let result: Result<Vec<_>, _> =
ScanIndex::from_reader(input.as_bytes()).collect();
assert!(result.is_err());
let input = "PKGNAME=\nALL_DEPENDS=hello\n";
let index: Vec<_> = ScanIndex::from_reader(input.as_bytes())
.collect::<Result<_, io::Error>>()
.expect("lazy parse should succeed");
let deps = index[0]
.all_depends
.as_ref()
.expect("should have all_depends");
assert!(deps.iter().any(|r| r.is_err()));
assert!(deps.depends().any(|r| r.is_err()));
}
#[test]
fn from_str() -> Result<(), KvError> {
use std::str::FromStr;
let input = "PKGNAME=test-1.0\nMAINTAINER=test@example.com\n";
let index = ScanIndex::from_str(input)?;
assert_eq!(index.pkgname.pkgname(), "test-1.0");
assert_eq!(index.maintainer.as_deref(), Some("test@example.com"));
Ok(())
}
#[test]
fn error_unknown_variable() -> Result<(), KvError> {
use std::str::FromStr;
let input = "PKGNAME=test-1.0\nUNKNOWN=value\n";
let err = ScanIndex::from_str(input)
.err()
.ok_or(KvError::Incomplete("expected error".to_string()))?;
match err {
KvError::UnknownVariable { variable, span } => {
assert_eq!(variable, "UNKNOWN");
assert_eq!(span.offset, 17);
assert_eq!(span.len, 7);
assert_eq!(
&input[span.offset..span.offset + span.len],
"UNKNOWN"
);
}
_ => panic!("expected UnknownVariable error, got {err:?}"),
}
Ok(())
}
#[test]
fn error_invalid_depend() -> Result<(), KvError> {
use std::str::FromStr;
let input = "PKGNAME=test-1.0\nALL_DEPENDS=invalid\n";
let index = ScanIndex::from_str(input)?;
let deps = index
.all_depends
.as_ref()
.ok_or(KvError::Incomplete("all_depends".to_string()))?;
assert_eq!(deps.len(), 1);
assert_eq!(deps.as_str(), "invalid");
let results: Vec<_> = deps.iter().collect();
assert!(results[0].is_err());
let results: Vec<_> = deps.depends().collect();
assert!(results[0].is_err());
Ok(())
}
#[test]
fn error_invalid_pkgpath() -> Result<(), KvError> {
use std::str::FromStr;
let input = "PKGNAME=test-1.0\nPKG_LOCATION=bad\n";
let err = ScanIndex::from_str(input)
.err()
.ok_or(KvError::Incomplete("expected error".to_string()))?;
match err {
KvError::Parse { message, span } => {
assert!(message.contains("Invalid path"));
assert_eq!(span.offset, 30);
assert_eq!(span.len, 3);
assert_eq!(&input[span.offset..span.offset + span.len], "bad");
}
_ => panic!("expected Parse error, got {err:?}"),
}
Ok(())
}
#[test]
fn error_missing_pkgname() -> Result<(), KvError> {
use std::str::FromStr;
let input = "MAINTAINER=test@example.com\n";
let err = ScanIndex::from_str(input)
.err()
.ok_or(KvError::Incomplete("expected error".to_string()))?;
match err {
KvError::Incomplete(field) => {
assert_eq!(field, "PKGNAME");
}
_ => panic!("expected Incomplete error, got {err:?}"),
}
Ok(())
}
#[test]
fn error_bad_line_format() -> Result<(), KvError> {
use std::str::FromStr;
let input = "PKGNAME=test-1.0\nbadline\n";
let err = ScanIndex::from_str(input)
.err()
.ok_or(KvError::Incomplete("expected error".to_string()))?;
match err {
KvError::ParseLine(span) => {
assert_eq!(span.offset, 17);
assert_eq!(span.len, 7);
assert_eq!(
&input[span.offset..span.offset + span.len],
"badline"
);
}
_ => panic!("expected ParseLine error, got {err:?}"),
}
Ok(())
}
#[test]
fn error_span_accessor() -> Result<(), KvError> {
use std::str::FromStr;
let input = "PKGNAME=test-1.0\nUNKNOWN=value\n";
let err = ScanIndex::from_str(input)
.err()
.ok_or(KvError::Incomplete("expected error".to_string()))?;
let span = err
.span()
.ok_or(KvError::Incomplete("expected span".to_string()))?;
assert_eq!(&input[span.offset..span.offset + span.len], "UNKNOWN");
Ok(())
}
#[test]
fn pscan_roundtrip() -> anyhow::Result<()> {
let mut scanfile = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
scanfile.push("tests/data/scanindex/pbulk-index.txt");
let file = File::open(&scanfile)?;
let reader = BufReader::new(file);
let original: Vec<_> =
ScanIndex::from_reader(reader).collect::<Result<_, _>>()?;
let output: String =
original.iter().map(|s| s.pscan().to_string()).collect();
let reparsed: Vec<_> = ScanIndex::from_reader(output.as_bytes())
.collect::<Result<_, _>>()?;
assert_eq!(original, reparsed);
Ok(())
}
#[test]
fn resolved_depends_none() -> Result<(), KvError> {
use std::str::FromStr;
let input = "PKGNAME=test-1.0\n";
let index = ScanIndex::from_str(input)?;
assert!(index.resolved_depends.is_none());
assert!(index.depends().is_empty());
assert!(!index.presolve().to_string().contains("\nDEPENDS="));
assert!(!index.pscan().to_string().contains("\nDEPENDS="));
Ok(())
}
#[test]
fn resolved_depends_some() -> Result<(), KvError> {
use std::str::FromStr;
let input = "PKGNAME=test-1.0\nDEPENDS=foo-1.0 bar-2.0\n";
let index = ScanIndex::from_str(input)?;
let deps = index.depends();
assert_eq!(deps.len(), 2);
assert_eq!(deps[0].pkgname(), "foo-1.0");
assert_eq!(deps[1].pkgname(), "bar-2.0");
assert!(
index
.presolve()
.to_string()
.contains("DEPENDS=foo-1.0 bar-2.0")
);
assert!(!index.pscan().to_string().contains("\nDEPENDS="));
Ok(())
}
#[test]
fn resolved_depends_roundtrip() -> Result<(), KvError> {
use std::str::FromStr;
let input = "PKGNAME=test-1.0\nDEPENDS=foo-1.0 bar-2.0\n";
let index = ScanIndex::from_str(input)?;
let output = index.presolve().to_string();
let reparsed = ScanIndex::from_str(&output)?;
assert_eq!(index.resolved_depends, reparsed.resolved_depends);
Ok(())
}
}