use crate::{CapabilityId, Config, ConfigError, IdError, ParseError, Reason, Row};
use std::collections::BTreeMap;
const EVIDENCE_COLUMNS: [&str; 4] = ["probed_at", "tool_version", "environment", "inherited_from"];
const PROBE_HERE: &str = "test";
const PROBE_FILE: &str = "probe";
pub(crate) fn parse(text: &str) -> Result<Vec<Row>, ParseError> {
let parsed = capant::parse(text).map_err(error_from)?;
let (header, data) = lines_of(text);
let reason_at = header.iter().position(|c| *c == "reason");
parsed
.matrix
.rows()
.iter()
.zip(data)
.map(|(r, (line, raw))| {
let reason_text = reason_at
.and_then(|i| raw.split('\t').nth(i))
.unwrap_or_default();
row_from(r, line, reason_text)
})
.collect()
}
pub(crate) fn write(rows: &[Row]) -> String {
capant::write(&capant::Matrix::from_rows(
rows.iter().map(row_to).collect(),
))
}
fn lines_of(text: &str) -> (Vec<&str>, Vec<(usize, &str)>) {
let mut header = None;
let mut data = Vec::new();
for (i, raw) in text.lines().enumerate() {
let l = raw.trim_end_matches('\r');
if l.is_empty() || l.starts_with('#') {
continue;
}
if header.is_none() {
header = Some(l.split('\t').collect());
} else {
data.push((i + 1, l));
}
}
(header.unwrap_or_default(), data)
}
fn error_from(e: capant::ParseError) -> ParseError {
use capant::ParseError as E;
match e {
E::NoHeader => ParseError::NoHeader,
E::MissingColumn(c) => ParseError::MissingColumn(c),
E::DuplicateColumn(c) => ParseError::DuplicateColumn(c),
E::FieldCount { line, want, got } => ParseError::FieldCount { line, want, got },
E::BadId { line, error } => ParseError::BadId {
line,
error: match error {
capant::IdError::Empty => IdError::Empty,
capant::IdError::BadDot => IdError::BadDot,
capant::IdError::BadChar { at } => IdError::BadChar { at },
_ => IdError::BadChar { at: 0 },
},
},
E::BadEnabled { line, value } => ParseError::BadEnabled { line, value },
E::ReasonMismatch { line } => ParseError::ReasonMismatch { line },
E::BadConfig {
line,
column,
error,
} => ParseError::BadConfig {
line,
column,
error: match error {
capant::ConfigError::BadPair(p) => ConfigError::BadPair(p),
capant::ConfigError::DuplicateKey(k) => ConfigError::DuplicateKey(k),
other => ConfigError::BadPair(other.to_string()),
},
},
E::BadTimestamp { line, value } => ParseError::BadTimestamp { line, value },
other => ParseError::Other(other.to_string()),
}
}
fn row_from(r: &capant::Row, line: usize, reason_text: &str) -> Result<Row, ParseError> {
let subject =
CapabilityId::new(r.subject.as_str()).map_err(|error| ParseError::BadId { line, error })?;
let (enabled, reason) = match &r.state {
capant::State::Yes => (true, None),
capant::State::No(capant::Reason::Missing) => (false, Some(Reason::Missing)),
capant::State::No(capant::Reason::Wrong) => (false, Some(Reason::Wrong)),
capant::State::No(capant::Reason::Other(t)) if t == "untested" => {
(false, Some(Reason::Untested))
}
_ => {
return Err(ParseError::BadReason {
line,
value: reason_text.to_owned(),
});
}
};
let bad = |column: &'static str, error: ConfigError| ParseError::BadConfig {
line,
column,
error,
};
let config = Config::parse(&r.config.to_string()).map_err(|e| bad("config", e))?;
let mut metadata = Config::parse(&r.metadata.to_string()).map_err(|e| bad("metadata", e))?;
if let Some(probe) = &r.evidence.probe {
let key = if metadata.get(PROBE_HERE).is_some() {
PROBE_FILE
} else {
PROBE_HERE
};
metadata = metadata.with(key, probe.as_str());
}
let mut extra: BTreeMap<String, String> = r.extra.clone();
let e = &r.evidence;
if let Some(t) = e.probed_at {
extra.insert("probed_at".into(), capant::time::format(t));
}
if let Some(v) = &e.tool_version {
extra.insert("tool_version".into(), v.clone());
}
if !e.environment.is_empty() {
extra.insert("environment".into(), e.environment.to_string());
}
if let Some(o) = &e.inherited_from {
let v = if o.is_empty() {
"?".to_owned()
} else {
o.to_string()
};
extra.insert("inherited_from".into(), v);
}
Ok(Row {
subject_kind: r.subject_kind.clone(),
subject,
backend: r.backend.clone(),
version: r.version.clone(),
config,
enabled,
reason,
metadata,
note: r.note.clone(),
extra,
})
}
fn config_to(c: &Config, skip: Option<&str>) -> capant::Config {
c.iter()
.filter(|(k, _)| Some(*k) != skip)
.fold(capant::Config::new(), |acc, (k, v)| acc.with(k, v))
}
fn row_to(r: &Row) -> capant::Row {
let state = match (r.enabled, r.reason) {
(true, _) => capant::State::Yes,
(false, Some(Reason::Missing)) => capant::State::No(capant::Reason::Missing),
(false, Some(Reason::Wrong)) => capant::State::No(capant::Reason::Wrong),
(false, Some(Reason::Untested)) => {
capant::State::No(capant::Reason::Other("untested".to_owned()))
}
(false, None) => capant::State::No(capant::Reason::Other(String::new())),
};
let probe_key = [PROBE_FILE, PROBE_HERE]
.into_iter()
.find(|k| r.metadata.get(k).is_some());
let mut evidence = capant::Evidence {
probe: probe_key.and_then(|k| r.metadata.get(k)).map(str::to_owned),
..capant::Evidence::default()
};
let mut extra = r.extra.clone();
let mut take = |name: &str| extra.remove(name).filter(|v| !v.is_empty());
for name in EVIDENCE_COLUMNS {
let Some(v) = take(name) else { continue };
match name {
"probed_at" => evidence.probed_at = capant::time::parse(&v),
"tool_version" => evidence.tool_version = Some(v),
"environment" => evidence.environment = capant::Config::parse(&v).unwrap_or_default(),
_ => {
evidence.inherited_from = if v == "?" {
Some(capant::Config::new())
} else {
capant::Config::parse(&v).ok()
}
}
}
}
capant::Row {
subject_kind: r.subject_kind.clone(),
subject: capant::CapabilityId::new(r.subject.as_str())
.expect("this crate's id grammar is inside capant's"),
backend: r.backend.clone(),
version: r.version.clone(),
config: config_to(&r.config, None),
state,
evidence,
note: r.note.clone(),
metadata: config_to(&r.metadata, probe_key),
extra,
}
}