pub const BLOB_SIZE_CUTOFF: usize = 1024;
const VERSIONS: [&[u8]; 3] = [b"http://git-media.io/v/2", b"https://hawser.github.com/spec/v1", b"https://git-lfs.github.com/spec/v1"];
const KEYS: [&[u8]; 3] = [b"version", b"oid", b"size"];
const MATCHERS: [&[u8]; 3] = [b"git-media", b"hawser", b"git-lfs"];
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum Verdict {
NotUploadable,
Pointer(String),
Decline,
}
fn contains(haystack: &[u8], needle: &[u8]) -> bool {
return haystack.windows(needle.len()).any(|window| {
return window == needle;
});
}
fn parse_oid(value: &[u8]) -> Option<String> {
let digits: &[u8] = value.strip_prefix(b"sha256:")?;
let valid: bool = digits.len() == 64 && digits.iter().all(|byte| {
return matches!(byte, b'0'..=b'9' | b'a'..=b'f');
});
return valid.then(|| {
return String::from_utf8_lossy(digits).into_owned();
});
}
fn is_extension_key(key: &[u8]) -> bool {
return key.len() > 6
&& key.starts_with(b"ext-")
&& key[4].is_ascii_digit()
&& key[5] == b'-'
&& (key[6].is_ascii_alphanumeric() || key[6] == b'_');
}
pub fn decode_pointer(blob: &[u8]) -> Verdict {
if blob.len() >= BLOB_SIZE_CUTOFF || blob.is_empty() {
return Verdict::NotUploadable;
}
if !MATCHERS.iter().any(|word| {
return contains(blob, word);
}) {
return Verdict::NotUploadable;
}
let trimmed: &[u8] = blob.trim_ascii();
let odd_edge: bool = trimmed.first().is_some_and(|byte| {
return !byte.is_ascii();
}) || trimmed.last().is_some_and(|byte| {
return !byte.is_ascii();
});
let mut values: [Option<&[u8]>; 3] = [None; 3];
let mut line_number: usize = 0;
for raw in trimmed.split(|byte| {
return *byte == b'\n';
}) {
let line: &[u8] = raw.strip_suffix(b"\r").unwrap_or(raw);
if line.is_empty() {
continue;
}
let Some(space) = line.iter().position(|byte| {
return *byte == b' ';
}) else {
return if odd_edge { Verdict::Decline } else { Verdict::NotUploadable };
};
let (key, value): (&[u8], &[u8]) = (&line[..space], &line[space + 1..]);
if line_number >= KEYS.len() {
return if odd_edge { Verdict::Decline } else { Verdict::NotUploadable };
}
if key != KEYS[line_number] {
return if is_extension_key(key) || odd_edge { Verdict::Decline } else { Verdict::NotUploadable };
}
values[line_number] = Some(value);
line_number += 1;
}
let [Some(version), Some(oid), Some(size)] = values else {
return if odd_edge { Verdict::Decline } else { Verdict::NotUploadable };
};
let size_value: Option<i64> = std::str::from_utf8(size).ok().and_then(|text| {
return text.parse::<i64>().ok();
});
let (Some(name), Some(bytes)) = (parse_oid(oid), size_value.filter(|number| {
return *number >= 0;
})) else {
return if odd_edge { Verdict::Decline } else { Verdict::NotUploadable };
};
if !VERSIONS.contains(&version) {
return Verdict::NotUploadable;
}
return if bytes == 0 { Verdict::NotUploadable } else { Verdict::Pointer(name) };
}
#[cfg(test)]
#[path = "lfs_pointer_tests.rs"]
mod tests;