use crate::core::hash::Sha256;
use anyhow::{bail, Context, Result};
use std::fs;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
static STAGING_SEQ: AtomicU64 = AtomicU64::new(0);
pub const MITTS_OK_MARKER: &str = ".mitts-ok";
#[derive(Debug, Clone)]
pub struct SourceStore {
root: PathBuf,
locks_dir: PathBuf,
}
impl SourceStore {
pub fn new(root: PathBuf) -> Result<Self> {
fs::create_dir_all(&root)
.with_context(|| format!("Failed to create source store at {:?}", root))?;
let locks_dir = root.join(".locks");
fs::create_dir_all(&locks_dir)
.with_context(|| format!("Failed to create source locks dir at {:?}", locks_dir))?;
Ok(Self { root, locks_dir })
}
pub fn default_root() -> PathBuf {
if let Ok(val) = std::env::var("MITTS_CACHE_DIR") {
return PathBuf::from(val).join("sources");
}
let base_dir = if let Ok(home) = std::env::var("HOME") {
PathBuf::from(home).join(".cache").join("mitts")
} else {
PathBuf::from("target").join("mitts").join("cache")
};
base_dir.join("sources")
}
pub fn default_store() -> Result<Self> {
Self::new(Self::default_root())
}
pub fn active_cargo_home() -> Option<PathBuf> {
if let Some(ch) = std::env::var_os("CARGO_HOME").filter(|v| !v.is_empty()) {
return Some(PathBuf::from(ch));
}
std::env::var_os("HOME")
.filter(|v| !v.is_empty())
.map(|h| PathBuf::from(h).join(".cargo"))
}
fn discover_cargo_registry_subdir(sub: &str) -> Option<PathBuf> {
let dir = Self::active_cargo_home()?.join("registry").join(sub);
for entry in fs::read_dir(dir).ok()?.flatten() {
let name = entry.file_name().to_string_lossy().to_string();
if (name.starts_with("index.crates.io-") || name.starts_with("github.com-"))
&& entry.path().is_dir()
{
return Some(entry.path());
}
}
None
}
pub fn discover_cargo_registry_src() -> Option<PathBuf> {
Self::discover_cargo_registry_subdir("src")
}
pub fn discover_cargo_registry_cache() -> Option<PathBuf> {
Self::discover_cargo_registry_subdir("cache")
}
pub fn root(&self) -> &Path {
&self.root
}
pub fn locks_dir(&self) -> &Path {
&self.locks_dir
}
pub fn package_dir(&self, name: &str, version: &str) -> PathBuf {
self.root.join(format!("{}-{}", name, version))
}
pub fn lock_path(&self, name: &str, version: &str) -> PathBuf {
self.locks_dir.join(format!("{}-{}.lock", name, version))
}
pub fn is_valid_source_dir(dir: &Path) -> bool {
dir.join(MITTS_OK_MARKER).is_file() && dir.join("Cargo.toml").is_file()
}
pub fn has_source(&self, name: &str, version: &str) -> bool {
Self::is_valid_source_dir(&self.package_dir(name, version))
}
fn next_staging_dir(&self, name: &str, version: &str, suffix: &str) -> PathBuf {
let seq = STAGING_SEQ.fetch_add(1, Ordering::Relaxed);
self.locks_dir.join(format!(
"{}-{}-{}-{}.{}",
name,
version,
std::process::id(),
seq,
suffix
))
}
pub fn cleanup_dead_staging(&self) -> usize {
let Ok(entries) = fs::read_dir(&self.locks_dir) else {
return 0;
};
let mut removed = 0;
for entry in entries.flatten() {
let path = entry.path();
if !path.is_dir() {
continue;
}
let fname = entry.file_name().to_string_lossy().to_string();
let Some(stem) = fname
.strip_suffix(".staging")
.or_else(|| fname.strip_suffix(".trash"))
else {
continue;
};
let mut parts = stem.rsplitn(3, '-');
let _seq = parts.next();
let Some(pid_str) = parts.next() else {
continue;
};
let Ok(pid) = pid_str.parse::<u32>() else {
continue;
};
if pid == std::process::id() {
continue;
}
#[cfg(unix)]
let is_dead = unsafe {
libc::kill(pid as i32, 0) == -1
&& std::io::Error::last_os_error().raw_os_error() == Some(libc::ESRCH)
};
#[cfg(not(unix))]
let is_dead = false;
let is_stale = entry
.metadata()
.and_then(|m| m.modified())
.ok()
.and_then(|mtime| std::time::SystemTime::now().duration_since(mtime).ok())
.is_some_and(|age| age >= std::time::Duration::from_secs(3600));
if is_dead && is_stale && fs::remove_dir_all(&path).is_ok() {
removed += 1;
}
}
removed
}
fn publish_staged_dir(
&self,
name: &str,
version: &str,
staging_root: &Path,
src_to_move: &Path,
) -> Result<PathBuf> {
let target_dir = self.package_dir(name, version);
if !src_to_move.join("Cargo.toml").is_file() {
let _ = fs::remove_dir_all(staging_root);
bail!(
"Extracted crate {}-{} is missing Cargo.toml in {:?}",
name,
version,
target_dir
);
}
if let Err(e) = fs::write(src_to_move.join(MITTS_OK_MARKER), b"ok\n") {
let _ = fs::remove_dir_all(staging_root);
return Err(e).with_context(|| {
format!("Failed to write {} in {:?}", MITTS_OK_MARKER, src_to_move)
});
}
if self.has_source(name, version) {
let _ = fs::remove_dir_all(staging_root);
return Ok(target_dir);
}
if target_dir.exists() && !self.has_source(name, version) {
let trash_dir = self.next_staging_dir(name, version, "trash");
if fs::rename(&target_dir, &trash_dir).is_ok() {
if trash_dir.join(MITTS_OK_MARKER).is_file() {
let _ = fs::rename(&trash_dir, &target_dir);
} else {
let _ = fs::remove_dir_all(&trash_dir);
}
}
}
match fs::rename(src_to_move, &target_dir) {
Ok(()) => {
if src_to_move != staging_root {
let _ = fs::remove_dir_all(staging_root);
}
Ok(target_dir)
}
Err(e) => {
let _ = fs::remove_dir_all(staging_root);
if self.has_source(name, version) {
return Ok(target_dir);
}
Err(e).with_context(|| {
format!("Failed to atomically publish crate {name}-{version} to {target_dir:?}")
})
}
}
}
pub fn unpack_archive(
&self,
name: &str,
version: &str,
archive_bytes: &[u8],
expected_checksum: &str,
) -> Result<PathBuf> {
if !expected_checksum.is_empty() {
let actual_checksum = Sha256::digest(archive_bytes);
if actual_checksum != expected_checksum {
bail!("Checksum mismatch for crate {name}-{version}: expected {expected_checksum}, got {actual_checksum}");
}
}
let target_dir = self.package_dir(name, version);
if self.has_source(name, version) {
return Ok(target_dir);
}
let staging_dir = self.next_staging_dir(name, version, "staging");
fs::create_dir_all(&staging_dir)
.with_context(|| format!("Failed to create staging dir {staging_dir:?}"))?;
let gz = flate2::read::GzDecoder::new(archive_bytes);
let mut archive = tar::Archive::new(gz);
archive.set_preserve_mtime(false);
archive.set_preserve_permissions(false);
if let Err(e) = archive.unpack(&staging_dir) {
let _ = fs::remove_dir_all(&staging_dir);
return Err(e).with_context(|| {
format!("Failed to unpack crate archive in-memory for {name}-{version}")
});
}
let nested_dir = staging_dir.join(format!("{}-{}", name, version));
let src_to_move = if nested_dir.join("Cargo.toml").is_file() {
nested_dir
} else {
staging_dir.clone()
};
self.publish_staged_dir(name, version, &staging_dir, &src_to_move)
}
pub fn stage_copy_dir(&self, name: &str, version: &str, src_dir: &Path) -> Result<PathBuf> {
let target_dir = self.package_dir(name, version);
if self.has_source(name, version) {
return Ok(target_dir);
}
let staging_dir = self.next_staging_dir(name, version, "staging");
if let Err(e) = Self::copy_dir_recursive(src_dir, &staging_dir) {
let _ = fs::remove_dir_all(&staging_dir);
return Err(e).with_context(|| {
format!(
"Failed to stage copy from {:?} to {:?}",
src_dir, staging_dir
)
});
}
self.publish_staged_dir(name, version, &staging_dir, &staging_dir)
}
pub fn copy_dir_recursive(src: &Path, dst: &Path) -> Result<()> {
fs::create_dir_all(dst)?;
for entry in fs::read_dir(src)? {
let entry = entry?;
let path = entry.path();
let dest_path = dst.join(entry.file_name());
if path.is_dir() {
Self::copy_dir_recursive(&path, &dest_path)?;
} else {
fs::copy(&path, &dest_path)?;
}
}
Ok(())
}
}
pub struct RegistryLookup {
cache_index_dirs: Vec<PathBuf>,
src_index_dirs: Vec<PathBuf>,
}
impl RegistryLookup {
pub fn discover(exclude_store_root: &Path) -> Self {
let mut cache_index_dirs = Vec::new();
let mut src_index_dirs = Vec::new();
let is_crates_io_dir = |entry: &fs::DirEntry| -> bool {
let name = entry.file_name().to_string_lossy().to_string();
(name.starts_with("index.crates.io-") || name.starts_with("github.com-"))
&& entry.path().is_dir()
};
if let Some(reg_dir) = SourceStore::active_cargo_home().map(|ch| ch.join("registry")) {
if let Ok(entries) = fs::read_dir(reg_dir.join("cache")) {
cache_index_dirs.extend(
entries
.flatten()
.filter(&is_crates_io_dir)
.map(|e| e.path()),
);
}
if let Ok(entries) = fs::read_dir(reg_dir.join("src")) {
src_index_dirs.extend(
entries
.flatten()
.filter(&is_crates_io_dir)
.map(|e| e.path())
.filter(|p| p != exclude_store_root),
);
}
}
Self {
cache_index_dirs,
src_index_dirs,
}
}
pub fn find_in_cache(&self, name: &str, version: &str) -> Option<PathBuf> {
let file_name = format!("{}-{}.crate", name, version);
self.cache_index_dirs
.iter()
.map(|dir| dir.join(&file_name))
.find(|c| c.is_file())
}
pub fn find_in_src(&self, name: &str, version: &str) -> Option<PathBuf> {
let dir_name = format!("{}-{}", name, version);
self.src_index_dirs
.iter()
.map(|dir| dir.join(&dir_name))
.find(|c| c.join("Cargo.toml").is_file() && c.join(".cargo-ok").is_file())
}
}