use crate::kv::Kv;
use crate::{Depend, PkgName, PkgPath};
use std::fmt;
use std::io::{self, BufRead};
use std::path::PathBuf;
use std::str::FromStr;
#[derive(Clone, Debug, PartialEq, Eq, Kv)]
pub struct ScanIndex {
pub pkgname: PkgName,
pub pkg_location: Option<PkgPath>,
pub all_depends: Option<Vec<Depend>>,
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<Vec<PathBuf>>,
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)
}
}
impl fmt::Display for ScanIndex {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> 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 {
for (i, d) in deps.iter().enumerate() {
if i > 0 {
write!(f, " ")?;
}
write!(f, "{d}")?;
}
}
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 paths) = self.scan_depends {
for (i, p) in paths.iter().enumerate() {
if i > 0 {
write!(f, " ")?;
}
write!(f, "{}", p.display())?;
}
}
writeln!(f)?;
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 let Some(ref deps) = self.resolved_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(())
}
}
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 {
lines: reader.lines(),
buffer: String::new(),
done: false,
}
}
}
pub struct ScanIndexIter<R> {
lines: io::Lines<R>,
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 {
match self.lines.next() {
Some(Ok(line)) => {
if line.starts_with("PKGNAME=") && !self.buffer.is_empty() {
let record = std::mem::take(&mut self.buffer);
self.buffer.push_str(&line);
self.buffer.push('\n');
return Some(parse_record(&record));
}
self.buffer.push_str(&line);
self.buffer.push('\n');
}
Some(Err(e)) => return Some(Err(e)),
None => {
self.done = true;
if self.buffer.is_empty() {
return None;
}
return Some(parse_record(&std::mem::take(
&mut self.buffer,
)));
}
}
}
}
}
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 result: Result<Vec<_>, _> =
ScanIndex::from_reader(input.as_bytes()).collect();
assert!(result.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 err = ScanIndex::from_str(input)
.err()
.ok_or(KvError::Incomplete("expected error".to_string()))?;
match err {
KvError::Parse { message, span } => {
assert!(message.contains("Invalid DEPENDS"));
assert_eq!(span.offset, 29);
assert_eq!(span.len, 7);
assert_eq!(
&input[span.offset..span.offset + span.len],
"invalid"
);
}
_ => panic!("expected Parse error, got {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 display_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.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.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
.resolved_depends
.as_ref()
.ok_or(KvError::Incomplete("resolved_depends".to_string()))?;
assert_eq!(deps.len(), 2);
assert_eq!(deps[0].pkgname(), "foo-1.0");
assert_eq!(deps[1].pkgname(), "bar-2.0");
assert!(index.to_string().contains("DEPENDS=foo-1.0 bar-2.0"));
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.to_string();
let reparsed = ScanIndex::from_str(&output)?;
assert_eq!(index.resolved_depends, reparsed.resolved_depends);
Ok(())
}
}