forge
ExploreLog in

Tahinli/forge

public
0 watching
★ 0
4.7 KiB
1//! Parser for Git LFS pointer files (spec v1, https://github.com/git-lfs/git-lfs/blob/main/docs/spec.md).
2//! A pointer is a small text blob:
3//!
4//! ```text
5//! version https://git-lfs.github.com/spec/v1
6//! oid sha256:<64 lowercase hex>
7//! size <decimal bytes>
8//! ```
9//!
10//! Extra `key value` lines are allowed. The parser is pure and accepts nothing else.
11
12use crate::lfs::is_valid_oid;
13
14/// Pointer files are about 130 bytes. Anything larger is never treated as a pointer.
15pub const MAX_POINTER_BYTES: usize = 1024;
16
17const VERSION_LINE: &str = "version https://git-lfs.github.com/spec/v1";
18
19#[derive(Debug, Clone, PartialEq, Eq)]
20pub struct Pointer {
21 /// The SHA-256 of the object, lowercase hex, without the `sha256:` prefix.
22 pub oid: String,
23 /// Object size in bytes.
24 pub size: u64,
25}
26
27/// Returns the pointer described by `data`, or `None` when `data` is not a valid pointer.
28pub fn parse(data: &[u8]) -> Option<Pointer> {
29 if data.len() > MAX_POINTER_BYTES {
30 return None;
31 }
32 let text = std::str::from_utf8(data).ok()?;
33 let mut lines = text.strip_suffix('\n').unwrap_or(text).split('\n');
34 if lines.next()? != VERSION_LINE {
35 return None;
36 }
37
38 let mut oid = None;
39 let mut size = None;
40 for line in lines {
41 let (key, value) = line.split_once(' ')?;
42 match key {
43 "oid" => {
44 let hex = value.strip_prefix("sha256:")?;
45 if oid.is_some() || !is_valid_oid(hex) {
46 return None;
47 }
48 oid = Some(hex.to_owned());
49 }
50 "size" => {
51 if size.is_some() || value.is_empty() || !value.bytes().all(|b| b.is_ascii_digit())
52 {
53 return None;
54 }
55 size = Some(value.parse::<u64>().ok()?);
56 }
57 // Other keys (such as `ext-0-...`) are allowed and ignored.
58 _ => {}
59 }
60 }
61 Some(Pointer {
62 oid: oid?,
63 size: size?,
64 })
65}
66
67#[cfg(test)]
68mod tests {
69 use super::*;
70
71 const OID: &str = "4d7a214614ab2935c943f9e0ff69d22eadbb8f32b1258daaa5e2ca24d17e2393";
72
73 fn pointer(oid: &str, size: &str) -> String {
74 format!("{VERSION_LINE}\noid sha256:{oid}\nsize {size}\n")
75 }
76
77 #[test]
78 fn parses_a_standard_pointer() {
79 let got = parse(pointer(OID, "12345").as_bytes());
80 assert_eq!(
81 got,
82 Some(Pointer {
83 oid: OID.to_owned(),
84 size: 12345
85 })
86 );
87 }
88
89 #[test]
90 fn accepts_missing_trailing_newline_and_extra_keys() {
91 let text = format!("{VERSION_LINE}\next-0-x sha256:abc\noid sha256:{OID}\nsize 0");
92 assert_eq!(
93 parse(text.as_bytes()),
94 Some(Pointer {
95 oid: OID.to_owned(),
96 size: 0
97 })
98 );
99 }
100
101 #[test]
102 fn rejects_wrong_version_line() {
103 let text = format!("version https://example.com/spec/v1\noid sha256:{OID}\nsize 1\n");
104 assert_eq!(parse(text.as_bytes()), None);
105 let text = format!("{VERSION_LINE} \noid sha256:{OID}\nsize 1\n");
106 assert_eq!(parse(text.as_bytes()), None);
107 }
108
109 #[test]
110 fn rejects_bad_oid() {
111 assert_eq!(parse(pointer(&OID.to_uppercase(), "1").as_bytes()), None);
112 assert_eq!(parse(pointer(&OID[..63], "1").as_bytes()), None);
113 assert_eq!(parse(pointer(&format!("{OID}0"), "1").as_bytes()), None);
114 let text = format!("{VERSION_LINE}\noid md5:{OID}\nsize 1\n");
115 assert_eq!(parse(text.as_bytes()), None);
116 }
117
118 #[test]
119 fn rejects_bad_size() {
120 assert_eq!(parse(pointer(OID, "").as_bytes()), None);
121 assert_eq!(parse(pointer(OID, "-1").as_bytes()), None);
122 assert_eq!(parse(pointer(OID, "1.5").as_bytes()), None);
123 assert_eq!(parse(pointer(OID, "0x10").as_bytes()), None);
124 assert_eq!(
125 parse(pointer(OID, "99999999999999999999999").as_bytes()),
126 None
127 );
128 }
129
130 #[test]
131 fn rejects_missing_or_duplicate_fields() {
132 let no_size = format!("{VERSION_LINE}\noid sha256:{OID}\n");
133 assert_eq!(parse(no_size.as_bytes()), None);
134 let no_oid = format!("{VERSION_LINE}\nsize 1\n");
135 assert_eq!(parse(no_oid.as_bytes()), None);
136 let twice = format!("{VERSION_LINE}\noid sha256:{OID}\noid sha256:{OID}\nsize 1\n");
137 assert_eq!(parse(twice.as_bytes()), None);
138 }
139
140 #[test]
141 fn rejects_oversized_and_non_text_input() {
142 let big = format!("{VERSION_LINE}\n{}", "x".repeat(MAX_POINTER_BYTES));
143 assert_eq!(parse(big.as_bytes()), None);
144 assert_eq!(parse(&[0xff, 0xfe, 0x00]), None);
145 assert_eq!(parse(b""), None);
146 assert_eq!(parse(b"hello\nworld\n"), None);
147 }
148}