pub mod clean;
pub mod identity;
pub mod sha256;
pub mod tree;
pub use clean::{UnsafeCacheRoot, clean_cache, guard_cache_root};
pub use identity::{absent_marker, content_hash, input_hash};
pub use sha256::sha256_hex;
pub use tree::hash_source_tree;
use std::fs;
use std::io;
use std::io::Write;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
use cargoless_proto::InputHash;
static PUT_SEQ: AtomicU64 = AtomicU64::new(0);
pub trait ContentStore {
fn put(&self, key: &InputHash, bytes: &[u8]) -> io::Result<()>;
fn get(&self, key: &InputHash) -> io::Result<Option<Vec<u8>>>;
fn contains(&self, key: &InputHash) -> io::Result<bool> {
Ok(self.get(key)?.is_some())
}
}
pub struct LocalDiskStore {
root: PathBuf,
}
impl LocalDiskStore {
pub fn new(root: impl Into<PathBuf>) -> Self {
Self { root: root.into() }
}
fn path_for(&self, key: &InputHash) -> PathBuf {
self.root.join(key.as_str())
}
fn tmp_path(final_path: &Path) -> PathBuf {
let seq = PUT_SEQ.fetch_add(1, Ordering::Relaxed);
let pid = std::process::id();
let name = final_path
.file_name()
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or_default();
let dir = final_path
.parent()
.map(Path::to_path_buf)
.unwrap_or_default();
dir.join(format!(".tmp.{name}.{pid}.{seq}"))
}
}
impl ContentStore for LocalDiskStore {
fn put(&self, key: &InputHash, bytes: &[u8]) -> io::Result<()> {
let path = self.path_for(key);
if path.exists() {
return Ok(());
}
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
let tmp = Self::tmp_path(&path);
let write_then_sync = || -> io::Result<()> {
let mut f = fs::File::create(&tmp)?;
f.write_all(bytes)?;
f.sync_all()?; Ok(())
};
if let Err(e) = write_then_sync() {
let _ = fs::remove_file(&tmp);
return Err(e);
}
match fs::rename(&tmp, &path) {
Ok(()) => Ok(()),
Err(e) => {
let _ = fs::remove_file(&tmp);
if path.exists() { Ok(()) } else { Err(e) }
}
}
}
fn get(&self, key: &InputHash) -> io::Result<Option<Vec<u8>>> {
match fs::read(self.path_for(key)) {
Ok(bytes) => Ok(Some(bytes)),
Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(None),
Err(e) => Err(e),
}
}
}
fn scratch_dir(tag: &str) -> PathBuf {
let mut p: PathBuf = std::env::temp_dir();
p.push(format!("cargoless-cas-{tag}-{}", std::process::id()));
p
}
pub fn default_scratch_root() -> PathBuf {
scratch_dir("default")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn roundtrip_and_dedupe() {
let dir = scratch_dir("roundtrip");
let _ = fs::remove_dir_all(&dir);
let store = LocalDiskStore::new(&dir);
let key = InputHash::new("abc123");
assert!(!store.contains(&key).unwrap());
store.put(&key, b"hello").unwrap();
assert!(store.contains(&key).unwrap());
assert_eq!(store.get(&key).unwrap().as_deref(), Some(&b"hello"[..]));
store.put(&key, b"ignored-second-write").unwrap();
assert_eq!(store.get(&key).unwrap().as_deref(), Some(&b"hello"[..]));
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn missing_key_is_none() {
let store = LocalDiskStore::new(default_scratch_root());
assert!(
store
.get(&InputHash::new("nope-not-here"))
.unwrap()
.is_none()
);
}
use std::sync::Arc;
use std::sync::Barrier;
use std::thread;
fn big_payload(seed: u8) -> Vec<u8> {
(0..256 * 1024)
.map(|i| (i as u8).wrapping_add(seed))
.collect()
}
#[test]
fn concurrent_same_key_never_torn_read() {
let dir = scratch_dir("cc-same-key");
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).unwrap();
let store = Arc::new(LocalDiskStore::new(&dir));
let key = InputHash::new(content_hash(&big_payload(0)).as_str());
let payload = Arc::new(big_payload(0));
let writers = 16usize;
let readers = 8usize;
let gate = Arc::new(Barrier::new(writers + readers));
let mut handles = Vec::new();
for _ in 0..writers {
let (s, k, p, g) = (store.clone(), key.clone(), payload.clone(), gate.clone());
handles.push(thread::spawn(move || {
g.wait();
for _ in 0..8 {
s.put(&k, &p).unwrap();
}
}));
}
for _ in 0..readers {
let (s, k, p, g) = (store.clone(), key.clone(), payload.clone(), gate.clone());
handles.push(thread::spawn(move || {
g.wait();
for _ in 0..200 {
if let Some(got) = s.get(&k).unwrap() {
assert_eq!(
got.len(),
p.len(),
"torn read: short file observed mid-write"
);
assert_eq!(&got, &*p, "torn read: corrupt content");
}
}
}));
}
for h in handles {
h.join().unwrap();
}
assert_eq!(store.get(&key).unwrap().as_deref(), Some(&payload[..]));
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn concurrent_distinct_keys_all_land_intact() {
let dir = scratch_dir("cc-distinct");
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).unwrap();
let store = Arc::new(LocalDiskStore::new(&dir));
let n = 24u8;
let gate = Arc::new(Barrier::new(n as usize));
let mut handles = Vec::new();
for seed in 0..n {
let (s, g) = (store.clone(), gate.clone());
handles.push(thread::spawn(move || {
let payload = big_payload(seed);
let key = InputHash::new(content_hash(&payload).as_str());
g.wait();
s.put(&key, &payload).unwrap();
(key, payload)
}));
}
for h in handles {
let (key, payload) = h.join().unwrap();
let got = store.get(&key).unwrap().expect("key must be present");
assert_eq!(got, payload, "cross-key corruption under concurrency");
assert_eq!(content_hash(&got).as_str(), key.as_str());
}
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn hash_reduction_is_concurrency_stable() {
use cargoless_proto::{BuildIdentity, ContentHash, Profile, TargetTriple};
let identity = BuildIdentity {
source_tree: ContentHash::new("src-tree"),
cargo_lock: ContentHash::new("lock"),
rust_toolchain: ContentHash::new("toolchain"),
tf_config: ContentHash::new("cfg"),
target: TargetTriple::new("wasm32-unknown-unknown"),
profile: Profile::Dev,
};
let expect = input_hash(&identity).as_str().to_string();
let identity = Arc::new(identity);
let gate = Arc::new(Barrier::new(32));
let mut handles = Vec::new();
for _ in 0..32 {
let (id, g, want) = (identity.clone(), gate.clone(), expect.clone());
handles.push(thread::spawn(move || {
g.wait();
for _ in 0..50 {
assert_eq!(
input_hash(&id).as_str(),
want,
"InputHash diverged under concurrency — \
wire-format preimage is not stable"
);
}
}));
}
for h in handles {
h.join().unwrap();
}
}
}