use std::path::Path;
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct CarriedWork {
pub line_status: Vec<String>,
pub line_index: Vec<String>,
pub attestations: usize,
pub attestation_blobs: usize,
pub other: Vec<String>,
pub unreadable: Option<String>,
}
impl CarriedWork {
pub fn is_empty(&self) -> bool {
self.line_status.is_empty()
&& self.line_index.is_empty()
&& self.attestations == 0
&& self.attestation_blobs == 0
&& self.other.is_empty()
&& self.unreadable.is_none()
}
pub fn describe(&self) -> String {
let mut parts: Vec<String> = Vec::new();
for line in &self.line_status {
parts.push(format!("a baseline line-status for {line}"));
}
for line in &self.line_index {
parts.push(format!("a signed line-index for {line}"));
}
if self.attestations > 0 {
parts.push(format!(
"{} carried attestation{}",
self.attestations,
plural(self.attestations)
));
} else if self.attestation_blobs > 0 {
parts.push(format!(
"{} attestation blob{}",
self.attestation_blobs,
plural(self.attestation_blobs)
));
}
for kind in &self.other {
parts.push(format!("a referrer of type '{kind}'"));
}
if let Some(reason) = &self.unreadable {
parts.push(format!(
"an index.json this varve cannot read ({reason}), so what it carries could not \
be established"
));
}
if parts.is_empty() {
return "nothing".to_string();
}
parts.join(", ")
}
pub fn recovery(&self, dest: &str) -> String {
let mut lines: Vec<String> = Vec::new();
for _ in &self.line_status {
lines.push(
" varve sign-status --file <status.json> --key <KEYFILE> --out <status.dsse>"
.to_string(),
);
lines.push(format!(
" varve attach-status --layout {dest} --status <status.dsse>"
));
}
for _ in &self.line_index {
lines.push(
" varve sign-index --file <index.json> --key <KEYFILE> --out <index.dsse>"
.to_string(),
);
lines.push(format!(
" varve attach-index --layout {dest} --index <index.dsse>"
));
}
if self.attestations > 0 || self.attestation_blobs > 0 {
lines.push(format!(
" varve sign-attestation --kind <kind> --file <evidence> --key <KEYFILE> \
--out <statement> --attach-to {dest} (once per attestation; the layer must \
be installed first)"
));
}
if lines.is_empty() {
return String::new();
}
format!("{}\n", lines.join("\n"))
}
}
fn plural(n: usize) -> &'static str {
if n == 1 { "" } else { "s" }
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WouldDestroy {
pub dest: String,
pub found: String,
pub recover: String,
}
impl std::fmt::Display for WouldDestroy {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let WouldDestroy {
dest,
found,
recover,
} = self;
write!(
f,
"refusing to write a layout into {dest}: it already carries signed work this would \
destroy — {found}. Writing a layout rewrites index.json wholesale, so every \
referrer above would be dropped and nothing would say so; for a realm declaring \
`signed-index = true` every consumer install afterwards fails closed. Nothing has \
been written — {dest} is byte-identical to what it was. Write into a FRESH \
directory, or write there and re-attach:\n{recover}\
Re-run with --force to overwrite the layout and drop them deliberately."
)
}
}
impl std::error::Error for WouldDestroy {}
pub fn guard(dest: &Path, force: bool) -> Result<(), WouldDestroy> {
if force {
return Ok(());
}
let carried = scan(dest);
if carried.is_empty() {
return Ok(());
}
let dest = dest.display().to_string();
Err(WouldDestroy {
found: carried.describe(),
recover: carried.recovery(&dest),
dest,
})
}
pub fn scan(layout: &Path) -> CarriedWork {
let index_path = layout.join("index.json");
let bytes = match std::fs::read(&index_path) {
Ok(bytes) => bytes,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return CarriedWork::default(),
Err(source) => {
return CarriedWork {
unreadable: Some(source.to_string()),
..CarriedWork::default()
};
}
};
let index: serde_json::Value = match serde_json::from_slice(&bytes) {
Ok(index) => index,
Err(e) => {
return CarriedWork {
unreadable: Some(e.to_string()),
..CarriedWork::default()
};
}
};
let Some(entries) = index["manifests"].as_array() else {
return CarriedWork::default();
};
let mut work = CarriedWork::default();
for entry in entries {
let Some(kind) = entry["artifactType"].as_str() else {
continue;
};
match kind {
crate::archive::SIGNATURE_ARTIFACT_TYPE => {}
crate::linestatus::LINE_STATUS_ARTIFACT_TYPE => work.line_status.push(
entry["annotations"][crate::linestatus::ANN_LINE]
.as_str()
.unwrap_or("an unnamed line")
.to_string(),
),
crate::lineindex::LINE_INDEX_ARTIFACT_TYPE => work.line_index.push(
entry["annotations"][crate::lineindex::ANN_INDEX_LINE]
.as_str()
.unwrap_or("an unnamed line")
.to_string(),
),
crate::attestcarry::STATEMENT_ARTIFACT_TYPE => work.attestations += 1,
crate::attestcarry::ATTESTATION_ARTIFACT_TYPE => work.attestation_blobs += 1,
other => {
if !work.other.iter().any(|k| k == other) {
work.other.push(other.to_string());
}
}
}
}
work
}
#[cfg(test)]
mod tests {
use super::*;
fn layout_with(entries: serde_json::Value) -> tempfile::TempDir {
let tmp = tempfile::tempdir().unwrap();
std::fs::write(
tmp.path().join("index.json"),
serde_json::to_vec_pretty(&serde_json::json!({
"schemaVersion": 2,
"manifests": entries,
}))
.unwrap(),
)
.unwrap();
tmp
}
#[test]
fn a_freshly_deposited_layout_carries_nothing_to_destroy() {
let tmp = layout_with(serde_json::json!([
{ "mediaType": "application/vnd.oci.image.index.v1+json", "digest": "sha256:aa" },
{
"mediaType": "application/json",
"artifactType": crate::archive::SIGNATURE_ARTIFACT_TYPE,
"digest": "sha256:bb",
},
]));
let work = scan(tmp.path());
assert!(work.is_empty(), "{work:?}");
assert_eq!(work.describe(), "nothing");
assert_eq!(work.recovery("/out"), "");
}
#[test]
fn an_absent_layout_carries_nothing_and_an_unreadable_one_is_not_silently_empty() {
let empty = tempfile::tempdir().unwrap();
assert!(scan(empty.path()).is_empty());
let broken = tempfile::tempdir().unwrap();
std::fs::write(broken.path().join("index.json"), b"{not json").unwrap();
let work = scan(broken.path());
assert!(
!work.is_empty(),
"an unreadable index must not read back as 'carries nothing'"
);
assert!(work.describe().contains("could not be established"));
}
#[test]
fn every_referrer_kind_is_named_including_one_varve_does_not_know() {
let tmp = layout_with(serde_json::json!([
{
"artifactType": crate::linestatus::LINE_STATUS_ARTIFACT_TYPE,
"digest": "sha256:aa",
"annotations": { crate::linestatus::ANN_LINE: "2026.08" },
},
{
"artifactType": crate::lineindex::LINE_INDEX_ARTIFACT_TYPE,
"digest": "sha256:bb",
"annotations": { crate::lineindex::ANN_INDEX_LINE: "2026.08" },
},
{
"artifactType": crate::attestcarry::STATEMENT_ARTIFACT_TYPE,
"digest": "sha256:cc",
},
{
"artifactType": crate::attestcarry::ATTESTATION_ARTIFACT_TYPE,
"digest": "sha256:dd",
},
{ "artifactType": "application/vnd.someone.invented.this.v1", "digest": "sha256:ee" },
]));
let work = scan(tmp.path());
assert_eq!(work.line_status, vec!["2026.08".to_string()]);
assert_eq!(work.line_index, vec!["2026.08".to_string()]);
assert_eq!(work.attestations, 1);
assert_eq!(work.attestation_blobs, 1);
assert_eq!(
work.other,
vec!["application/vnd.someone.invented.this.v1".to_string()]
);
assert!(!work.is_empty());
let described = work.describe();
for expected in [
"a baseline line-status for 2026.08",
"a signed line-index for 2026.08",
"1 carried attestation",
"application/vnd.someone.invented.this.v1",
] {
assert!(described.contains(expected), "{described}");
}
let recovery = work.recovery("/tmp/layout");
for expected in [
"varve sign-status --file",
"varve attach-status --layout /tmp/layout",
"varve sign-index --file",
"varve attach-index --layout /tmp/layout",
"--attach-to /tmp/layout",
] {
assert!(recovery.contains(expected), "{recovery}");
}
}
#[test]
fn an_attestation_that_lost_its_statement_still_counts_as_work() {
let tmp = layout_with(serde_json::json!([
{
"artifactType": crate::attestcarry::ATTESTATION_ARTIFACT_TYPE,
"digest": "sha256:dd",
},
]));
let work = scan(tmp.path());
assert!(!work.is_empty());
assert!(work.describe().contains("1 attestation blob"), "{work:?}");
assert!(work.recovery("/out").contains("sign-attestation"));
}
}