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 is_line_feed_byte(byte: &u8) -> bool {
return *byte == b'\n';
}
fn is_space_byte(byte: &u8) -> bool {
return *byte == b' ';
}
fn contains(haystack: &[u8], needle: &[u8]) -> bool {
for window in haystack.windows(needle.len()) {
if window == needle {
return true;
}
}
return false;
}
fn parse_oid(value: &[u8]) -> Option<String> {
let digits: &[u8] = value.strip_prefix(b"sha256:")?;
if digits.len() != 64 {
return None;
}
for byte in digits {
if !matches!(byte, b'0'..=b'9' | b'a'..=b'f') {
return None;
}
}
return Some(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;
}
let mut holds_matcher: bool = false;
for word in MATCHERS {
if contains(blob, word) {
holds_matcher = true;
break;
}
}
if !holds_matcher {
return Verdict::NotUploadable;
}
let trimmed: &[u8] = blob.trim_ascii();
let first_is_odd: bool = match trimmed.first() {
Some(byte) => !byte.is_ascii(),
None => false,
};
let last_is_odd: bool = match trimmed.last() {
Some(byte) => !byte.is_ascii(),
None => false,
};
let odd_edge: bool = first_is_odd || last_is_odd;
let mut values: [Option<&[u8]>; 3] = [None; 3];
let mut line_number: usize = 0;
for raw in trimmed.split(is_line_feed_byte) {
let line: &[u8] = raw.strip_suffix(b"\r").unwrap_or(raw);
if line.is_empty() {
continue;
}
let Some(space) = line.iter().position(is_space_byte) 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 parsed_size: Option<i64> = match std::str::from_utf8(size) {
Ok(text) => text.parse::<i64>().ok(),
Err(_) => None,
};
let size_value: Option<i64> = match parsed_size {
Some(number) if number >= 0 => Some(number),
_ => None,
};
let (Some(name), Some(bytes)) = (parse_oid(oid), size_value) 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;