use std::{
collections::BTreeSet,
fs,
path::{Component, Path},
};
use crate::interface::CliError;
const EXPECTED: [(&str, &str, &str, &str); 6] = [
(
"pi-bootstrap",
"pi-bootstrap",
"pi-bootstrap_good.txt",
"pi-bootstrap_bad.txt",
),
(
"least-authority",
"least-authority",
"v656/least-authority_good.txt",
"v656/least-authority_bad.txt",
),
(
"first-run",
"first-run",
"v656/first-run_good.txt",
"v656/first-run_bad.txt",
),
(
"gate-provenance",
"gate-provenance",
"v656/gate-provenance_good.txt",
"v656/gate-provenance_bad.txt",
),
(
"release-trust",
"release-trust",
"v656/release-trust_good.txt",
"v656/release-trust_bad.txt",
),
(
"harness-semantics",
"harness-semantics",
"v656/harness-semantics_good.txt",
"v656/harness-semantics_bad.txt",
),
];
const LEGACY: [&str; 14] = [
"reflection_good.txt",
"reflection_bad.txt",
"discovery_good.txt",
"discovery_bad.txt",
"dispatch_good.txt",
"dispatch_bad.txt",
"pi_tool_call_id_good.txt",
"pi_tool_call_id_bad.txt",
"content_good.txt",
"content_bad.txt",
"plugin_distribution_good.txt",
"plugin_distribution_bad.txt",
"cargo_native_distribution_good.txt",
"cargo_native_distribution_bad.txt",
];
#[derive(Clone, Debug, Eq, PartialEq)]
struct Row {
owner: String,
kind: String,
good: String,
bad: String,
}
pub(crate) fn validate_v656(
manifest: &Path,
cases: &Path,
rubrics: &Path,
) -> Result<String, CliError> {
let raw =
fs::read(manifest).map_err(|e| err(format!("cannot read {}: {e}", manifest.display())))?;
let rows = parse(&raw)?;
contract(&rows)?;
for row in &rows {
regular(&cases.join(&row.good), "good sibling", &row.good)?;
regular(&cases.join(&row.bad), "bad sibling", &row.bad)?;
regular(
&rubrics.join(format!("{}.rubric.json", row.kind)),
"rubric",
&row.kind,
)?;
}
Ok(rows
.iter()
.map(|r| format!("{}\t{}\t{}\t{}", r.owner, r.kind, r.good, r.bad))
.collect::<Vec<_>>()
.join("\n")
+ "\n")
}
fn parse(raw: &[u8]) -> Result<Vec<Row>, CliError> {
if !raw.is_empty() && !raw.ends_with(b"\n") {
return Err(err("manifest must end with a newline"));
}
let text = std::str::from_utf8(raw).map_err(|e| err(format!("manifest must be UTF-8: {e}")))?;
let mut rows = Vec::new();
for (index, line) in text.lines().enumerate() {
let number = index + 1;
if line.is_empty() {
continue;
}
if line.starts_with('#') {
if number == 1 && line == "# owner\tkind\tgood\tbad" {
continue;
}
return Err(err(if number == 1 {
"manifest header comment is malformed".into()
} else {
format!("unexpected manifest comment on line {number}")
}));
}
let fields = line.split('\t').collect::<Vec<_>>();
if fields.len() != 4 || fields.iter().any(|field| field.is_empty()) {
return Err(err(format!(
"manifest line {number} must contain exactly four non-empty TSV fields"
)));
}
safe(fields[2], "good input")?;
safe(fields[3], "bad input")?;
if fields[2] == fields[3] {
return Err(err(format!(
"same good/bad input for {}: {}",
fields[0], fields[2]
)));
}
rows.push(Row {
owner: fields[0].into(),
kind: fields[1].into(),
good: fields[2].into(),
bad: fields[3].into(),
});
}
Ok(rows)
}
fn safe(value: &str, label: &str) -> Result<(), CliError> {
let path = Path::new(value);
if value.is_empty()
|| value.contains('\\')
|| path.is_absolute()
|| !path.components().all(|c| matches!(c, Component::Normal(_)))
{
return Err(err(format!(
"{label} must be a contained relative POSIX path: {value}"
)));
}
Ok(())
}
fn contract(rows: &[Row]) -> Result<(), CliError> {
if rows.len() != 6 {
return Err(err(format!(
"manifest must contain exactly six owned partitions, found {}",
rows.len()
)));
}
let inputs = rows
.iter()
.flat_map(|r| [&r.good, &r.bad])
.map(String::as_str)
.collect::<Vec<_>>();
let unique = inputs.iter().copied().collect::<BTreeSet<_>>();
if inputs.len() != unique.len() {
return Err(err("duplicate eval input in v656 manifest"));
}
let legacy = LEGACY.iter().copied().collect::<BTreeSet<_>>();
let collisions = unique.intersection(&legacy).copied().collect::<Vec<_>>();
if !collisions.is_empty() {
return Err(err(format!(
"v656 input collides with legacy pair: {}",
collisions.join(", ")
)));
}
let expected = EXPECTED
.iter()
.map(|(o, k, g, b)| Row {
owner: (*o).into(),
kind: (*k).into(),
good: (*g).into(),
bad: (*b).into(),
})
.collect::<Vec<_>>();
if rows != expected {
return Err(err(
"manifest owner/kind/input partitions do not match the closed v656 contract",
));
}
Ok(())
}
fn regular(path: &Path, label: &str, value: &str) -> Result<(), CliError> {
let metadata =
fs::symlink_metadata(path).map_err(|_| err(format!("missing {label}: {value}")))?;
if !metadata.file_type().is_file() {
return Err(err(format!("{label} is not a regular file: {value}")));
}
Ok(())
}
fn err(message: impl Into<String>) -> CliError {
CliError::message(format!("v656 manifest: {}", message.into()))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn syntax_rejects_traversal_and_missing_newline() {
assert!(
parse(b"o\tk\t../g\tb\n")
.unwrap_err()
.message_text()
.is_some_and(|message| message.contains("contained relative POSIX path"))
);
assert!(
parse(b"o\tk\tg\tb")
.unwrap_err()
.message_text()
.is_some_and(|message| message.contains("must end with a newline"))
);
}
#[test]
fn contract_is_exactly_six_canonical_rows() {
let rows = EXPECTED
.iter()
.map(|(o, k, g, b)| Row {
owner: (*o).into(),
kind: (*k).into(),
good: (*g).into(),
bad: (*b).into(),
})
.collect::<Vec<_>>();
contract(&rows).unwrap();
assert!(contract(&rows[..5]).is_err());
}
}