diff --git a/crates/formats/src/tarcpio.rs b/crates/formats/src/tarcpio.rs index 7f63de2f..78afa0bc 100644 --- a/crates/formats/src/tarcpio.rs +++ b/crates/formats/src/tarcpio.rs @@ -177,7 +177,8 @@ pub fn tar(data: &[u8], max_members: usize) -> Option { } b'g' => {} _ => { - let ustar = h.get(257..262) == Some(b"ustar"); + // Old GNU headers use these bytes for timestamps and sparse-file metadata. + let ustar = h.get(257..263) == Some(b"ustar\0"); let base = text(h.get(0..100).unwrap_or(&[])); let prefix = if ustar { text(h.get(345..500).unwrap_or(&[])) } else { String::new() }; let joined = if prefix.is_empty() { base } else { format!("{prefix}/{base}") }; diff --git a/crates/formats/tests/tar_gnu_names.rs b/crates/formats/tests/tar_gnu_names.rs new file mode 100644 index 00000000..1d0b698e --- /dev/null +++ b/crates/formats/tests/tar_gnu_names.rs @@ -0,0 +1,31 @@ +use craft_anything_formats::tarcpio::{member_data, tar}; +use craft_anything_testkit::firmware as fx; + +fn checksum(header: &mut [u8]) { + header[148..156].fill(b' '); + let sum: u32 = header.iter().map(|&b| u32::from(b)).sum(); + header[148..156].copy_from_slice(format!("{sum:06o}\0 ").as_bytes()); +} + +#[test] +fn old_gnu_timestamps_are_not_posix_path_prefixes() { + let mut archive = fx::tar(&[("program", b"payload")]); + let header = &mut archive[..512]; + header[257..265].copy_from_slice(b"ustar \0"); + header[345..357].copy_from_slice(b"00000000123\0"); + header[357..369].copy_from_slice(b"00000000456\0"); + checksum(header); + let listing = tar(&archive, 10).unwrap(); + assert!(listing.complete); + assert_eq!(listing.members[0].name, "program"); + assert_eq!(member_data(&archive, &listing.members[0]), Some(b"payload".as_slice())); +} + +#[test] +fn posix_ustar_prefixes_are_still_joined() { + let mut archive = fx::tar(&[("program", b"payload")]); + let header = &mut archive[..512]; + header[345..352].copy_from_slice(b"usr/bin"); + checksum(header); + assert_eq!(tar(&archive, 10).unwrap().members[0].name, "usr/bin/program"); +}