1use crate::crc::{Crc16Stream, Crc32Stream};
33use crate::error::{try_vec, Error, ParseError};
34use std::io::{self, Read, Seek, SeekFrom, Write};
35
36pub const MAGIC: &[u8; 4] = b"CBIN";
37
38const HEAD_LEN: usize = 0x18;
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43pub enum Generation {
44 V0,
45 V1,
46}
47
48impl Generation {
49 pub fn body_start(self) -> u64 {
51 match self {
52 Generation::V0 => 0x18,
53 Generation::V1 => 0x2c,
54 }
55 }
56
57 pub(crate) fn trailer_len(self) -> u64 {
59 match self {
60 Generation::V0 => 2,
61 Generation::V1 => 0,
62 }
63 }
64}
65
66pub type Tag = [u8; 4];
71
72#[track_caller]
76fn tag(format: &str) -> Tag {
77 format
78 .as_bytes()
79 .try_into()
80 .unwrap_or_else(|_| panic!("format tag {format:?} is not 4 bytes — bug in format module"))
81}
82
83fn tag_str(tag: &Tag) -> String {
84 String::from_utf8_lossy(tag).into_owned()
85}
86
87#[derive(Clone, PartialEq, Eq)]
93pub struct Header {
94 pub generation: Generation,
95 pub tag: Tag,
96 pub location: u32,
99 pub aux: u32,
102 pub version: u32,
105}
106
107impl Header {
108 pub fn new(format: &str, location: (u16, u16), version: u32) -> Header {
111 Header {
112 generation: Generation::V1,
113 tag: tag(format),
114 location: (location.0 as u32) | ((location.1 as u32) << 16),
115 aux: 0xFFFF_FFFF,
116 version,
117 }
118 }
119
120 pub fn slot(&self) -> (u16, u16) {
122 (self.location as u16, (self.location >> 16) as u16)
123 }
124
125 pub fn category(&self) -> Option<u16> {
131 match (self.aux >> 16, self.aux as u16) {
132 (0, id) => Some(id),
133 _ => None,
134 }
135 }
136
137 pub fn set_slot(&mut self, (bank, slot): (u16, u16)) {
138 self.location = (bank as u32) | ((slot as u32) << 16);
139 }
140
141 fn head_bytes(&self) -> [u8; HEAD_LEN] {
143 let generation: u32 = match self.generation {
144 Generation::V0 => 0,
145 Generation::V1 => 1,
146 };
147 let mut out = [0u8; HEAD_LEN];
148 out[0..4].copy_from_slice(MAGIC);
149 out[4..8].copy_from_slice(&generation.to_le_bytes());
150 out[8..12].copy_from_slice(&self.tag);
151 out[12..16].copy_from_slice(&self.location.to_le_bytes());
152 out[16..20].copy_from_slice(&self.aux.to_le_bytes());
153 out[20..24].copy_from_slice(&self.version.to_le_bytes());
154 out
155 }
156}
157
158impl std::fmt::Debug for Header {
159 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
160 f.debug_struct("Header")
161 .field("generation", &self.generation)
162 .field("tag", &tag_str(&self.tag))
163 .field("location", &format_args!("{:#010x}", self.location))
164 .field("aux", &format_args!("{:#010x}", self.aux))
165 .field("version", &self.version)
166 .finish()
167 }
168}
169
170pub trait Body: Sized {
172 const LEN: Option<u64> = None;
174
175 fn read<R: Read + Seek>(r: &mut BodyReader<'_, R>, header: &Header) -> Result<Self, Error>;
180
181 fn write<W: Write + Seek>(&self, w: &mut BodyWriter<'_, W>) -> Result<(), Error>;
183}
184
185#[derive(Debug)]
190pub struct Cbin<B> {
191 pub header: Header,
192 pub body: B,
193}
194
195impl<B> std::ops::Deref for Cbin<B> {
196 type Target = B;
197
198 fn deref(&self) -> &B {
199 &self.body
200 }
201}
202
203impl<B> std::ops::DerefMut for Cbin<B> {
204 fn deref_mut(&mut self) -> &mut B {
205 &mut self.body
206 }
207}
208
209enum Hash {
211 V0(Crc16Stream<'static>),
212 V1(Crc32Stream<'static>),
213}
214
215impl Hash {
216 fn new(generation: Generation) -> Hash {
217 match generation {
218 Generation::V0 => Hash::V0(Crc16Stream::new()),
219 Generation::V1 => Hash::V1(Crc32Stream::new()),
220 }
221 }
222
223 fn update(&mut self, bytes: &[u8]) {
224 match self {
225 Hash::V0(h) => h.update(bytes),
226 Hash::V1(h) => h.update(bytes),
227 }
228 }
229}
230
231fn le_u32(bytes: &[u8], at: usize) -> u32 {
232 u32::from_le_bytes([bytes[at], bytes[at + 1], bytes[at + 2], bytes[at + 3]])
233}
234
235fn tag_from_slice(bytes: &[u8]) -> Tag {
236 [bytes[0], bytes[1], bytes[2], bytes[3]]
237}
238
239pub(crate) fn read_header(r: &mut impl Read) -> Result<(Header, u32), Error> {
242 let mut head = [0u8; HEAD_LEN];
243 r.read_exact(&mut head)?;
244 if &head[0..4] != MAGIC {
245 return Err(ParseError::UnknownFileType(tag_str(&tag_from_slice(&head[0..4]))).into());
246 }
247 let le = |at: usize| le_u32(&head, at);
248 let generation = match le(4) {
249 0 => Generation::V0,
250 1 => Generation::V1,
251 other => {
252 return Err(ParseError::UnknownFormat(format!("CBIN header type {other}")).into());
253 }
254 };
255 let header = Header {
256 generation,
257 tag: tag_from_slice(&head[8..12]),
258 location: le(12),
259 aux: le(16),
260 version: le(20),
261 };
262
263 let mut stored_crc32 = 0;
264 if generation == Generation::V1 {
265 let mut rest = [0u8; 20];
266 r.read_exact(&mut rest)?;
267 stored_crc32 = le_u32(&rest, 0);
268 if rest[4..] != [0u8; 16] {
272 return Err(ParseError::AssertFail(
273 "nonzero bytes in the 0x1c..0x2c header pad".into(),
274 )
275 .into());
276 }
277 }
278 Ok((header, stored_crc32))
279}
280
281fn stream_end(r: &mut impl Seek) -> io::Result<u64> {
283 let pos = r.stream_position()?;
284 let end = r.seek(SeekFrom::End(0))?;
285 r.seek(SeekFrom::Start(pos))?;
286 Ok(end)
287}
288
289pub fn read<B: Body>(r: &mut (impl Read + Seek), format: &'static str) -> Result<Cbin<B>, Error> {
291 read_inner(r, Some(format))
292}
293
294pub fn read_raw(r: &mut (impl Read + Seek)) -> Result<Cbin<RawBody>, Error> {
296 read_inner(r, None)
297}
298
299fn read_inner<B: Body>(
300 r: &mut (impl Read + Seek),
301 format: Option<&'static str>,
302) -> Result<Cbin<B>, Error> {
303 let start = r.stream_position()?;
304 let (header, stored_crc32) = read_header(r)?;
305 if let Some(expected) = format {
306 if header.tag != tag(expected) {
307 return Err(ParseError::WrongFormat {
308 expected,
309 got: tag_str(&header.tag),
310 }
311 .into());
312 }
313 }
314 let format = format.map_or_else(|| tag_str(&header.tag), str::to_string);
317
318 let end = stream_end(r)?;
319 let overhead = header.generation.body_start() + header.generation.trailer_len();
320 if end < start + overhead {
321 return Err(ParseError::AssertFail(format!(
322 "{format}: {} bytes is shorter than the {overhead}-byte container",
323 end - start,
324 ))
325 .into());
326 }
327 let body_start = start + header.generation.body_start();
328 let body_len = end - body_start - header.generation.trailer_len();
329 if let Some(expected) = B::LEN {
330 if body_len != expected {
331 return Err(ParseError::WrongBodyLength {
332 format,
333 got: body_len,
334 expected,
335 }
336 .into());
337 }
338 }
339
340 let mut hash = Hash::new(header.generation);
341 if header.generation == Generation::V0 {
342 hash.update(&header.head_bytes());
345 }
346 let mut reader = BodyReader {
347 inner: r,
348 start: body_start,
349 len: body_len,
350 pos: 0,
351 hashed: 0,
352 hash,
353 };
354 let body = B::read(&mut reader, &header)?;
355 reader.verify(stored_crc32, &format)?;
356 Ok(Cbin { header, body })
357}
358
359impl<B: Body> Cbin<B> {
360 pub fn write_to(&self, w: &mut (impl Write + Seek)) -> Result<(), Error> {
361 let start = w.stream_position()?;
362 let head = self.header.head_bytes();
363 let mut hash = Hash::new(self.header.generation);
364 w.write_all(&head)?;
365 match self.header.generation {
366 Generation::V1 => w.write_all(&[0u8; 20])?,
369 Generation::V0 => hash.update(&head),
370 }
371
372 let body_start = start + self.header.generation.body_start();
373 let mut writer = BodyWriter {
374 inner: w,
375 pos: 0,
376 hash,
377 };
378 self.body.write(&mut writer)?;
379 let BodyWriter {
380 pos: written, hash, ..
381 } = writer;
382
383 if let Some(expected) = B::LEN {
384 if written != expected {
385 return Err(ParseError::WrongBodyLength {
386 format: tag_str(&self.header.tag),
387 got: written,
388 expected,
389 }
390 .into());
391 }
392 }
393
394 match hash {
395 Hash::V1(h) => {
396 w.seek(SeekFrom::Start(start + 0x18))?;
397 w.write_all(&h.value().to_le_bytes())?;
398 w.seek(SeekFrom::Start(body_start + written))?;
399 }
400 Hash::V0(h) => w.write_all(&h.value().to_le_bytes())?,
401 }
402 Ok(())
403 }
404}
405
406#[derive(Debug, Clone, PartialEq, Eq)]
413pub struct RawBody(pub Vec<u8>);
414
415impl Body for RawBody {
416 fn read<R: Read + Seek>(r: &mut BodyReader<'_, R>, _: &Header) -> Result<RawBody, Error> {
417 let len = usize::try_from(r.len()).map_err(|_| ParseError::OutOfBounds {
418 value: format!("{} body bytes", r.len()),
419 bound: "a length that fits this platform's usize".into(),
420 })?;
421 let mut bytes = try_vec(len)?;
422 r.read_exact(&mut bytes)?;
423 Ok(RawBody(bytes))
424 }
425
426 fn write<W: Write + Seek>(&self, w: &mut BodyWriter<'_, W>) -> Result<(), Error> {
427 w.write_all(&self.0)?;
428 Ok(())
429 }
430}
431
432#[derive(Debug, Clone)]
434pub struct Info {
435 pub header: Header,
436 pub body_len: u64,
437 pub checksum_ok: bool,
440}
441
442pub fn inspect(r: &mut (impl Read + Seek)) -> Result<Info, Error> {
445 let start = r.stream_position()?;
446 let (header, stored_crc32) = read_header(r)?;
447 let end = stream_end(r)?;
448 let overhead = header.generation.body_start() + header.generation.trailer_len();
449 if end < start + overhead {
450 return Err(ParseError::AssertFail(format!(
451 "{}: {} bytes is shorter than the {overhead}-byte container",
452 tag_str(&header.tag),
453 end - start,
454 ))
455 .into());
456 }
457 let body_len = end - start - overhead;
458
459 let mut hash = Hash::new(header.generation);
460 if header.generation == Generation::V0 {
461 hash.update(&header.head_bytes());
462 }
463 let mut remaining = body_len;
464 let mut scratch = [0u8; 8192];
465 while remaining > 0 {
466 let take = remaining.min(scratch.len() as u64) as usize;
467 r.read_exact(&mut scratch[..take])?;
468 hash.update(&scratch[..take]);
469 remaining -= take as u64;
470 }
471 let checksum_ok = match hash {
472 Hash::V1(h) => h.value() == stored_crc32,
473 Hash::V0(h) => {
474 let mut trailer = [0u8; 2];
475 r.read_exact(&mut trailer)?;
476 h.value() == u16::from_le_bytes(trailer)
477 }
478 };
479 Ok(Info {
480 header,
481 body_len,
482 checksum_ok,
483 })
484}
485
486pub struct BodyReader<'a, R: Read + Seek> {
495 inner: &'a mut R,
496 start: u64,
498 len: u64,
499 pos: u64,
501 hashed: u64,
504 hash: Hash,
505}
506
507impl<R: Read + Seek> BodyReader<'_, R> {
508 #[allow(clippy::len_without_is_empty)]
510 pub fn len(&self) -> u64 {
511 self.len
512 }
513
514 pub fn remaining(&self) -> u64 {
516 self.len - self.pos
517 }
518
519 fn verify(mut self, stored_crc32: u32, format: &str) -> Result<(), Error> {
521 self.seek(SeekFrom::Start(self.len))?;
522 match self.hash {
523 Hash::V1(h) => {
524 let computed = h.value();
525 if computed != stored_crc32 {
526 return Err(ParseError::AssertFail(format!(
527 "{format}: stored checksum {stored_crc32:#010x} does not match the \
528 body's {computed:#010x}"
529 ))
530 .into());
531 }
532 }
533 Hash::V0(h) => {
534 let mut trailer = [0u8; 2];
535 self.inner.read_exact(&mut trailer)?;
536 let stored = u16::from_le_bytes(trailer);
537 let computed = h.value();
538 if computed != stored {
539 return Err(ParseError::AssertFail(format!(
540 "{format}: stored checksum {stored:#06x} does not match the \
541 file's {computed:#06x}"
542 ))
543 .into());
544 }
545 }
546 }
547 Ok(())
548 }
549}
550
551impl<R: Read + Seek> Read for BodyReader<'_, R> {
552 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
553 let remaining = self.len - self.pos;
554 if remaining == 0 || buf.is_empty() {
555 return Ok(0);
556 }
557 let want = (buf.len() as u64).min(remaining) as usize;
558 let n = self.inner.read(&mut buf[..want])?;
559 let end = self.pos + n as u64;
560 if end > self.hashed {
561 let from = (self.hashed - self.pos) as usize;
564 self.hash.update(&buf[from..n]);
565 self.hashed = end;
566 }
567 self.pos = end;
568 Ok(n)
569 }
570}
571
572impl<R: Read + Seek> Seek for BodyReader<'_, R> {
573 fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
574 let target = match pos {
575 SeekFrom::Start(p) => p as i128,
576 SeekFrom::Current(d) => self.pos as i128 + d as i128,
577 SeekFrom::End(d) => self.len as i128 + d as i128,
578 };
579 if target < 0 || target > self.len as i128 {
580 return Err(io::Error::new(
581 io::ErrorKind::InvalidInput,
582 format!("seek to {target} outside the {}-byte body", self.len),
583 ));
584 }
585 let target = target as u64;
586
587 if target <= self.hashed {
588 self.inner.seek(SeekFrom::Start(self.start + target))?;
589 } else {
590 self.inner.seek(SeekFrom::Start(self.start + self.hashed))?;
592 let mut scratch = [0u8; 8192];
593 while self.hashed < target {
594 let take = (target - self.hashed).min(scratch.len() as u64) as usize;
595 self.inner.read_exact(&mut scratch[..take])?;
596 self.hash.update(&scratch[..take]);
597 self.hashed += take as u64;
598 }
599 }
600 self.pos = target;
601 Ok(target)
602 }
603}
604
605pub struct BodyWriter<'a, W: Write + Seek> {
611 inner: &'a mut W,
612 pos: u64,
614 hash: Hash,
615}
616
617impl<W: Write + Seek> Write for BodyWriter<'_, W> {
618 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
619 let n = self.inner.write(buf)?;
620 self.hash.update(&buf[..n]);
623 self.pos += n as u64;
624 Ok(n)
625 }
626
627 fn flush(&mut self) -> io::Result<()> {
628 self.inner.flush()
629 }
630}
631
632#[cfg(test)]
633mod tests {
634 use super::*;
635 use crate::crc::{crc16, crc32};
636 use std::io::Cursor;
637
638 #[test]
641 fn category_reads_only_the_program_shape() {
642 let mut h = Header::new("ne5p", (0, 0), 4);
643 assert_eq!(h.category(), None, "0xFFFFFFFF is no category");
644 h.aux = 0x0000_0017;
645 assert_eq!(h.category(), Some(0x17));
646 h.aux = 0x0000_0000;
647 assert_eq!(h.category(), Some(0), "zero is a value, not unset");
648 h.aux = 0x000a_0004; assert_eq!(
650 h.category(),
651 None,
652 "a set high u16 is not the category shape"
653 );
654 }
655
656 #[derive(Debug)]
658 struct Five([u8; 5]);
659
660 impl Body for Five {
661 const LEN: Option<u64> = Some(5);
662
663 fn read<R: Read + Seek>(r: &mut BodyReader<'_, R>, _: &Header) -> Result<Five, Error> {
664 let mut b = [0u8; 5];
665 r.read_exact(&mut b)?;
666 Ok(Five(b))
667 }
668
669 fn write<W: Write + Seek>(&self, w: &mut BodyWriter<'_, W>) -> Result<(), Error> {
670 w.write_all(&self.0)?;
671 Ok(())
672 }
673 }
674
675 fn v1_file(body: &[u8]) -> Vec<u8> {
676 let mut out = Vec::new();
677 out.extend_from_slice(b"CBIN");
678 out.extend_from_slice(&1u32.to_le_bytes());
679 out.extend_from_slice(b"test");
680 out.extend_from_slice(&0x0007_0003u32.to_le_bytes());
681 out.extend_from_slice(&u32::MAX.to_le_bytes());
682 out.extend_from_slice(&4u32.to_le_bytes());
683 out.extend_from_slice(&crc32(body).to_le_bytes());
684 out.extend_from_slice(&[0u8; 16]);
685 out.extend_from_slice(body);
686 out
687 }
688
689 fn v0_file(body: &[u8]) -> Vec<u8> {
690 let mut out = Vec::new();
691 out.extend_from_slice(b"CBIN");
692 out.extend_from_slice(&0u32.to_le_bytes());
693 out.extend_from_slice(b"test");
694 out.extend_from_slice(&0x0007_0003u32.to_le_bytes());
695 out.extend_from_slice(&u32::MAX.to_le_bytes());
696 out.extend_from_slice(&4u32.to_le_bytes());
697 out.extend_from_slice(body);
698 let crc = crc16(&out);
699 out.extend_from_slice(&crc.to_le_bytes());
700 out
701 }
702
703 #[test]
705 fn both_generations_round_trip_and_differ_by_18_bytes() {
706 let body = [0xaa, 0xbb, 0xcc, 0xdd, 0xee];
707 for bytes in [v1_file(&body), v0_file(&body)] {
708 let file: Cbin<Five> = read(&mut Cursor::new(&bytes), "test").unwrap();
709 assert_eq!(file.body.0, body);
710 assert_eq!(file.header.slot(), (3, 7));
711
712 let mut out = Cursor::new(Vec::new());
713 file.write_to(&mut out).unwrap();
714 assert_eq!(out.into_inner(), bytes, "round trip changed the bytes");
715 }
716 assert_eq!(v1_file(&body).len() - v0_file(&body).len(), 18);
717 }
718
719 #[test]
720 fn a_corrupted_byte_fails_either_checksum() {
721 let body = [1, 2, 3, 4, 5];
722 for mut bytes in [v1_file(&body), v0_file(&body)] {
723 let at = bytes.len() - 3;
724 bytes[at] ^= 0xff;
725 assert!(
726 read::<Five>(&mut Cursor::new(&bytes), "test").is_err(),
727 "a corrupted body must not verify"
728 );
729 }
730 }
731
732 #[test]
735 fn the_v0_checksum_covers_the_header() {
736 let mut bytes = v0_file(&[1, 2, 3, 4, 5]);
737 bytes[0x0c] ^= 0xff;
738 assert!(read::<Five>(&mut Cursor::new(&bytes), "test").is_err());
739
740 let mut bytes = v1_file(&[1, 2, 3, 4, 5]);
742 bytes[0x0c] ^= 0xff;
743 assert!(read::<Five>(&mut Cursor::new(&bytes), "test").is_ok());
744 }
745
746 #[test]
747 fn the_wrong_tag_is_refused_by_name() {
748 let bytes = v1_file(&[1, 2, 3, 4, 5]);
749 let err = read::<Five>(&mut Cursor::new(&bytes), "ne5p").unwrap_err();
750 assert!(
751 matches!(
752 err,
753 Error::Parse(ParseError::WrongFormat {
754 expected: "ne5p",
755 ..
756 })
757 ),
758 "refused for the wrong reason: {err}",
759 );
760 }
761
762 #[test]
763 fn the_wrong_length_is_refused_before_the_body_decodes() {
764 let bytes = v1_file(&[1, 2, 3]);
765 let err = read::<Five>(&mut Cursor::new(&bytes), "test").unwrap_err();
766 assert!(
767 matches!(
768 err,
769 Error::Parse(ParseError::WrongBodyLength {
770 got: 3,
771 expected: 5,
772 ..
773 })
774 ),
775 "refused for the wrong reason: {err}",
776 );
777 }
778
779 #[test]
782 fn an_unread_tail_is_still_verified() {
783 struct TwoOfFive;
784 impl Body for TwoOfFive {
785 fn read<R: Read + Seek>(
786 r: &mut BodyReader<'_, R>,
787 _: &Header,
788 ) -> Result<TwoOfFive, Error> {
789 let mut b = [0u8; 2];
790 r.read_exact(&mut b)?;
791 Ok(TwoOfFive)
792 }
793 fn write<W: Write + Seek>(&self, _: &mut BodyWriter<'_, W>) -> Result<(), Error> {
794 Ok(())
795 }
796 }
797
798 let mut bytes = v1_file(&[1, 2, 3, 4, 5]);
799 assert!(read::<TwoOfFive>(&mut Cursor::new(&bytes), "test").is_ok());
800 *bytes.last_mut().unwrap() ^= 0xff;
801 assert!(
802 read::<TwoOfFive>(&mut Cursor::new(&bytes), "test").is_err(),
803 "a corrupt byte the body never read must still fail verification",
804 );
805 }
806
807 #[test]
810 fn seeking_bodies_keep_the_checksum_exact() {
811 struct Skipper;
812 impl Body for Skipper {
813 fn read<R: Read + Seek>(
814 r: &mut BodyReader<'_, R>,
815 _: &Header,
816 ) -> Result<Skipper, Error> {
817 r.seek(SeekFrom::Start(4))?; let mut b = [0u8; 1];
819 r.read_exact(&mut b)?;
820 r.seek(SeekFrom::Start(0))?; r.read_exact(&mut b)?; Ok(Skipper)
823 }
824 fn write<W: Write + Seek>(&self, _: &mut BodyWriter<'_, W>) -> Result<(), Error> {
825 Ok(())
826 }
827 }
828
829 let bytes = v1_file(&[9, 8, 7, 6, 5]);
830 assert!(read::<Skipper>(&mut Cursor::new(&bytes), "test").is_ok());
831 }
832
833 #[test]
834 fn inspect_reports_both_generations_without_a_body() {
835 for (bytes, generation) in [
836 (v1_file(&[1, 2, 3]), Generation::V1),
837 (v0_file(&[1, 2, 3]), Generation::V0),
838 ] {
839 let info = inspect(&mut Cursor::new(&bytes)).unwrap();
840 assert_eq!(info.header.generation, generation);
841 assert_eq!(info.body_len, 3);
842 assert!(info.checksum_ok);
843
844 let mut corrupt = bytes.clone();
845 let at = corrupt.len() - 3;
846 corrupt[at] ^= 0xff;
847 let info = inspect(&mut Cursor::new(&corrupt)).unwrap();
848 assert!(!info.checksum_ok, "inspect reports, it does not refuse");
849 }
850 }
851
852 #[test]
855 fn a_header_type_that_is_neither_generation_is_refused() {
856 let mut bytes = v1_file(&[1, 2, 3, 4, 5]);
857 bytes[4..8].copy_from_slice(&2u32.to_le_bytes());
858 let err = read::<Five>(&mut Cursor::new(&bytes), "test").unwrap_err();
859 assert!(
860 matches!(err, Error::Parse(ParseError::UnknownFormat(ref what)) if what.contains("type 2")),
861 "refused for the wrong reason: {err}",
862 );
863 }
864
865 #[test]
868 fn a_nonzero_header_pad_is_refused() {
869 for at in 0x1c..0x2c {
870 let mut bytes = v1_file(&[1, 2, 3, 4, 5]);
871 bytes[at] = 0xff;
872 let err = read::<Five>(&mut Cursor::new(&bytes), "test")
873 .unwrap_err()
874 .to_string();
875 assert!(err.contains("header pad"), "byte {at:#x}: {err}");
876 }
877 }
878
879 #[test]
882 fn a_file_shorter_than_its_container_is_refused() {
883 let short = &v0_file(&[1, 2, 3, 4, 5])[..HEAD_LEN + 1];
885 let err = read::<Five>(&mut Cursor::new(short), "test").unwrap_err();
886 assert!(
887 matches!(&err, Error::Parse(ParseError::AssertFail(why))
888 if why.contains("shorter than the 26-byte container")),
889 "refused for the wrong reason: {err}",
890 );
891 assert!(inspect(&mut Cursor::new(short)).is_err());
892
893 let truncated = &v1_file(&[1, 2, 3, 4, 5])[..0x2b];
895 assert!(read::<Five>(&mut Cursor::new(truncated), "test").is_err());
896 }
897
898 #[test]
899 fn raw_bodies_round_trip_any_tag() {
900 let bytes = v0_file(&[1, 2, 3, 4, 5, 6, 7]);
901 let file = read_raw(&mut Cursor::new(&bytes)).unwrap();
902 assert_eq!(file.body.0, [1, 2, 3, 4, 5, 6, 7]);
903
904 let mut out = Cursor::new(Vec::new());
905 file.write_to(&mut out).unwrap();
906 assert_eq!(out.into_inner(), bytes);
907 }
908}