use crate::core::hash::Sha256;
use anyhow::{bail, Context, Result};
use std::fs;
use std::path::{Path, PathBuf};
#[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_store() -> Result<Self> {
if let Ok(val) = std::env::var("MITTS_CACHE_DIR") {
return Self::new(PathBuf::from(val).join("sources"));
}
if let Some(cargo_src) = Self::discover_cargo_registry_src() {
return Self::new(cargo_src);
}
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")
};
Self::new(base_dir.join("sources"))
}
pub fn discover_cargo_registry_src() -> Option<PathBuf> {
let home = std::env::var("HOME").ok()?;
let reg_src = Path::new(&home).join(".cargo").join("registry").join("src");
if reg_src.is_dir() {
if let Ok(entries) = fs::read_dir(®_src) {
for entry in entries.flatten() {
let name = entry.file_name().to_string_lossy().to_string();
if name.starts_with("index.crates.io-") {
return Some(entry.path());
}
}
}
let default_dir = reg_src.join("index.crates.io-1949cf8c6b5b557f");
let _ = fs::create_dir_all(&default_dir);
return Some(default_dir);
}
None
}
pub fn discover_cargo_registry_cache() -> Option<PathBuf> {
let home = std::env::var("HOME").ok()?;
let reg_cache = Path::new(&home)
.join(".cargo")
.join("registry")
.join("cache");
if reg_cache.is_dir() {
if let Ok(entries) = fs::read_dir(®_cache) {
for entry in entries.flatten() {
let name = entry.file_name().to_string_lossy().to_string();
if name.starts_with("index.crates.io-") {
return Some(entry.path());
}
}
}
let default_dir = reg_cache.join("index.crates.io-1949cf8c6b5b557f");
let _ = fs::create_dir_all(&default_dir);
return Some(default_dir);
}
None
}
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 has_source(&self, name: &str, version: &str) -> bool {
let dir = self.package_dir(name, version);
dir.is_dir() && dir.join("Cargo.toml").is_file()
}
pub fn unpack_archive(
&self,
name: &str,
version: &str,
archive_bytes: &[u8],
expected_checksum: &str,
) -> Result<PathBuf> {
let actual_checksum = Sha256::digest(archive_bytes);
if !expected_checksum.is_empty() && actual_checksum != expected_checksum {
bail!(
"Checksum mismatch for crate {}-{}: expected {}, got {}",
name,
version,
expected_checksum,
actual_checksum
);
}
let target_dir = self.package_dir(name, version);
if let Some(cargo_cache) = Self::discover_cargo_registry_cache() {
let crate_archive = cargo_cache.join(format!("{}-{}.crate", name, version));
if !crate_archive.is_file() {
let _ = fs::write(&crate_archive, archive_bytes);
}
}
if self.has_source(name, version) {
return Ok(target_dir);
}
let run_id = format!(
"{}-{}-{}",
name,
version,
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0)
);
let staging_dir = self.locks_dir.join(format!("{}.staging", run_id));
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.unpack(&staging_dir).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.is_dir() && nested_dir.join("Cargo.toml").is_file() {
nested_dir
} else {
staging_dir.clone()
};
if target_dir.exists() {
let _ = fs::remove_dir_all(&target_dir);
}
if let Err(e) = fs::rename(&src_to_move, &target_dir) {
Self::copy_dir_recursive(&src_to_move, &target_dir).with_context(|| {
format!(
"Failed to move unpacked crate to destination {:?}: {}",
target_dir, e
)
})?;
}
let _ = fs::remove_dir_all(&staging_dir);
if !self.has_source(name, version) {
bail!(
"Extracted crate {}-{} is missing Cargo.toml in {:?}",
name,
version,
target_dir
);
}
Ok(target_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(())
}
}