//! File format signatures and structural validators for file carving. //! //! Each detector parses the internal container structure (headers, chunks, footers) //! to determine the exact file size and confidence level rather than relying on //! magic bytes alone. use crate::source::{read_vec, Source}; use crate::types::{Category, Confidence}; /// Output of a format detector: a file recognized starting at `offset`. #[derive(Debug, Clone)] pub struct Hit { pub len: u64, pub ext: &'static str, pub confidence: Confidence, } pub fn category_for_ext(ext: &str) -> Category { match ext.to_ascii_lowercase().as_str() { "jpg" | "jpeg" | "png" | "gif" | "bmp" | "webp" | "heic" | "avif" | "tif" | "tiff" | "psd" | "svg" | "ico" | "cr2" | "nef" | "dng" | "jp2" => Category::Image, "pdf" | "doc" | "docx" | "xls" | "xlsx" | "ppt" | "pptx" | "odt" | "ods" | "odp" | "txt" | "rtf" | "epub" | "md" | "csv" | "html" | "xml" | "json" | "swf" => { Category::Document } "mp4" | "mov" | "m4v" | "avi" | "mkv" | "webm" | "wmv" | "flv" | "mpg" | "mpeg" | "3gp" | "ts" => Category::Video, "mp3" | "wav" | "flac" | "ogg" | "m4a" | "aac" | "wma" | "opus" | "mid" | "aif" => { Category::Audio } "zip" | "rar" | "7z" | "gz" | "tar" | "bz2" | "xz" | "jar" | "apk" | "zst" | "cab" | "deb" => Category::Archive, "sqlite" | "db" | "sqlite3" | "mdb" | "accdb" | "pst" | "dat" | "evtx" => { Category::Database } _ => Category::Other, } } /// Checks whether the leading bytes `head` match the expected magic for `ext`. /// Returns `None` if `ext` has no known magic signature. pub fn matches_ext(ext: &str, head: &[u8]) -> Option { let ext = ext.to_ascii_lowercase(); let starts = |m: &[u8]| head.starts_with(m); let r = match ext.as_str() { "jpg" | "jpeg" => starts(&[0xFF, 0xD8, 0xFF]), "png" => starts(&[0x89, b'P', b'N', b'G']), "gif" => starts(b"GIF8"), "bmp" => starts(b"BM"), "pdf" => starts(b"%PDF"), "zip" | "docx" | "xlsx" | "pptx" | "odt" | "ods" | "odp" | "jar" | "epub" | "apk" => { starts(b"PK\x03\x04") || starts(b"PK\x05\x06") } "doc" | "xls" | "ppt" => starts(&[0xD0, 0xCF, 0x11, 0xE0]), "7z" => starts(&[b'7', b'z', 0xBC, 0xAF, 0x27, 0x1C]), "rar" => starts(b"Rar!"), "gz" => starts(&[0x1F, 0x8B]), "mp4" | "mov" | "m4v" | "m4a" | "3gp" | "heic" | "avif" => { head.len() >= 8 && &head[4..8] == b"ftyp" || (head.len() >= 8 && matches!(&head[4..8], b"moov" | b"mdat" | b"free" | b"wide")) } "avi" | "wav" | "webp" => starts(b"RIFF"), "mp3" => starts(b"ID3") || (head.len() >= 2 && head[0] == 0xFF && head[1] & 0xE0 == 0xE0), "flac" => starts(b"fLaC"), "ogg" | "opus" => starts(b"OggS"), "mkv" | "webm" => starts(&[0x1A, 0x45, 0xDF, 0xA3]), "sqlite" | "sqlite3" | "db" => starts(b"SQLite format 3\0"), "exe" | "dll" => starts(b"MZ"), "bz2" => starts(b"BZh"), "zst" => starts(&[0x28, 0xB5, 0x2F, 0xFD]), "cab" => starts(b"MSCF"), "deb" => starts(b"!"), "elf" => starts(&[0x7F, b'E', b'L', b'F']), "tif" | "tiff" => starts(&[b'I', b'I', 42, 0]) || starts(&[b'M', b'M', 0, 42]), "ico" => starts(&[0, 0, 1, 0]), "rtf" => starts(b"{\\rtf"), "swf" => starts(b"FWS") || starts(b"CWS"), "flv" => starts(b"FLV"), "pst" => starts(b"!BDN"), "evtx" => starts(b"ElfFile"), "mid" => starts(b"MThd"), "pcap" => starts(&[0xD4, 0xC3, 0xB2, 0xA1]) || starts(&[0xA1, 0xB2, 0xC3, 0xD4]), "wmv" | "wma" => starts(&[0x30, 0x26, 0xB2, 0x75]), _ => return None, }; Some(r) } /// Dispatch table mapping leading byte -> candidate format detectors. /// The carver queries `candidates(first_byte)` and invokes `detect`. pub fn might_start(b0: u8) -> bool { crate::formats::might_start(b0) || matches!( b0, 0xFF | 0x89 | b'G' | b'B' | b'%' | b'P' | 0xD0 | b'7' | b'S' | b'R' | b'I' | b'M' | 0x00 | b'f' | 0x1F ) } const MAX_HEADER: usize = 64 * 1024; /// Attempts to recognize a file starting at `off`. /// `head` contains the leading bytes already read at `off` (at least 16 when available). pub fn detect(src: &dyn Source, off: u64, head: &[u8], max_size: u64) -> Option { if head.len() < 12 { return None; } let h = head; if h.starts_with(&[0xFF, 0xD8, 0xFF]) { return jpeg(src, off, max_size); } if h.starts_with(&[0x89, b'P', b'N', b'G', 0x0D, 0x0A, 0x1A, 0x0A]) { return png(src, off, max_size); } if h.starts_with(b"GIF87a") || h.starts_with(b"GIF89a") { return gif(src, off, max_size); } if h.starts_with(b"%PDF-") { return pdf(src, off, max_size); } if h.starts_with(b"PK\x03\x04") { return zip(src, off, max_size); } if &h[4..8] == b"ftyp" { return isobmff(src, off, max_size); } if h.starts_with(b"RIFF") { return riff(h, max_size); } if h.starts_with(b"BM") { return bmp(h, max_size); } if h.starts_with(b"SQLite format 3\0") { return sqlite(h, max_size); } if h.starts_with(&[b'7', b'z', 0xBC, 0xAF, 0x27, 0x1C]) { return sevenz(h, max_size); } if h.starts_with(&[0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1]) { return ole(src, off, max_size); } if h.starts_with(b"ID3") { return mp3(src, off, max_size); } if h.starts_with(b"MZ") { return pe(src, off, max_size); } crate::formats::detect(src, off, h, max_size) } // ---------------------------------------------------------------- JPEG fn jpeg(src: &dyn Source, off: u64, max: u64) -> Option { let head = read_vec(src, off, MAX_HEADER).ok()?; let mut p = 2usize; let mut sos_end = None; let mut saw_sof = false; while p + 4 <= head.len() { if head[p] != 0xFF { return None; } let m = head[p + 1]; if m == 0xFF { p += 1; continue; } if m == 0xD9 { return Some(Hit { len: p as u64 + 2, ext: "jpg", confidence: Confidence::Medium, }); } if (0xD0..=0xD8).contains(&m) || m == 0x01 { p += 2; continue; } let seg = u16::from_be_bytes([head[p + 2], head[p + 3]]) as usize; if seg < 2 { return None; } if matches!(m, 0xC0..=0xC3 | 0xC5..=0xC7 | 0xC9..=0xCB | 0xCD..=0xCF) { saw_sof = true; } p += 2 + seg; if m == 0xDA { sos_end = Some(p); break; } } let start = sos_end?; if !saw_sof { return None; } // Entropy-coded scan data: scan for EOI (`FF D9`), skipping `FF 00` byte stuffing and `RSTn`. let mut pos = off + start as u64; let limit = off + max.min(512 * 1024 * 1024); let mut buf = vec![0u8; 1 << 20]; let mut prev_ff = false; while pos < limit { let n = src.read_at(pos, &mut buf).ok()?; if n == 0 { break; } for (i, &b) in buf[..n].iter().enumerate() { if prev_ff { if b == 0xD9 { return Some(Hit { len: pos + i as u64 + 1 - off, ext: "jpg", confidence: Confidence::High, }); } prev_ff = b == 0xFF; } else { prev_ff = b == 0xFF; } } pos += n as u64; } // Missing EOI: truncated or overwritten JPEG; still emit with Low confidence. Some(Hit { len: (pos - off).min(4 * 1024 * 1024), ext: "jpg", confidence: Confidence::Low, }) } // ---------------------------------------------------------------- PNG fn png(src: &dyn Source, off: u64, max: u64) -> Option { let total = src.size().saturating_sub(off); let mut p = 8u64; let mut first = true; let mut saw_idat = false; for _ in 0..200_000 { let c = read_vec(src, off + p, 8).ok()?; if c.len() < 8 { break; } let len = u32::from_be_bytes([c[0], c[1], c[2], c[3]]) as u64; let ty = &c[4..8]; // Valid PNG chunk: 4 ASCII letters with uppercase 3rd letter (reserved bit), plausible size within // the source. Prevents random ASCII text after a truncated PNG from being read as a ~1.9 GB chunk. let valid = ty.iter().all(|b| b.is_ascii_alphabetic()) && ty[2].is_ascii_uppercase() && len <= 128 * 1024 * 1024 && p + 12 + len <= total; if !valid { break; } if first && ty != b"IHDR" { return None; } first = false; saw_idat |= ty == b"IDAT"; p += 12 + len; if p > max { break; } if ty == b"IEND" { return Some(Hit { len: p, ext: "png", confidence: Confidence::High, }); } } // Missing `IEND`: only keep as a truncated PNG if at least one `IDAT` was seen and size is reasonable. if first || !saw_idat || p > 64 * 1024 * 1024 { return None; } Some(Hit { len: p, ext: "png", confidence: Confidence::Low, }) } // ---------------------------------------------------------------- GIF fn gif(src: &dyn Source, off: u64, max: u64) -> Option { let d = read_vec(src, off, (16usize * 1024 * 1024).min(max as usize)).ok()?; if d.len() < 13 { return None; } let flags = d[10]; let mut p = 13usize; if flags & 0x80 != 0 { p += 3 * (1usize << ((flags & 7) + 1)); } while p < d.len() { match d[p] { 0x3B => { return Some(Hit { len: p as u64 + 1, ext: "gif", confidence: Confidence::High, }) } 0x21 => { p += 2; p = skip_subblocks(&d, p)?; } 0x2C => { if p + 10 > d.len() { break; } let f = d[p + 9]; p += 10; if f & 0x80 != 0 { p += 3 * (1usize << ((f & 7) + 1)); } p += 1; p = skip_subblocks(&d, p)?; } _ => return None, } } Some(Hit { len: d.len() as u64, ext: "gif", confidence: Confidence::Low, }) } fn skip_subblocks(d: &[u8], mut p: usize) -> Option { loop { let n = *d.get(p)? as usize; p += 1 + n; if n == 0 { return Some(p); } } } // ---------------------------------------------------------------- BMP fn bmp(h: &[u8], max: u64) -> Option { if h.len() < 18 { return None; } let size = u32::from_le_bytes([h[2], h[3], h[4], h[5]]) as u64; let reserved = u32::from_le_bytes([h[6], h[7], h[8], h[9]]); let dib = u32::from_le_bytes([h[14], h[15], h[16], h[17]]); if reserved != 0 || !matches!(dib, 12 | 40 | 52 | 56 | 64 | 108 | 124) || size < 26 || size > max { return None; } Some(Hit { len: size, ext: "bmp", confidence: Confidence::High, }) } // ---------------------------------------------------------------- PDF fn pdf(src: &dyn Source, off: u64, max: u64) -> Option { let limit = max.min(1 << 30); let mut pos = off; let mut buf = vec![0u8; 1 << 20 | 16]; let mut last_eof: Option = None; let mut carry: Vec = Vec::new(); while pos - off < limit { let n = src.read_at(pos, &mut buf[..(1 << 20)]).ok()?; if n == 0 { break; } let mut hay = std::mem::take(&mut carry); let base = pos - hay.len() as u64; hay.extend_from_slice(&buf[..n]); let mut i = 0; while let Some(f) = find(&hay[i..], b"%%EOF") { let end = base + (i + f) as u64 + 5; // Continue scanning if `%%EOF` is followed by an incremental PDF update. let tail = read_vec(src, end, 32).ok()?; let rest: Vec = tail .iter() .copied() .skip_while(|b| b.is_ascii_whitespace()) .collect(); let skipped = tail.len() - rest.len(); last_eof = Some(end + skipped.min(2) as u64 - off); let more = rest.first().map(|b| b.is_ascii_digit()).unwrap_or(false) || rest.starts_with(b"%PDF"); if !more { let conf = if last_eof.is_some() { Confidence::High } else { Confidence::Low }; return Some(Hit { len: last_eof.unwrap(), ext: "pdf", confidence: conf, }); } i += f + 5; } let keep = hay.len().min(8); carry = hay[hay.len() - keep..].to_vec(); pos += n as u64; } last_eof.map(|l| Hit { len: l, ext: "pdf", confidence: Confidence::Medium, }) } fn find(hay: &[u8], needle: &[u8]) -> Option { hay.windows(needle.len()).position(|w| w == needle) } // ---------------------------------------------------------------- ZIP & derivatives fn zip(src: &dyn Source, off: u64, max: u64) -> Option { let limit = max.min(4 << 30); let mut pos = off; let mut buf = vec![0u8; 1 << 20]; let mut carry: Vec = Vec::new(); while pos - off < limit { let n = src.read_at(pos, &mut buf).ok()?; if n == 0 { break; } let mut hay = std::mem::take(&mut carry); let base = pos - hay.len() as u64; hay.extend_from_slice(&buf[..n]); let mut i = 0; while let Some(f) = find(&hay[i..], b"PK\x05\x06") { let at = base + (i + f) as u64; if let Some(hit) = check_eocd(src, off, at) { return Some(hit); } i += f + 4; } let keep = hay.len().min(3); carry = hay[hay.len() - keep..].to_vec(); pos += n as u64; } None } fn check_eocd(src: &dyn Source, start: u64, at: u64) -> Option { let e = read_vec(src, at, 22).ok()?; if e.len() < 22 { return None; } let entries = u16::from_le_bytes([e[10], e[11]]) as u64; let cd_size = u32::from_le_bytes([e[12], e[13], e[14], e[15]]) as u64; let cd_off = u32::from_le_bytes([e[16], e[17], e[18], e[19]]) as u64; let comment = u16::from_le_bytes([e[20], e[21]]) as u64; if entries == 0 || cd_off + cd_size != at - start { return None; } let cd = read_vec(src, start + cd_off, cd_size.min(4 << 20) as usize).ok()?; let names = zip_names(&cd); let ext = classify_zip(src, start, &names); Some(Hit { len: at - start + 22 + comment, ext, confidence: Confidence::High, }) } fn zip_names(cd: &[u8]) -> Vec { let mut out = vec![]; let mut p = 0; while p + 46 <= cd.len() && &cd[p..p + 4] == b"PK\x01\x02" { let n = u16::from_le_bytes([cd[p + 28], cd[p + 29]]) as usize; let m = u16::from_le_bytes([cd[p + 30], cd[p + 31]]) as usize; let k = u16::from_le_bytes([cd[p + 32], cd[p + 33]]) as usize; if p + 46 + n > cd.len() { break; } out.push(String::from_utf8_lossy(&cd[p + 46..p + 46 + n]).into_owned()); p += 46 + n + m + k; if out.len() > 20_000 { break; } } out } fn classify_zip(src: &dyn Source, start: u64, names: &[String]) -> &'static str { let has = |pre: &str| names.iter().any(|n| n.starts_with(pre)); if has("word/") { "docx" } else if has("xl/") { "xlsx" } else if has("ppt/") { "pptx" } else if names.first().map(|n| n == "mimetype").unwrap_or(false) { // An uncompressed `mimetype` entry at the start identifies ODF/EPUB containers. let head = read_vec(src, start, 256).unwrap_or_default(); let s = String::from_utf8_lossy(&head); if s.contains("opendocument.text") { "odt" } else if s.contains("opendocument.spreadsheet") { "ods" } else if s.contains("opendocument.presentation") { "odp" } else if s.contains("epub+zip") { "epub" } else { "zip" } } else if names.iter().any(|n| n == "AndroidManifest.xml") { "apk" } else if has("META-INF/") { "jar" } else { "zip" } } // ---------------------------------------------------------------- MP4 / MOV / HEIC (ISO BMFF) fn isobmff(src: &dyn Source, off: u64, max: u64) -> Option { let head = read_vec(src, off, 32).ok()?; let brand = head.get(8..12)?.to_vec(); let mut p = 0u64; let mut saw_moov = false; let mut saw_mdat = false; let total = src.size().saturating_sub(off); for _ in 0..100_000 { let b = read_vec(src, off + p, 16).ok()?; if b.len() < 8 { break; } let mut size = u32::from_be_bytes([b[0], b[1], b[2], b[3]]) as u64; let ty = [b[4], b[5], b[6], b[7]]; let known = matches!( &ty, b"ftyp" | b"moov" | b"mdat" | b"free" | b"skip" | b"wide" | b"uuid" | b"meta" | b"moof" | b"mfra" | b"sidx" | b"pnot" | b"styp" | b"pdin" | b"junk" ); if !known { break; } if size == 1 { if b.len() < 16 { break; } size = u64::from_be_bytes(b[8..16].try_into().unwrap()); } else if size == 0 { // Box extends to EOF: exact size unknown. size = total - p; } if size < 8 || p + size > max { break; } match &ty { b"moov" | b"moof" => saw_moov = true, b"mdat" => saw_mdat = true, _ => {} } p += size; if p >= total { break; } } if p < 16 || !(saw_moov || saw_mdat) { return None; } let ext = match &brand[..] { b"qt " => "mov", b"heic" | b"heix" | b"mif1" | b"msf1" | b"heim" | b"hevc" => "heic", b"avif" | b"avis" => "avif", b"M4A " | b"M4B " => "m4a", b"M4V " => "m4v", b"3gp4" | b"3gp5" | b"3gp6" | b"3g2a" => "3gp", _ => "mp4", }; let conf = if saw_moov && p <= total { Confidence::High } else { Confidence::Low }; Some(Hit { len: p.min(total), ext, confidence: conf, }) } // ---------------------------------------------------------------- RIFF (AVI / WAV / WEBP) fn riff(h: &[u8], max: u64) -> Option { let size = u32::from_le_bytes([h[4], h[5], h[6], h[7]]) as u64 + 8; let ext = match &h[8..12] { b"AVI " => "avi", b"WAVE" => "wav", b"WEBP" => "webp", _ => return None, }; if size < 16 || size > max { return None; } Some(Hit { len: size + (size & 1), ext, confidence: Confidence::High, }) } // ---------------------------------------------------------------- SQLite fn sqlite(h: &[u8], max: u64) -> Option { if h.len() < 100 { return None; } let ps = u16::from_be_bytes([h[16], h[17]]) as u64; let ps = if ps == 1 { 65536 } else { ps }; if !(512..=65536).contains(&ps) || !ps.is_power_of_two() { return None; } let pages = u32::from_be_bytes([h[28], h[29], h[30], h[31]]) as u64; let change = &h[24..28]; let valid_for = &h[92..96]; if pages == 0 { return None; } let len = pages * ps; if len > max { return None; } let conf = if change == valid_for { Confidence::High } else { Confidence::Medium }; Some(Hit { len, ext: "sqlite", confidence: conf, }) } // ---------------------------------------------------------------- 7z fn sevenz(h: &[u8], max: u64) -> Option { if h.len() < 32 { return None; } let crc = u32::from_le_bytes([h[8], h[9], h[10], h[11]]); if crc32(&h[12..32]) != crc { return None; } let next_off = u64::from_le_bytes(h[12..20].try_into().unwrap()); let next_len = u64::from_le_bytes(h[20..28].try_into().unwrap()); let len = 32u64.checked_add(next_off)?.checked_add(next_len)?; if len > max { return None; } Some(Hit { len, ext: "7z", confidence: Confidence::High, }) } pub fn crc32(data: &[u8]) -> u32 { let mut crc = 0xFFFF_FFFFu32; for &b in data { crc ^= b as u32; for _ in 0..8 { crc = if crc & 1 != 0 { (crc >> 1) ^ 0xEDB8_8320 } else { crc >> 1 }; } } !crc } // ---------------------------------------------------------------- OLE2 (legacy doc/xls/ppt) fn ole(src: &dyn Source, off: u64, max: u64) -> Option { let hdr = read_vec(src, off, 512).ok()?; if hdr.len() < 512 { return None; } let shift = u16::from_le_bytes([hdr[30], hdr[31]]) as u32; if !(shift == 9 || shift == 12) { return None; } let ssz = 1u64 << shift; let n_fat = u32::from_le_bytes(hdr[44..48].try_into().unwrap()) as usize; if n_fat == 0 || n_fat > 109 { return None; } let mut last_used = 0u64; for i in 0..n_fat { let sec = u32::from_le_bytes(hdr[76 + i * 4..80 + i * 4].try_into().unwrap()) as u64; let fat = read_vec(src, off + (sec + 1) * ssz, ssz as usize).ok()?; for (j, e) in fat.chunks_exact(4).enumerate() { let v = u32::from_le_bytes(e.try_into().unwrap()); if v != 0xFFFF_FFFF { last_used = last_used.max(i as u64 * (ssz / 4) + j as u64); } } } let len = (last_used + 2) * ssz; if len > max { return None; } let data = read_vec(src, off, len.min(8 << 20) as usize).ok()?; let u16s = |s: &str| -> Vec { s.encode_utf16().flat_map(|c| c.to_le_bytes()).collect() }; let ext = if find(&data, &u16s("WordDocument")).is_some() { "doc" } else if find(&data, &u16s("Workbook")).is_some() || find(&data, &u16s("Book")).is_some() { "xls" } else if find(&data, &u16s("PowerPoint Document")).is_some() { "ppt" } else { "ole" }; Some(Hit { len, ext, confidence: Confidence::High, }) } // ---------------------------------------------------------------- MP3 fn mp3(src: &dyn Source, off: u64, max: u64) -> Option { let h = read_vec(src, off, 10).ok()?; if h.len() < 10 || h[3] == 0xFF || h[4] == 0xFF { return None; } let tag = ((h[6] as u64 & 0x7F) << 21) | ((h[7] as u64 & 0x7F) << 14) | ((h[8] as u64 & 0x7F) << 7) | (h[9] as u64 & 0x7F); let mut p = 10 + tag; let mut frames = 0u32; loop { let f = read_vec(src, off + p, 4).ok()?; if f.len() < 4 { break; } let Some(len) = mpeg_frame_len(&f) else { break }; p += len; frames += 1; if p > max { break; } } if frames < 20 { return None; } // Include optional trailing 128-byte ID3v1 tag. if let Ok(t) = read_vec(src, off + p, 3) { if t == b"TAG" { p += 128; } } Some(Hit { len: p, ext: "mp3", confidence: Confidence::Medium, }) } fn mpeg_frame_len(h: &[u8]) -> Option { if h[0] != 0xFF || h[1] & 0xE0 != 0xE0 { return None; } let ver = (h[1] >> 3) & 3; // 3 = MPEG1, 2 = MPEG2, 0 = 2.5 let layer = (h[1] >> 1) & 3; // 1 = layer III if ver == 1 || layer != 1 { return None; } let br_idx = (h[2] >> 4) as usize; let sr_idx = ((h[2] >> 2) & 3) as usize; if br_idx == 0 || br_idx == 15 || sr_idx == 3 { return None; } const BR1: [u32; 16] = [ 0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 0, ]; const BR2: [u32; 16] = [ 0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, 0, ]; const SR1: [u32; 3] = [44100, 48000, 32000]; let (br, sr, coef) = if ver == 3 { (BR1[br_idx], SR1[sr_idx], 144) } else { (BR2[br_idx], SR1[sr_idx] / if ver == 2 { 2 } else { 4 }, 72) }; let pad = ((h[2] >> 1) & 1) as u32; Some((coef * br * 1000 / sr + pad) as u64) } // ---------------------------------------------------------------- PE (exe/dll) fn pe(src: &dyn Source, off: u64, max: u64) -> Option { let h = read_vec(src, off, 4096).ok()?; if h.len() < 0x40 { return None; } let lfanew = u32::from_le_bytes(h[0x3C..0x40].try_into().unwrap()) as usize; if lfanew + 24 > h.len() || &h[lfanew..lfanew + 4] != b"PE\0\0" { return None; } let nsec = u16::from_le_bytes([h[lfanew + 6], h[lfanew + 7]]) as usize; let opt = u16::from_le_bytes([h[lfanew + 20], h[lfanew + 21]]) as usize; let mut s = lfanew + 24 + opt; let mut end = 0u64; for _ in 0..nsec.min(96) { if s + 40 > h.len() { return None; } let raw_size = u32::from_le_bytes(h[s + 16..s + 20].try_into().unwrap()) as u64; let raw_ptr = u32::from_le_bytes(h[s + 20..s + 24].try_into().unwrap()) as u64; end = end.max(raw_ptr + raw_size); s += 40; } if end < 512 || end > max { return None; } let ext = if u16::from_le_bytes([h[lfanew + 22], h[lfanew + 23]]) & 0x2000 != 0 { "dll" } else { "exe" }; Some(Hit { len: end, ext, confidence: Confidence::Medium, }) }