git_policy_cli/
lfs_pointer.rs1pub const BLOB_SIZE_CUTOFF: usize = 1024;
19
20const 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"];
22
23const KEYS: [&[u8]; 3] = [b"version", b"oid", b"size"];
25
26const MATCHERS: [&[u8]; 3] = [b"git-media", b"hawser", b"git-lfs"];
28
29#[derive(Clone, Debug, Eq, PartialEq)]
31pub enum Verdict {
32 NotUploadable,
34 Pointer(String),
36 Decline,
38}
39
40fn contains(haystack: &[u8], needle: &[u8]) -> bool {
42 return haystack.windows(needle.len()).any(|window| {
43 return window == needle;
44 });
45}
46
47fn parse_oid(value: &[u8]) -> Option<String> {
49 let digits: &[u8] = value.strip_prefix(b"sha256:")?;
50 let valid: bool = digits.len() == 64 && digits.iter().all(|byte| {
51 return matches!(byte, b'0'..=b'9' | b'a'..=b'f');
52 });
53 return valid.then(|| {
54 return String::from_utf8_lossy(digits).into_owned();
55 });
56}
57
58fn is_extension_key(key: &[u8]) -> bool {
60 return key.len() > 6
61 && key.starts_with(b"ext-")
62 && key[4].is_ascii_digit()
63 && key[5] == b'-'
64 && (key[6].is_ascii_alphanumeric() || key[6] == b'_');
65}
66
67pub fn decode_pointer(blob: &[u8]) -> Verdict {
77 if blob.len() >= BLOB_SIZE_CUTOFF || blob.is_empty() {
78 return Verdict::NotUploadable;
79 }
80 if !MATCHERS.iter().any(|word| {
81 return contains(blob, word);
82 }) {
83 return Verdict::NotUploadable;
84 }
85 let trimmed: &[u8] = blob.trim_ascii();
86 let odd_edge: bool = trimmed.first().is_some_and(|byte| {
88 return !byte.is_ascii();
89 }) || trimmed.last().is_some_and(|byte| {
90 return !byte.is_ascii();
91 });
92 let mut values: [Option<&[u8]>; 3] = [None; 3];
93 let mut line_number: usize = 0;
94 for raw in trimmed.split(|byte| {
95 return *byte == b'\n';
96 }) {
97 let line: &[u8] = raw.strip_suffix(b"\r").unwrap_or(raw);
98 if line.is_empty() {
99 continue;
100 }
101 let Some(space) = line.iter().position(|byte| {
102 return *byte == b' ';
103 }) else {
104 return if odd_edge { Verdict::Decline } else { Verdict::NotUploadable };
105 };
106 let (key, value): (&[u8], &[u8]) = (&line[..space], &line[space + 1..]);
107 if line_number >= KEYS.len() {
108 return if odd_edge { Verdict::Decline } else { Verdict::NotUploadable };
109 }
110 if key != KEYS[line_number] {
111 return if is_extension_key(key) || odd_edge { Verdict::Decline } else { Verdict::NotUploadable };
113 }
114 values[line_number] = Some(value);
115 line_number += 1;
116 }
117 let [Some(version), Some(oid), Some(size)] = values else {
118 return if odd_edge { Verdict::Decline } else { Verdict::NotUploadable };
119 };
120 let size_value: Option<i64> = std::str::from_utf8(size).ok().and_then(|text| {
121 return text.parse::<i64>().ok();
122 });
123 let (Some(name), Some(bytes)) = (parse_oid(oid), size_value.filter(|number| {
124 return *number >= 0;
125 })) else {
126 return if odd_edge { Verdict::Decline } else { Verdict::NotUploadable };
127 };
128 if !VERSIONS.contains(&version) {
129 return Verdict::NotUploadable;
130 }
131 return if bytes == 0 { Verdict::NotUploadable } else { Verdict::Pointer(name) };
132}
133
134#[cfg(test)]
136#[path = "lfs_pointer_tests.rs"]
137mod tests;