use std::collections::BTreeMap;
use std::fs;
use std::io;
use std::path::Path;
use crate::freshness::record::{
self, ArtifactIdentity, DigestReadError, DigestTable, Record, RecordState,
};
use crate::freshness::validate;
use crate::freshness::{FreshnessState, UnknownReason};
use crate::graph::{self, FragMessaging, Graph, StrictCacheError, GRAPH_SCHEMA_VERSION};
use crate::layout;
use crate::query::{self, TestVia};
use super::refusal::ExportRefusal;
pub(crate) struct Snapshot {
pub(crate) record: Record,
pub(crate) digests: DigestTable,
pub(crate) graph: Graph,
pub(crate) messaging: Vec<(String, FragMessaging)>,
pub(crate) test_via: BTreeMap<String, TestVia>,
pub(crate) compiler_facts: Option<String>,
pub(crate) fragment_generation: u32,
pub(crate) freshness: FreshnessState,
pub(crate) freshness_warning: Option<String>,
}
pub(crate) fn load(root: &Path, invocation_dir: &Path) -> Result<Snapshot, ExportRefusal> {
load_bracketed(root, invocation_dir, &mut |_| {})
}
pub(crate) fn load_bracketed(
root: &Path,
invocation_dir: &Path,
between: &mut dyn FnMut(&Path),
) -> Result<Snapshot, ExportRefusal> {
let begin = validate::begin(root, invocation_dir);
let record = published_record(root)?;
let digests = digest_table(root, &record)?;
let graph_path = graph::graph_json_path(root);
let manifest_path = validate::manifest_artifact_path(root);
let expected_graph = record.artifacts.graph.ok_or_else(|| {
ExportRefusal::ArtifactMissing(format!("the index certifies no {}", layout::GRAPH_FILE))
})?;
if !graph_path.is_file() {
return Err(ExportRefusal::ArtifactMissing(
layout::GRAPH_FILE.to_string(),
));
}
let bytes = fs::read(&graph_path).map_err(|_| {
ExportRefusal::ArtifactUnreadable(format!("{} could not be read", layout::GRAPH_FILE))
})?;
let graph: Graph = serde_json::from_slice(&bytes).map_err(|_| {
ExportRefusal::ArtifactUnreadable(format!("{} does not parse", layout::GRAPH_FILE))
})?;
if graph.schema_version != GRAPH_SCHEMA_VERSION {
return Err(ExportRefusal::UnknownGeneration(format!(
"{} is schema version {}, this build writes {GRAPH_SCHEMA_VERSION}",
layout::GRAPH_FILE,
graph.schema_version
)));
}
if ArtifactIdentity::of(&graph_path) != Some(expected_graph)
|| ArtifactIdentity::of(&manifest_path) != record.artifacts.manifest
{
return Err(ExportRefusal::IndexChanged(format!(
"{} or {} is not the artifact the index record certifies",
layout::GRAPH_FILE,
layout::MANIFEST_FILE
)));
}
let messaging = read_messaging(root, &record)?;
let fragment_identity = ArtifactIdentity::of(&graph::fragments_cache_path(root));
let test_via = classify_test_files(root, &graph, &messaging);
let compiler_identity = ArtifactIdentity::of(&graph::compiler_facts_json_path(root));
let compiler_facts = read_compiler_facts(root)?;
between(root);
let validation = begin.finish();
confirm_unchanged(
root,
&record,
&graph_path,
&manifest_path,
fragment_identity,
compiler_identity,
&validation,
)?;
Ok(Snapshot {
record,
digests,
graph,
messaging,
test_via,
compiler_facts,
fragment_generation: graph::fragment_generation(),
freshness: validation.state,
freshness_warning: validation.warning,
})
}
fn published_record(root: &Path) -> Result<Record, ExportRefusal> {
match record::read_record(root) {
RecordState::Published(record) => Ok(*record),
RecordState::Missing => Err(if graph::graph_json_path(root).is_file() {
ExportRefusal::NoIndexState(format!(
"a {} is here with no published index record",
layout::GRAPH_FILE
))
} else {
ExportRefusal::NoIndex("this repository has no index".to_string())
}),
RecordState::Uncertifiable => Err(ExportRefusal::LegacyIndex(
"the index record is not a shape this build certifies".to_string(),
)),
RecordState::InProgress(_) => Err(ExportRefusal::IndexChanged(
"a `map` is running in this repository".to_string(),
)),
}
}
fn digest_table(root: &Path, record: &Record) -> Result<DigestTable, ExportRefusal> {
let table = match record::read_digests_strict(root) {
Ok(table) => table,
Err(DigestReadError::Missing) => {
return Err(ExportRefusal::LegacyIndex(
"the index has no per-file digest table".to_string(),
))
}
Err(DigestReadError::Unreadable) => {
return Err(ExportRefusal::ArtifactUnreadable(
"the per-file digest table does not parse".to_string(),
))
}
Err(DigestReadError::UnknownFormat) => {
return Err(ExportRefusal::UnknownGeneration(
"the per-file digest table is of an unknown format".to_string(),
))
}
Err(DigestReadError::Changed) => {
return Err(ExportRefusal::IndexChanged(
"the per-file digest table changed while it was read".to_string(),
))
}
};
if table.generation != record.generation {
return Err(ExportRefusal::IndexChanged(
"the per-file digest table belongs to another `map` run".to_string(),
));
}
Ok(table)
}
fn read_messaging(
root: &Path,
record: &Record,
) -> Result<Vec<(String, FragMessaging)>, ExportRefusal> {
match graph::read_fragments_messaging(root, &record.owner) {
Ok(entries) => Ok(entries),
Err(StrictCacheError::Missing) => Err(if graph::superseded_cache_present(root) {
ExportRefusal::UnknownGeneration(
"only a fragment cache of another generation is here".to_string(),
)
} else {
ExportRefusal::ArtifactMissing("the fragment cache".to_string())
}),
Err(StrictCacheError::Unreadable) => Err(ExportRefusal::ArtifactUnreadable(
"the fragment cache does not parse".to_string(),
)),
Err(StrictCacheError::ForeignOwner) => Err(ExportRefusal::IndexChanged(
"the fragment cache was written by another worktree".to_string(),
)),
Err(StrictCacheError::Changed) => Err(ExportRefusal::IndexChanged(
"the fragment cache changed while it was read".to_string(),
)),
}
}
fn classify_test_files(
root: &Path,
graph: &Graph,
messaging: &[(String, FragMessaging)],
) -> BTreeMap<String, TestVia> {
let index = query::load_graph_index(graph, root);
messaging
.iter()
.filter_map(|(file, _)| query::test_via(&index, file).map(|via| (file.clone(), via)))
.collect()
}
fn read_compiler_facts(root: &Path) -> Result<Option<String>, ExportRefusal> {
match fs::read(graph::compiler_facts_json_path(root)) {
Ok(bytes) => Ok(Some(format!("sha256:{}", record::sha256_hex(&bytes)))),
Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(None),
Err(_) => Err(ExportRefusal::ArtifactUnreadable(
"the admitted compiler-fact artifact could not be read".to_string(),
)),
}
}
fn confirm_unchanged(
root: &Path,
record: &Record,
graph_path: &Path,
manifest_path: &Path,
fragment_identity: Option<ArtifactIdentity>,
compiler_identity: Option<ArtifactIdentity>,
validation: &validate::Validation,
) -> Result<(), ExportRefusal> {
if let FreshnessState::Unknown { reason } = &validation.state {
if matches!(
reason,
UnknownReason::IndexChanged | UnknownReason::NoIndexState
) {
return Err(ExportRefusal::IndexChanged(
"the index moved while the export ran".to_string(),
));
}
}
match record::read_record(root) {
RecordState::Published(end) if end.generation == record.generation => {}
_ => {
return Err(ExportRefusal::IndexChanged(
"the index was republished while the export ran".to_string(),
))
}
}
if ArtifactIdentity::of(graph_path) != record.artifacts.graph
|| ArtifactIdentity::of(manifest_path) != record.artifacts.manifest
|| ArtifactIdentity::of(&graph::fragments_cache_path(root)) != fragment_identity
|| ArtifactIdentity::of(&graph::compiler_facts_json_path(root)) != compiler_identity
{
return Err(ExportRefusal::IndexChanged(
"an index artifact changed while the export ran".to_string(),
));
}
Ok(())
}