This repository has no description
1use crate::{ClaimedSize, LfsOid};
2
3pub const POINTER_MAX_BYTES: u64 = 1024;
4
5const SPEC_URLS: [&str; 2] = [
6 "https://git-lfs.github.com/spec/v1",
7 "https://hawser.github.com/spec/v1",
8];
9
10#[derive(Debug, Clone, PartialEq, Eq)]
11pub struct LfsPointer {
12 pub oid: LfsOid,
13 pub size: ClaimedSize,
14}
15
16pub fn parse_pointer(blob: &[u8]) -> Option<LfsPointer> {
17 if blob.len() as u64 > POINTER_MAX_BYTES {
18 return None;
19 }
20 let text = std::str::from_utf8(blob).ok()?;
21 let mut lines = text.lines();
22 let spec = lines.next()?.strip_prefix("version ")?;
23 SPEC_URLS.contains(&spec).then_some(())?;
24 let fields: Vec<(&str, &str)> = lines.filter_map(|line| line.split_once(' ')).collect();
25 let value_of = |key: &str| {
26 fields
27 .iter()
28 .find_map(|(name, value)| (*name == key).then_some(*value))
29 };
30 let oid = LfsOid::new(value_of("oid")?.strip_prefix("sha256:")?).ok()?;
31 let size = ClaimedSize::new(value_of("size")?.parse().ok()?);
32 Some(LfsPointer { oid, size })
33}
34
35#[cfg(test)]
36mod tests {
37 use super::*;
38
39 const OID: &str = "6c17f2007cbe934aee6e309b28b2fba3c119d98be6ea4156da3aa3173456ad16";
40
41 fn pointer_text() -> String {
42 format!("version https://git-lfs.github.com/spec/v1\noid sha256:{OID}\nsize 12345\n")
43 }
44
45 #[test]
46 fn valid_variants_beyond_the_canonical_form_still_parse() {
47 let hawser = pointer_text().replace("git-lfs.github.com", "hawser.github.com");
48 assert!(
49 parse_pointer(hawser.as_bytes()).is_some(),
50 "the legacy hawser spec still counts"
51 );
52 let extra = format!(
53 "version https://git-lfs.github.com/spec/v1\nname media/clip.mp4\noid sha256:{OID}\nsize 7\n"
54 );
55 assert!(
56 parse_pointer(extra.as_bytes()).is_some(),
57 "extra sorted keys are tolerated"
58 );
59 }
60
61 #[test]
62 fn non_pointers_are_rejected() {
63 let cases: Vec<Vec<u8>> = vec![
64 b"".to_vec(),
65 b"plain source file\n".to_vec(),
66 format!("oid sha256:{OID}\nsize 7\n").into_bytes(),
67 format!("version https://oyster.cafe/spec/v1\noid sha256:{OID}\nsize 7\n").into_bytes(),
68 pointer_text().replace("sha256:", "sha512:").into_bytes(),
69 pointer_text()
70 .replace("size 12345", "size lots")
71 .into_bytes(),
72 format!("version https://git-lfs.github.com/spec/v1\noid sha256:{OID}\n").into_bytes(),
73 b"version https://git-lfs.github.com/spec/v1\nsize 7\n".to_vec(),
74 [pointer_text().into_bytes(), vec![0xff, 0xfe]].concat(),
75 [
76 pointer_text().into_bytes(),
77 vec![b' '; POINTER_MAX_BYTES as usize],
78 ]
79 .concat(),
80 ];
81 cases.iter().enumerate().for_each(|(index, blob)| {
82 assert!(
83 parse_pointer(blob).is_none(),
84 "case {index} parsed as a pointer"
85 );
86 });
87 }
88}