1#![allow(non_upper_case_globals)]
32
33use crate::{BLOCKSIZE, NAME_LEN, PREFIX_LEN, TarFormatDecimal, TarFormatOctal, TarFormatString};
34use core::error::Error;
35use core::fmt::{Debug, Display, Formatter};
36use core::num::ParseIntError;
37
38const CKSUM_OFFSET: usize = 148;
39const CKSUM_LEN: usize = 8;
40
41#[derive(Debug)]
43pub enum ModeError {
44 ParseInt(ParseIntError),
45 IllegalMode,
46}
47
48impl Display for ModeError {
49 fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
50 Debug::fmt(self, f)
51 }
52}
53
54impl Error for ModeError {
55 fn source(&self) -> Option<&(dyn Error + 'static)> {
56 match self {
57 Self::ParseInt(e) => Some(e),
58 Self::IllegalMode => None,
59 }
60 }
61}
62
63#[derive(Copy, Clone, PartialEq, Eq)]
65#[repr(transparent)]
66pub struct Mode(TarFormatOctal<8>);
67
68impl Mode {
69 pub fn to_flags(self) -> Result<ModeFlags, ModeError> {
74 let bits = self.0.as_number::<u64>().map_err(ModeError::ParseInt)?;
75 ModeFlags::from_bits(bits & 0o7777).ok_or(ModeError::IllegalMode)
76 }
77}
78
79impl Debug for Mode {
80 fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
81 Debug::fmt(&self.to_flags(), f)
82 }
83}
84
85#[derive(Copy, Clone, Debug, PartialOrd, PartialEq, Eq)]
86pub struct InvalidTypeFlagError(u8);
87
88impl Display for InvalidTypeFlagError {
89 fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
90 f.write_fmt(format_args!("{:x} is not a valid TypeFlag", self.0))
91 }
92}
93
94impl core::error::Error for InvalidTypeFlagError {}
95
96#[derive(Copy, Clone, PartialOrd, PartialEq, Eq)]
97pub struct TypeFlagRaw(u8);
98
99impl TypeFlagRaw {
100 pub fn try_to_type_flag(self) -> Result<TypeFlag, InvalidTypeFlagError> {
106 TypeFlag::try_from(self)
107 }
108}
109
110impl Debug for TypeFlagRaw {
111 fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
112 Debug::fmt(&self.try_to_type_flag(), f)
113 }
114}
115
116#[derive(Debug, Copy, Clone, PartialEq, Eq)]
120#[repr(u8)]
121#[allow(unused)]
122pub enum TypeFlag {
123 REGTYPE = b'0',
128 AREGTYPE = b'\0',
133 LINK = b'1',
137 SYMTYPE = b'2',
140 CHRTYPE = b'3',
145 BLKTYPE = b'4',
150 DIRTYPE = b'5',
157 FIFOTYPE = b'6',
160 CONTTYPE = b'7',
165 XHDTYPE = b'x',
167 XGLTYPE = b'g',
169}
170
171impl TypeFlag {
172 #[must_use]
174 pub fn is_regular_file(self) -> bool {
175 self == Self::AREGTYPE || self == Self::REGTYPE
177 }
178
179 pub(crate) fn has_payload(self) -> bool {
181 self.is_regular_file() || matches!(self, Self::CONTTYPE | Self::XHDTYPE | Self::XGLTYPE)
182 }
183}
184
185impl TryFrom<TypeFlagRaw> for TypeFlag {
186 type Error = InvalidTypeFlagError;
187
188 fn try_from(value: TypeFlagRaw) -> Result<Self, Self::Error> {
189 match value.0 {
190 b'0' => Ok(Self::REGTYPE),
191 b'\0' => Ok(Self::AREGTYPE),
192 b'1' => Ok(Self::LINK),
193 b'2' => Ok(Self::SYMTYPE),
194 b'3' => Ok(Self::CHRTYPE),
195 b'4' => Ok(Self::BLKTYPE),
196 b'5' => Ok(Self::DIRTYPE),
197 b'6' => Ok(Self::FIFOTYPE),
198 b'7' => Ok(Self::CONTTYPE),
199 b'x' => Ok(Self::XHDTYPE),
200 b'g' => Ok(Self::XGLTYPE),
201 e => Err(InvalidTypeFlagError(e)),
202 }
203 }
204}
205
206bitflags::bitflags! {
207 #[repr(transparent)]
209 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
210 pub struct ModeFlags: u64 {
211 const SetUID = 0o4000;
213 const SetGID = 0o2000;
215 const TSVTX = 0o1000;
217 const OwnerRead = 0o400;
219 const OwnerWrite = 0o200;
221 const OwnerExec = 0o100;
223 const GroupRead = 0o040;
225 const GroupWrite = 0o020;
227 const GroupExec = 0o010;
229 const OthersRead = 0o004;
231 const OthersWrite = 0o002;
233 const OthersExec = 0o001;
235 }
236}
237
238#[derive(Debug, Copy, Clone, PartialEq, Eq)]
251#[repr(C, packed)]
252pub struct PosixHeader {
253 pub name: TarFormatString<NAME_LEN>,
254 pub mode: Mode,
255 pub uid: TarFormatOctal<8>,
256 pub gid: TarFormatOctal<8>,
257 pub size: TarFormatOctal<12>,
259 pub mtime: TarFormatDecimal<12>,
260 pub cksum: TarFormatOctal<8>,
261 pub typeflag: TypeFlagRaw,
262 pub linkname: TarFormatString<NAME_LEN>,
265 pub magic: TarFormatString<6>,
266 pub version: TarFormatString<2>,
267 pub uname: TarFormatString<32>,
270 pub gname: TarFormatString<32>,
273 pub dev_major: TarFormatOctal<8>,
274 pub dev_minor: TarFormatOctal<8>,
275 pub prefix: TarFormatString<PREFIX_LEN>,
276 pub _pad: [u8; 12],
278}
279
280impl PosixHeader {
281 pub fn payload_block_count(&self) -> Result<usize, ParseIntError> {
288 let parsed_size = self.size.as_number::<usize>()?;
289 Ok(parsed_size.div_ceil(BLOCKSIZE))
290 }
291
292 pub(crate) fn has_valid_checksum(&self) -> bool {
293 self.cksum
294 .as_number::<u64>()
295 .is_ok_and(|cksum| cksum == self.computed_checksum())
296 }
297
298 pub(crate) fn computed_checksum(&self) -> u64 {
299 let ptr = core::ptr::addr_of!(*self);
300 let ptr = ptr.cast::<u8>();
301
302 let self_bytes = unsafe { core::slice::from_raw_parts(ptr, BLOCKSIZE) };
304
305 self_bytes
306 .iter()
307 .enumerate()
308 .map(|(i, &byte)| {
309 if (CKSUM_OFFSET..CKSUM_OFFSET + CKSUM_LEN).contains(&i) {
310 u64::from(b' ')
311 } else {
312 u64::from(byte)
313 }
314 })
315 .sum()
316 }
317
318 #[must_use]
321 pub fn is_zero_block(&self) -> bool {
322 let ptr = core::ptr::addr_of!(*self);
323 let ptr = ptr.cast::<u8>();
324
325 let self_bytes = unsafe { core::slice::from_raw_parts(ptr, BLOCKSIZE) };
327 self_bytes.iter().filter(|x| **x == 0).count() == BLOCKSIZE
328 }
329}
330
331#[cfg(test)]
332mod tests {
333 use crate::BLOCKSIZE;
334 use crate::header::{PosixHeader, TypeFlag};
335 use std::mem::size_of;
336
337 fn bytes_to_archive(tar_archive_data: &[u8]) -> &PosixHeader {
339 unsafe { (tar_archive_data.as_ptr().cast::<PosixHeader>()).as_ref() }.unwrap()
341 }
342
343 #[test]
344 fn test_display_header() {
345 let archive = bytes_to_archive(include_bytes!("../tests/gnu_tar_default.tar"));
346 assert_eq!(archive.name.as_str(), Ok("bye_world_513b.txt"));
347 println!("{archive:#?}'");
348 }
349
350 #[test]
351 fn test_payload_block_count() {
352 let archive = bytes_to_archive(include_bytes!("../tests/gnu_tar_default.tar"));
354 assert_eq!(archive.payload_block_count(), Ok(2));
355 }
356
357 #[test]
358 fn test_show_tar_header_magics() {
359 let archive = bytes_to_archive(include_bytes!("../tests/gnu_tar_default.tar"));
360 println!(
361 "default: magic='{:?}', version='{:?}'",
362 archive.magic, archive.version
363 );
364 let archive = bytes_to_archive(include_bytes!("../tests/gnu_tar_gnu.tar"));
365 println!(
366 "gnu: magic='{:?}', version='{:?}'",
367 archive.magic, archive.version
368 );
369 let archive = bytes_to_archive(include_bytes!("../tests/gnu_tar_oldgnu.tar"));
370 println!(
371 "oldgnu: magic='{:?}', version='{:?}'",
372 archive.magic, archive.version
373 );
374 let archive = bytes_to_archive(include_bytes!("../tests/gnu_tar_pax.tar"));
375 println!(
376 "pax: magic='{:?}', version='{:?}'",
377 archive.magic, archive.version
378 );
379 let archive = bytes_to_archive(include_bytes!("../tests/gnu_tar_posix.tar"));
380 println!(
381 "posix: magic='{:?}', version='{:?}'",
382 archive.magic, archive.version
383 );
384 let archive = bytes_to_archive(include_bytes!("../tests/gnu_tar_ustar.tar"));
385 println!(
386 "ustar: magic='{:?}', version='{:?}'",
387 archive.magic, archive.version
388 );
389 let archive = bytes_to_archive(include_bytes!("../tests/gnu_tar_v7.tar"));
390 println!(
391 "v7: magic='{:?}', version='{:?}'",
392 archive.magic, archive.version
393 );
394 }
395
396 #[test]
397 fn test_parse_tar_header_filename() {
398 let archive = bytes_to_archive(include_bytes!("../tests/gnu_tar_default.tar"));
399 assert_eq!(
400 archive.typeflag.try_to_type_flag(),
401 Ok(TypeFlag::REGTYPE),
402 "the first entry is a regular file!"
403 );
404 assert_eq!(archive.name.as_str(), Ok("bye_world_513b.txt"));
405
406 let archive = bytes_to_archive(include_bytes!("../tests/gnu_tar_gnu.tar"));
407 assert_eq!(
408 archive.typeflag.try_to_type_flag(),
409 Ok(TypeFlag::REGTYPE),
410 "the first entry is a regular file!"
411 );
412 assert_eq!(archive.name.as_str(), Ok("bye_world_513b.txt"));
413
414 let archive = bytes_to_archive(include_bytes!("../tests/gnu_tar_oldgnu.tar"));
415 assert_eq!(
416 archive.typeflag.try_to_type_flag(),
417 Ok(TypeFlag::REGTYPE),
418 "the first entry is a regular file!"
419 );
420 assert_eq!(archive.name.as_str(), Ok("bye_world_513b.txt"));
421
422 let archive = bytes_to_archive(include_bytes!("../tests/gnu_tar_ustar.tar"));
423 assert_eq!(
424 archive.typeflag.try_to_type_flag(),
425 Ok(TypeFlag::REGTYPE),
426 "the first entry is a regular file!"
427 );
428 assert_eq!(archive.name.as_str(), Ok("bye_world_513b.txt"));
429
430 let archive = bytes_to_archive(include_bytes!("../tests/gnu_tar_v7.tar"));
431 assert_eq!(
433 archive.typeflag.try_to_type_flag(),
434 Ok(TypeFlag::AREGTYPE),
435 "the first entry is a regular file!"
436 );
437 assert_eq!(archive.name.as_str(), Ok("bye_world_513b.txt"));
438 }
439
440 #[test]
441 fn test_parse_pax_headers() {
442 for archive_data in [
443 include_bytes!("../tests/gnu_tar_pax.tar") as &[u8],
444 include_bytes!("../tests/gnu_tar_posix.tar") as &[u8],
445 ] {
446 let archive = bytes_to_archive(archive_data);
447 assert_eq!(archive.typeflag.try_to_type_flag(), Ok(TypeFlag::XHDTYPE));
448 }
449 }
450
451 #[test]
452 fn test_size() {
453 assert_eq!(BLOCKSIZE, size_of::<PosixHeader>());
454 }
455}