diff --git a/crates/formats/src/firmware.rs b/crates/formats/src/firmware.rs index d047e555..9fb5652f 100644 --- a/crates/formats/src/firmware.rs +++ b/crates/formats/src/firmware.rs @@ -803,7 +803,7 @@ impl<'a> Scanner<'a> { format!("cpio archive ({}), {} member(s)", l.format.trim_start_matches("cpio-"), l.members.len()), if l.complete { "high" } else { "medium" }, Unpack::Members, - json!({"members": l.members.len(), "complete": l.complete}), + json!({"members": l.members.len(), "complete": l.complete, "warnings": l.warnings}), )) } diff --git a/crates/formats/src/tarcpio.rs b/crates/formats/src/tarcpio.rs index 8f1c9576..6039691d 100644 --- a/crates/formats/src/tarcpio.rs +++ b/crates/formats/src/tarcpio.rs @@ -340,6 +340,19 @@ pub fn cpio(data: &[u8], max_members: usize) -> Option { out.length = pos as u64; break; } + if format == "cpio-crc" { + match h.get(102..110).and_then(hex_field).and_then(|n| u32::try_from(n).ok()) { + Some(expected) => { + if let Some(body) = r.slice(data_off, size) { + let actual = body.iter().fold(0u32, |sum, &byte| sum.wrapping_add(u32::from(byte))); + if actual != expected { + out.warnings.push(format!("{name}: data checksum mismatch (stored {expected:08x}, computed {actual:08x})")); + } + } + } + None => out.warnings.push(format!("{name}: unreadable data checksum")), + } + } out.members.push(Member { name, kind, size: filesize, data_offset: data_off as u64, link_target, mode }); if next > data.len() { out.warnings.push("archive ends early (no trailer)".into()); diff --git a/crates/formats/tests/cpio_checksum.rs b/crates/formats/tests/cpio_checksum.rs new file mode 100644 index 00000000..f06deb9a --- /dev/null +++ b/crates/formats/tests/cpio_checksum.rs @@ -0,0 +1,66 @@ +use craft_anything_formats::firmware::{ScanOptions, scan}; +use craft_anything_formats::tarcpio::{cpio, member_data}; +use craft_anything_testkit::firmware as fx; + +fn crc_archive() -> Vec { + let mut archive = fx::cpio_newc(&[("file", &[255, 128, 65], 0o100644), ("link", b"file", 0o120777)]); + let mut offset = 0; + for expected in [448u32, 416, 0] { + let size = usize::from_str_radix(std::str::from_utf8(&archive[offset + 54..offset + 62]).unwrap(), 16).unwrap(); + let name = usize::from_str_radix(std::str::from_utf8(&archive[offset + 94..offset + 102]).unwrap(), 16).unwrap(); + archive[offset..offset + 6].copy_from_slice(b"070702"); + archive[offset + 102..offset + 110].copy_from_slice(format!("{expected:08x}").as_bytes()); + let data = (offset + 110 + name + 3) & !3; + offset = (data + size + 3) & !3; + } + archive +} + +#[test] +fn crc_members_report_changed_data_without_losing_the_inventory() { + let mut archive = crc_archive(); + let clean = cpio(&archive, 10).unwrap(); + let first = clean.members[0].data_offset as usize; + archive[first] = 0; + let listing = cpio(&archive, 10).unwrap(); + assert!(listing.complete); + assert_eq!(listing.members.len(), 2); + assert!(listing.warnings.iter().any(|w| w.contains("file") && w.contains("checksum")), "{:?}", listing.warnings); + assert_eq!(member_data(&archive, &listing.members[0]), Some([0, 128, 65].as_slice())); +} + +#[test] +fn crc_checks_cover_symlink_data_and_valid_unsigned_bytes() { + let mut archive = crc_archive(); + let clean = cpio(&archive, 10).unwrap(); + assert!(clean.complete); + assert!(clean.warnings.is_empty(), "{:?}", clean.warnings); + let link = clean.members[1].data_offset as usize; + archive[link] = b'F'; + assert!(cpio(&archive, 10).unwrap().warnings.iter().any(|w| w.contains("link") && w.contains("checksum"))); +} + +#[test] +fn newc_has_no_data_checksum_and_truncated_data_is_not_validated() { + let mut newc = fx::cpio_newc(&[("file", b"abc", 0o100644)]); + newc[102..110].copy_from_slice(b"00000126"); + assert!(cpio(&newc, 10).unwrap().warnings.is_empty()); + let archive = crc_archive(); + let data = cpio(&archive, 10).unwrap().members[0].data_offset as usize; + let partial = cpio(&archive[..data + 2], 10).unwrap(); + assert!(!partial.complete); + assert!(!partial.warnings.iter().any(|w| w.contains("checksum mismatch"))); +} + +#[test] +fn firmware_inventory_exposes_checksum_damage() { + let mut archive = crc_archive(); + let clean = scan(&archive, &ScanOptions::default(), &|| false); + assert_eq!(clean.regions.iter().find(|r| r.offset == 0 && r.kind == "cpio-crc").unwrap().confidence, "high"); + let data = cpio(&archive, 10).unwrap().members[0].data_offset as usize; + archive[data] = 0; + let damaged = scan(&archive, &ScanOptions::default(), &|| false); + let region = damaged.regions.iter().find(|r| r.offset == 0 && r.kind == "cpio-crc").unwrap(); + assert_eq!(region.confidence, "high"); + assert!(region.details["warnings"].as_array().unwrap().iter().any(|w| w.as_str().unwrap().contains("checksum mismatch"))); +}