use crate::error::{Error, Result};
use crate::hash::Hash;
use crate::store::Store;
use std::fs;
use std::path::PathBuf;
pub struct RefManager<'a> {
store: &'a Store,
}
impl<'a> RefManager<'a> {
pub(crate) fn new(store: &'a Store) -> Self {
Self { store }
}
fn ref_path(&self, name: &str) -> Result<PathBuf> {
if name.contains("..") || name.contains('/') || name.contains('\\') {
return Err(Error::invalid_ref(format!(
"Invalid ref name: {} (must not contain .. or path separators)",
name
)));
}
if name.is_empty() {
return Err(Error::invalid_ref("Ref name cannot be empty"));
}
Ok(self.store.root().join("refs").join(name))
}
pub fn add(&self, name: &str, hash: &Hash) -> Result<()> {
let path = self.ref_path(name)?;
let hash_str = format!("{}\n", hash.to_hex());
let mut file = fs::OpenOptions::new()
.create(true)
.append(true)
.open(&path)?;
use std::io::Write;
file.write_all(hash_str.as_bytes())?;
Ok(())
}
pub fn get(&self, name: &str) -> Result<Option<Hash>> {
let path = self.ref_path(name)?;
if !path.exists() {
return Ok(None);
}
let content = fs::read_to_string(&path)?;
let mut last_hash = None;
for line in content.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
match Hash::from_hex(line) {
Ok(hash) => last_hash = Some(hash),
Err(_) => continue, }
}
Ok(last_hash)
}
pub fn list(&self) -> Result<Vec<(String, Hash)>> {
let refs_dir = self.store.root().join("refs");
let mut refs = Vec::new();
if !refs_dir.exists() {
return Ok(refs);
}
for entry in fs::read_dir(&refs_dir)? {
let entry = entry?;
let path = entry.path();
if path.is_file()
&& let Some(name) = path.file_name().and_then(|n| n.to_str())
&& let Some(hash) = self.get(name)?
{
refs.push((name.to_string(), hash));
}
}
refs.sort_by(|a, b| a.0.cmp(&b.0));
Ok(refs)
}
pub fn remove(&self, name: &str) -> Result<()> {
let path = self.ref_path(name)?;
if !path.exists() {
return Err(Error::ref_not_found(name));
}
fs::remove_file(&path)?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::hash::Algorithm;
use tempfile::TempDir;
#[test]
fn test_ref_add_and_get() {
let temp_dir = TempDir::new().unwrap();
let store = Store::init(temp_dir.path(), Algorithm::Blake3).unwrap();
let refs = store.refs();
let hash = Hash::hash_bytes(b"test");
refs.add("myref", &hash).unwrap();
let retrieved = refs.get("myref").unwrap();
assert_eq!(retrieved, Some(hash));
}
#[test]
fn test_ref_get_nonexistent() {
let temp_dir = TempDir::new().unwrap();
let store = Store::init(temp_dir.path(), Algorithm::Blake3).unwrap();
let refs = store.refs();
let retrieved = refs.get("nonexistent").unwrap();
assert_eq!(retrieved, None);
}
#[test]
fn test_ref_update() {
let temp_dir = TempDir::new().unwrap();
let store = Store::init(temp_dir.path(), Algorithm::Blake3).unwrap();
let refs = store.refs();
let hash1 = Hash::hash_bytes(b"test1");
let hash2 = Hash::hash_bytes(b"test2");
refs.add("myref", &hash1).unwrap();
refs.add("myref", &hash2).unwrap();
let retrieved = refs.get("myref").unwrap();
assert_eq!(retrieved, Some(hash2));
}
#[test]
fn test_ref_list() {
let temp_dir = TempDir::new().unwrap();
let store = Store::init(temp_dir.path(), Algorithm::Blake3).unwrap();
let refs = store.refs();
let hash1 = Hash::hash_bytes(b"test1");
let hash2 = Hash::hash_bytes(b"test2");
refs.add("ref1", &hash1).unwrap();
refs.add("ref2", &hash2).unwrap();
let list = refs.list().unwrap();
assert_eq!(list.len(), 2);
assert_eq!(list[0].0, "ref1");
assert_eq!(list[0].1, hash1);
assert_eq!(list[1].0, "ref2");
assert_eq!(list[1].1, hash2);
}
#[test]
fn test_ref_remove() {
let temp_dir = TempDir::new().unwrap();
let store = Store::init(temp_dir.path(), Algorithm::Blake3).unwrap();
let refs = store.refs();
let hash = Hash::hash_bytes(b"test");
refs.add("myref", &hash).unwrap();
refs.remove("myref").unwrap();
let retrieved = refs.get("myref").unwrap();
assert_eq!(retrieved, None);
}
#[test]
fn test_ref_invalid_name() {
let temp_dir = TempDir::new().unwrap();
let store = Store::init(temp_dir.path(), Algorithm::Blake3).unwrap();
let refs = store.refs();
let hash = Hash::hash_bytes(b"test");
assert!(refs.add("../etc/passwd", &hash).is_err());
assert!(refs.add("foo/bar", &hash).is_err());
assert!(refs.add("", &hash).is_err());
}
use proptest::prelude::*;
proptest! {
#![proptest_config(ProptestConfig {
cases: 256,
max_shrink_iters: 10000,
..ProptestConfig::default()
})]
#[test]
fn prop_valid_ref_names_accepted(
name in "[a-zA-Z0-9_-]{1,50}"
.prop_filter("no path separators or dots", |n| {
!n.contains("..") && !n.contains('/') && !n.contains('\\')
})
) {
let temp_dir = TempDir::new().unwrap();
let store = Store::init(temp_dir.path(), Algorithm::Blake3)?;
let refs = store.refs();
let hash = Hash::hash_bytes(b"test data");
let result = refs.add(&name, &hash);
prop_assert!(
result.is_ok(),
"Valid ref name '{}' should be accepted",
name
);
let retrieved = refs.get(&name)?;
prop_assert_eq!(retrieved, Some(hash), "Should retrieve the same hash");
}
}
}