use alloc::string::String;
use alloc::vec::Vec;
use purecrypto::hash::{Digest, Sha256};
use crate::error::{Error, Result};
pub const VERSION_URL: &str = "https://git-lfs.github.com/spec/v1";
pub const MAX_POINTER_LEN: usize = 1024;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Pointer {
pub oid: String,
pub size: u64,
}
impl Pointer {
pub fn for_content(content: &[u8]) -> Pointer {
let digest = Sha256::digest(content);
Pointer {
oid: hex_lower(digest.as_ref()),
size: content.len() as u64,
}
}
pub fn oid_with_algo(&self) -> String {
let mut s = String::with_capacity(7 + self.oid.len());
s.push_str("sha256:");
s.push_str(&self.oid);
s
}
pub fn storage_path(&self) -> Result<String> {
let oid = &self.oid;
if oid.len() != 64 || !oid.bytes().all(|b| matches!(b, b'0'..=b'9' | b'a'..=b'f')) {
return Err(Error::InvalidOid(alloc::format!(
"lfs oid is not 64 lowercase hex chars: {oid:?}"
)));
}
Ok(alloc::format!("{}/{}/{}", &oid[..2], &oid[2..4], oid))
}
pub fn serialize(&self) -> Vec<u8> {
alloc::format!(
"version {VERSION_URL}\noid sha256:{}\nsize {}\n",
self.oid,
self.size
)
.into_bytes()
}
pub fn is_pointer(blob: &[u8]) -> bool {
blob.len() <= MAX_POINTER_LEN
&& blob.starts_with(b"version https://git-lfs.github.com/spec/v1")
}
pub fn parse(blob: &[u8]) -> Result<Pointer> {
if blob.len() > MAX_POINTER_LEN {
return Err(Error::Parse("lfs pointer: too large".into()));
}
let text =
core::str::from_utf8(blob).map_err(|_| Error::Parse("lfs pointer: non-utf8".into()))?;
let mut version = None;
let mut oid = None;
let mut size = None;
for line in text.lines() {
if line.is_empty() {
continue;
}
let (key, value) = line
.split_once(' ')
.ok_or_else(|| Error::Parse("lfs pointer: malformed line".into()))?;
match key {
"version" => version = Some(value),
"oid" => oid = Some(value),
"size" => size = Some(value),
_ => {}
}
}
if version != Some(VERSION_URL) {
return Err(Error::Parse("lfs pointer: wrong or missing version".into()));
}
let oid = oid.ok_or_else(|| Error::Parse("lfs pointer: missing oid".into()))?;
let oid = oid
.strip_prefix("sha256:")
.ok_or_else(|| Error::Parse("lfs pointer: oid is not sha256".into()))?;
if oid.len() != 64
|| !oid
.bytes()
.all(|b| b.is_ascii_hexdigit() && !b.is_ascii_uppercase())
{
return Err(Error::Parse("lfs pointer: invalid sha256 oid".into()));
}
let size: u64 = size
.ok_or_else(|| Error::Parse("lfs pointer: missing size".into()))?
.parse()
.map_err(|_| Error::Parse("lfs pointer: invalid size".into()))?;
Ok(Pointer {
oid: oid.into(),
size,
})
}
}
fn hex_lower(bytes: &[u8]) -> String {
const HEX: &[u8; 16] = b"0123456789abcdef";
let mut s = String::with_capacity(bytes.len() * 2);
for &b in bytes {
s.push(HEX[(b >> 4) as usize] as char);
s.push(HEX[(b & 0xf) as usize] as char);
}
s
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn for_content_matches_known_sha256() {
let p = Pointer::for_content(b"hello\n");
assert_eq!(
p.oid,
"5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03"
);
assert_eq!(p.size, 6);
assert_eq!(
p.storage_path().unwrap(),
"58/91/5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03"
);
}
#[test]
fn storage_path_rejects_bad_oid() {
for bad in [
"aé23",
"ab",
"",
"../../etc/passwd",
"ABCDEF0123456789abcdef0123456789abcdef0123456789abcdef0123456789",
] {
let p = Pointer {
oid: bad.into(),
size: 1,
};
assert!(
p.storage_path().is_err(),
"storage_path({bad:?}) should err"
);
}
}
#[test]
fn serialize_parse_roundtrip() {
let p = Pointer::for_content(b"some large content here");
let bytes = p.serialize();
assert!(Pointer::is_pointer(&bytes));
let back = Pointer::parse(&bytes).unwrap();
assert_eq!(back, p);
}
#[test]
fn detects_and_rejects() {
let good = b"version https://git-lfs.github.com/spec/v1\noid sha256:5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03\nsize 6\n";
assert!(Pointer::is_pointer(good));
assert!(Pointer::parse(good).is_ok());
assert!(!Pointer::is_pointer(b"just a normal file\n"));
assert!(
Pointer::parse(b"version https://example.com/v9\noid sha256:00\nsize 1\n").is_err()
);
assert!(
Pointer::parse(b"version https://git-lfs.github.com/spec/v1\noid sha256:ABC\nsize 1\n")
.is_err()
);
}
}