1use alloc::format;
21use alloc::string::String;
22use alloc::vec;
23use alloc::vec::Vec;
24
25use crate::{
26 EntryType, GnuExtSparseHeader, Header, HeaderError, Result, SparseEntry, UstarHeader,
27 HEADER_SIZE, PAX_ATIME, PAX_CTIME, PAX_GID, PAX_GNAME, PAX_GNU_SPARSE_MAJOR,
28 PAX_GNU_SPARSE_MINOR, PAX_GNU_SPARSE_NAME, PAX_GNU_SPARSE_REALSIZE, PAX_LINKPATH, PAX_MTIME,
29 PAX_PATH, PAX_SIZE, PAX_UID, PAX_UNAME,
30};
31
32pub(crate) struct DecU64 {
37 buf: [u8; 20],
38 start: u8,
39}
40
41impl DecU64 {
42 pub(crate) fn new(mut value: u64) -> Self {
43 let mut buf = [0u8; 20];
44 if value == 0 {
45 buf[19] = b'0';
46 return Self { buf, start: 19 };
47 }
48 let mut pos = 20u8;
49 while value > 0 {
50 pos -= 1;
51 buf[pos as usize] = b'0' + (value % 10) as u8;
52 value /= 10;
53 }
54 Self { buf, start: pos }
55 }
56
57 pub(crate) fn as_bytes(&self) -> &[u8] {
58 &self.buf[self.start as usize..]
59 }
60}
61
62fn write_bytes<const N: usize>(field: &mut [u8; N], value: &[u8]) -> Result<()> {
71 if value.len() > N {
72 return Err(HeaderError::FieldOverflow {
73 field_len: N,
74 detail: format!("{}-byte value", value.len()),
75 });
76 }
77 field.fill(0);
78 field[..value.len()].copy_from_slice(value);
79 Ok(())
80}
81
82#[derive(Clone)]
106pub struct HeaderBuilder {
107 header: Header,
108}
109
110impl HeaderBuilder {
111 #[must_use]
113 pub fn new_ustar() -> Self {
114 Self {
115 header: Header::new_ustar(),
116 }
117 }
118
119 #[must_use]
121 pub fn new_gnu() -> Self {
122 Self {
123 header: Header::new_gnu(),
124 }
125 }
126
127 fn fields_mut(&mut self) -> &mut UstarHeader {
133 self.header.as_ustar_mut()
134 }
135
136 pub fn path(&mut self, path: impl AsRef<[u8]>) -> Result<&mut Self> {
142 write_bytes(&mut self.fields_mut().name, path.as_ref())?;
143 Ok(self)
144 }
145
146 pub fn mode(&mut self, mode: u32) -> Result<&mut Self> {
153 self.header.set_mode(mode)?;
154 Ok(self)
155 }
156
157 pub fn uid(&mut self, uid: u64) -> Result<&mut Self> {
167 self.header.set_uid(uid)?;
168 Ok(self)
169 }
170
171 pub fn gid(&mut self, gid: u64) -> Result<&mut Self> {
181 self.header.set_gid(gid)?;
182 Ok(self)
183 }
184
185 pub fn size(&mut self, size: u64) -> Result<&mut Self> {
195 self.header.set_size(size)?;
196 Ok(self)
197 }
198
199 pub fn mtime(&mut self, mtime: u64) -> Result<&mut Self> {
209 self.header.set_mtime(mtime)?;
210 Ok(self)
211 }
212
213 pub fn entry_type(&mut self, entry_type: EntryType) -> &mut Self {
215 self.fields_mut().typeflag[0] = entry_type.to_byte();
216 self
217 }
218
219 pub fn link_name(&mut self, link: impl AsRef<[u8]>) -> Result<&mut Self> {
225 write_bytes(&mut self.fields_mut().linkname, link.as_ref())?;
226 Ok(self)
227 }
228
229 pub fn username(&mut self, name: impl AsRef<[u8]>) -> Result<&mut Self> {
235 write_bytes(&mut self.fields_mut().uname, name.as_ref())?;
236 Ok(self)
237 }
238
239 pub fn groupname(&mut self, name: impl AsRef<[u8]>) -> Result<&mut Self> {
245 write_bytes(&mut self.fields_mut().gname, name.as_ref())?;
246 Ok(self)
247 }
248
249 pub fn device(&mut self, major: u32, minor: u32) -> Result<&mut Self> {
258 self.header.set_device(major, minor)?;
259 Ok(self)
260 }
261
262 pub fn prefix(&mut self, prefix: impl AsRef<[u8]>) -> Result<&mut Self> {
268 write_bytes(&mut self.fields_mut().prefix, prefix.as_ref())?;
269 Ok(self)
270 }
271
272 #[must_use]
277 pub fn as_header(&self) -> &Header {
278 &self.header
279 }
280
281 pub fn as_header_mut(&mut self) -> &mut Header {
286 &mut self.header
287 }
288
289 #[must_use]
294 pub fn finish(&mut self) -> Header {
295 self.header.as_ustar_mut().cksum.fill(b' ');
297
298 let checksum: u64 = self.header.as_bytes().iter().map(|&b| u64::from(b)).sum();
300
301 crate::encode_octal(&mut self.header.as_ustar_mut().cksum, checksum)
304 .expect("checksum always fits in 8-byte octal field");
305
306 self.header
307 }
308}
309
310impl Default for HeaderBuilder {
311 fn default() -> Self {
312 Self::new_ustar()
313 }
314}
315
316impl core::fmt::Debug for HeaderBuilder {
317 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
318 f.debug_struct("HeaderBuilder")
319 .field("header", self.as_header())
320 .finish()
321 }
322}
323
324#[derive(Clone, Default)]
347pub struct PaxBuilder {
348 data: Vec<u8>,
349}
350
351impl PaxBuilder {
352 #[must_use]
354 pub fn new() -> Self {
355 Self { data: Vec::new() }
356 }
357
358 pub fn add(&mut self, key: &str, value: impl AsRef<[u8]>) -> &mut Self {
366 let value = value.as_ref();
367 let rest_len = 3 + key.len() + value.len();
370
371 let mut len_len = 1;
375 let mut max_len = 10;
376 while rest_len + len_len >= max_len {
377 len_len += 1;
378 max_len *= 10;
379 }
380 let total_len = rest_len + len_len;
381
382 let len_dec = DecU64::new(total_len as u64);
383 self.data.extend_from_slice(len_dec.as_bytes());
384 self.data.push(b' ');
385 self.data.extend_from_slice(key.as_bytes());
386 self.data.push(b'=');
387 self.data.extend_from_slice(value);
388 self.data.push(b'\n');
389
390 self
391 }
392
393 pub fn path(&mut self, path: impl AsRef<[u8]>) -> &mut Self {
395 self.add(PAX_PATH, path)
396 }
397
398 pub fn linkpath(&mut self, path: impl AsRef<[u8]>) -> &mut Self {
400 self.add(PAX_LINKPATH, path)
401 }
402
403 pub fn add_u64(&mut self, key: &str, value: u64) -> &mut Self {
405 let buf = DecU64::new(value);
406 self.add(key, buf.as_bytes())
407 }
408
409 pub fn size(&mut self, size: u64) -> &mut Self {
411 self.add_u64(PAX_SIZE, size)
412 }
413
414 pub fn uid(&mut self, uid: u64) -> &mut Self {
416 self.add_u64(PAX_UID, uid)
417 }
418
419 pub fn gid(&mut self, gid: u64) -> &mut Self {
421 self.add_u64(PAX_GID, gid)
422 }
423
424 pub fn uname(&mut self, name: impl AsRef<[u8]>) -> &mut Self {
426 self.add(PAX_UNAME, name)
427 }
428
429 pub fn gname(&mut self, name: impl AsRef<[u8]>) -> &mut Self {
431 self.add(PAX_GNAME, name)
432 }
433
434 pub fn mtime(&mut self, mtime: u64) -> &mut Self {
436 self.add_u64(PAX_MTIME, mtime)
437 }
438
439 pub fn atime(&mut self, atime: u64) -> &mut Self {
441 self.add_u64(PAX_ATIME, atime)
442 }
443
444 pub fn ctime(&mut self, ctime: u64) -> &mut Self {
446 self.add_u64(PAX_CTIME, ctime)
447 }
448
449 #[must_use]
451 pub fn as_bytes(&self) -> &[u8] {
452 &self.data
453 }
454
455 #[must_use]
457 pub fn finish(self) -> Vec<u8> {
458 self.data
459 }
460}
461
462impl core::fmt::Debug for PaxBuilder {
463 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
464 f.debug_struct("PaxBuilder")
465 .field("data", &String::from_utf8_lossy(&self.data))
466 .finish()
467 }
468}
469
470#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
480pub enum ExtensionMode {
481 #[default]
487 Gnu,
488 Pax,
494}
495
496pub const NAME_MAX_LEN: usize = 100;
498
499pub const LINKNAME_MAX_LEN: usize = 100;
501
502const GNU_LONGLINK_NAME: &[u8] = b"././@LongLink";
504
505#[derive(Clone)]
550pub struct EntryBuilder {
551 header: HeaderBuilder,
553 long_path: Option<Vec<u8>>,
555 long_link: Option<Vec<u8>>,
557 pax: Option<PaxBuilder>,
559 mode: ExtensionMode,
561 sparse: Option<SparseInfo>,
563}
564
565#[derive(Clone)]
567struct SparseInfo {
568 map: Vec<SparseEntry>,
570 real_size: u64,
572}
573
574impl EntryBuilder {
575 #[must_use]
579 pub fn new_gnu() -> Self {
580 Self {
581 header: HeaderBuilder::new_gnu(),
582 long_path: None,
583 long_link: None,
584 pax: None,
585 mode: ExtensionMode::Gnu,
586 sparse: None,
587 }
588 }
589
590 #[must_use]
594 pub fn new_ustar() -> Self {
595 Self {
596 header: HeaderBuilder::new_ustar(),
597 long_path: None,
598 long_link: None,
599 pax: None,
600 mode: ExtensionMode::Pax,
601 sparse: None,
602 }
603 }
604
605 #[must_use]
607 pub fn with_mode(header: HeaderBuilder, mode: ExtensionMode) -> Self {
608 Self {
609 header,
610 long_path: None,
611 long_link: None,
612 pax: None,
613 mode,
614 sparse: None,
615 }
616 }
617
618 #[must_use]
620 pub fn extension_mode(&self) -> ExtensionMode {
621 self.mode
622 }
623
624 pub fn set_extension_mode(&mut self, mode: ExtensionMode) -> &mut Self {
626 self.mode = mode;
627 self
628 }
629
630 pub fn path(&mut self, path: impl AsRef<[u8]>) -> &mut Self {
636 let path = path.as_ref();
637 if path.len() > NAME_MAX_LEN {
638 self.long_path = Some(path.to_vec());
639 let truncated = &path[..NAME_MAX_LEN];
642 self.header
643 .path(truncated)
644 .expect("truncated path fits in name field");
645 } else {
646 self.long_path = None;
647 self.header
648 .path(path)
649 .expect("path within NAME_MAX_LEN fits in name field");
650 }
651 self
652 }
653
654 pub fn link_name(&mut self, link: impl AsRef<[u8]>) -> &mut Self {
659 let link = link.as_ref();
660 if link.len() > LINKNAME_MAX_LEN {
661 self.long_link = Some(link.to_vec());
662 let truncated = &link[..LINKNAME_MAX_LEN];
663 self.header
664 .link_name(truncated)
665 .expect("truncated link fits in linkname field");
666 } else {
667 self.long_link = None;
668 self.header
669 .link_name(link)
670 .expect("link within LINKNAME_MAX_LEN fits in linkname field");
671 }
672 self
673 }
674
675 pub fn mode(&mut self, mode: u32) -> Result<&mut Self> {
683 self.header.mode(mode)?;
684 Ok(self)
685 }
686
687 pub fn uid(&mut self, uid: u64) -> Result<&mut Self> {
698 match self.header.uid(uid) {
699 Ok(_) => Ok(self),
700 Err(_) if self.mode == ExtensionMode::Pax => {
701 self.header.uid(0).expect("zero fits");
702 self.pax_mut().uid(uid);
703 Ok(self)
704 }
705 Err(e) => Err(e),
706 }
707 }
708
709 pub fn gid(&mut self, gid: u64) -> Result<&mut Self> {
720 match self.header.gid(gid) {
721 Ok(_) => Ok(self),
722 Err(_) if self.mode == ExtensionMode::Pax => {
723 self.header.gid(0).expect("zero fits");
724 self.pax_mut().gid(gid);
725 Ok(self)
726 }
727 Err(e) => Err(e),
728 }
729 }
730
731 pub fn size(&mut self, size: u64) -> Result<&mut Self> {
742 match self.header.size(size) {
743 Ok(_) => Ok(self),
744 Err(_) if self.mode == ExtensionMode::Pax => {
745 self.header.size(0).expect("zero fits");
746 self.pax_mut().size(size);
747 Ok(self)
748 }
749 Err(e) => Err(e),
750 }
751 }
752
753 pub fn mtime(&mut self, mtime: u64) -> Result<&mut Self> {
764 match self.header.mtime(mtime) {
765 Ok(_) => Ok(self),
766 Err(_) if self.mode == ExtensionMode::Pax => {
767 self.header.mtime(0).expect("zero fits");
768 self.pax_mut().mtime(mtime);
769 Ok(self)
770 }
771 Err(e) => Err(e),
772 }
773 }
774
775 pub fn entry_type(&mut self, entry_type: EntryType) -> &mut Self {
777 self.header.entry_type(entry_type);
778 self
779 }
780
781 pub fn username(&mut self, name: impl AsRef<[u8]>) -> Result<&mut Self> {
792 let name = name.as_ref();
793 match self.header.username(name) {
794 Ok(_) => Ok(self),
795 Err(_) if self.mode == ExtensionMode::Pax => {
796 self.header.username([]).expect("empty fits");
798 self.pax_mut().uname(name);
799 Ok(self)
800 }
801 Err(e) => Err(e),
802 }
803 }
804
805 pub fn groupname(&mut self, name: impl AsRef<[u8]>) -> Result<&mut Self> {
816 let name = name.as_ref();
817 match self.header.groupname(name) {
818 Ok(_) => Ok(self),
819 Err(_) if self.mode == ExtensionMode::Pax => {
820 self.header.groupname([]).expect("empty fits");
821 self.pax_mut().gname(name);
822 Ok(self)
823 }
824 Err(e) => Err(e),
825 }
826 }
827
828 pub fn device(&mut self, major: u32, minor: u32) -> Result<&mut Self> {
836 self.header.device(major, minor)?;
837 Ok(self)
838 }
839
840 pub fn sparse(&mut self, sparse_map: &[SparseEntry], real_size: u64) -> &mut Self {
856 self.sparse = Some(SparseInfo {
857 map: sparse_map.to_vec(),
858 real_size,
859 });
860 self
861 }
862
863 pub fn add_pax(&mut self, key: &str, value: impl AsRef<[u8]>) -> &mut Self {
869 self.pax_mut().add(key, value);
870 self
871 }
872
873 fn pax_mut(&mut self) -> &mut PaxBuilder {
875 self.pax.get_or_insert_with(PaxBuilder::new)
876 }
877
878 #[must_use]
880 pub fn header(&self) -> &HeaderBuilder {
881 &self.header
882 }
883
884 pub fn header_mut(&mut self) -> &mut HeaderBuilder {
886 &mut self.header
887 }
888
889 #[must_use]
891 pub fn needs_extension(&self) -> bool {
892 self.long_path.is_some() || self.long_link.is_some() || self.pax.is_some()
893 }
894
895 #[must_use]
905 pub fn finish(&mut self) -> Vec<Header> {
906 let sparse = self.sparse.take();
907 let mut blocks = Vec::new();
908
909 let pax_sparse_map_data: Option<Vec<u8>> = match (&sparse, self.mode) {
913 (Some(si), ExtensionMode::Pax) => Some(Self::build_sparse_map_data(si)),
914 _ => None,
915 };
916
917 match self.mode {
918 ExtensionMode::Gnu => {
919 if let Some(ref si) = sparse {
921 self.header.entry_type(EntryType::GnuSparse);
922 if let Some(gnu) = self.header.as_header_mut().try_as_gnu_mut() {
923 gnu.set_real_size(si.real_size);
924 for (i, entry) in si.map.iter().take(4).enumerate() {
925 gnu.sparse[i].set(entry);
926 }
927 gnu.set_is_extended(si.map.len() > 4);
928 }
929 }
930
931 if let Some(ref long_link) = self.long_link {
933 self.emit_gnu_long_entry(&mut blocks, EntryType::GnuLongLink, long_link);
934 }
935
936 if let Some(ref long_path) = self.long_path {
938 self.emit_gnu_long_entry(&mut blocks, EntryType::GnuLongName, long_path);
939 }
940 }
941 ExtensionMode::Pax => {
942 if let Some(ref si) = sparse {
945 let map_padded = pax_sparse_map_data
946 .as_ref()
947 .unwrap()
948 .len()
949 .next_multiple_of(HEADER_SIZE);
950
951 let current_size = self.header.as_header().entry_size().unwrap_or(0);
955 self.header
956 .size(current_size + map_padded as u64)
957 .expect("adjusted size fits");
958
959 let real_size_str = DecU64::new(si.real_size);
960 self.pax_mut().add(PAX_GNU_SPARSE_MAJOR, b"1");
961 self.pax_mut().add(PAX_GNU_SPARSE_MINOR, b"0");
962 self.pax_mut()
963 .add(PAX_GNU_SPARSE_REALSIZE, real_size_str.as_bytes());
964
965 let real_path = self
968 .long_path
969 .take()
970 .unwrap_or_else(|| self.header.as_header().path_bytes().to_vec());
971 self.pax_mut().add(PAX_GNU_SPARSE_NAME, &real_path);
972
973 let synthetic = b"GNUSparseFile.0/placeholder";
975 self.header
976 .path(synthetic)
977 .expect("synthetic sparse path fits");
978 }
979
980 let pax_data = self.build_pax_data();
982 if !pax_data.is_empty() {
983 self.emit_pax_entry(&mut blocks, &pax_data);
984 }
985 }
986 }
987
988 let main_header = self.header.finish();
990 blocks.push(main_header);
991
992 if let Some(ref si) = sparse {
994 match self.mode {
995 ExtensionMode::Gnu => {
996 self.emit_gnu_sparse_ext_blocks(&mut blocks, si);
999 }
1000 ExtensionMode::Pax => {
1001 let map_data = pax_sparse_map_data.as_ref().unwrap();
1003 let map_padded = map_data.len().next_multiple_of(HEADER_SIZE);
1004 let mut buf = vec![0u8; map_padded];
1005 buf[..map_data.len()].copy_from_slice(map_data);
1006 for chunk in buf.chunks_exact(HEADER_SIZE) {
1007 blocks.push(*Header::from_bytes(
1008 chunk.try_into().expect("chunks_exact guarantees size"),
1009 ));
1010 }
1011 }
1012 }
1013 }
1014
1015 blocks
1016 }
1017
1018 #[must_use]
1022 pub fn finish_bytes(&mut self) -> Vec<u8> {
1023 let blocks = self.finish();
1024 let mut out = Vec::with_capacity(blocks.len() * HEADER_SIZE);
1025 for block in &blocks {
1026 out.extend_from_slice(block.as_bytes());
1027 }
1028 out
1029 }
1030
1031 fn emit_gnu_long_entry(&self, blocks: &mut Vec<Header>, entry_type: EntryType, data: &[u8]) {
1033 let data_with_null_len = data.len() + 1;
1035
1036 let mut ext_header = HeaderBuilder::new_gnu();
1039 ext_header
1040 .path(GNU_LONGLINK_NAME)
1041 .expect("GNU longlink name fits");
1042 ext_header.mode(0).expect("zero fits");
1043 ext_header.uid(0).expect("zero fits");
1044 ext_header.gid(0).expect("zero fits");
1045 ext_header
1046 .size(data_with_null_len as u64)
1047 .expect("extension data size fits");
1048 ext_header.mtime(0).expect("zero fits");
1049 ext_header.entry_type(entry_type);
1050
1051 blocks.push(ext_header.finish());
1052
1053 let num_data_blocks = data_with_null_len.div_ceil(HEADER_SIZE);
1055 let mut data_buf = vec![0u8; num_data_blocks * HEADER_SIZE];
1056 data_buf[..data.len()].copy_from_slice(data);
1057 for chunk in data_buf.chunks_exact(HEADER_SIZE) {
1060 blocks.push(*Header::from_bytes(
1061 chunk.try_into().expect("chunks_exact guarantees size"),
1062 ));
1063 }
1064 }
1065
1066 fn build_pax_data(&self) -> Vec<u8> {
1068 let mut pax = self.pax.clone().unwrap_or_default();
1069
1070 if let Some(ref long_path) = self.long_path {
1071 pax.path(long_path);
1072 }
1073
1074 if let Some(ref long_link) = self.long_link {
1075 pax.linkpath(long_link);
1076 }
1077
1078 pax.finish()
1079 }
1080
1081 fn emit_pax_entry(&self, blocks: &mut Vec<Header>, pax_data: &[u8]) {
1083 let pax_name = self.build_pax_header_name();
1086
1087 let mut pax_header = HeaderBuilder::new_ustar();
1090 pax_header.path(&pax_name).expect("PAX header name fits");
1091 pax_header.mode(0o644).expect("mode 0644 fits");
1092 pax_header.uid(0).expect("zero fits");
1093 pax_header.gid(0).expect("zero fits");
1094 pax_header
1095 .size(pax_data.len() as u64)
1096 .expect("PAX data size fits");
1097 pax_header.mtime(0).expect("zero fits");
1098 pax_header.entry_type(EntryType::XHeader);
1099
1100 blocks.push(pax_header.finish());
1101
1102 let num_data_blocks = pax_data.len().div_ceil(HEADER_SIZE);
1104 let mut data_buf = vec![0u8; num_data_blocks * HEADER_SIZE];
1105 data_buf[..pax_data.len()].copy_from_slice(pax_data);
1106
1107 for chunk in data_buf.chunks_exact(HEADER_SIZE) {
1108 blocks.push(*Header::from_bytes(
1109 chunk.try_into().expect("chunks_exact guarantees size"),
1110 ));
1111 }
1112 }
1113
1114 fn build_pax_header_name(&self) -> Vec<u8> {
1116 let path = self.header.as_header().path_bytes();
1118 let base_name = path.rsplit(|&b| b == b'/').next().unwrap_or(path);
1119
1120 let mut name = b"PaxHeaders.0/".to_vec();
1122 let remaining = NAME_MAX_LEN.saturating_sub(name.len());
1123 let truncated_base = &base_name[..remaining.min(base_name.len())];
1124 name.extend_from_slice(truncated_base);
1125
1126 name
1127 }
1128
1129 fn build_sparse_map_data(si: &SparseInfo) -> Vec<u8> {
1133 let mut data = format!("{}\n", si.map.len());
1134 for entry in &si.map {
1135 data.push_str(&format!("{}\n{}\n", entry.offset, entry.length));
1136 }
1137 data.into_bytes()
1138 }
1139
1140 fn emit_gnu_sparse_ext_blocks(&self, blocks: &mut Vec<Header>, si: &SparseInfo) {
1142 if si.map.len() <= 4 {
1143 return;
1144 }
1145
1146 let remaining = &si.map[4..];
1147 let chunks: Vec<&[SparseEntry]> = remaining.chunks(21).collect();
1148
1149 for (i, chunk) in chunks.iter().enumerate() {
1150 let is_last = i == chunks.len() - 1;
1151 let mut ext = GnuExtSparseHeader::default();
1152 for (j, entry) in chunk.iter().enumerate() {
1153 ext.sparse[j].set(entry);
1154 }
1155 ext.set_is_extended(!is_last);
1156
1157 let ext_bytes = zerocopy::IntoBytes::as_bytes(&ext);
1159 blocks.push(*Header::from_bytes(
1160 ext_bytes
1161 .try_into()
1162 .expect("GnuExtSparseHeader is 512 bytes"),
1163 ));
1164 }
1165 }
1166}
1167
1168impl Default for EntryBuilder {
1169 fn default() -> Self {
1170 Self::new_gnu()
1171 }
1172}
1173
1174impl core::fmt::Debug for EntryBuilder {
1175 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
1176 f.debug_struct("EntryBuilder")
1177 .field("mode", &self.mode)
1178 .field("needs_extension", &self.needs_extension())
1179 .field("long_path_len", &self.long_path.as_ref().map(|p| p.len()))
1180 .field("long_link_len", &self.long_link.as_ref().map(|l| l.len()))
1181 .field("header", &self.header)
1182 .finish()
1183 }
1184}
1185
1186#[must_use]
1190pub const fn blocks_for_size(size: u64) -> u64 {
1191 size.div_ceil(HEADER_SIZE as u64)
1192}
1193
1194#[cfg(test)]
1195mod tests {
1196 use super::*;
1197 use crate::PaxExtensions;
1198
1199 #[test]
1200 fn test_write_bytes() {
1201 let mut field = [0u8; 10];
1202
1203 write_bytes(&mut field, b"hello").unwrap();
1205 assert_eq!(&field[..5], b"hello");
1206 assert_eq!(field[5..], [0, 0, 0, 0, 0]);
1207
1208 write_bytes(&mut field, b"0123456789").unwrap();
1210 assert_eq!(&field, b"0123456789");
1211
1212 assert!(write_bytes(&mut field, b"12345678901").is_err());
1214 }
1215
1216 #[test]
1217 fn test_encode_octal() {
1218 let mut field = [0u8; 8];
1220
1221 crate::encode_octal(&mut field, 0o644).unwrap();
1222 assert_eq!(&field, b"0000644\0");
1223
1224 crate::encode_octal(&mut field, 0o755).unwrap();
1225 assert_eq!(&field, b"0000755\0");
1226
1227 crate::encode_octal(&mut field, 0).unwrap();
1228 assert_eq!(&field, b"0000000\0");
1229
1230 let mut field12 = [0u8; 12];
1232 crate::encode_octal(&mut field12, 0o77777777777).unwrap();
1233 assert_eq!(&field12, b"77777777777\0");
1234
1235 crate::encode_octal(&mut field, 0o7777777).unwrap();
1237 assert_eq!(&field, b"7777777\0");
1238 }
1239
1240 #[test]
1241 fn test_encode_octal_overflow() {
1242 let mut field = [0u8; 8];
1243 assert!(crate::encode_octal(&mut field, 0o100000000).is_err());
1245 }
1246
1247 #[test]
1248 fn test_header_builder_basic() {
1249 let builder = HeaderBuilder::new_ustar();
1250 let header = builder.as_header();
1251 assert!(header.is_ustar());
1252 assert!(!header.is_gnu());
1253 }
1254
1255 #[test]
1256 fn test_header_builder_gnu() {
1257 let builder = HeaderBuilder::new_gnu();
1258 let header = builder.as_header();
1259 assert!(header.is_gnu());
1260 assert!(!header.is_ustar());
1261 }
1262
1263 #[test]
1264 fn test_header_builder_file() {
1265 let mut builder = HeaderBuilder::new_ustar();
1266 builder
1267 .path(b"test.txt")
1268 .unwrap()
1269 .mode(0o644)
1270 .unwrap()
1271 .uid(1000)
1272 .unwrap()
1273 .gid(1000)
1274 .unwrap()
1275 .size(1024)
1276 .unwrap()
1277 .mtime(1234567890)
1278 .unwrap()
1279 .entry_type(EntryType::Regular)
1280 .username(b"user")
1281 .unwrap()
1282 .groupname(b"group")
1283 .unwrap();
1284
1285 let header = builder.finish();
1286
1287 assert_eq!(header.path_bytes(), b"test.txt");
1288 assert_eq!(header.mode().unwrap(), 0o644);
1289 assert_eq!(header.uid().unwrap(), 1000);
1290 assert_eq!(header.gid().unwrap(), 1000);
1291 assert_eq!(header.entry_size().unwrap(), 1024);
1292 assert_eq!(header.mtime().unwrap(), 1234567890);
1293 assert_eq!(header.entry_type(), EntryType::Regular);
1294 assert_eq!(header.username().unwrap(), b"user");
1295 assert_eq!(header.groupname().unwrap(), b"group");
1296
1297 assert!(header.verify_checksum().is_ok());
1299 }
1300
1301 #[test]
1302 fn test_header_builder_symlink() {
1303 let mut builder = HeaderBuilder::new_ustar();
1304 builder
1305 .path(b"link")
1306 .unwrap()
1307 .mode(0o777)
1308 .unwrap()
1309 .entry_type(EntryType::Symlink)
1310 .link_name(b"target")
1311 .unwrap()
1312 .size(0)
1313 .unwrap()
1314 .mtime(0)
1315 .unwrap()
1316 .uid(0)
1317 .unwrap()
1318 .gid(0)
1319 .unwrap();
1320
1321 let header = builder.finish();
1322
1323 assert_eq!(header.path_bytes(), b"link");
1324 assert_eq!(header.entry_type(), EntryType::Symlink);
1325 assert_eq!(header.link_name_bytes(), b"target");
1326 assert!(header.verify_checksum().is_ok());
1327 }
1328
1329 #[test]
1330 fn test_header_builder_directory() {
1331 let mut builder = HeaderBuilder::new_ustar();
1332 builder
1333 .path(b"mydir/")
1334 .unwrap()
1335 .mode(0o755)
1336 .unwrap()
1337 .entry_type(EntryType::Directory)
1338 .size(0)
1339 .unwrap()
1340 .mtime(0)
1341 .unwrap()
1342 .uid(0)
1343 .unwrap()
1344 .gid(0)
1345 .unwrap();
1346
1347 let header = builder.finish();
1348
1349 assert_eq!(header.entry_type(), EntryType::Directory);
1350 assert!(header.verify_checksum().is_ok());
1351 }
1352
1353 #[test]
1354 fn test_header_builder_device() {
1355 let mut builder = HeaderBuilder::new_ustar();
1356 builder
1357 .path(b"null")
1358 .unwrap()
1359 .mode(0o666)
1360 .unwrap()
1361 .entry_type(EntryType::Char)
1362 .device(1, 3)
1363 .unwrap()
1364 .size(0)
1365 .unwrap()
1366 .mtime(0)
1367 .unwrap()
1368 .uid(0)
1369 .unwrap()
1370 .gid(0)
1371 .unwrap();
1372
1373 let header = builder.finish();
1374
1375 assert_eq!(header.entry_type(), EntryType::Char);
1376 assert_eq!(header.device_major().unwrap(), Some(1));
1377 assert_eq!(header.device_minor().unwrap(), Some(3));
1378 assert!(header.verify_checksum().is_ok());
1379 }
1380
1381 #[test]
1382 fn test_pax_builder_basic() {
1383 let mut builder = PaxBuilder::new();
1384 builder.add("key", b"value").add("another", b"test");
1385 let data = builder.finish();
1386
1387 let mut iter = PaxExtensions::new(&data);
1389
1390 let ext1 = iter.next().unwrap().unwrap();
1391 assert_eq!(ext1.key().unwrap(), "key");
1392 assert_eq!(ext1.value().unwrap(), "value");
1393
1394 let ext2 = iter.next().unwrap().unwrap();
1395 assert_eq!(ext2.key().unwrap(), "another");
1396 assert_eq!(ext2.value().unwrap(), "test");
1397
1398 assert!(iter.next().is_none());
1399 }
1400
1401 #[test]
1402 fn test_pax_builder_path() {
1403 let long_path = b"/very/long/path/that/exceeds/one/hundred/characters/which/is/the/limit/for/the/standard/tar/name/field.txt";
1404
1405 let mut builder = PaxBuilder::new();
1406 builder.path(long_path);
1407 let data = builder.finish();
1408
1409 let ext = PaxExtensions::new(&data).next().unwrap().unwrap();
1410 assert_eq!(ext.key().unwrap(), "path");
1411 assert_eq!(ext.value_bytes(), long_path);
1412 }
1413
1414 #[test]
1415 fn test_pax_builder_size() {
1416 let mut builder = PaxBuilder::new();
1417 builder.size(1_000_000_000_000);
1418 let data = builder.finish();
1419
1420 let exts = PaxExtensions::new(&data);
1421 assert_eq!(exts.get_u64("size"), Some(1_000_000_000_000));
1422 }
1423
1424 #[test]
1425 fn test_pax_builder_multiple() {
1426 let mut builder = PaxBuilder::new();
1427 builder
1428 .path(b"/some/path")
1429 .uid(65534)
1430 .gid(65534)
1431 .uname(b"nobody")
1432 .gname(b"nogroup")
1433 .mtime(1700000000);
1434 let data = builder.finish();
1435
1436 let exts = PaxExtensions::new(&data);
1437 assert_eq!(exts.get("path"), Some("/some/path"));
1438 assert_eq!(exts.get_u64("uid"), Some(65534));
1439 assert_eq!(exts.get_u64("gid"), Some(65534));
1440 assert_eq!(exts.get("uname"), Some("nobody"));
1441 assert_eq!(exts.get("gname"), Some("nogroup"));
1442 assert_eq!(exts.get_u64("mtime"), Some(1700000000));
1443 }
1444
1445 #[test]
1446 fn test_pax_record_length_calculation() {
1447 let mut builder = PaxBuilder::new();
1452 builder.add("k", b"v");
1453 let data = builder.finish();
1454 assert_eq!(&data, b"6 k=v\n");
1455
1456 let mut builder = PaxBuilder::new();
1458 builder.add("path", b"/a/b/c/d/e/f");
1459 let data = builder.finish();
1460 assert!(data.starts_with(b"21 path="));
1465 }
1466
1467 #[test]
1468 fn test_roundtrip() {
1469 let mut builder = HeaderBuilder::new_ustar();
1471 builder
1472 .path(b"roundtrip_test.txt")
1473 .unwrap()
1474 .mode(0o755)
1475 .unwrap()
1476 .uid(1001)
1477 .unwrap()
1478 .gid(1002)
1479 .unwrap()
1480 .size(4096)
1481 .unwrap()
1482 .mtime(1609459200)
1483 .unwrap()
1484 .entry_type(EntryType::Regular)
1485 .username(b"testuser")
1486 .unwrap()
1487 .groupname(b"testgroup")
1488 .unwrap();
1489
1490 let parsed = builder.finish();
1492
1493 assert_eq!(parsed.path_bytes(), b"roundtrip_test.txt");
1495 assert_eq!(parsed.mode().unwrap(), 0o755);
1496 assert_eq!(parsed.uid().unwrap(), 1001);
1497 assert_eq!(parsed.gid().unwrap(), 1002);
1498 assert_eq!(parsed.entry_size().unwrap(), 4096);
1499 assert_eq!(parsed.mtime().unwrap(), 1609459200);
1500 assert_eq!(parsed.entry_type(), EntryType::Regular);
1501 assert_eq!(parsed.username().unwrap(), b"testuser");
1502 assert_eq!(parsed.groupname().unwrap(), b"testgroup");
1503
1504 parsed.verify_checksum().unwrap();
1506 }
1507
1508 #[test]
1509 fn test_roundtrip_gnu() {
1510 let mut builder = HeaderBuilder::new_gnu();
1511 builder
1512 .path(b"gnu_test.dat")
1513 .unwrap()
1514 .mode(0o600)
1515 .unwrap()
1516 .size(0)
1517 .unwrap()
1518 .mtime(0)
1519 .unwrap()
1520 .uid(0)
1521 .unwrap()
1522 .gid(0)
1523 .unwrap()
1524 .entry_type(EntryType::Regular);
1525
1526 let parsed = builder.finish();
1527
1528 assert!(parsed.is_gnu());
1529 assert_eq!(parsed.path_bytes(), b"gnu_test.dat");
1530 parsed.verify_checksum().unwrap();
1531 }
1532
1533 #[test]
1534 fn test_header_builder_default() {
1535 let builder = HeaderBuilder::default();
1536 assert!(builder.as_header().is_ustar());
1537 }
1538
1539 #[test]
1540 fn test_header_builder_debug() {
1541 let builder = HeaderBuilder::new_ustar();
1542 let debug_str = format!("{builder:?}");
1543 assert!(debug_str.contains("HeaderBuilder"));
1544 }
1545
1546 #[test]
1547 fn test_pax_builder_debug() {
1548 let builder = PaxBuilder::new();
1549 let debug_str = format!("{builder:?}");
1550 assert!(debug_str.contains("PaxBuilder"));
1551 }
1552
1553 #[test]
1554 fn test_path_too_long() {
1555 let mut builder = HeaderBuilder::new_ustar();
1556 let long_path = [b'a'; 101];
1557 assert!(builder.path(long_path).is_err());
1558 }
1559
1560 #[test]
1561 fn test_link_name_too_long() {
1562 let mut builder = HeaderBuilder::new_ustar();
1563 let long_link = [b'b'; 101];
1564 assert!(builder.link_name(long_link).is_err());
1565 }
1566
1567 #[test]
1568 fn test_username_too_long() {
1569 let mut builder = HeaderBuilder::new_ustar();
1570 let long_name = [b'u'; 33];
1571 assert!(builder.username(long_name).is_err());
1572 }
1573
1574 #[test]
1575 fn test_pax_builder_linkpath() {
1576 let mut builder = PaxBuilder::new();
1577 builder.linkpath(b"/target/of/symlink");
1578 let data = builder.finish();
1579
1580 let exts = PaxExtensions::new(&data);
1581 assert_eq!(exts.get("linkpath"), Some("/target/of/symlink"));
1582 }
1583
1584 #[test]
1585 fn test_pax_builder_times() {
1586 let mut builder = PaxBuilder::new();
1587 builder.mtime(1000).atime(2000).ctime(3000);
1588 let data = builder.finish();
1589
1590 let exts = PaxExtensions::new(&data);
1591 assert_eq!(exts.get_u64("mtime"), Some(1000));
1592 assert_eq!(exts.get_u64("atime"), Some(2000));
1593 assert_eq!(exts.get_u64("ctime"), Some(3000));
1594 }
1595
1596 #[test]
1601 fn test_entry_builder_short_path_no_extension() {
1602 let mut builder = EntryBuilder::new_gnu();
1603 builder
1604 .path(b"hello.txt")
1605 .mode(0o644)
1606 .unwrap()
1607 .size(1024)
1608 .unwrap()
1609 .mtime(1234567890)
1610 .unwrap()
1611 .uid(1000)
1612 .unwrap()
1613 .gid(1000)
1614 .unwrap()
1615 .entry_type(EntryType::Regular);
1616
1617 assert!(!builder.needs_extension());
1618
1619 let blocks = builder.finish();
1620 assert_eq!(blocks.len(), 1, "short path should produce single header");
1621
1622 let header = &blocks[0];
1624 assert_eq!(header.path_bytes(), b"hello.txt");
1625 assert_eq!(header.mode().unwrap(), 0o644);
1626 assert_eq!(header.entry_size().unwrap(), 1024);
1627 assert!(header.verify_checksum().is_ok());
1628 }
1629
1630 #[test]
1631 fn test_entry_builder_path_exactly_100_bytes() {
1632 let path = "a".repeat(100);
1634 let mut builder = EntryBuilder::new_gnu();
1635 builder
1636 .path(path.as_bytes())
1637 .mode(0o644)
1638 .unwrap()
1639 .size(0)
1640 .unwrap()
1641 .entry_type(EntryType::Regular);
1642
1643 assert!(!builder.needs_extension());
1644
1645 let blocks = builder.finish();
1646 assert_eq!(blocks.len(), 1);
1647
1648 let header = &blocks[0];
1649 assert_eq!(header.path_bytes().len(), 100);
1650 }
1651
1652 #[test]
1653 fn test_entry_builder_gnu_long_path() {
1654 let long_path = "a/".repeat(60) + "file.txt"; assert!(long_path.len() > 100);
1657
1658 let mut builder = EntryBuilder::new_gnu();
1659 builder
1660 .path(long_path.as_bytes())
1661 .mode(0o644)
1662 .unwrap()
1663 .size(0)
1664 .unwrap()
1665 .mtime(0)
1666 .unwrap()
1667 .uid(0)
1668 .unwrap()
1669 .gid(0)
1670 .unwrap()
1671 .entry_type(EntryType::Regular);
1672
1673 assert!(builder.needs_extension());
1674
1675 let blocks = builder.finish();
1676 assert!(blocks.len() >= 3, "got {} blocks", blocks.len());
1678
1679 let ext_header = &blocks[0];
1681 assert_eq!(ext_header.entry_type(), EntryType::GnuLongName);
1682 assert_eq!(ext_header.path_bytes(), b"././@LongLink");
1683 assert!(ext_header.verify_checksum().is_ok());
1684
1685 assert_eq!(ext_header.entry_size().unwrap(), long_path.len() as u64 + 1);
1687
1688 let data_block = blocks[1].as_bytes();
1690 assert_eq!(&data_block[..long_path.len()], long_path.as_bytes());
1691 assert_eq!(data_block[long_path.len()], 0); let main_header = blocks.last().unwrap();
1695 assert_eq!(main_header.entry_type(), EntryType::Regular);
1696 assert!(main_header.verify_checksum().is_ok());
1697 }
1698
1699 #[test]
1700 fn test_entry_builder_gnu_long_link() {
1701 let long_target = "/very/long/symlink/target/".repeat(5); assert!(long_target.len() > 100);
1704
1705 let mut builder = EntryBuilder::new_gnu();
1706 builder
1707 .path(b"mylink")
1708 .link_name(long_target.as_bytes())
1709 .mode(0o777)
1710 .unwrap()
1711 .size(0)
1712 .unwrap()
1713 .mtime(0)
1714 .unwrap()
1715 .uid(0)
1716 .unwrap()
1717 .gid(0)
1718 .unwrap()
1719 .entry_type(EntryType::Symlink);
1720
1721 assert!(builder.needs_extension());
1722
1723 let blocks = builder.finish();
1724 assert!(blocks.len() >= 3);
1725
1726 let ext_header = &blocks[0];
1728 assert_eq!(ext_header.entry_type(), EntryType::GnuLongLink);
1729 assert_eq!(ext_header.path_bytes(), b"././@LongLink");
1730
1731 let main_header = blocks.last().unwrap();
1733 assert_eq!(main_header.entry_type(), EntryType::Symlink);
1734 }
1735
1736 #[test]
1737 fn test_entry_builder_gnu_long_path_and_link() {
1738 let long_path = "dir/".repeat(30) + "file"; let long_target = "target/".repeat(20); assert!(long_path.len() > 100);
1743 assert!(long_target.len() > 100);
1744
1745 let mut builder = EntryBuilder::new_gnu();
1746 builder
1747 .path(long_path.as_bytes())
1748 .link_name(long_target.as_bytes())
1749 .mode(0o777)
1750 .unwrap()
1751 .size(0)
1752 .unwrap()
1753 .mtime(0)
1754 .unwrap()
1755 .uid(0)
1756 .unwrap()
1757 .gid(0)
1758 .unwrap()
1759 .entry_type(EntryType::Symlink);
1760
1761 let blocks = builder.finish();
1762 assert!(blocks.len() >= 5, "got {} blocks", blocks.len());
1765
1766 let first = &blocks[0];
1768 assert_eq!(first.entry_type(), EntryType::GnuLongLink);
1769
1770 let longname_idx = blocks
1773 .iter()
1774 .position(|b| b.entry_type() == EntryType::GnuLongName);
1775 assert!(longname_idx.is_some(), "should have LongName header");
1776
1777 let main = blocks.last().unwrap();
1779 assert_eq!(main.entry_type(), EntryType::Symlink);
1780 }
1781
1782 #[test]
1783 fn test_entry_builder_pax_long_path() {
1784 let long_path = "pax/".repeat(30) + "file.txt"; assert!(long_path.len() > 100);
1786
1787 let mut builder = EntryBuilder::new_ustar(); builder
1789 .path(long_path.as_bytes())
1790 .mode(0o644)
1791 .unwrap()
1792 .size(0)
1793 .unwrap()
1794 .mtime(0)
1795 .unwrap()
1796 .uid(0)
1797 .unwrap()
1798 .gid(0)
1799 .unwrap()
1800 .entry_type(EntryType::Regular);
1801
1802 assert_eq!(builder.extension_mode(), ExtensionMode::Pax);
1803 assert!(builder.needs_extension());
1804
1805 let blocks = builder.finish();
1806 assert!(blocks.len() >= 3);
1808
1809 let pax_header = &blocks[0];
1811 assert_eq!(pax_header.entry_type(), EntryType::XHeader);
1812 assert!(pax_header.verify_checksum().is_ok());
1813
1814 let pax_data = blocks[1].as_bytes();
1816 let pax_str = String::from_utf8_lossy(pax_data);
1818 assert!(pax_str.contains("path="));
1819 assert!(pax_str.contains(&long_path));
1820
1821 let main_header = blocks.last().unwrap();
1823 assert_eq!(main_header.entry_type(), EntryType::Regular);
1824 assert!(main_header.is_ustar());
1825 }
1826
1827 #[test]
1828 fn test_entry_builder_pax_long_link() {
1829 let long_target = "/long/symlink/target/".repeat(6);
1830 assert!(long_target.len() > 100);
1831
1832 let mut builder = EntryBuilder::new_ustar();
1833 builder
1834 .path(b"link")
1835 .link_name(long_target.as_bytes())
1836 .mode(0o777)
1837 .unwrap()
1838 .size(0)
1839 .unwrap()
1840 .entry_type(EntryType::Symlink);
1841
1842 let blocks = builder.finish();
1843
1844 let pax_header = &blocks[0];
1846 assert_eq!(pax_header.entry_type(), EntryType::XHeader);
1847
1848 let pax_data = blocks[1].as_bytes();
1850 let pax_str = String::from_utf8_lossy(pax_data);
1851 assert!(pax_str.contains("linkpath="));
1852 }
1853
1854 #[test]
1855 fn test_entry_builder_custom_pax_extension() {
1856 let mut builder = EntryBuilder::new_ustar();
1857 builder
1858 .path(b"file.txt")
1859 .mode(0o644)
1860 .unwrap()
1861 .size(0)
1862 .unwrap()
1863 .add_pax("SCHILY.xattr.user.test", b"value")
1864 .entry_type(EntryType::Regular);
1865
1866 assert!(builder.needs_extension()); let blocks = builder.finish();
1869 assert!(blocks.len() >= 3);
1870
1871 let pax_header = &blocks[0];
1872 assert_eq!(pax_header.entry_type(), EntryType::XHeader);
1873
1874 let pax_data = blocks[1].as_bytes();
1875 let pax_str = String::from_utf8_lossy(pax_data);
1876 assert!(pax_str.contains("SCHILY.xattr.user.test=value"));
1877 }
1878
1879 #[test]
1880 fn test_entry_builder_extension_mode_switching() {
1881 let long_path = "x/".repeat(60);
1882
1883 let mut builder = EntryBuilder::new_gnu();
1885 assert_eq!(builder.extension_mode(), ExtensionMode::Gnu);
1886
1887 builder.set_extension_mode(ExtensionMode::Pax);
1889 assert_eq!(builder.extension_mode(), ExtensionMode::Pax);
1890
1891 builder
1892 .path(long_path.as_bytes())
1893 .mode(0o644)
1894 .unwrap()
1895 .size(0)
1896 .unwrap()
1897 .entry_type(EntryType::Regular);
1898
1899 let blocks = builder.finish();
1900 let first = &blocks[0];
1902 assert_eq!(first.entry_type(), EntryType::XHeader);
1903 }
1904
1905 #[test]
1906 fn test_entry_builder_finish_bytes() {
1907 let mut builder = EntryBuilder::new_gnu();
1908 builder
1909 .path(b"test.txt")
1910 .mode(0o644)
1911 .unwrap()
1912 .size(0)
1913 .unwrap()
1914 .entry_type(EntryType::Regular);
1915
1916 let bytes = builder.finish_bytes();
1917 assert_eq!(bytes.len(), 512);
1918 assert_eq!(&bytes[..512], builder.header().as_header().as_bytes());
1919 }
1920
1921 #[test]
1922 fn test_entry_builder_directory() {
1923 let mut builder = EntryBuilder::new_gnu();
1924 builder
1925 .path(b"mydir/")
1926 .mode(0o755)
1927 .unwrap()
1928 .size(0)
1929 .unwrap()
1930 .mtime(1234567890)
1931 .unwrap()
1932 .uid(1000)
1933 .unwrap()
1934 .gid(1000)
1935 .unwrap()
1936 .entry_type(EntryType::Directory);
1937
1938 let blocks = builder.finish();
1939 assert_eq!(blocks.len(), 1);
1940
1941 let header = &blocks[0];
1942 assert_eq!(header.entry_type(), EntryType::Directory);
1943 assert!(header.verify_checksum().is_ok());
1944 }
1945
1946 #[test]
1947 fn test_entry_builder_device() {
1948 let mut builder = EntryBuilder::new_gnu();
1949 builder
1950 .path(b"null")
1951 .mode(0o666)
1952 .unwrap()
1953 .size(0)
1954 .unwrap()
1955 .device(1, 3)
1956 .unwrap()
1957 .entry_type(EntryType::Char);
1958
1959 let blocks = builder.finish();
1960 let header = &blocks[0];
1961 assert_eq!(header.entry_type(), EntryType::Char);
1962 assert_eq!(header.device_major().unwrap(), Some(1));
1963 assert_eq!(header.device_minor().unwrap(), Some(3));
1964 }
1965
1966 #[test]
1967 fn test_entry_builder_with_mode() {
1968 let header = HeaderBuilder::new_gnu();
1969 let builder = EntryBuilder::with_mode(header, ExtensionMode::Pax);
1970 assert_eq!(builder.extension_mode(), ExtensionMode::Pax);
1971 assert!(builder.header().as_header().is_gnu());
1972 }
1973
1974 #[test]
1975 fn test_entry_builder_default() {
1976 let builder = EntryBuilder::default();
1977 assert_eq!(builder.extension_mode(), ExtensionMode::Gnu);
1978 }
1979
1980 #[test]
1981 fn test_entry_builder_debug() {
1982 let mut builder = EntryBuilder::new_gnu();
1983 builder.path(b"test.txt");
1984 let debug_str = format!("{builder:?}");
1985 assert!(debug_str.contains("EntryBuilder"));
1986 assert!(debug_str.contains("Gnu"));
1987 }
1988
1989 #[test]
1990 fn test_blocks_for_size() {
1991 assert_eq!(blocks_for_size(0), 0);
1992 assert_eq!(blocks_for_size(1), 1);
1993 assert_eq!(blocks_for_size(511), 1);
1994 assert_eq!(blocks_for_size(512), 1);
1995 assert_eq!(blocks_for_size(513), 2);
1996 assert_eq!(blocks_for_size(1024), 2);
1997 assert_eq!(blocks_for_size(1025), 3);
1998 }
1999
2000 #[test]
2001 fn test_entry_builder_very_long_path() {
2002 let very_long_path = "x/".repeat(300); assert!(very_long_path.len() > 512);
2005
2006 let mut builder = EntryBuilder::new_gnu();
2007 builder
2008 .path(very_long_path.as_bytes())
2009 .mode(0o644)
2010 .unwrap()
2011 .size(0)
2012 .unwrap()
2013 .entry_type(EntryType::Regular);
2014
2015 let blocks = builder.finish();
2016 assert!(blocks.len() >= 4, "got {} blocks", blocks.len());
2018
2019 let ext_header = &blocks[0];
2020 assert_eq!(ext_header.entry_type(), EntryType::GnuLongName);
2021 assert_eq!(ext_header.entry_size().unwrap(), 601);
2023 }
2024
2025 #[test]
2026 fn test_entry_builder_username_groupname() {
2027 let mut builder = EntryBuilder::new_gnu();
2028 builder
2029 .path(b"file.txt")
2030 .mode(0o644)
2031 .unwrap()
2032 .size(0)
2033 .unwrap()
2034 .username(b"testuser")
2035 .unwrap()
2036 .groupname(b"testgroup")
2037 .unwrap()
2038 .entry_type(EntryType::Regular);
2039
2040 let blocks = builder.finish();
2041 let header = &blocks[0];
2042 assert_eq!(header.username().unwrap(), b"testuser");
2043 assert_eq!(header.groupname().unwrap(), b"testgroup");
2044 }
2045
2046 #[test]
2047 fn test_entry_builder_header_access() {
2048 let mut builder = EntryBuilder::new_gnu();
2049 builder.path(b"test.txt");
2050
2051 assert!(builder.header().as_header().is_gnu());
2053
2054 builder.header_mut().mode(0o755).unwrap();
2056 assert_eq!(builder.header().as_header().mode().unwrap(), 0o755);
2057 }
2058
2059 #[test]
2060 fn test_entry_builder_pax_numeric_fallback() {
2061 use crate::PaxExtensions;
2062
2063 let large_uid: u64 = 5_000_000; let large_size: u64 = 10_000_000_000; let mut builder = EntryBuilder::new_ustar();
2068 builder
2069 .path(b"big.dat")
2070 .uid(large_uid)
2071 .unwrap()
2072 .gid(large_uid)
2073 .unwrap()
2074 .size(large_size)
2075 .unwrap()
2076 .mtime(large_size)
2077 .unwrap()
2078 .entry_type(EntryType::Regular);
2079
2080 let blocks = builder.finish();
2081 assert!(blocks.len() >= 3, "expected PAX extension blocks");
2083
2084 let pax_header = &blocks[0];
2086 assert!(pax_header.entry_type().is_pax_local_extensions());
2087
2088 let pax_data_blocks = blocks.len() - 2; let pax_data: Vec<u8> = blocks[1..1 + pax_data_blocks]
2091 .iter()
2092 .flat_map(|b| b.as_bytes().iter().copied())
2093 .collect();
2094 let exts = PaxExtensions::new(&pax_data);
2095 assert_eq!(exts.get_u64("uid"), Some(large_uid));
2096 assert_eq!(exts.get_u64("gid"), Some(large_uid));
2097 assert_eq!(exts.get_u64("size"), Some(large_size));
2098 assert_eq!(exts.get_u64("mtime"), Some(large_size));
2099
2100 let main_header = blocks.last().unwrap();
2102 assert_eq!(main_header.uid().unwrap(), 0);
2103 assert_eq!(main_header.entry_size().unwrap(), 0);
2104 }
2105
2106 #[test]
2107 fn test_entry_builder_gnu_large_uid() {
2108 let large_uid: u64 = 5_000_000;
2110 let mut builder = EntryBuilder::new_gnu();
2111 builder
2112 .path(b"big.dat")
2113 .uid(large_uid)
2114 .unwrap()
2115 .gid(large_uid)
2116 .unwrap()
2117 .size(0)
2118 .unwrap()
2119 .mtime(0)
2120 .unwrap()
2121 .entry_type(EntryType::Regular);
2122
2123 let blocks = builder.finish();
2124 assert_eq!(blocks.len(), 1);
2126 let header = &blocks[0];
2127 assert_eq!(header.uid().unwrap(), large_uid);
2128 assert_eq!(header.gid().unwrap(), large_uid);
2129 }
2130
2131 use crate::parse::{Limits, ParseEvent, Parser};
2136 use crate::SparseEntry;
2137 use zerocopy::FromBytes;
2138
2139 #[test]
2140 fn test_entry_builder_gnu_sparse_basic() {
2141 let sparse_map = [
2142 SparseEntry {
2143 offset: 0,
2144 length: 100,
2145 },
2146 SparseEntry {
2147 offset: 1000,
2148 length: 200,
2149 },
2150 ];
2151 let on_disk: u64 = 300;
2152 let real_size: u64 = 1200;
2153
2154 let mut builder = EntryBuilder::new_gnu();
2155 builder
2156 .path(b"sparse.bin")
2157 .mode(0o644)
2158 .unwrap()
2159 .size(on_disk)
2160 .unwrap()
2161 .mtime(0)
2162 .unwrap()
2163 .uid(0)
2164 .unwrap()
2165 .gid(0)
2166 .unwrap()
2167 .sparse(&sparse_map, real_size);
2168
2169 let blocks = builder.finish();
2170 assert_eq!(blocks.len(), 1, "2 inline entries => no extension blocks");
2171
2172 let header = &blocks[0];
2173 assert_eq!(header.entry_type(), EntryType::GnuSparse);
2174 header.verify_checksum().unwrap();
2175
2176 let gnu = header.try_as_gnu().unwrap();
2177 assert_eq!(gnu.real_size().unwrap(), real_size);
2178 assert!(!gnu.is_extended());
2179
2180 let s0 = gnu.sparse[0].to_sparse_entry().unwrap();
2181 assert_eq!(s0, sparse_map[0]);
2182 let s1 = gnu.sparse[1].to_sparse_entry().unwrap();
2183 assert_eq!(s1, sparse_map[1]);
2184 assert!(gnu.sparse[2].is_empty());
2185 }
2186
2187 #[test]
2188 fn test_entry_builder_gnu_sparse_four_inline() {
2189 let sparse_map = [
2190 SparseEntry {
2191 offset: 0,
2192 length: 50,
2193 },
2194 SparseEntry {
2195 offset: 100,
2196 length: 50,
2197 },
2198 SparseEntry {
2199 offset: 200,
2200 length: 50,
2201 },
2202 SparseEntry {
2203 offset: 300,
2204 length: 50,
2205 },
2206 ];
2207 let on_disk: u64 = 200;
2208 let real_size: u64 = 350;
2209
2210 let mut builder = EntryBuilder::new_gnu();
2211 builder
2212 .path(b"sparse4.bin")
2213 .mode(0o644)
2214 .unwrap()
2215 .size(on_disk)
2216 .unwrap()
2217 .mtime(0)
2218 .unwrap()
2219 .uid(0)
2220 .unwrap()
2221 .gid(0)
2222 .unwrap()
2223 .sparse(&sparse_map, real_size);
2224
2225 let blocks = builder.finish();
2226 assert_eq!(blocks.len(), 1, "exactly 4 entries fit inline");
2227
2228 let header = &blocks[0];
2229 assert_eq!(header.entry_type(), EntryType::GnuSparse);
2230 header.verify_checksum().unwrap();
2231
2232 let gnu = header.try_as_gnu().unwrap();
2233 assert_eq!(gnu.real_size().unwrap(), real_size);
2234 assert!(!gnu.is_extended());
2235
2236 for (i, expected) in sparse_map.iter().enumerate() {
2237 assert_eq!(gnu.sparse[i].to_sparse_entry().unwrap(), *expected);
2238 }
2239 }
2240
2241 #[test]
2242 fn test_entry_builder_gnu_sparse_with_extensions() {
2243 let sparse_map: Vec<SparseEntry> = (0..6)
2245 .map(|i| SparseEntry {
2246 offset: i * 1000,
2247 length: 100,
2248 })
2249 .collect();
2250 let on_disk: u64 = 600;
2251 let real_size: u64 = 5100;
2252
2253 let mut builder = EntryBuilder::new_gnu();
2254 builder
2255 .path(b"sparse_ext.bin")
2256 .mode(0o644)
2257 .unwrap()
2258 .size(on_disk)
2259 .unwrap()
2260 .mtime(0)
2261 .unwrap()
2262 .uid(0)
2263 .unwrap()
2264 .gid(0)
2265 .unwrap()
2266 .sparse(&sparse_map, real_size);
2267
2268 let blocks = builder.finish();
2269 assert_eq!(blocks.len(), 2, "main header + 1 extension block");
2270
2271 let header = &blocks[0];
2273 assert_eq!(header.entry_type(), EntryType::GnuSparse);
2274 header.verify_checksum().unwrap();
2275 let gnu = header.try_as_gnu().unwrap();
2276 assert!(gnu.is_extended(), "more entries follow");
2277 assert_eq!(gnu.real_size().unwrap(), real_size);
2278
2279 for (i, expected) in sparse_map.iter().enumerate().take(4) {
2280 assert_eq!(gnu.sparse[i].to_sparse_entry().unwrap(), *expected);
2281 }
2282
2283 let ext = GnuExtSparseHeader::ref_from_bytes(blocks[1].as_bytes()).unwrap();
2285 assert!(!ext.is_extended(), "last extension block");
2286 for i in 0..2 {
2287 assert_eq!(ext.sparse[i].to_sparse_entry().unwrap(), sparse_map[4 + i]);
2288 }
2289 assert!(ext.sparse[2].is_empty());
2290 }
2291
2292 #[test]
2293 fn test_entry_builder_gnu_sparse_many_extensions() {
2294 let sparse_map: Vec<SparseEntry> = (0..28)
2296 .map(|i| SparseEntry {
2297 offset: i * 500,
2298 length: 50,
2299 })
2300 .collect();
2301 let on_disk: u64 = 28 * 50;
2302 let real_size: u64 = 27 * 500 + 50;
2303
2304 let mut builder = EntryBuilder::new_gnu();
2305 builder
2306 .path(b"sparse_many.bin")
2307 .mode(0o644)
2308 .unwrap()
2309 .size(on_disk)
2310 .unwrap()
2311 .mtime(0)
2312 .unwrap()
2313 .uid(0)
2314 .unwrap()
2315 .gid(0)
2316 .unwrap()
2317 .sparse(&sparse_map, real_size);
2318
2319 let blocks = builder.finish();
2320 assert_eq!(blocks.len(), 3, "main + 2 extension blocks");
2321
2322 let gnu = blocks[0].try_as_gnu().unwrap();
2323 assert!(gnu.is_extended());
2324
2325 let ext1 = GnuExtSparseHeader::ref_from_bytes(blocks[1].as_bytes()).unwrap();
2326 assert!(ext1.is_extended(), "ext1 chains to ext2");
2327 for i in 0..21 {
2328 assert_eq!(ext1.sparse[i].to_sparse_entry().unwrap(), sparse_map[4 + i]);
2329 }
2330
2331 let ext2 = GnuExtSparseHeader::ref_from_bytes(blocks[2].as_bytes()).unwrap();
2332 assert!(!ext2.is_extended(), "ext2 is last");
2333 for i in 0..3 {
2334 assert_eq!(
2335 ext2.sparse[i].to_sparse_entry().unwrap(),
2336 sparse_map[25 + i]
2337 );
2338 }
2339 assert!(ext2.sparse[3].is_empty());
2340 }
2341
2342 #[test]
2343 fn test_entry_builder_pax_sparse_basic() {
2344 let sparse_map = [
2345 SparseEntry {
2346 offset: 0,
2347 length: 100,
2348 },
2349 SparseEntry {
2350 offset: 2000,
2351 length: 300,
2352 },
2353 ];
2354 let on_disk: u64 = 400;
2355 let real_size: u64 = 2300;
2356
2357 let mut builder = EntryBuilder::new_ustar();
2358 builder
2359 .path(b"pax_sparse.dat")
2360 .mode(0o644)
2361 .unwrap()
2362 .size(on_disk)
2363 .unwrap()
2364 .mtime(0)
2365 .unwrap()
2366 .uid(0)
2367 .unwrap()
2368 .gid(0)
2369 .unwrap()
2370 .sparse(&sparse_map, real_size);
2371
2372 let blocks = builder.finish();
2373
2374 assert_eq!(blocks[0].entry_type(), EntryType::XHeader);
2376 blocks[0].verify_checksum().unwrap();
2377
2378 let pax_size = blocks[0].entry_size().unwrap() as usize;
2380 let pax_data_blocks = pax_size.div_ceil(HEADER_SIZE);
2381 let pax_data: Vec<u8> = blocks[1..1 + pax_data_blocks]
2382 .iter()
2383 .flat_map(|b| b.as_bytes())
2384 .copied()
2385 .collect();
2386 let pax_str = std::str::from_utf8(&pax_data[..pax_size]).unwrap();
2387
2388 assert!(pax_str.contains("GNU.sparse.major=1\n"));
2389 assert!(pax_str.contains("GNU.sparse.minor=0\n"));
2390 assert!(pax_str.contains(&format!("GNU.sparse.realsize={real_size}\n")));
2391 assert!(pax_str.contains("GNU.sparse.name=pax_sparse.dat\n"));
2392
2393 let main_idx = 1 + pax_data_blocks;
2395 let main = &blocks[main_idx];
2396 assert!(
2397 main.path_bytes().starts_with(b"GNUSparseFile"),
2398 "synthetic path should start with GNUSparseFile, got {:?}",
2399 String::from_utf8_lossy(main.path_bytes())
2400 );
2401 main.verify_checksum().unwrap();
2402
2403 assert!(blocks.len() > main_idx + 1, "should have map data prefix");
2405 let map_block = blocks[main_idx + 1].as_bytes();
2406 let map_str = std::str::from_utf8(map_block).unwrap();
2407 assert!(
2409 map_str.starts_with("2\n"),
2410 "map prefix starts with entry count"
2411 );
2412 assert!(map_str.contains("0\n100\n"));
2413 assert!(map_str.contains("2000\n300\n"));
2414 }
2415
2416 #[test]
2417 fn test_entry_builder_gnu_sparse_with_long_path() {
2418 let long_path = "d/".repeat(60) + "sparse.bin"; assert!(long_path.len() > 100);
2420
2421 let sparse_map: Vec<SparseEntry> = (0..6)
2422 .map(|i| SparseEntry {
2423 offset: i * 1000,
2424 length: 100,
2425 })
2426 .collect();
2427 let on_disk: u64 = 600;
2428 let real_size: u64 = 5100;
2429
2430 let mut builder = EntryBuilder::new_gnu();
2431 builder
2432 .path(long_path.as_bytes())
2433 .mode(0o644)
2434 .unwrap()
2435 .size(on_disk)
2436 .unwrap()
2437 .mtime(0)
2438 .unwrap()
2439 .uid(0)
2440 .unwrap()
2441 .gid(0)
2442 .unwrap()
2443 .sparse(&sparse_map, real_size);
2444
2445 let blocks = builder.finish();
2446
2447 assert_eq!(blocks[0].entry_type(), EntryType::GnuLongName);
2449 blocks[0].verify_checksum().unwrap();
2450
2451 let main_idx = blocks
2453 .iter()
2454 .position(|b| b.entry_type() == EntryType::GnuSparse)
2455 .expect("should have GnuSparse header");
2456
2457 assert!(main_idx >= 2, "LongName header + data before main");
2459
2460 let gnu = blocks[main_idx].try_as_gnu().unwrap();
2462 assert!(gnu.is_extended());
2463
2464 let ext_blocks = blocks.len() - main_idx - 1;
2466 assert!(ext_blocks >= 1, "should have extension block(s) after main");
2467 }
2468
2469 fn build_archive(builder: &mut EntryBuilder, on_disk_size: u64) -> Vec<u8> {
2475 let mut archive = Vec::new();
2476 let header_bytes = builder.finish_bytes();
2477 archive.extend_from_slice(&header_bytes);
2478 archive.extend(vec![0u8; on_disk_size.next_multiple_of(512) as usize]);
2480 archive.extend(vec![0u8; 1024]);
2482 archive
2483 }
2484
2485 fn parse_sparse_event(archive: &[u8]) -> (Vec<SparseEntry>, u64, Vec<u8>) {
2487 let mut parser = Parser::new(Limits::default());
2488 match parser.parse(archive).unwrap() {
2489 ParseEvent::SparseEntry {
2490 sparse_map,
2491 real_size,
2492 entry,
2493 ..
2494 } => (sparse_map, real_size, entry.path.to_vec()),
2495 other => panic!("Expected SparseEntry, got {other:?}"),
2496 }
2497 }
2498
2499 #[test]
2500 fn test_sparse_roundtrip_gnu_basic() {
2501 let sparse_map = vec![
2502 SparseEntry {
2503 offset: 0,
2504 length: 100,
2505 },
2506 SparseEntry {
2507 offset: 5000,
2508 length: 200,
2509 },
2510 ];
2511 let on_disk: u64 = 300;
2512 let real_size: u64 = 5200;
2513
2514 let mut builder = EntryBuilder::new_gnu();
2515 builder
2516 .path(b"rt_gnu.bin")
2517 .mode(0o644)
2518 .unwrap()
2519 .size(on_disk)
2520 .unwrap()
2521 .mtime(0)
2522 .unwrap()
2523 .uid(0)
2524 .unwrap()
2525 .gid(0)
2526 .unwrap()
2527 .sparse(&sparse_map, real_size);
2528
2529 let archive = build_archive(&mut builder, on_disk);
2530 let (parsed_map, parsed_rs, parsed_path) = parse_sparse_event(&archive);
2531
2532 assert_eq!(parsed_path, b"rt_gnu.bin");
2533 assert_eq!(parsed_rs, real_size);
2534 assert_eq!(parsed_map, sparse_map);
2535 }
2536
2537 #[test]
2538 fn test_sparse_roundtrip_gnu_extended() {
2539 let sparse_map: Vec<SparseEntry> = (0..6)
2540 .map(|i| SparseEntry {
2541 offset: i * 2000,
2542 length: 100,
2543 })
2544 .collect();
2545 let on_disk: u64 = 600;
2546 let real_size: u64 = 10100;
2547
2548 let mut builder = EntryBuilder::new_gnu();
2549 builder
2550 .path(b"rt_gnu_ext.bin")
2551 .mode(0o644)
2552 .unwrap()
2553 .size(on_disk)
2554 .unwrap()
2555 .mtime(0)
2556 .unwrap()
2557 .uid(0)
2558 .unwrap()
2559 .gid(0)
2560 .unwrap()
2561 .sparse(&sparse_map, real_size);
2562
2563 let archive = build_archive(&mut builder, on_disk);
2564 let (parsed_map, parsed_rs, _) = parse_sparse_event(&archive);
2565
2566 assert_eq!(parsed_rs, real_size);
2567 assert_eq!(parsed_map, sparse_map);
2568 }
2569
2570 #[test]
2571 fn test_sparse_roundtrip_pax_basic() {
2572 let sparse_map = vec![
2573 SparseEntry {
2574 offset: 0,
2575 length: 100,
2576 },
2577 SparseEntry {
2578 offset: 3000,
2579 length: 400,
2580 },
2581 ];
2582 let on_disk: u64 = 500;
2583 let real_size: u64 = 3400;
2584
2585 let mut builder = EntryBuilder::new_ustar();
2586 builder
2587 .path(b"rt_pax.dat")
2588 .mode(0o644)
2589 .unwrap()
2590 .size(on_disk)
2591 .unwrap()
2592 .mtime(0)
2593 .unwrap()
2594 .uid(0)
2595 .unwrap()
2596 .gid(0)
2597 .unwrap()
2598 .sparse(&sparse_map, real_size);
2599
2600 let archive = build_archive(&mut builder, on_disk);
2601 let (parsed_map, parsed_rs, parsed_path) = parse_sparse_event(&archive);
2602
2603 assert_eq!(parsed_path, b"rt_pax.dat");
2604 assert_eq!(parsed_rs, real_size);
2605 assert_eq!(parsed_map, sparse_map);
2606 }
2607
2608 #[test]
2609 fn test_sparse_roundtrip_pax_many_entries() {
2610 let sparse_map: Vec<SparseEntry> = (0..10)
2611 .map(|i| SparseEntry {
2612 offset: i * 1000,
2613 length: 50,
2614 })
2615 .collect();
2616 let on_disk: u64 = 500;
2617 let real_size: u64 = 9050;
2618
2619 let mut builder = EntryBuilder::new_ustar();
2620 builder
2621 .path(b"rt_pax_many.dat")
2622 .mode(0o644)
2623 .unwrap()
2624 .size(on_disk)
2625 .unwrap()
2626 .mtime(0)
2627 .unwrap()
2628 .uid(0)
2629 .unwrap()
2630 .gid(0)
2631 .unwrap()
2632 .sparse(&sparse_map, real_size);
2633
2634 let archive = build_archive(&mut builder, on_disk);
2635 let (parsed_map, parsed_rs, parsed_path) = parse_sparse_event(&archive);
2636
2637 assert_eq!(parsed_path, b"rt_pax_many.dat");
2638 assert_eq!(parsed_rs, real_size);
2639 assert_eq!(parsed_map, sparse_map);
2640 }
2641
2642 mod proptest_tests {
2643 use super::*;
2644 use proptest::prelude::*;
2645
2646 fn sparse_map_strategy(max_entries: usize) -> impl Strategy<Value = Vec<SparseEntry>> {
2648 proptest::collection::vec((0u64..0x10_000, 1u64..0x1000), 0..=max_entries).prop_map(
2649 |raw| {
2650 let mut entries = Vec::new();
2651 let mut cursor = 0u64;
2652 for (gap, length) in raw {
2653 let offset = cursor.saturating_add(gap);
2654 entries.push(SparseEntry { offset, length });
2655 cursor = offset.saturating_add(length);
2656 }
2657 entries
2658 },
2659 )
2660 }
2661
2662 proptest! {
2663 #[test]
2664 fn test_decu64_roundtrip(value: u64) {
2665 let d = DecU64::new(value);
2666 let s = core::str::from_utf8(d.as_bytes()).unwrap();
2667 let parsed: u64 = s.parse().unwrap();
2668 prop_assert_eq!(parsed, value);
2669 }
2670
2671 #[test]
2672 fn test_sparse_builder_roundtrip_gnu(
2673 map in sparse_map_strategy(30),
2674 ) {
2675 let on_disk: u64 = map.iter().map(|e| e.length).sum();
2676 let real_size = map.last().map(|e| e.offset + e.length).unwrap_or(0);
2677
2678 let mut builder = EntryBuilder::new_gnu();
2679 builder
2680 .path(b"proptest_gnu.bin")
2681 .mode(0o644).unwrap()
2682 .size(on_disk).unwrap()
2683 .mtime(0).unwrap()
2684 .uid(0).unwrap()
2685 .gid(0).unwrap()
2686 .sparse(&map, real_size);
2687
2688 let archive = build_archive(&mut builder, on_disk);
2689 let mut parser = Parser::new(Limits::default());
2690 let event = parser.parse(&archive).unwrap();
2691
2692 match event {
2693 ParseEvent::SparseEntry {
2694 sparse_map,
2695 real_size: rs,
2696 ..
2697 } => {
2698 prop_assert_eq!(rs, real_size);
2699 prop_assert_eq!(sparse_map.len(), map.len());
2700 for (i, expected) in map.iter().enumerate() {
2701 prop_assert_eq!(sparse_map[i], *expected);
2702 }
2703 }
2704 other => {
2705 return Err(proptest::test_runner::TestCaseError::fail(
2706 format!("Expected SparseEntry, got {other:?}")));
2707 }
2708 }
2709 }
2710
2711 #[test]
2712 fn test_sparse_builder_roundtrip_pax(
2713 map in sparse_map_strategy(20),
2714 ) {
2715 let on_disk: u64 = map.iter().map(|e| e.length).sum();
2716 let real_size = map.last().map(|e| e.offset + e.length).unwrap_or(0);
2717
2718 let mut builder = EntryBuilder::new_ustar();
2719 builder
2720 .path(b"proptest_pax.dat")
2721 .mode(0o644).unwrap()
2722 .size(on_disk).unwrap()
2723 .mtime(0).unwrap()
2724 .uid(0).unwrap()
2725 .gid(0).unwrap()
2726 .sparse(&map, real_size);
2727
2728 let archive = build_archive(&mut builder, on_disk);
2729 let mut parser = Parser::new(Limits::default());
2730 let event = parser.parse(&archive).unwrap();
2731
2732 match event {
2733 ParseEvent::SparseEntry {
2734 sparse_map,
2735 real_size: rs,
2736 entry,
2737 ..
2738 } => {
2739 prop_assert_eq!(&entry.path[..], b"proptest_pax.dat");
2740 prop_assert_eq!(rs, real_size);
2741 prop_assert_eq!(sparse_map.len(), map.len());
2742 for (i, expected) in map.iter().enumerate() {
2743 prop_assert_eq!(sparse_map[i], *expected);
2744 }
2745 }
2746 other => {
2747 return Err(proptest::test_runner::TestCaseError::fail(
2748 format!("Expected SparseEntry, got {other:?}")));
2749 }
2750 }
2751 }
2752 }
2753 }
2754}
2755
2756#[cfg(kani)]
2757mod kani_proofs {
2758 use super::*;
2759
2760 #[kani::proof]
2761 #[kani::unwind(21)]
2762 fn check_decu64_panic_freedom() {
2763 let value: u64 = kani::any();
2764 let d = DecU64::new(value);
2765 let bytes = d.as_bytes();
2766 kani::assert(!bytes.is_empty(), "output is never empty");
2767 kani::assert(bytes.len() <= 20, "output fits in buffer");
2768 }
2769
2770 }