Bootstrap geth Rust workspace

This commit is contained in:
Eric Wendland 2026-05-15 15:08:20 +02:00
commit 26f81ff1ef
73 changed files with 4835 additions and 0 deletions

View file

@ -0,0 +1,15 @@
[package]
name = "geth-cas"
version = "0.1.0"
edition.workspace = true
rust-version.workspace = true
license.workspace = true
[dependencies]
blake3.workspace = true
hex.workspace = true
thiserror.workspace = true
geth-types = { path = "../geth-types" }
[dev-dependencies]
tempfile.workspace = true

157
crates/geth-cas/src/lib.rs Normal file
View file

@ -0,0 +1,157 @@
use geth_types::BlobHash;
use std::path::{Path, PathBuf};
#[derive(Debug, thiserror::Error)]
pub enum CasError {
#[error("io error: {0}")]
Io(#[from] std::io::Error),
#[error("invalid blob hash: {0}")]
InvalidHash(String),
#[error("blob not found: {0}")]
NotFound(String),
}
#[derive(Clone, Debug)]
pub struct LocalCas {
root: PathBuf,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct BlobInfo {
pub hash: BlobHash,
pub size_bytes: u64,
pub path: PathBuf,
}
impl LocalCas {
#[must_use]
pub fn new(root: impl Into<PathBuf>) -> Self {
Self { root: root.into() }
}
pub fn add_path(&self, path: &Path) -> Result<BlobInfo, CasError> {
let bytes = std::fs::read(path)?;
self.add_bytes(&bytes)
}
pub fn add_bytes(&self, bytes: &[u8]) -> Result<BlobInfo, CasError> {
let hash = hash_bytes(bytes);
let path = self.blob_path(&hash)?;
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
if !path.exists() {
let tmp = path.with_extension("tmp");
std::fs::write(&tmp, bytes)?;
std::fs::rename(tmp, &path)?;
}
Ok(BlobInfo {
hash,
size_bytes: bytes.len() as u64,
path,
})
}
pub fn get_to_path(&self, hash: &BlobHash, out: &Path) -> Result<u64, CasError> {
let path = self.blob_path(hash)?;
if !path.exists() {
return Err(CasError::NotFound(hash.to_string()));
}
if let Some(parent) = out.parent() {
std::fs::create_dir_all(parent)?;
}
std::fs::copy(path, out).map_err(CasError::from)
}
pub fn has(&self, hash: &BlobHash) -> Result<bool, CasError> {
Ok(self.blob_path(hash)?.exists())
}
pub fn list(&self) -> Result<Vec<BlobInfo>, CasError> {
let blobs = self.root.join("blobs");
if !blobs.exists() {
return Ok(Vec::new());
}
let mut infos = Vec::new();
for first in std::fs::read_dir(blobs)? {
let first = first?;
if !first.file_type()?.is_dir() {
continue;
}
for second in std::fs::read_dir(first.path())? {
let second = second?;
if !second.file_type()?.is_dir() {
continue;
}
for entry in std::fs::read_dir(second.path())? {
let entry = entry?;
if !entry.file_type()?.is_file() {
continue;
}
let hash = entry.file_name().to_string_lossy().to_string();
if is_valid_hash(&hash) {
let meta = entry.metadata()?;
infos.push(BlobInfo {
hash: BlobHash::new(hash),
size_bytes: meta.len(),
path: entry.path(),
});
}
}
}
}
infos.sort_by(|a, b| a.hash.as_str().cmp(b.hash.as_str()));
Ok(infos)
}
pub fn blob_path(&self, hash: &BlobHash) -> Result<PathBuf, CasError> {
validate_hash(hash)?;
let hash = hash.as_str();
Ok(self
.root
.join("blobs")
.join(&hash[0..2])
.join(&hash[2..4])
.join(hash))
}
}
#[must_use]
pub fn hash_bytes(bytes: &[u8]) -> BlobHash {
BlobHash::new(blake3::hash(bytes).to_hex().to_string())
}
pub fn hash_path(path: &Path) -> Result<BlobHash, CasError> {
let bytes = std::fs::read(path)?;
Ok(hash_bytes(&bytes))
}
pub fn validate_hash(hash: &BlobHash) -> Result<(), CasError> {
if is_valid_hash(hash.as_str()) {
Ok(())
} else {
Err(CasError::InvalidHash(hash.to_string()))
}
}
#[must_use]
pub fn is_valid_hash(hash: &str) -> bool {
hash.len() == 64 && hash.bytes().all(|byte| byte.is_ascii_hexdigit())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cas_add_get_has_list() {
let dir = tempfile::tempdir().expect("tempdir");
let cas = LocalCas::new(dir.path());
let info = cas.add_bytes(b"hello geth").expect("add");
assert!(cas.has(&info.hash).expect("has"));
assert_eq!(cas.list().expect("list").len(), 1);
let out = dir.path().join("out.txt");
cas.get_to_path(&info.hash, &out).expect("get");
assert_eq!(std::fs::read(out).expect("read"), b"hello geth");
}
}