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 = parse_oid_value(value).map_err(DecodeError::Malformed)?;
184 extensions.push(Extension {
185 name: name.to_owned(),
186 priority,
187 oid: ext_oid,
188 });
189 continue;
190 }
191
192 return Err(if expected == "version" {
196 DecodeError::NotAPointer(NotAPointerReason::NotVersionFirst { got: key.into() })
197 } else {
198 DecodeError::Malformed(MalformedReason::UnexpectedKey {
199 expected,
200 got: key.into(),
201 })
202 });
203 }
204
205 let version = filled[0].ok_or(DecodeError::NotAPointer(NotAPointerReason::MissingVersion))?;
206 if !VERSION_ALIASES.contains(&version) {
207 return Err(DecodeError::Malformed(MalformedReason::InvalidVersion(
208 version.into(),
209 )));
210 }
211
212 let oid_value =
213 filled[1].ok_or(DecodeError::Malformed(MalformedReason::MissingField("oid")))?;
214 let oid = parse_oid_value(oid_value).map_err(DecodeError::Malformed)?;
215
216 let size_value = filled[2].ok_or(DecodeError::Malformed(MalformedReason::MissingField(
217 "size",
218 )))?;
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.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'_') {
268 return None;
269 }
270 Some((bytes[0] - b'0', name))
271}
272
273#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
274pub enum DecodeError {
275 #[error("not a git-lfs pointer: {0}")]
277 NotAPointer(NotAPointerReason),
278 #[error("malformed git-lfs pointer: {0}")]
280 Malformed(MalformedReason),
281}
282
283impl DecodeError {
284 pub fn is_not_a_pointer(&self) -> bool {
287 matches!(self, DecodeError::NotAPointer(_))
288 }
289}
290
291#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
292pub enum NotAPointerReason {
293 #[error("size {size} bytes is not below the {MAX_POINTER_SIZE}-byte cutoff")]
294 TooLarge { size: usize },
295 #[error("input is not valid UTF-8")]
296 NotUtf8,
297 #[error("missing git-lfs spec marker")]
298 MissingHeader,
299 #[error("line {line} has no key/value separator")]
300 MalformedLine { line: usize },
301 #[error("missing version line")]
302 MissingVersion,
303 #[error("first key is {got:?}, expected version")]
304 NotVersionFirst { got: String },
305 #[error("extra content on line {line}: {content:?}")]
306 ExtraLine { line: usize, content: String },
307}
308
309#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
310pub enum MalformedReason {
311 #[error("unrecognized version: {0:?}")]
312 InvalidVersion(String),
313 #[error("expected key {expected:?}, got {got:?}")]
314 UnexpectedKey { expected: &'static str, got: String },
315 #[error("missing required {0:?} line")]
316 MissingField(&'static str),
317 #[error("oid value {0:?} is not in the form <type>:<hash>")]
318 MalformedOidValue(String),
319 #[error("unsupported oid type {0:?}; only sha256 is supported")]
320 UnsupportedOidType(String),
321 #[error("invalid oid hash: {0}")]
322 InvalidOidHash(#[source] OidParseError),
323 #[error("size value {0:?} is not a non-negative integer")]
324 InvalidSize(String),
325 #[error("duplicate extension priority {0}")]
326 DuplicateExtensionPriority(u8),
327}
328
329#[cfg(test)]
330mod tests {
331 use super::*;
332
333 fn sha(hex: &str) -> Oid {
334 Oid::from_hex(hex).unwrap()
335 }
336
337 const SAMPLE_OID_HEX: &str = "4d7a214614ab2935c943f9e0ff69d22eadbb8f32b1258daaa5e2ca24d17e2393";
338
339 #[test]
342 fn encode_simple() {
343 let p = Pointer::new(sha(SAMPLE_OID_HEX), 12345);
344 let expected =
345 format!("version {VERSION_LATEST}\noid sha256:{SAMPLE_OID_HEX}\nsize 12345\n");
346 assert_eq!(p.encode(), expected);
347 }
348
349 #[test]
350 fn encode_empty() {
351 assert_eq!(Pointer::empty().encode(), "");
353 let p = Pointer::new(sha(SAMPLE_OID_HEX), 0);
355 assert_eq!(p.encode(), "");
356 }
357
358 #[test]
359 fn encode_extensions_sorted_on_output() {
360 let exts = vec![
361 Extension {
362 name: "baz".into(),
363 priority: 2,
364 oid: sha("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"),
365 },
366 Extension {
367 name: "foo".into(),
368 priority: 0,
369 oid: sha("ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"),
370 },
371 Extension {
372 name: "bar".into(),
373 priority: 1,
374 oid: sha("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"),
375 },
376 ];
377 let p = Pointer {
378 oid: sha(SAMPLE_OID_HEX),
379 size: 12345,
380 extensions: exts,
381 canonical: true,
382 };
383 let expected = format!(
384 "version {VERSION_LATEST}\n\
385 ext-0-foo sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\n\
386 ext-1-bar sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\n\
387 ext-2-baz sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n\
388 oid sha256:{SAMPLE_OID_HEX}\n\
389 size 12345\n",
390 );
391 assert_eq!(p.encode(), expected);
392 }
393
394 #[test]
397 fn parse_standard() {
398 let input = format!("version {VERSION_LATEST}\noid sha256:{SAMPLE_OID_HEX}\nsize 12345\n");
399 let p = Pointer::parse(input.as_bytes()).unwrap();
400 assert_eq!(p.oid, sha(SAMPLE_OID_HEX));
401 assert_eq!(p.size, 12345);
402 assert!(p.extensions.is_empty());
403 assert!(p.canonical);
404 }
405
406 #[test]
407 fn parse_empty_input_is_empty_pointer() {
408 let p = Pointer::parse(b"").unwrap();
409 assert_eq!(p, Pointer::empty());
410 assert!(p.canonical);
411 }
412
413 #[test]
414 fn parse_extensions_sorted() {
415 let input = format!(
416 "version {VERSION_LATEST}\n\
417 ext-0-foo sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\n\
418 ext-1-bar sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\n\
419 ext-2-baz sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n\
420 oid sha256:{SAMPLE_OID_HEX}\n\
421 size 12345\n",
422 );
423 let p = Pointer::parse(input.as_bytes()).unwrap();
424 assert_eq!(p.extensions.len(), 3);
425 assert_eq!(p.extensions[0].name, "foo");
426 assert_eq!(p.extensions[0].priority, 0);
427 assert_eq!(p.extensions[1].name, "bar");
428 assert_eq!(p.extensions[2].name, "baz");
429 assert!(p.canonical);
430 }
431
432 #[test]
433 fn parse_unsorted_extensions_sorts_and_marks_noncanonical() {
434 let input = format!(
436 "version {VERSION_LATEST}\n\
437 ext-2-baz sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n\
438 ext-0-foo sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\n\
439 ext-1-bar sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\n\
440 oid sha256:{SAMPLE_OID_HEX}\n\
441 size 12345\n",
442 );
443 let p = Pointer::parse(input.as_bytes()).unwrap();
444 assert_eq!(p.extensions[0].priority, 0);
445 assert_eq!(p.extensions[1].priority, 1);
446 assert_eq!(p.extensions[2].priority, 2);
447 assert!(!p.canonical);
448 }
449
450 #[test]
451 fn parse_pre_release_version_alias() {
452 let input = format!(
453 "version https://hawser.github.com/spec/v1\noid sha256:{SAMPLE_OID_HEX}\nsize 12345\n"
454 );
455 let p = Pointer::parse(input.as_bytes()).unwrap();
456 assert_eq!(p.size, 12345);
457 assert!(!p.canonical);
459 assert!(
460 p.encode()
461 .starts_with(&format!("version {VERSION_LATEST}\n"))
462 );
463 }
464
465 #[test]
466 fn parse_round_trip() {
467 let p = Pointer::new(sha(SAMPLE_OID_HEX), 12345);
468 let encoded = p.encode();
469 let parsed = Pointer::parse(encoded.as_bytes()).unwrap();
470 assert_eq!(parsed.oid, p.oid);
471 assert_eq!(parsed.size, p.size);
472 assert!(parsed.canonical);
473 }
474
475 #[test]
478 fn canonical_examples() {
479 let s = format!("version {VERSION_LATEST}\noid sha256:{SAMPLE_OID_HEX}\nsize 12345\n");
481 assert!(Pointer::parse(s.as_bytes()).unwrap().canonical);
482
483 assert!(Pointer::parse(b"").unwrap().canonical);
485 }
486
487 #[test]
488 fn non_canonical_examples() {
489 let cases: &[&str] = &[
490 "version https://git-lfs.github.com/spec/v1\noid sha256:4d7a214614ab2935c943f9e0ff69d22eadbb8f32b1258daaa5e2ca24d17e2393\nsize 12345",
492 "version https://git-lfs.github.com/spec/v1\r\noid sha256:4d7a214614ab2935c943f9e0ff69d22eadbb8f32b1258daaa5e2ca24d17e2393\r\nsize 12345\r\n",
494 "version https://git-lfs.github.com/spec/v1\noid sha256:4d7a214614ab2935c943f9e0ff69d22eadbb8f32b1258daaa5e2ca24d17e2393\nsize 12345 \n",
496 ];
497 for case in cases {
498 let p = Pointer::parse(case.as_bytes())
499 .unwrap_or_else(|e| panic!("failed to parse {case:?}: {e}"));
500 assert!(!p.canonical, "expected non-canonical for {case:?}");
501 }
502 }
503
504 #[test]
507 fn tiny_non_pointer_is_not_a_pointer() {
508 let err = Pointer::parse(b"this is not a git-lfs file!").unwrap_err();
509 assert!(err.is_not_a_pointer(), "expected NotAPointer, got {err:?}");
510 }
511
512 #[test]
513 fn header_only_is_not_a_pointer() {
514 let err = Pointer::parse(b"# git-media").unwrap_err();
516 assert!(err.is_not_a_pointer(), "expected NotAPointer, got {err:?}");
517 }
518
519 #[test]
520 fn oversized_input_is_not_a_pointer() {
521 let big = vec![b'x'; MAX_POINTER_SIZE + 1];
522 let err = Pointer::parse(&big).unwrap_err();
523 assert!(matches!(
524 err,
525 DecodeError::NotAPointer(NotAPointerReason::TooLarge { .. })
526 ));
527 }
528
529 #[test]
530 fn exactly_max_size_is_not_a_pointer() {
531 let exact = vec![b'x'; MAX_POINTER_SIZE];
533 let err = Pointer::parse(&exact).unwrap_err();
534 assert!(matches!(
535 err,
536 DecodeError::NotAPointer(NotAPointerReason::TooLarge { .. })
537 ));
538 }
539
540 #[test]
541 fn equals_separator_is_not_a_pointer() {
542 let s = "version=https://git-lfs.github.com/spec/v1\n\
544 oid=sha256:4d7a214614ab2935c943f9e0ff69d22eadbb8f32b1258daaa5e2ca24d17e2393\n\
545 size=fif";
546 let err = Pointer::parse(s.as_bytes()).unwrap_err();
547 assert!(err.is_not_a_pointer());
548 }
549
550 #[test]
551 fn no_marker_is_not_a_pointer() {
552 let err = Pointer::parse(b"version=http://wat.io/v/2\noid=foo\nsize=fif").unwrap_err();
553 assert!(matches!(
554 err,
555 DecodeError::NotAPointer(NotAPointerReason::MissingHeader)
556 ));
557 }
558
559 #[test]
560 fn missing_version_first_is_not_a_pointer() {
561 let s = format!("oid sha256:{SAMPLE_OID_HEX}\nsize 12345\n");
563 let err = Pointer::parse(s.as_bytes()).unwrap_err();
564 assert!(err.is_not_a_pointer(), "got {err:?}");
565 }
566
567 #[test]
568 fn extra_line_after_size_is_not_a_pointer() {
569 let s =
570 format!("version {VERSION_LATEST}\noid sha256:{SAMPLE_OID_HEX}\nsize 12345\nwat wat\n");
571 let err = Pointer::parse(s.as_bytes()).unwrap_err();
572 assert!(matches!(
573 err,
574 DecodeError::NotAPointer(NotAPointerReason::ExtraLine { .. })
575 ));
576 }
577
578 #[test]
581 fn invalid_version_is_malformed() {
582 let s = format!(
584 "version http://git-media.io/v/whatever\noid sha256:{SAMPLE_OID_HEX}\nsize 12345\n"
585 );
586 let err = Pointer::parse(s.as_bytes()).unwrap_err();
587 assert!(matches!(
588 err,
589 DecodeError::Malformed(MalformedReason::InvalidVersion(_))
590 ));
591 }
592
593 #[test]
594 fn missing_oid_is_malformed() {
595 let s = format!("version {VERSION_LATEST}\nsize 12345\n");
596 let err = Pointer::parse(s.as_bytes()).unwrap_err();
597 assert!(matches!(err, DecodeError::Malformed(_)));
598 }
599
600 #[test]
601 fn missing_size_is_malformed() {
602 let s = format!("version {VERSION_LATEST}\noid sha256:{SAMPLE_OID_HEX}\n");
603 let err = Pointer::parse(s.as_bytes()).unwrap_err();
604 assert!(matches!(
605 err,
606 DecodeError::Malformed(MalformedReason::MissingField("size"))
607 ));
608 }
609
610 #[test]
611 fn keys_out_of_order_is_malformed() {
612 let s = format!("version {VERSION_LATEST}\nsize 12345\noid sha256:{SAMPLE_OID_HEX}\n");
613 let err = Pointer::parse(s.as_bytes()).unwrap_err();
614 assert!(matches!(
615 err,
616 DecodeError::Malformed(MalformedReason::UnexpectedKey { .. })
617 ));
618 }
619
620 #[test]
621 fn bad_oid_hex_is_malformed() {
622 let s = format!("version {VERSION_LATEST}\noid sha256:boom\nsize 12345\n");
623 let err = Pointer::parse(s.as_bytes()).unwrap_err();
624 assert!(matches!(
625 err,
626 DecodeError::Malformed(MalformedReason::InvalidOidHash(_))
627 ));
628 }
629
630 #[test]
631 fn bad_oid_type_is_malformed() {
632 let s = format!("version {VERSION_LATEST}\noid shazam:{SAMPLE_OID_HEX}\nsize 12345\n");
633 let err = Pointer::parse(s.as_bytes()).unwrap_err();
634 assert!(matches!(
635 err,
636 DecodeError::Malformed(MalformedReason::UnsupportedOidType(_))
637 ));
638 }
639
640 #[test]
641 fn bad_size_is_malformed() {
642 let s = format!("version {VERSION_LATEST}\noid sha256:{SAMPLE_OID_HEX}\nsize fif\n");
643 let err = Pointer::parse(s.as_bytes()).unwrap_err();
644 assert!(matches!(
645 err,
646 DecodeError::Malformed(MalformedReason::InvalidSize(_))
647 ));
648 }
649
650 #[test]
651 fn negative_size_is_malformed() {
652 let s = format!("version {VERSION_LATEST}\noid sha256:{SAMPLE_OID_HEX}\nsize -1\n");
653 let err = Pointer::parse(s.as_bytes()).unwrap_err();
654 assert!(matches!(
655 err,
656 DecodeError::Malformed(MalformedReason::InvalidSize(_))
657 ));
658 }
659
660 #[test]
661 fn oid_with_trailing_garbage_is_malformed() {
662 let s = format!("version {VERSION_LATEST}\noid sha256:{SAMPLE_OID_HEX}&\nsize 177735\n");
663 let err = Pointer::parse(s.as_bytes()).unwrap_err();
664 assert!(matches!(
665 err,
666 DecodeError::Malformed(MalformedReason::InvalidOidHash(_))
667 ));
668 }
669
670 #[test]
673 fn ext_priority_over_9_is_malformed() {
674 let s = format!(
676 "version {VERSION_LATEST}\n\
677 ext-10-foo sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\n\
678 oid sha256:{SAMPLE_OID_HEX}\n\
679 size 12345\n",
680 );
681 let err = Pointer::parse(s.as_bytes()).unwrap_err();
682 assert!(matches!(err, DecodeError::Malformed(_)), "got {err:?}");
683 }
684
685 #[test]
686 fn ext_with_non_digit_priority_is_malformed() {
687 let s = format!(
688 "version {VERSION_LATEST}\n\
689 ext-#-foo sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\n\
690 oid sha256:{SAMPLE_OID_HEX}\n\
691 size 12345\n",
692 );
693 let err = Pointer::parse(s.as_bytes()).unwrap_err();
694 assert!(matches!(err, DecodeError::Malformed(_)), "got {err:?}");
695 }
696
697 #[test]
698 fn ext_with_non_word_name_is_malformed() {
699 let s = format!(
700 "version {VERSION_LATEST}\n\
701 ext-0-$$$$ sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\n\
702 oid sha256:{SAMPLE_OID_HEX}\n\
703 size 12345\n",
704 );
705 let err = Pointer::parse(s.as_bytes()).unwrap_err();
706 assert!(matches!(err, DecodeError::Malformed(_)), "got {err:?}");
707 }
708
709 #[test]
710 fn ext_bad_oid_is_malformed() {
711 let s = format!(
712 "version {VERSION_LATEST}\n\
713 ext-0-foo sha256:boom\n\
714 oid sha256:{SAMPLE_OID_HEX}\n\
715 size 12345\n",
716 );
717 let err = Pointer::parse(s.as_bytes()).unwrap_err();
718 assert!(matches!(
719 err,
720 DecodeError::Malformed(MalformedReason::InvalidOidHash(_))
721 ));
722 }
723
724 #[test]
725 fn ext_bad_oid_type_is_malformed() {
726 let s = format!(
727 "version {VERSION_LATEST}\n\
728 ext-0-foo boom:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\n\
729 oid sha256:{SAMPLE_OID_HEX}\n\
730 size 12345\n",
731 );
732 let err = Pointer::parse(s.as_bytes()).unwrap_err();
733 assert!(matches!(
734 err,
735 DecodeError::Malformed(MalformedReason::UnsupportedOidType(_))
736 ));
737 }
738
739 #[test]
740 fn duplicate_ext_priority_is_malformed() {
741 let s = format!(
742 "version {VERSION_LATEST}\n\
743 ext-0-foo sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\n\
744 ext-0-bar sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\n\
745 oid sha256:{SAMPLE_OID_HEX}\n\
746 size 12345\n",
747 );
748 let err = Pointer::parse(s.as_bytes()).unwrap_err();
749 assert!(matches!(
750 err,
751 DecodeError::Malformed(MalformedReason::DuplicateExtensionPriority(0))
752 ));
753 }
754}