use super::manifest::CargoToml;
use crate::core::hash::Sha256;
use anyhow::{bail, Context, Result};
use std::cell::RefCell;
use std::collections::HashMap;
use std::marker::PhantomData;
use std::path::{Path, PathBuf};
use std::rc::Rc;
use std::sync::{Arc, Mutex, OnceLock};
type Cached<T> = OnceLock<std::result::Result<T, String>>;
struct Entry {
bytes: Arc<str>,
parsed: Cached<CargoToml>,
digests: Cached<(String, String)>,
raw_digest: OnceLock<String>,
}
#[derive(Default)]
struct Contents {
entries: Mutex<HashMap<PathBuf, Arc<Entry>>>,
probes: Mutex<HashMap<PathBuf, bool>>,
errors: Mutex<Vec<String>>,
}
#[derive(Clone, Default)]
pub struct ManifestSnapshot(Arc<Contents>);
impl std::fmt::Debug for ManifestSnapshot {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ManifestSnapshot")
.field("captured_files", &self.captured_file_count())
.finish_non_exhaustive()
}
}
thread_local! {
static CURRENT: RefCell<Option<ManifestSnapshot>> = const { RefCell::new(None) };
}
pub struct ManifestScope {
snapshot: ManifestSnapshot,
previous: Option<ManifestSnapshot>,
_thread_bound: PhantomData<Rc<()>>,
}
impl ManifestSnapshot {
pub fn enter(&self) -> ManifestScope {
let previous = CURRENT.with(|slot| slot.replace(Some(self.clone())));
ManifestScope {
snapshot: self.clone(),
previous,
_thread_bound: PhantomData,
}
}
pub fn captured_file_count(&self) -> usize {
self.0.entries.lock().unwrap().len()
}
fn path(path: &Path) -> Result<PathBuf> {
Ok(if path.is_absolute() {
path.to_path_buf()
} else {
std::env::current_dir()?.join(path)
})
}
fn entry(&self, path: &Path) -> Result<Arc<Entry>> {
let path = Self::path(path)?;
let mut entries = self.0.entries.lock().unwrap();
if let Some(entry) = entries.get(&path) {
return Ok(Arc::clone(entry));
}
let bytes = match std::fs::read_to_string(&path) {
Ok(bytes) => bytes,
Err(error) => {
let message = format!("Failed to read manifest {}: {error}", path.display());
self.0.errors.lock().unwrap().push(message.clone());
bail!(message);
}
};
let entry = Arc::new(Entry {
bytes: Arc::from(bytes),
parsed: OnceLock::new(),
digests: OnceLock::new(),
raw_digest: OnceLock::new(),
});
entries.insert(path, Arc::clone(&entry));
Ok(entry)
}
fn is_file(&self, path: &Path) -> bool {
let path = match Self::path(path) {
Ok(path) => path,
Err(error) => {
self.0.errors.lock().unwrap().push(error.to_string());
return false;
}
};
let mut probes = self.0.probes.lock().unwrap();
if let Some(found) = probes.get(&path) {
return *found;
}
let found = match std::fs::metadata(&path) {
Ok(metadata) => metadata.is_file(),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => false,
Err(error) => {
self.0.errors.lock().unwrap().push(format!(
"Cannot account for manifest {}: {error}",
path.display()
));
false
}
};
probes.insert(path, found);
found
}
pub fn verify_fresh(&self) -> Result<()> {
if let Some(error) = self.0.errors.lock().unwrap().first() {
bail!("Manifest snapshot has an unknown input: {error}");
}
let entries = self.0.entries.lock().unwrap().clone();
let probes = self.0.probes.lock().unwrap().clone();
for (path, entry) in &entries {
let current = std::fs::read(path).with_context(|| {
format!("Manifest changed while using plan: {}", path.display())
})?;
if current != entry.bytes.as_bytes() {
bail!("Manifest changed while using plan: {}", path.display());
}
}
for (path, was_file) in probes {
if entries.contains_key(&path) {
continue;
}
let is_file = match std::fs::metadata(&path) {
Ok(metadata) => metadata.is_file(),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => false,
Err(error) => return Err(error).context("Cannot verify manifest presence"),
};
if is_file != was_file {
bail!(
"Ancestor manifest changed while using plan: {}",
path.display()
);
}
}
Ok(())
}
}
impl ManifestScope {
pub(crate) fn snapshot(&self) -> ManifestSnapshot {
self.snapshot.clone()
}
}
impl Drop for ManifestScope {
fn drop(&mut self) {
CURRENT.with(|slot| slot.replace(self.previous.take()));
}
}
pub(crate) fn current() -> Option<ManifestSnapshot> {
CURRENT.with(|slot| slot.borrow().clone())
}
pub(crate) fn scope() -> ManifestScope {
current().unwrap_or_default().enter()
}
pub(crate) fn read_text(path: &Path) -> Result<Arc<str>> {
match current() {
Some(snapshot) => Ok(Arc::clone(&snapshot.entry(path)?.bytes)),
None => Ok(Arc::from(
std::fs::read_to_string(path).with_context(|| format!("Failed to read {:?}", path))?,
)),
}
}
pub(crate) fn is_manifest_file(path: &Path) -> bool {
current().map_or_else(|| path.is_file(), |snapshot| snapshot.is_file(path))
}
pub(crate) fn parse(path: &Path) -> Result<CargoToml> {
let parse_bytes = |bytes: &str| -> Result<CargoToml> {
let mut manifest = CargoToml::parse(bytes)?;
manifest.resolve_inherited_workspace_fields(path);
Ok(manifest)
};
if let Some(snapshot) = current() {
let entry = snapshot.entry(path)?;
return entry
.parsed
.get_or_init(|| parse_bytes(&entry.bytes).map_err(|error| error.to_string()))
.clone()
.map_err(anyhow::Error::msg);
}
parse_bytes(&read_text(path)?)
}
pub(crate) fn raw_digest(path: &Path) -> Result<String> {
if let Some(snapshot) = current() {
let entry = snapshot.entry(path)?;
return Ok(entry
.raw_digest
.get_or_init(|| Sha256::digest(entry.bytes.as_bytes()))
.clone());
}
Sha256::digest_file(path)
}
pub(crate) fn digests(path: &Path) -> Result<(String, String)> {
if let Some(snapshot) = current() {
let entry = snapshot.entry(path)?;
return entry
.digests
.get_or_init(|| {
super::manifest_digest::digests_from_content(&entry.bytes)
.map_err(|error| error.to_string())
})
.clone()
.map_err(anyhow::Error::msg);
}
super::manifest_digest::digests_from_content(&read_text(path)?)
}