use std::fs;
use std::io;
use std::path::Path;
use serde::{Deserialize, Serialize};
use super::fragment_types::{AnyFragment, FragHandlerRegistration, FragPublish, FragRegistration};
use super::ordered::OrderedMap;
use super::paths::{atomic_write_json, fragments_cache_path, fragments_index_path};
use super::rebuild::GraphFile;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct FragmentCacheEntry {
pub mtime: i64,
pub fragment: AnyFragment,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct OwnerStamped<V> {
payload: u32,
owner: Option<String>,
entries: OrderedMap<V>,
}
pub(super) const PAYLOAD_FORM: u32 = 1;
impl<V> OwnerStamped<V> {
fn of(root: &Path, entries: OrderedMap<V>) -> Self {
OwnerStamped {
payload: PAYLOAD_FORM,
owner: owning_worktree(root),
entries,
}
}
}
fn parse_stamped<V: serde::de::DeserializeOwned>(text: &str) -> Option<OwnerStamped<V>> {
serde_json::from_str::<OwnerStamped<V>>(text)
.ok()
.filter(|stamped| stamped.payload == PAYLOAD_FORM)
}
fn owning_worktree(root: &Path) -> Option<String> {
crate::freshness::record::owner_of(root)
}
pub(crate) fn read_fragments_cache(root: &Path) -> OrderedMap<FragmentCacheEntry> {
let Ok(text) = fs::read_to_string(fragments_cache_path(root)) else {
return OrderedMap::default();
};
let Some(stamped) = parse_stamped::<FragmentCacheEntry>(&text) else {
return OrderedMap::default();
};
if stamped.owner != owning_worktree(root) {
return OrderedMap::default();
}
stamped.entries
}
pub(crate) fn write_fragments_cache(
root: &Path,
cache: &OrderedMap<FragmentCacheEntry>,
) -> io::Result<()> {
atomic_write_json(
&fragments_cache_path(root),
&OwnerStamped::of(root, cache.clone()),
)
}
pub fn read_fragments_index(root: &Path) -> OrderedMap<i64> {
let Ok(text) = fs::read_to_string(fragments_index_path(root)) else {
return OrderedMap::default();
};
let Some(stamped) = parse_stamped::<i64>(&text) else {
return OrderedMap::default();
};
if stamped.owner != owning_worktree(root) {
return OrderedMap::default();
}
stamped.entries
}
pub(crate) fn write_fragments_index(
root: &Path,
cache: &OrderedMap<FragmentCacheEntry>,
) -> io::Result<()> {
let mut index = OrderedMap::new();
for (rel, entry) in cache.iter() {
index.insert(rel.clone(), entry.mtime);
}
atomic_write_json(&fragments_index_path(root), &OwnerStamped::of(root, index))
}
pub fn index_is_stale(index: &OrderedMap<i64>, graph_files: &[GraphFile]) -> bool {
graph_files
.iter()
.any(|f| index.get(&f.rel) != Some(&f.mtime))
|| index.len() != graph_files.len()
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum StrictCacheError {
Missing,
Unreadable,
ForeignOwner,
Changed,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct FragMessaging {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub registrations: Vec<FragRegistration>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub publishes: Vec<FragPublish>,
#[serde(
default,
rename = "handlerRegistrations",
skip_serializing_if = "Vec::is_empty"
)]
pub handler_registrations: Vec<FragHandlerRegistration>,
}
#[derive(Debug, Clone, Deserialize)]
struct LightEntry {
#[serde(default)]
fragment: FragMessaging,
}
pub(crate) fn read_fragments_messaging(
root: &Path,
owner: &str,
) -> Result<Vec<(String, FragMessaging)>, StrictCacheError> {
let path = fragments_cache_path(root);
let before = crate::freshness::record::ArtifactIdentity::of(&path);
let text = fs::read_to_string(&path).map_err(|_| StrictCacheError::Missing)?;
let stamped = parse_stamped::<LightEntry>(&text).ok_or(StrictCacheError::Unreadable)?;
if stamped.owner.as_deref() != Some(owner) {
return Err(StrictCacheError::ForeignOwner);
}
let after = crate::freshness::record::ArtifactIdentity::of(&path);
if before.is_none() || before != after {
return Err(StrictCacheError::Changed);
}
Ok(stamped
.entries
.iter()
.map(|(rel, entry)| (rel.clone(), entry.fragment.clone()))
.collect())
}
pub(crate) fn superseded_cache_present(root: &Path) -> bool {
let current = fragments_cache_path(root);
let Some(dir) = current.parent().map(Path::to_path_buf) else {
return false;
};
let Ok(entries) = fs::read_dir(&dir) else {
return false;
};
entries.flatten().any(|entry| {
let name = entry.file_name();
let Some(name) = name.to_str() else {
return false;
};
entry.path() != current && name.starts_with("fragments-v") && name.ends_with(".json")
})
}