use std::ffi::OsString;
use std::fs;
use std::path::{Path, PathBuf};
use chrono::NaiveDate;
pub const FILENAME_PREFIX: &str = "sigstore_trusted_root_";
pub const FILENAME_SUFFIX: &str = ".json";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EmbeddedSnapshot {
pub date: String,
pub filename: String,
}
#[derive(Debug)]
pub enum DiscoveryError {
ReadDir { dir: PathBuf, err: std::io::Error },
Entry { dir: PathBuf, err: std::io::Error },
FileType { path: PathBuf, err: std::io::Error },
Symlink { path: PathBuf },
InvalidCalendarDate {
filename: String,
date_segment: String,
err: chrono::ParseError,
},
NoCandidates { dir: PathBuf },
MultipleCandidates {
dir: PathBuf,
filenames: Vec<String>,
},
}
impl std::fmt::Display for DiscoveryError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::ReadDir { dir, err } => write!(
f,
"cannot read `{}` to discover the embedded Sigstore trusted_root.json snapshot: {err}",
dir.display()
),
Self::Entry { dir, err } => {
write!(f, "I/O error iterating `{}`: {err}", dir.display())
}
Self::FileType { path, err } => write!(
f,
"cannot determine file type for `{}`: {err}",
path.display()
),
Self::Symlink { path } => write!(
f,
"refusing to follow symlinked snapshot candidate `{}` — \
the embedded TUF trust-root floor MUST be a plain regular file in the \
source tree (Red Team v2 F2-S1: a name-friendly symlink planted at \
checkout time could redirect `include_bytes!` to attacker-controlled \
JSON). Replace the symlink with the actual `sigstore_trusted_root_\
YYYY-MM-DD.json` payload or delete it.",
path.display()
),
Self::InvalidCalendarDate {
filename,
date_segment,
err,
} => write!(
f,
"embedded snapshot `{filename}` has a syntactically \
`YYYY-MM-DD`-shaped but calendar-invalid date `{date_segment}` \
({err}) — refresh-trust authors MUST use a real date (e.g. \
`2026-02-28`, not `2026-02-30`); the runtime \
`TrustRootStalenessAnchor::resolve` `NaiveDate::parse_from_str` would \
otherwise fail at first call."
),
Self::NoCandidates { dir } => write!(
f,
"found zero `sigstore_trusted_root_YYYY-MM-DD.json` \
snapshots under `{}` — the embedded TUF trust-root floor cannot be \
empty (ADR 0013 Mechanism C)",
dir.display()
),
Self::MultipleCandidates { dir, filenames } => write!(
f,
"found {n} `sigstore_trusted_root_YYYY-MM-DD.json` \
snapshots under `{}` ({}) — operator intent is ambiguous; remove all \
but the active snapshot before building",
dir.display(),
filenames.join(", "),
n = filenames.len()
),
}
}
}
impl std::error::Error for DiscoveryError {}
#[derive(Debug, Clone)]
pub struct Discovery {
pub snapshot: EmbeddedSnapshot,
pub matched_filenames: Vec<String>,
}
pub fn discover_embedded_snapshot(dir: &Path) -> Result<Discovery, DiscoveryError> {
let entries = fs::read_dir(dir).map_err(|err| DiscoveryError::ReadDir {
dir: dir.to_path_buf(),
err,
})?;
let mut matched: Vec<EmbeddedSnapshot> = Vec::new();
for entry in entries {
let entry = entry.map_err(|err| DiscoveryError::Entry {
dir: dir.to_path_buf(),
err,
})?;
let file_name: OsString = entry.file_name();
let name_str = match file_name.to_str() {
Some(s) => s,
None => continue, };
let Some(date_and_suffix) = name_str.strip_prefix(FILENAME_PREFIX) else {
continue;
};
let Some(date_segment) = date_and_suffix.strip_suffix(FILENAME_SUFFIX) else {
continue;
};
let file_type = entry.file_type().map_err(|err| DiscoveryError::FileType {
path: entry.path(),
err,
})?;
if file_type.is_symlink() {
return Err(DiscoveryError::Symlink { path: entry.path() });
}
if let Err(err) = NaiveDate::parse_from_str(date_segment, "%Y-%m-%d") {
return Err(DiscoveryError::InvalidCalendarDate {
filename: name_str.to_string(),
date_segment: date_segment.to_string(),
err,
});
}
matched.push(EmbeddedSnapshot {
date: date_segment.to_string(),
filename: name_str.to_string(),
});
}
match matched.len() {
0 => Err(DiscoveryError::NoCandidates {
dir: dir.to_path_buf(),
}),
1 => {
let snapshot = matched.remove(0);
let matched_filenames = vec![snapshot.filename.clone()];
Ok(Discovery {
snapshot,
matched_filenames,
})
}
_ => {
let filenames = matched.into_iter().map(|m| m.filename).collect();
Err(DiscoveryError::MultipleCandidates {
dir: dir.to_path_buf(),
filenames,
})
}
}
}