1mod oid;
24
25pub use oid::{EMPTY_HEX, Oid, OidParseError};
26
27pub const VERSION_LATEST: &str = "https://git-lfs.github.com/spec/v1";
29
30pub const MAX_POINTER_SIZE: usize = 1024;
33
34const VERSION_ALIASES: &[&str] = &[
36 "http://git-media.io/v/2", "https://hawser.github.com/spec/v1", "https://git-lfs.github.com/spec/v1", ];
40
41#[derive(Debug, Clone, PartialEq, Eq)]
47pub struct Pointer {
48 pub oid: Oid,
49 pub size: u64,
50 pub extensions: Vec<Extension>,
52 pub canonical: bool,
56}
57
58#[derive(Debug, Clone, PartialEq, Eq)]
63pub struct Extension {
64 pub name: String,
65 pub priority: u8,
66 pub oid: Oid,
67}
68
69impl Pointer {
70 pub fn new(oid: Oid, size: u64) -> Self {
72 Self {
73 oid,
74 size,
75 extensions: Vec::new(),
76 canonical: true,
77 }
78 }
79
80 pub fn empty() -> Self {
84 Self {
85 oid: Oid::EMPTY,
86 size: 0,
87 extensions: Vec::new(),
88 canonical: true,
89 }
90 }
91
92 pub fn is_empty(&self) -> bool {
94 self.size == 0
95 }
96
97 pub fn encode(&self) -> String {
102 use std::fmt::Write as _;
103 if self.size == 0 {
104 return String::new();
105 }
106 let mut exts: Vec<&Extension> = self.extensions.iter().collect();
107 exts.sort_by_key(|e| e.priority);
108
109 let mut out = String::with_capacity(160 + 80 * exts.len());
110 writeln!(out, "version {VERSION_LATEST}").unwrap();
111 for ext in exts {
112 writeln!(out, "ext-{}-{} sha256:{}", ext.priority, ext.name, ext.oid).unwrap();
113 }
114 writeln!(out, "oid sha256:{}", self.oid).unwrap();
115 writeln!(out, "size {}", self.size).unwrap();
116 out
117 }
118
119 pub fn parse(input: &[u8]) -> Result<Self, DecodeError> {
126 if input.is_empty() {
127 return Ok(Self::empty());
128 }
129 if input.len() >= MAX_POINTER_SIZE {
130 return Err(DecodeError::NotAPointer(NotAPointerReason::TooLarge {
131 size: input.len(),
132 }));
133 }
134 let text = std::str::from_utf8(input)
135 .map_err(|_| DecodeError::NotAPointer(NotAPointerReason::NotUtf8))?;
136 if !contains_spec_marker(text) {
137 return Err(DecodeError::NotAPointer(NotAPointerReason::MissingHeader));
138 }
139
140 let mut pointer = parse_lines(text.trim())?;
141 pointer.canonical = pointer.encode().as_bytes() == input;
142 Ok(pointer)
143 }
144}
145
146fn contains_spec_marker(text: &str) -> bool {
147 text.contains("git-lfs") || text.contains("git-media") || text.contains("hawser")
148}
149
150fn parse_lines(text: &str) -> Result<Pointer, DecodeError> {
151 const REQUIRED: [&str; 3] = ["version", "oid", "size"];
152 let mut filled: [Option<&str>; 3] = [None, None, None];
153 let mut consumed = 0usize;
154 let mut extensions: Vec<Extension> = Vec::new();
155
156 for (line_no, raw_line) in text.split('\n').enumerate() {
157 let line = raw_line.strip_suffix('\r').unwrap_or(raw_line);
159 if line.is_empty() {
160 continue;
161 }
162
163 let (key, value) = line.split_once(' ').ok_or(DecodeError::NotAPointer(
164 NotAPointerReason::MalformedLine { line: line_no },
165 ))?;
166
167 if consumed == REQUIRED.len() {
168 return Err(DecodeError::NotAPointer(NotAPointerReason::ExtraLine {
169 line: line_no,
170 content: line.into(),
171 }));
172 }
173
174 let expected = REQUIRED[consumed];
175 if key == expected {
176 filled[consumed] = Some(value);
177 consumed += 1;
178 continue;
179 }
180
181 if let Some((priority, name)) = parse_extension_key(key) {
183 let ext_oid =
184 parse_oid_value(value).map_err(DecodeError::Malformed)?;
185 extensions.push(Extension {
186 name: name.to_owned(),
187 priority,
188 oid: ext_oid,
189 });
190 continue;
191 }
192
193 return Err(if expected == "version" {
197 DecodeError::NotAPointer(NotAPointerReason::NotVersionFirst { got: key.into() })
198 } else {
199 DecodeError::Malformed(MalformedReason::UnexpectedKey {
200 expected,
201 got: key.into(),
202 })
203 });
204 }
205
206 let version = filled[0].ok_or(DecodeError::NotAPointer(NotAPointerReason::MissingVersion))?;
207 if !VERSION_ALIASES.contains(&version) {
208 return Err(DecodeError::Malformed(MalformedReason::InvalidVersion(
209 version.into(),
210 )));
211 }
212
213 let oid_value = filled[1]
214 .ok_or(DecodeError::Malformed(MalformedReason::MissingField("oid")))?;
215 let oid = parse_oid_value(oid_value).map_err(DecodeError::Malformed)?;
216
217 let size_value = filled[2]
218 .ok_or(DecodeError::Malformed(MalformedReason::MissingField("size")))?;
219 let size = parse_size(size_value).map_err(DecodeError::Malformed)?;
220
221 extensions.sort_by_key(|e| e.priority);
222 for w in extensions.windows(2) {
223 if w[0].priority == w[1].priority {
224 return Err(DecodeError::Malformed(
225 MalformedReason::DuplicateExtensionPriority(w[0].priority),
226 ));
227 }
228 }
229
230 Ok(Pointer {
231 oid,
232 size,
233 extensions,
234 canonical: true, })
236}
237
238fn parse_oid_value(value: &str) -> Result<Oid, MalformedReason> {
239 let (oid_type, hash) = value
240 .split_once(':')
241 .ok_or_else(|| MalformedReason::MalformedOidValue(value.into()))?;
242 if oid_type != "sha256" {
243 return Err(MalformedReason::UnsupportedOidType(oid_type.into()));
244 }
245 Oid::from_hex(hash).map_err(MalformedReason::InvalidOidHash)
246}
247
248fn parse_size(value: &str) -> Result<u64, MalformedReason> {
249 value
251 .parse::<u64>()
252 .map_err(|_| MalformedReason::InvalidSize(value.into()))
253}
254
255fn parse_extension_key(key: &str) -> Option<(u8, &str)> {
258 let rest = key.strip_prefix("ext-")?;
259 let bytes = rest.as_bytes();
260 if bytes.len() < 3 {
261 return None;
262 }
263 if !bytes[0].is_ascii_digit() || bytes[1] != b'-' {
264 return None;
265 }
266 let name = &rest[2..];
267 if !name
268 .bytes()
269 .all(|b| b.is_ascii_alphanumeric() || b == b'_')
270 {
271 return None;
272 }
273 Some((bytes[0] - b'0', name))
274}
275
276#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
277pub enum DecodeError {
278 #[error("not a git-lfs pointer: {0}")]
280 NotAPointer(NotAPointerReason),
281 #[error("malformed git-lfs pointer: {0}")]
283 Malformed(MalformedReason),
284}
285
286impl DecodeError {
287 pub fn is_not_a_pointer(&self) -> bool {
290 matches!(self, DecodeError::NotAPointer(_))
291 }
292}
293
294#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
295pub enum NotAPointerReason {
296 #[error("size {size} bytes is not below the {MAX_POINTER_SIZE}-byte cutoff")]
297 TooLarge { size: usize },
298 #[error("input is not valid UTF-8")]
299 NotUtf8,
300 #[error("missing git-lfs spec marker")]
301 MissingHeader,
302 #[error("line {line} has no key/value separator")]
303 MalformedLine { line: usize },
304 #[error("missing version line")]
305 MissingVersion,
306 #[error("first key is {got:?}, expected version")]
307 NotVersionFirst { got: String },
308 #[error("extra content on line {line}: {content:?}")]
309 ExtraLine { line: usize, content: String },
310}
311
312#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
313pub enum MalformedReason {
314 #[error("unrecognized version: {0:?}")]
315 InvalidVersion(String),
316 #[error("expected key {expected:?}, got {got:?}")]
317 UnexpectedKey {
318 expected: &'static str,
319 got: String,
320 },
321 #[error("missing required {0:?} line")]
322 MissingField(&'static str),
323 #[error("oid value {0:?} is not in the form <type>:<hash>")]
324 MalformedOidValue(String),
325 #[error("unsupported oid type {0:?}; only sha256 is supported")]
326 UnsupportedOidType(String),
327 #[error("invalid oid hash: {0}")]
328 InvalidOidHash(#[source] OidParseError),
329 #[error("size value {0:?} is not a non-negative integer")]
330 InvalidSize(String),
331 #[error("duplicate extension priority {0}")]
332 DuplicateExtensionPriority(u8),
333}
334
335#[cfg(test)]
336mod tests {
337 use super::*;
338
339 fn sha(hex: &str) -> Oid {
340 Oid::from_hex(hex).unwrap()
341 }
342
343 const SAMPLE_OID_HEX: &str =
344 "4d7a214614ab2935c943f9e0ff69d22eadbb8f32b1258daaa5e2ca24d17e2393";
345
346 #[test]
349 fn encode_simple() {
350 let p = Pointer::new(sha(SAMPLE_OID_HEX), 12345);
351 let expected = format!("version {VERSION_LATEST}\noid sha256:{SAMPLE_OID_HEX}\nsize 12345\n");
352 assert_eq!(p.encode(), expected);
353 }
354
355 #[test]
356 fn encode_empty() {
357 assert_eq!(Pointer::empty().encode(), "");
359 let p = Pointer::new(sha(SAMPLE_OID_HEX), 0);
361 assert_eq!(p.encode(), "");
362 }
363
364 #[test]
365 fn encode_extensions_sorted_on_output() {
366 let exts = vec![
367 Extension {
368 name: "baz".into(),
369 priority: 2,
370 oid: sha("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"),
371 },
372 Extension {
373 name: "foo".into(),
374 priority: 0,
375 oid: sha("ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"),
376 },
377 Extension {
378 name: "bar".into(),
379 priority: 1,
380 oid: sha("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"),
381 },
382 ];
383 let p = Pointer {
384 oid: sha(SAMPLE_OID_HEX),
385 size: 12345,
386 extensions: exts,
387 canonical: true,
388 };
389 let expected = format!(
390 "version {VERSION_LATEST}\n\
391 ext-0-foo sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\n\
392 ext-1-bar sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\n\
393 ext-2-baz sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n\
394 oid sha256:{SAMPLE_OID_HEX}\n\
395 size 12345\n",
396 );
397 assert_eq!(p.encode(), expected);
398 }
399
400 #[test]
403 fn parse_standard() {
404 let input = format!("version {VERSION_LATEST}\noid sha256:{SAMPLE_OID_HEX}\nsize 12345\n");
405 let p = Pointer::parse(input.as_bytes()).unwrap();
406 assert_eq!(p.oid, sha(SAMPLE_OID_HEX));
407 assert_eq!(p.size, 12345);
408 assert!(p.extensions.is_empty());
409 assert!(p.canonical);
410 }
411
412 #[test]
413 fn parse_empty_input_is_empty_pointer() {
414 let p = Pointer::parse(b"").unwrap();
415 assert_eq!(p, Pointer::empty());
416 assert!(p.canonical);
417 }
418
419 #[test]
420 fn parse_extensions_sorted() {
421 let input = format!(
422 "version {VERSION_LATEST}\n\
423 ext-0-foo sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\n\
424 ext-1-bar sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\n\
425 ext-2-baz sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n\
426 oid sha256:{SAMPLE_OID_HEX}\n\
427 size 12345\n",
428 );
429 let p = Pointer::parse(input.as_bytes()).unwrap();
430 assert_eq!(p.extensions.len(), 3);
431 assert_eq!(p.extensions[0].name, "foo");
432 assert_eq!(p.extensions[0].priority, 0);
433 assert_eq!(p.extensions[1].name, "bar");
434 assert_eq!(p.extensions[2].name, "baz");
435 assert!(p.canonical);
436 }
437
438 #[test]
439 fn parse_unsorted_extensions_sorts_and_marks_noncanonical() {
440 let input = format!(
442 "version {VERSION_LATEST}\n\
443 ext-2-baz sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n\
444 ext-0-foo sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\n\
445 ext-1-bar sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\n\
446 oid sha256:{SAMPLE_OID_HEX}\n\
447 size 12345\n",
448 );
449 let p = Pointer::parse(input.as_bytes()).unwrap();
450 assert_eq!(p.extensions[0].priority, 0);
451 assert_eq!(p.extensions[1].priority, 1);
452 assert_eq!(p.extensions[2].priority, 2);
453 assert!(!p.canonical);
454 }
455
456 #[test]
457 fn parse_pre_release_version_alias() {
458 let input = format!(
459 "version https://hawser.github.com/spec/v1\noid sha256:{SAMPLE_OID_HEX}\nsize 12345\n"
460 );
461 let p = Pointer::parse(input.as_bytes()).unwrap();
462 assert_eq!(p.size, 12345);
463 assert!(!p.canonical);
465 assert!(p.encode().starts_with(&format!("version {VERSION_LATEST}\n")));
466 }
467
468 #[test]
469 fn parse_round_trip() {
470 let p = Pointer::new(sha(SAMPLE_OID_HEX), 12345);
471 let encoded = p.encode();
472 let parsed = Pointer::parse(encoded.as_bytes()).unwrap();
473 assert_eq!(parsed.oid, p.oid);
474 assert_eq!(parsed.size, p.size);
475 assert!(parsed.canonical);
476 }
477
478 #[test]
481 fn canonical_examples() {
482 let s = format!("version {VERSION_LATEST}\noid sha256:{SAMPLE_OID_HEX}\nsize 12345\n");
484 assert!(Pointer::parse(s.as_bytes()).unwrap().canonical);
485
486 assert!(Pointer::parse(b"").unwrap().canonical);
488 }
489
490 #[test]
491 fn non_canonical_examples() {
492 let cases: &[&str] = &[
493 "version https://git-lfs.github.com/spec/v1\noid sha256:4d7a214614ab2935c943f9e0ff69d22eadbb8f32b1258daaa5e2ca24d17e2393\nsize 12345",
495 "version https://git-lfs.github.com/spec/v1\r\noid sha256:4d7a214614ab2935c943f9e0ff69d22eadbb8f32b1258daaa5e2ca24d17e2393\r\nsize 12345\r\n",
497 "version https://git-lfs.github.com/spec/v1\noid sha256:4d7a214614ab2935c943f9e0ff69d22eadbb8f32b1258daaa5e2ca24d17e2393\nsize 12345 \n",
499 ];
500 for case in cases {
501 let p = Pointer::parse(case.as_bytes())
502 .unwrap_or_else(|e| panic!("failed to parse {case:?}: {e}"));
503 assert!(!p.canonical, "expected non-canonical for {case:?}");
504 }
505 }
506
507 #[test]
510 fn tiny_non_pointer_is_not_a_pointer() {
511 let err = Pointer::parse(b"this is not a git-lfs file!").unwrap_err();
512 assert!(err.is_not_a_pointer(), "expected NotAPointer, got {err:?}");
513 }
514
515 #[test]
516 fn header_only_is_not_a_pointer() {
517 let err = Pointer::parse(b"# git-media").unwrap_err();
519 assert!(err.is_not_a_pointer(), "expected NotAPointer, got {err:?}");
520 }
521
522 #[test]
523 fn oversized_input_is_not_a_pointer() {
524 let big = vec![b'x'; MAX_POINTER_SIZE + 1];
525 let err = Pointer::parse(&big).unwrap_err();
526 assert!(matches!(
527 err,
528 DecodeError::NotAPointer(NotAPointerReason::TooLarge { .. })
529 ));
530 }
531
532 #[test]
533 fn exactly_max_size_is_not_a_pointer() {
534 let exact = vec![b'x'; MAX_POINTER_SIZE];
536 let err = Pointer::parse(&exact).unwrap_err();
537 assert!(matches!(
538 err,
539 DecodeError::NotAPointer(NotAPointerReason::TooLarge { .. })
540 ));
541 }
542
543 #[test]
544 fn equals_separator_is_not_a_pointer() {
545 let s = "version=https://git-lfs.github.com/spec/v1\n\
547 oid=sha256:4d7a214614ab2935c943f9e0ff69d22eadbb8f32b1258daaa5e2ca24d17e2393\n\
548 size=fif";
549 let err = Pointer::parse(s.as_bytes()).unwrap_err();
550 assert!(err.is_not_a_pointer());
551 }
552
553 #[test]
554 fn no_marker_is_not_a_pointer() {
555 let err = Pointer::parse(b"version=http://wat.io/v/2\noid=foo\nsize=fif").unwrap_err();
556 assert!(matches!(
557 err,
558 DecodeError::NotAPointer(NotAPointerReason::MissingHeader)
559 ));
560 }
561
562 #[test]
563 fn missing_version_first_is_not_a_pointer() {
564 let s = format!("oid sha256:{SAMPLE_OID_HEX}\nsize 12345\n");
566 let err = Pointer::parse(s.as_bytes()).unwrap_err();
567 assert!(err.is_not_a_pointer(), "got {err:?}");
568 }
569
570 #[test]
571 fn extra_line_after_size_is_not_a_pointer() {
572 let s = format!(
573 "version {VERSION_LATEST}\noid sha256:{SAMPLE_OID_HEX}\nsize 12345\nwat wat\n"
574 );
575 let err = Pointer::parse(s.as_bytes()).unwrap_err();
576 assert!(matches!(
577 err,
578 DecodeError::NotAPointer(NotAPointerReason::ExtraLine { .. })
579 ));
580 }
581
582 #[test]
585 fn invalid_version_is_malformed() {
586 let s = format!(
588 "version http://git-media.io/v/whatever\noid sha256:{SAMPLE_OID_HEX}\nsize 12345\n"
589 );
590 let err = Pointer::parse(s.as_bytes()).unwrap_err();
591 assert!(matches!(
592 err,
593 DecodeError::Malformed(MalformedReason::InvalidVersion(_))
594 ));
595 }
596
597 #[test]
598 fn missing_oid_is_malformed() {
599 let s = format!("version {VERSION_LATEST}\nsize 12345\n");
600 let err = Pointer::parse(s.as_bytes()).unwrap_err();
601 assert!(matches!(err, DecodeError::Malformed(_)));
602 }
603
604 #[test]
605 fn missing_size_is_malformed() {
606 let s = format!("version {VERSION_LATEST}\noid sha256:{SAMPLE_OID_HEX}\n");
607 let err = Pointer::parse(s.as_bytes()).unwrap_err();
608 assert!(matches!(
609 err,
610 DecodeError::Malformed(MalformedReason::MissingField("size"))
611 ));
612 }
613
614 #[test]
615 fn keys_out_of_order_is_malformed() {
616 let s = format!("version {VERSION_LATEST}\nsize 12345\noid sha256:{SAMPLE_OID_HEX}\n");
617 let err = Pointer::parse(s.as_bytes()).unwrap_err();
618 assert!(matches!(
619 err,
620 DecodeError::Malformed(MalformedReason::UnexpectedKey { .. })
621 ));
622 }
623
624 #[test]
625 fn bad_oid_hex_is_malformed() {
626 let s = format!("version {VERSION_LATEST}\noid sha256:boom\nsize 12345\n");
627 let err = Pointer::parse(s.as_bytes()).unwrap_err();
628 assert!(matches!(
629 err,
630 DecodeError::Malformed(MalformedReason::InvalidOidHash(_))
631 ));
632 }
633
634 #[test]
635 fn bad_oid_type_is_malformed() {
636 let s = format!("version {VERSION_LATEST}\noid shazam:{SAMPLE_OID_HEX}\nsize 12345\n");
637 let err = Pointer::parse(s.as_bytes()).unwrap_err();
638 assert!(matches!(
639 err,
640 DecodeError::Malformed(MalformedReason::UnsupportedOidType(_))
641 ));
642 }
643
644 #[test]
645 fn bad_size_is_malformed() {
646 let s = format!("version {VERSION_LATEST}\noid sha256:{SAMPLE_OID_HEX}\nsize fif\n");
647 let err = Pointer::parse(s.as_bytes()).unwrap_err();
648 assert!(matches!(
649 err,
650 DecodeError::Malformed(MalformedReason::InvalidSize(_))
651 ));
652 }
653
654 #[test]
655 fn negative_size_is_malformed() {
656 let s = format!("version {VERSION_LATEST}\noid sha256:{SAMPLE_OID_HEX}\nsize -1\n");
657 let err = Pointer::parse(s.as_bytes()).unwrap_err();
658 assert!(matches!(
659 err,
660 DecodeError::Malformed(MalformedReason::InvalidSize(_))
661 ));
662 }
663
664 #[test]
665 fn oid_with_trailing_garbage_is_malformed() {
666 let s = format!(
667 "version {VERSION_LATEST}\noid sha256:{SAMPLE_OID_HEX}&\nsize 177735\n"
668 );
669 let err = Pointer::parse(s.as_bytes()).unwrap_err();
670 assert!(matches!(
671 err,
672 DecodeError::Malformed(MalformedReason::InvalidOidHash(_))
673 ));
674 }
675
676 #[test]
679 fn ext_priority_over_9_is_malformed() {
680 let s = format!(
682 "version {VERSION_LATEST}\n\
683 ext-10-foo sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\n\
684 oid sha256:{SAMPLE_OID_HEX}\n\
685 size 12345\n",
686 );
687 let err = Pointer::parse(s.as_bytes()).unwrap_err();
688 assert!(matches!(err, DecodeError::Malformed(_)), "got {err:?}");
689 }
690
691 #[test]
692 fn ext_with_non_digit_priority_is_malformed() {
693 let s = format!(
694 "version {VERSION_LATEST}\n\
695 ext-#-foo sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\n\
696 oid sha256:{SAMPLE_OID_HEX}\n\
697 size 12345\n",
698 );
699 let err = Pointer::parse(s.as_bytes()).unwrap_err();
700 assert!(matches!(err, DecodeError::Malformed(_)), "got {err:?}");
701 }
702
703 #[test]
704 fn ext_with_non_word_name_is_malformed() {
705 let s = format!(
706 "version {VERSION_LATEST}\n\
707 ext-0-$$$$ sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\n\
708 oid sha256:{SAMPLE_OID_HEX}\n\
709 size 12345\n",
710 );
711 let err = Pointer::parse(s.as_bytes()).unwrap_err();
712 assert!(matches!(err, DecodeError::Malformed(_)), "got {err:?}");
713 }
714
715 #[test]
716 fn ext_bad_oid_is_malformed() {
717 let s = format!(
718 "version {VERSION_LATEST}\n\
719 ext-0-foo sha256:boom\n\
720 oid sha256:{SAMPLE_OID_HEX}\n\
721 size 12345\n",
722 );
723 let err = Pointer::parse(s.as_bytes()).unwrap_err();
724 assert!(matches!(
725 err,
726 DecodeError::Malformed(MalformedReason::InvalidOidHash(_))
727 ));
728 }
729
730 #[test]
731 fn ext_bad_oid_type_is_malformed() {
732 let s = format!(
733 "version {VERSION_LATEST}\n\
734 ext-0-foo boom:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\n\
735 oid sha256:{SAMPLE_OID_HEX}\n\
736 size 12345\n",
737 );
738 let err = Pointer::parse(s.as_bytes()).unwrap_err();
739 assert!(matches!(
740 err,
741 DecodeError::Malformed(MalformedReason::UnsupportedOidType(_))
742 ));
743 }
744
745 #[test]
746 fn duplicate_ext_priority_is_malformed() {
747 let s = format!(
748 "version {VERSION_LATEST}\n\
749 ext-0-foo sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\n\
750 ext-0-bar sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\n\
751 oid sha256:{SAMPLE_OID_HEX}\n\
752 size 12345\n",
753 );
754 let err = Pointer::parse(s.as_bytes()).unwrap_err();
755 assert!(matches!(
756 err,
757 DecodeError::Malformed(MalformedReason::DuplicateExtensionPriority(0))
758 ));
759 }
760}