diff --git a/src/common.rs b/src/common.rs index 868b17406..1c2a39c26 100644 --- a/src/common.rs +++ b/src/common.rs @@ -93,6 +93,53 @@ pub fn crc32(data: &[u8]) -> u32 { crc32_v2::crc32(0, data) } +/// CRC-32 variant used by the Fritz!Box EVA kernel image file signature. +/// +/// Differs from the standard [`crc32`] in three ways: bits are processed +/// MSB-first (polynomial 0x04C11DB7, not the reflected form), the CRC starts +/// at 0 instead of 0xFFFFFFFF, and the payload length is folded into the CRC +/// byte-by-byte before the final one's-complement. +/// +/// ## Example +/// +/// ``` +/// use binwalk_ng::common::eva_file_signature_crc32; +/// +/// assert_eq!(eva_file_signature_crc32(b""), 0xFFFFFFFF); +/// ``` +pub fn eva_file_signature_crc32(data: &[u8]) -> u32 { + const POLY: u32 = 0x04C11DB7; + const TABLE: [u32; 256] = { + let mut table = [0u32; 256]; + let mut i = 1; + while i < 256 { + let mut crc = table[i / 2]; + let c = (crc >> 31) ^ ((i as u32) & 1); + crc <<= 1; + if c & 1 != 0 { + crc ^= POLY; + } + table[i] = crc; + i += 1; + } + table + }; + + let mut crc: u32 = 0; + for &byte in data { + crc = (crc << 8) ^ TABLE[((crc >> 24) as u8 ^ byte) as usize]; + } + + // Fold the payload length into the CRC + let mut length = data.len(); + while length > 0 { + crc = (crc << 8) ^ TABLE[((crc >> 24) as u8 ^ (length & 0xFF) as u8) as usize]; + length >>= 8; + } + + !crc +} + /// Converts an epoch time to a formatted time string. /// /// ## Example diff --git a/src/extractors.rs b/src/extractors.rs index 04a121260..1d24e1e87 100644 --- a/src/extractors.rs +++ b/src/extractors.rs @@ -153,6 +153,7 @@ pub mod dtb; pub mod dumpifs; pub mod dxbc; pub mod encfw; +pub mod eva; pub mod gif; pub mod gpg; pub mod gzip; diff --git a/src/extractors/eva.rs b/src/extractors/eva.rs new file mode 100644 index 000000000..e6951d4e2 --- /dev/null +++ b/src/extractors/eva.rs @@ -0,0 +1,96 @@ +use crate::extractors::common::{Chroot, ExtractionResult, Extractor, ExtractorType}; +use crate::structures::eva::{ + EVA_LZMA_HEADER_SIZE, EVA_LZMA_STREAM_HEADER, EvaTiRecord, EvaTiRecordKind, TI_HEADER_SIZE, + parse_eva_image, +}; + +/// Defines the internal extractor for Fritz!Box EVA kernel images +pub fn eva_extractor() -> Extractor { + Extractor { + utility: ExtractorType::Internal(extract_eva), + ..Default::default() + } +} + +/// Internal EVA kernel image extractor +pub fn extract_eva( + file_data: &[u8], + offset: usize, + output_directory: Option<&str>, +) -> ExtractionResult { + let mut result = ExtractionResult::default(); + + let Some(data) = file_data.get(offset..) else { + return result; + }; + let Ok(image) = parse_eva_image(data) else { + return result; + }; + + // Refuse to carve garbage even if the signature parser let a weak match through + if !image.primary.checksum_valid || !image.primary.lzma.data_checksum_valid { + return result; + } + if let Some(ref secondary) = image.secondary + && (!secondary.checksum_valid || !secondary.lzma.data_checksum_valid) + { + return result; + } + + let Some(primary_lzma) = reconstruct_lzma_alone(data, &image.primary) else { + return result; + }; + let secondary_lzma = match image.secondary.as_ref() { + Some(secondary) => match reconstruct_lzma_alone(data, secondary) { + Some(bytes) => Some(bytes), + None => return result, + }, + None => None, + }; + + let primary_file_name = match image.primary.kind { + EvaTiRecordKind::Primary => "kernel.lzma", + EvaTiRecordKind::Secondary => "kernel_2nd.lzma", + }; + + if output_directory.is_some() { + let chroot = Chroot::new(output_directory); + if !chroot.create_file(primary_file_name, &primary_lzma) { + return result; + } + if let Some(ref bytes) = secondary_lzma + && !chroot.create_file("kernel_2nd.lzma", bytes) + { + return result; + } + } + + result.success = true; + result.size = Some(image.total_size); + result +} + +/// Reconstruct a standard LZMA alone stream from an EVA TI record +/// +/// Standard LZMA alone format: +/// ```text +/// [properties: 1 byte] +/// [dict_size: 4 bytes, little-endian] +/// [uncompressed_size: 8 bytes, little-endian] (upper 4 bytes zero-padded) +/// [compressed_data: compressed_len bytes] +/// ``` +/// +/// The 3 "unknown" padding bytes from the EVA stream header are dropped +fn reconstruct_lzma_alone(image_data: &[u8], record: &EvaTiRecord) -> Option> { + let compressed_data_offset = + record.header_offset + TI_HEADER_SIZE + EVA_LZMA_HEADER_SIZE + EVA_LZMA_STREAM_HEADER; + let compressed_data_end = compressed_data_offset.checked_add(record.lzma.compressed_len)?; + let compressed_data = image_data.get(compressed_data_offset..compressed_data_end)?; + + let mut out = Vec::with_capacity(13 + compressed_data.len()); + out.push(record.lzma.properties); + out.extend_from_slice(&record.lzma.dict_size.to_le_bytes()); + out.extend_from_slice(&(record.lzma.uncompressed_len as u64).to_le_bytes()); + out.extend_from_slice(compressed_data); + Some(out) +} diff --git a/src/magic.rs b/src/magic.rs index 6f129ccef..3cc0ef6bc 100644 --- a/src/magic.rs +++ b/src/magic.rs @@ -93,6 +93,17 @@ pub fn patterns() -> Vec { //extractor: Some(extractors::sevenzip::sevenzip_extractor()), extractor: Some(extractors::lzma::lzma_extractor()), }, + // Fritz!Box EVA kernel image (TI record format, LZMA-compressed) + signatures::common::Signature { + name: "eva".to_string(), + short: false, + magic_offset: 0, + always_display: false, + magic: signatures::eva::eva_magic(), + parser: signatures::eva::eva_parser, + description: signatures::eva::DESCRIPTION.to_string(), + extractor: Some(extractors::eva::eva_extractor()), + }, // bmp signatures::common::Signature { name: "bmp".to_string(), diff --git a/src/signatures.rs b/src/signatures.rs index 68c1ac50b..967c628df 100644 --- a/src/signatures.rs +++ b/src/signatures.rs @@ -141,6 +141,7 @@ pub mod efigpt; pub mod elf; pub mod encfw; pub mod encrpted_img; +pub mod eva; pub mod ext; pub mod fat; pub mod gif; diff --git a/src/signatures/eva.rs b/src/signatures/eva.rs new file mode 100644 index 000000000..1acdcb276 --- /dev/null +++ b/src/signatures/eva.rs @@ -0,0 +1,128 @@ +use crate::signatures::common::{ + CONFIDENCE_HIGH, CONFIDENCE_LOW, CONFIDENCE_MEDIUM, SignatureError, SignatureResult, +}; +use crate::structures::eva::{EvaTiRecordKind, parse_eva_image}; + +/// Human readable description +pub const DESCRIPTION: &str = "Fritz!Box EVA kernel image"; + +/// EVA magic bytes (dual-kernel, primary TI record, secondary TI record) +/// +/// The secondary magic is registered to detect isolated secondary-only +/// fragments. Inside a dual-kernel image the scanner will also hit on the +/// embedded secondary magic; that duplicate lands inside the already-validated +/// dual-kernel region and is filtered out by the conflict-resolution pass in +/// `Binwalk::scan`. +pub fn eva_magic() -> Vec> { + vec![ + // DUAL_KERNEL (little-endian 0xFEED9112) + b"\x12\x91\xed\xfe".to_vec(), + // TI_AR7 primary (little-endian 0xFEED1281) + b"\x81\x12\xed\xfe".to_vec(), + // TI_AR7_2ND secondary (little-endian 0xFEEDB007) + b"\x07\xb0\xed\xfe".to_vec(), + ] +} + +/// Validates EVA kernel image signatures +pub fn eva_parser(file_data: &[u8], offset: usize) -> Result { + let data = file_data.get(offset..).ok_or(SignatureError)?; + let image = parse_eva_image(data).map_err(|_| SignatureError)?; + + let secondary_checksums_ok = image + .secondary + .as_ref() + .is_none_or(|s| s.checksum_valid && s.lzma.data_checksum_valid); + let file_signature_ok = image.file_signature.is_none_or(|sig| sig.valid); + + let all_checksums_valid = image.primary.checksum_valid + && image.primary.lzma.data_checksum_valid + && secondary_checksums_ok + && (!image.is_dual_kernel || image.dual_checksum_valid) + && file_signature_ok; + + // A bad checksum at offset 0 is usually a real-but-modified image; deeper in a file it's more likely a false 4-byte match + let confidence = if all_checksums_valid { + CONFIDENCE_HIGH + } else if offset == 0 { + CONFIDENCE_MEDIUM + } else { + CONFIDENCE_LOW + }; + + let kernel_phrase = if image.is_dual_kernel { + "dual-kernel" + } else if image.primary.kind == EvaTiRecordKind::Secondary { + "secondary-kernel fragment" + } else { + "single-kernel" + }; + let primary_label = match image.primary.kind { + EvaTiRecordKind::Primary => "primary", + EvaTiRecordKind::Secondary => "secondary", + }; + let primary_desc = format!( + "{} load: {:#X}, entry: {:#X}, compressed: {} bytes, uncompressed: {} bytes", + primary_label, + image.primary.load_addr, + image.primary.entry_addr, + image.primary.lzma.compressed_len, + image.primary.lzma.uncompressed_len, + ); + let secondary_desc = image + .secondary + .as_ref() + .map(|s| { + format!( + ", secondary load: {:#X}, entry: {:#X}, compressed: {} bytes, uncompressed: {} bytes", + s.load_addr, s.entry_addr, s.lzma.compressed_len, s.lzma.uncompressed_len, + ) + }) + .unwrap_or_default(); + let signature_desc = image + .file_signature + .map(|sig| format!(", file signature CRC: {:#010X}", sig.crc)) + .unwrap_or_default(); + let validation_desc = if all_checksums_valid { + String::new() + } else { + let mut failures: Vec<&str> = Vec::new(); + if !image.primary.checksum_valid { + failures.push("primary TI"); + } + if !image.primary.lzma.data_checksum_valid { + failures.push("primary LZMA data"); + } + if let Some(ref s) = image.secondary { + if !s.checksum_valid { + failures.push("secondary TI"); + } + if !s.lzma.data_checksum_valid { + failures.push("secondary LZMA data"); + } + } + if image.is_dual_kernel && !image.dual_checksum_valid { + failures.push("dual trailer"); + } + if image.file_signature.is_some_and(|sig| !sig.valid) { + failures.push("file signature"); + } + format!(", checksum validation failed ({})", failures.join(", ")) + }; + + Ok(SignatureResult { + offset, + size: image.total_size, + confidence, + description: format!( + "{}, {}, {}{}{}{}", + DESCRIPTION, + kernel_phrase, + primary_desc, + secondary_desc, + signature_desc, + validation_desc, + ), + ..Default::default() + }) +} diff --git a/src/structures.rs b/src/structures.rs index 1e66f5bb9..d55329722 100644 --- a/src/structures.rs +++ b/src/structures.rs @@ -121,6 +121,7 @@ pub mod dtb; pub mod dxbc; pub mod efigpt; pub mod elf; +pub mod eva; pub mod ext; pub mod fat; pub mod gif; diff --git a/src/structures/eva.rs b/src/structures/eva.rs new file mode 100644 index 000000000..38d54664d --- /dev/null +++ b/src/structures/eva.rs @@ -0,0 +1,364 @@ +use crate::common::{crc32, eva_file_signature_crc32}; +use crate::structures::common::{self, StructureError}; + +/// Outer dual-kernel container magic +pub const DUAL_KERNEL_MAGIC: u32 = 0xFEED9112; + +/// Primary TI record magic +pub const TI_AR7_MAGIC: u32 = 0xFEED1281; + +/// Secondary TI record magic +pub const TI_AR7_2ND_MAGIC: u32 = 0xFEEDB007; + +/// EVA LZMA payload type marker +pub const EVA_LZMA_TYPE_MAGIC: u32 = 0x075A0201; + +/// Trailing file signature magic +pub const FILE_SIGNATURE_MAGIC: u32 = 0xC453DE23; + +/// Size of a TI record header (magic + payload_length + load_addr) +pub const TI_HEADER_SIZE: usize = 12; + +/// Size of a TI record trailer (checksum + zero + entry_addr) +pub const TI_TRAILER_SIZE: usize = 12; + +/// Size of the EVA LZMA header (type + compressed_len + uncompressed_len + data_checksum) +pub const EVA_LZMA_HEADER_SIZE: usize = 16; + +/// Size of the EVA LZMA stream header (properties + dict_size + 3 padding bytes) +pub const EVA_LZMA_STREAM_HEADER: usize = 8; + +/// Size of the trailing file signature (magic + crc) +pub const FILE_SIGNATURE_SIZE: usize = 8; + +// Max bytes to scan for a trailing file signature after the last TI trailer +const FILE_SIGNATURE_SCAN_WINDOW: usize = 4096; + +/// Parsed EVA LZMA payload (16-byte header + 8-byte stream header, followed by compressed data) +#[derive(Debug, Default, Clone)] +pub struct EvaLzmaPayload { + pub compressed_len: usize, + pub uncompressed_len: usize, + pub data_checksum_valid: bool, + pub properties: u8, + pub dict_size: u32, +} + +/// Which TI record magic matched this record +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +pub enum EvaTiRecordKind { + #[default] + Primary, + Secondary, +} + +/// Parsed TI record (header + EVA LZMA payload + trailer) +#[derive(Debug, Default, Clone)] +pub struct EvaTiRecord { + pub kind: EvaTiRecordKind, + pub load_addr: u32, + pub checksum_valid: bool, + pub entry_addr: u32, + pub lzma: EvaLzmaPayload, + pub header_offset: usize, + pub total_size: usize, +} + +/// Located trailing EVA file signature +#[derive(Debug, Default, Clone, Copy)] +pub struct EvaFileSignature { + pub crc: u32, + pub valid: bool, + /// Absolute end offset of the signature (i.e. where the EVA image ends) + pub end_offset: usize, +} + +/// Parsed EVA kernel image +#[derive(Debug, Default, Clone)] +pub struct EvaImage { + pub is_dual_kernel: bool, + pub primary: EvaTiRecord, + pub secondary: Option, + pub dual_checksum_valid: bool, + pub file_signature: Option, + pub total_size: usize, +} + +/// Parse a Fritz!Box EVA kernel image +pub fn parse_eva_image(data: &[u8]) -> Result { + if data.len() < TI_HEADER_SIZE { + return Err(StructureError); + } + + let header = common::parse(data, &vec![("magic", "u32")], "little")?; + match header["magic"] as u32 { + DUAL_KERNEL_MAGIC => parse_dual_kernel_image(data), + TI_AR7_MAGIC => parse_single_kernel_image(data, TI_AR7_MAGIC), + TI_AR7_2ND_MAGIC => parse_single_kernel_image(data, TI_AR7_2ND_MAGIC), + _ => Err(StructureError), + } +} + +/// Parse a dual-kernel EVA image +fn parse_dual_kernel_image(data: &[u8]) -> Result { + let dual_header_structure = vec![ + ("magic", "u32"), + ("payload_length", "u32"), + ("load_addr", "u32"), + ]; + + let dual_header = common::parse(data, &dual_header_structure, "little")?; + let dual_payload_length = dual_header["payload_length"]; + let dual_payload_length_u32 = u32::try_from(dual_payload_length).map_err(|_| StructureError)?; + let dual_load_addr = dual_header["load_addr"] as u32; + + let primary = parse_ti_record(data, TI_HEADER_SIZE, TI_AR7_MAGIC)?; + let after_primary = primary.header_offset + primary.total_size; + + // Secondary TI record starts at the next 4-byte aligned offset + let aligned_secondary_offset = align_up(after_primary, 4); + let secondary = parse_ti_record(data, aligned_secondary_offset, TI_AR7_2ND_MAGIC)?; + let after_secondary = secondary.header_offset + secondary.total_size; + + let dual_trailer_offset = TI_HEADER_SIZE + .checked_add(dual_payload_length) + .ok_or(StructureError)?; + if dual_trailer_offset < after_secondary { + return Err(StructureError); + } + let dual_trailer_end = dual_trailer_offset + .checked_add(TI_TRAILER_SIZE) + .ok_or(StructureError)?; + if data.len() < dual_trailer_end { + return Err(StructureError); + } + + let dual_trailer_data = data + .get(dual_trailer_offset..dual_trailer_end) + .ok_or(StructureError)?; + let dual_trailer_structure = vec![("checksum", "u32"), ("zero", "u32"), ("entry_addr", "u32")]; + let dual_trailer = common::parse(dual_trailer_data, &dual_trailer_structure, "little")?; + if dual_trailer["zero"] != 0 { + return Err(StructureError); + } + + // Dual checksum covers every byte between the dual header and the dual trailer + let dual_payload = data + .get(TI_HEADER_SIZE..dual_trailer_offset) + .ok_or(StructureError)?; + let expected_dual_checksum = + calculate_ti_checksum(dual_payload_length_u32, dual_load_addr, dual_payload); + let dual_checksum_valid = dual_trailer["checksum"] as u32 == expected_dual_checksum; + + let file_signature = detect_file_signature(data, dual_trailer_end); + let total_size = file_signature + .map(|sig| sig.end_offset) + .unwrap_or(dual_trailer_end); + + Ok(EvaImage { + is_dual_kernel: true, + primary, + secondary: Some(secondary), + dual_checksum_valid, + file_signature, + total_size, + }) +} + +/// Parse a single-kernel EVA image, or an isolated secondary TI record fragment +fn parse_single_kernel_image(data: &[u8], expected_magic: u32) -> Result { + let primary = parse_ti_record(data, 0, expected_magic)?; + let content_end = primary.total_size; + + // Secondary-only fragments never carry a trailing file signature + let file_signature = if primary.kind == EvaTiRecordKind::Primary { + detect_file_signature(data, content_end) + } else { + None + }; + let total_size = file_signature + .map(|sig| sig.end_offset) + .unwrap_or(content_end); + + Ok(EvaImage { + is_dual_kernel: false, + primary, + secondary: None, + file_signature, + total_size, + ..Default::default() + }) +} + +/// Parse a TI record at the given offset +fn parse_ti_record( + data: &[u8], + offset: usize, + expected_magic: u32, +) -> Result { + let header_end = offset.checked_add(TI_HEADER_SIZE).ok_or(StructureError)?; + let header_data = data.get(offset..header_end).ok_or(StructureError)?; + + let header_structure = vec![ + ("magic", "u32"), + ("payload_length", "u32"), + ("load_addr", "u32"), + ]; + let header = common::parse(header_data, &header_structure, "little")?; + if header["magic"] as u32 != expected_magic { + return Err(StructureError); + } + let payload_length = header["payload_length"]; + let payload_length_u32 = u32::try_from(payload_length).map_err(|_| StructureError)?; + let load_addr = header["load_addr"] as u32; + + let payload_start = header_end; + let payload_end = payload_start + .checked_add(payload_length) + .ok_or(StructureError)?; + let trailer_end = payload_end + .checked_add(TI_TRAILER_SIZE) + .ok_or(StructureError)?; + if data.len() < trailer_end { + return Err(StructureError); + } + + let payload = data.get(payload_start..payload_end).ok_or(StructureError)?; + let lzma = parse_eva_lzma_payload(payload)?; + + let trailer_data = data.get(payload_end..trailer_end).ok_or(StructureError)?; + let trailer_structure = vec![("checksum", "u32"), ("zero", "u32"), ("entry_addr", "u32")]; + let trailer = common::parse(trailer_data, &trailer_structure, "little")?; + if trailer["zero"] != 0 { + return Err(StructureError); + } + let entry_addr = trailer["entry_addr"] as u32; + + let expected_checksum = calculate_ti_checksum(payload_length_u32, load_addr, payload); + let checksum_valid = trailer["checksum"] as u32 == expected_checksum; + + let kind = match expected_magic { + TI_AR7_MAGIC => EvaTiRecordKind::Primary, + TI_AR7_2ND_MAGIC => EvaTiRecordKind::Secondary, + _ => return Err(StructureError), + }; + + Ok(EvaTiRecord { + kind, + load_addr, + checksum_valid, + entry_addr, + lzma, + header_offset: offset, + total_size: trailer_end - offset, + }) +} + +/// Parse an EVA LZMA payload (16-byte header + 8-byte stream header + compressed data) +fn parse_eva_lzma_payload(payload: &[u8]) -> Result { + if payload.len() < EVA_LZMA_HEADER_SIZE + EVA_LZMA_STREAM_HEADER { + return Err(StructureError); + } + + let lzma_header_structure = vec![ + ("type", "u32"), + ("compressed_len", "u32"), + ("uncompressed_len", "u32"), + ("data_checksum", "u32"), + ]; + let lzma_header = common::parse(payload, &lzma_header_structure, "little")?; + if lzma_header["type"] as u32 != EVA_LZMA_TYPE_MAGIC { + return Err(StructureError); + } + let compressed_len = lzma_header["compressed_len"]; + let uncompressed_len = lzma_header["uncompressed_len"]; + let stored_data_checksum = lzma_header["data_checksum"] as u32; + + // Stream header: properties(1) + dict_size(4) + unknown(3) + let stream_header = payload + .get(EVA_LZMA_HEADER_SIZE..EVA_LZMA_HEADER_SIZE + EVA_LZMA_STREAM_HEADER) + .ok_or(StructureError)?; + let properties = stream_header[0]; + let dict_size = u32::from_le_bytes([ + stream_header[1], + stream_header[2], + stream_header[3], + stream_header[4], + ]); + + // Compressed data lives immediately after the stream header + let data_start = EVA_LZMA_HEADER_SIZE + EVA_LZMA_STREAM_HEADER; + let data_end = data_start + .checked_add(compressed_len) + .ok_or(StructureError)?; + if payload.len() < data_end { + return Err(StructureError); + } + let compressed_data = payload.get(data_start..data_end).ok_or(StructureError)?; + + let data_checksum_valid = crc32(compressed_data) == stored_data_checksum; + + Ok(EvaLzmaPayload { + compressed_len, + uncompressed_len, + data_checksum_valid, + properties, + dict_size, + }) +} + +/// Two's complement additive checksum: `(payload_length + load_addr + sum_of_payload_bytes + checksum) ≡ 0 (mod 2^32)` +fn calculate_ti_checksum(payload_length: u32, load_addr: u32, payload: &[u8]) -> u32 { + let byte_sum: u32 = payload + .iter() + .fold(0u32, |acc, &b| acc.wrapping_add(u32::from(b))); + let total = payload_length + .wrapping_add(load_addr) + .wrapping_add(byte_sum); + (!total).wrapping_add(1) +} + +/// Round `n` up to the next multiple of `alignment` +fn align_up(n: usize, alignment: usize) -> usize { + (n + alignment - 1) & !(alignment - 1) +} + +/// Locate and validate a trailing EVA file signature within a bounded window +/// after `content_end`. The window is capped so an embedded EVA match inside a +/// larger firmware cannot accidentally over-extend its reported size by +/// matching random trailing bytes. +fn detect_file_signature(data: &[u8], content_end: usize) -> Option { + let try_offset = |candidate: usize| -> Option { + let sig_end = candidate.checked_add(FILE_SIGNATURE_SIZE)?; + let bytes: &[u8; FILE_SIGNATURE_SIZE] = data.get(candidate..sig_end)?.try_into().ok()?; + let magic = u32::from_le_bytes(bytes[0..4].try_into().unwrap()); + if magic != FILE_SIGNATURE_MAGIC { + return None; + } + let crc = u32::from_le_bytes(bytes[4..8].try_into().unwrap()); + // CRC input is everything before the 8-byte signature + let crc_input = data.get(0..candidate)?; + let expected_crc = eva_file_signature_crc32(crc_input); + Some(EvaFileSignature { + crc, + valid: crc == expected_crc, + end_offset: sig_end, + }) + }; + + // Forward scan at 4-byte aligned offsets within the bounded window. + let scan_start = align_up(content_end, 4); + let scan_end = std::cmp::min( + data.len(), + content_end.saturating_add(FILE_SIGNATURE_SCAN_WINDOW), + ); + let mut off = scan_start; + while off + FILE_SIGNATURE_SIZE <= scan_end { + if let Some(result) = try_offset(off) { + return Some(result); + } + off += 4; + } + + None +} diff --git a/tests/eva_dual_kernel.rs b/tests/eva_dual_kernel.rs new file mode 100644 index 000000000..530d73cdc --- /dev/null +++ b/tests/eva_dual_kernel.rs @@ -0,0 +1,8 @@ +mod common; + +#[test] +fn integration_test_dual_kernel_eva() { + const SIGNATURE_TYPE: &str = "eva"; + const INPUT_FILE_NAME: &str = "eva_dual_kernel.bin"; + common::integration_test(SIGNATURE_TYPE, INPUT_FILE_NAME); +} diff --git a/tests/eva_secondary_only.rs b/tests/eva_secondary_only.rs new file mode 100644 index 000000000..f2e3c4c0b --- /dev/null +++ b/tests/eva_secondary_only.rs @@ -0,0 +1,8 @@ +mod common; + +#[test] +fn integration_test_secondary_only_eva() { + const SIGNATURE_TYPE: &str = "eva"; + const INPUT_FILE_NAME: &str = "eva_secondary_only.bin"; + common::integration_test(SIGNATURE_TYPE, INPUT_FILE_NAME); +} diff --git a/tests/eva_single_kernel.rs b/tests/eva_single_kernel.rs new file mode 100644 index 000000000..8436799af --- /dev/null +++ b/tests/eva_single_kernel.rs @@ -0,0 +1,8 @@ +mod common; + +#[test] +fn integration_test_single_kernel_eva() { + const SIGNATURE_TYPE: &str = "eva"; + const INPUT_FILE_NAME: &str = "eva_single_kernel.bin"; + common::integration_test(SIGNATURE_TYPE, INPUT_FILE_NAME); +} diff --git a/tests/inputs/eva_dual_kernel.bin b/tests/inputs/eva_dual_kernel.bin new file mode 100644 index 000000000..081fcdd88 Binary files /dev/null and b/tests/inputs/eva_dual_kernel.bin differ diff --git a/tests/inputs/eva_secondary_only.bin b/tests/inputs/eva_secondary_only.bin new file mode 100644 index 000000000..86265a199 Binary files /dev/null and b/tests/inputs/eva_secondary_only.bin differ diff --git a/tests/inputs/eva_single_kernel.bin b/tests/inputs/eva_single_kernel.bin new file mode 100644 index 000000000..475deeb5a Binary files /dev/null and b/tests/inputs/eva_single_kernel.bin differ