use alloc::vec::Vec;
use crate::error::{Error, ObjectKindHint, Result};
use crate::vfs::Vfs;
use super::Pointer;
#[derive(Debug, Clone)]
pub struct LfsStore<V: Vfs> {
vfs: V,
}
impl<V: Vfs> LfsStore<V> {
pub fn new(vfs: V) -> Self {
LfsStore { vfs }
}
fn object_path(oid: &str) -> alloc::string::String {
alloc::format!("objects/{}/{}/{}", &oid[..2], &oid[2..4], oid)
}
pub fn contains(&self, oid: &str) -> bool {
if validate_oid(oid).is_err() {
return false;
}
self.vfs.exists(&Self::object_path(oid))
}
pub fn read(&self, oid: &str) -> Result<Vec<u8>> {
validate_oid(oid)?;
if !self.contains(oid) {
return Err(Error::NotFound(ObjectKindHint::Object(oid.into())));
}
let content = self.vfs.read(&Self::object_path(oid))?;
let actual = Pointer::for_content(&content);
if actual.oid != oid {
return Err(Error::InvalidOid(alloc::format!(
"lfs object {oid} hashes to {}",
actual.oid
)));
}
Ok(content)
}
pub fn write(&self, content: &[u8]) -> Result<Pointer> {
let pointer = Pointer::for_content(content);
validate_oid(&pointer.oid)?;
let path = Self::object_path(&pointer.oid);
if !self.vfs.exists(&path) {
self.vfs.write(&path, content)?;
}
Ok(pointer)
}
pub fn write_verified(&self, pointer: &Pointer, content: &[u8]) -> Result<()> {
validate_oid(&pointer.oid)?;
let actual = Pointer::for_content(content);
if &actual != pointer {
return Err(Error::InvalidOid(
"lfs object content does not match its pointer".into(),
));
}
let path = Self::object_path(&pointer.oid);
if !self.vfs.exists(&path) {
self.vfs.write(&path, content)?;
}
Ok(())
}
}
fn validate_oid(oid: &str) -> Result<()> {
if oid.len() == 64 && oid.bytes().all(|b| matches!(b, b'0'..=b'9' | b'a'..=b'f')) {
Ok(())
} else {
Err(Error::InvalidOid(alloc::format!(
"lfs oid is not 64 lowercase hex chars: {oid:?}"
)))
}
}
#[cfg(all(test, feature = "std"))]
mod tests {
use super::*;
use crate::vfs::StdFs;
#[test]
fn store_roundtrip() {
let dir = std::env::temp_dir().join(alloc::format!("puregit-lfs-{}", core::line!()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let store = LfsStore::new(StdFs::new(&dir));
let pointer = store.write(b"the real large content").unwrap();
assert!(store.contains(&pointer.oid));
assert_eq!(store.read(&pointer.oid).unwrap(), b"the real large content");
assert!(
store
.write_verified(&pointer, b"the real large content")
.is_ok()
);
assert!(store.write_verified(&pointer, b"tampered").is_err());
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn invalid_oid_never_panics() {
let dir = std::env::temp_dir().join(alloc::format!("puregit-lfs-{}", core::line!()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let store = LfsStore::new(StdFs::new(&dir));
let bad = [
"aé23", "ab", "", "../../etc/passwd",
"ABCDEF0123456789abcdef0123456789abcdef0123456789abcdef0123456789", "z891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03", ];
for oid in bad {
assert!(!store.contains(oid), "contains({oid:?}) should be false");
assert!(store.read(oid).is_err(), "read({oid:?}) should error");
}
let bogus = Pointer {
oid: "aé23".into(),
size: 3,
};
assert!(store.write_verified(&bogus, b"abc").is_err());
let _ = std::fs::remove_dir_all(&dir);
}
}