551 lines
22 KiB
Rust
551 lines
22 KiB
Rust
use std::fs;
|
|
use std::path::Path;
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
const WIN_COMMON_HEADER_LEN: usize = 0x50;
|
|
const WIN_INLINE_RESOURCE_OFFSET: usize = 0x50;
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct WinHeaderWord {
|
|
pub offset: usize,
|
|
pub offset_hex: String,
|
|
pub value: u32,
|
|
pub value_hex: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct WinResourceReference {
|
|
pub offset: usize,
|
|
pub offset_hex: String,
|
|
pub name: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct WinReferenceDeltaFrequency {
|
|
pub delta: usize,
|
|
pub delta_hex: String,
|
|
pub count: usize,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct WinResourceRecordSample {
|
|
pub offset: usize,
|
|
pub offset_hex: String,
|
|
pub name: String,
|
|
pub delta_from_previous: Option<usize>,
|
|
pub delta_from_previous_hex: Option<String>,
|
|
pub prelude_words: Vec<WinHeaderWord>,
|
|
pub post_name_word_0: u32,
|
|
pub post_name_word_0_hex: String,
|
|
pub post_name_word_0_high_u16: u16,
|
|
pub post_name_word_0_high_u16_hex: String,
|
|
pub post_name_word_0_low_u16: u16,
|
|
pub post_name_word_0_low_u16_hex: String,
|
|
pub post_name_word_1: u32,
|
|
pub post_name_word_1_hex: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct WinResourceSelectorRecord {
|
|
pub offset: usize,
|
|
pub offset_hex: String,
|
|
pub name: String,
|
|
pub post_name_word_0: u32,
|
|
pub post_name_word_0_hex: String,
|
|
pub selector_high_u16: u16,
|
|
pub selector_high_u16_hex: String,
|
|
pub selector_low_u16: u16,
|
|
pub selector_low_u16_hex: String,
|
|
pub post_name_word_1: u32,
|
|
pub post_name_word_1_hex: String,
|
|
pub post_name_word_1_high_u16: u16,
|
|
pub post_name_word_1_high_u16_hex: String,
|
|
pub post_name_word_1_middle_u16: u16,
|
|
pub post_name_word_1_middle_u16_hex: String,
|
|
pub post_name_word_1_low_u16: u16,
|
|
pub post_name_word_1_low_u16_hex: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct WinAnonymousSelectorRecord {
|
|
pub record_offset: usize,
|
|
pub record_offset_hex: String,
|
|
pub preceding_named_record_name: Option<String>,
|
|
pub preceding_named_record_offset_hex: Option<String>,
|
|
pub following_named_record_name: Option<String>,
|
|
pub following_named_record_offset_hex: Option<String>,
|
|
pub selector_word_0: u32,
|
|
pub selector_word_0_hex: String,
|
|
pub selector_word_0_high_u16: u16,
|
|
pub selector_word_0_high_u16_hex: String,
|
|
pub selector_word_0_low_u16: u16,
|
|
pub selector_word_0_low_u16_hex: String,
|
|
pub selector_word_1: u32,
|
|
pub selector_word_1_hex: String,
|
|
pub selector_word_1_middle_u16: u16,
|
|
pub selector_word_1_middle_u16_hex: String,
|
|
pub body_word_0: u32,
|
|
pub body_word_0_hex: String,
|
|
pub body_word_1: u32,
|
|
pub body_word_1_hex: String,
|
|
pub body_word_2: u32,
|
|
pub body_word_2_hex: String,
|
|
pub body_word_3: u32,
|
|
pub body_word_3_hex: String,
|
|
pub footer_word_0: u32,
|
|
pub footer_word_0_hex: String,
|
|
pub footer_word_1: u32,
|
|
pub footer_word_1_hex: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct WinInspectionReport {
|
|
pub file_size: usize,
|
|
pub common_header_len: usize,
|
|
pub common_header_len_hex: String,
|
|
pub shared_header_words: Vec<WinHeaderWord>,
|
|
pub matches_observed_common_signature: bool,
|
|
pub common_resource_record_prelude_prefix_words: Option<Vec<String>>,
|
|
pub name_len_matches_prelude_word_3_plus_nul_count: usize,
|
|
pub inline_root_resource_name: Option<String>,
|
|
pub inline_root_resource_offset: Option<usize>,
|
|
pub inline_root_resource_offset_hex: Option<String>,
|
|
pub imb_reference_count: usize,
|
|
pub unique_imb_reference_count: usize,
|
|
pub unique_imb_references: Vec<String>,
|
|
pub dominant_reference_deltas: Vec<WinReferenceDeltaFrequency>,
|
|
pub resource_selector_records: Vec<WinResourceSelectorRecord>,
|
|
pub anonymous_selector_records: Vec<WinAnonymousSelectorRecord>,
|
|
pub first_resource_record_samples: Vec<WinResourceRecordSample>,
|
|
pub first_imb_references: Vec<WinResourceReference>,
|
|
pub notes: Vec<String>,
|
|
}
|
|
|
|
pub fn inspect_win_file(path: &Path) -> Result<WinInspectionReport, Box<dyn std::error::Error>> {
|
|
let bytes = fs::read(path)?;
|
|
inspect_win_bytes(&bytes)
|
|
}
|
|
|
|
pub fn inspect_win_bytes(bytes: &[u8]) -> Result<WinInspectionReport, Box<dyn std::error::Error>> {
|
|
if bytes.len() < WIN_COMMON_HEADER_LEN {
|
|
return Err(format!(
|
|
"window resource is too short for the observed common header: {} < 0x{WIN_COMMON_HEADER_LEN:x}",
|
|
bytes.len()
|
|
)
|
|
.into());
|
|
}
|
|
|
|
let header_offsets = [
|
|
0x00usize, 0x04, 0x08, 0x0c, 0x10, 0x14, 0x18, 0x1c, 0x20, 0x24, 0x28, 0x2c, 0x30, 0x34,
|
|
0x38, 0x3c, 0x40, 0x44, 0x48, 0x4c,
|
|
];
|
|
let shared_header_words = header_offsets
|
|
.iter()
|
|
.map(|offset| {
|
|
let value = read_u32_le(bytes, *offset).expect("validated common header length");
|
|
WinHeaderWord {
|
|
offset: *offset,
|
|
offset_hex: format!("0x{offset:02x}"),
|
|
value,
|
|
value_hex: format!("0x{value:08x}"),
|
|
}
|
|
})
|
|
.collect::<Vec<_>>();
|
|
|
|
let matches_observed_common_signature = read_u32_le(bytes, 0x00) == Some(0x0000_07d0)
|
|
&& read_u32_le(bytes, 0x04) == Some(0)
|
|
&& read_u32_le(bytes, 0x08) == Some(0)
|
|
&& read_u32_le(bytes, 0x0c) == Some(0x8000_0000)
|
|
&& read_u32_le(bytes, 0x10) == Some(0x8000_003f)
|
|
&& read_u32_le(bytes, 0x14) == Some(0x0000_003f)
|
|
&& read_u32_le(bytes, 0x34) == Some(0x0007_d100)
|
|
&& read_u32_le(bytes, 0x38) == Some(0x0007_d200)
|
|
&& read_u32_le(bytes, 0x40) == Some(0x000b_b800)
|
|
&& read_u32_le(bytes, 0x48) == Some(0x000b_b900);
|
|
|
|
let inline_root_resource_name = parse_inline_ascii_name(bytes, WIN_INLINE_RESOURCE_OFFSET);
|
|
let inline_root_resource_offset = inline_root_resource_name
|
|
.as_ref()
|
|
.map(|_| WIN_INLINE_RESOURCE_OFFSET + 1);
|
|
let inline_root_resource_offset_hex =
|
|
inline_root_resource_offset.map(|offset| format!("0x{offset:04x}"));
|
|
|
|
let all_imb_references = collect_imb_references(bytes);
|
|
let resource_record_samples = build_resource_record_samples(bytes, &all_imb_references);
|
|
let resource_selector_records = build_resource_selector_records(&resource_record_samples);
|
|
let anonymous_selector_records = collect_anonymous_selector_records(bytes, &all_imb_references);
|
|
let common_resource_record_prelude_prefix_words =
|
|
shared_prelude_prefix_hex(&resource_record_samples);
|
|
let name_len_matches_prelude_word_3_plus_nul_count = resource_record_samples
|
|
.iter()
|
|
.filter(|sample| {
|
|
sample.prelude_words.len() == 4
|
|
&& sample.prelude_words[3].value == (sample.name.len() as u32 + 1)
|
|
})
|
|
.count();
|
|
let mut unique_imb_references = Vec::new();
|
|
for reference in &all_imb_references {
|
|
if !unique_imb_references.contains(&reference.name) {
|
|
unique_imb_references.push(reference.name.clone());
|
|
}
|
|
}
|
|
|
|
let mut notes = Vec::new();
|
|
if matches_observed_common_signature {
|
|
notes.push(
|
|
"Header matches the observed shared .win signature seen in Campaign.win, CompanyDetail.win, and setup.win."
|
|
.to_string(),
|
|
);
|
|
} else {
|
|
notes.push(
|
|
"Header diverges from the currently observed shared .win signature; treat field meanings as provisional."
|
|
.to_string(),
|
|
);
|
|
}
|
|
if inline_root_resource_name.is_some() {
|
|
notes.push(
|
|
"The blob carries an inline root .imb resource name immediately after the common 0x50-byte header."
|
|
.to_string(),
|
|
);
|
|
} else {
|
|
notes.push(
|
|
"No inline root .imb resource name appears at 0x50; this window likely starts directly with control records."
|
|
.to_string(),
|
|
);
|
|
}
|
|
notes.push(
|
|
"Embedded .imb strings are reported as resource references with selector lanes; this inspector still does not decode full control record semantics."
|
|
.to_string(),
|
|
);
|
|
|
|
Ok(WinInspectionReport {
|
|
file_size: bytes.len(),
|
|
common_header_len: WIN_COMMON_HEADER_LEN,
|
|
common_header_len_hex: format!("0x{WIN_COMMON_HEADER_LEN:02x}"),
|
|
shared_header_words,
|
|
matches_observed_common_signature,
|
|
common_resource_record_prelude_prefix_words,
|
|
name_len_matches_prelude_word_3_plus_nul_count,
|
|
inline_root_resource_name,
|
|
inline_root_resource_offset,
|
|
inline_root_resource_offset_hex,
|
|
imb_reference_count: all_imb_references.len(),
|
|
unique_imb_reference_count: unique_imb_references.len(),
|
|
unique_imb_references,
|
|
dominant_reference_deltas: build_delta_histogram(&resource_record_samples),
|
|
resource_selector_records,
|
|
anonymous_selector_records,
|
|
first_resource_record_samples: resource_record_samples.into_iter().take(32).collect(),
|
|
first_imb_references: all_imb_references.into_iter().take(32).collect(),
|
|
notes,
|
|
})
|
|
}
|
|
|
|
fn collect_imb_references(bytes: &[u8]) -> Vec<WinResourceReference> {
|
|
let mut references = Vec::new();
|
|
let mut offset = 0usize;
|
|
while offset < bytes.len() {
|
|
if let Some(name) = parse_imb_reference_at(bytes, offset) {
|
|
references.push(WinResourceReference {
|
|
offset,
|
|
offset_hex: format!("0x{offset:04x}"),
|
|
name,
|
|
});
|
|
}
|
|
offset += 1;
|
|
}
|
|
references
|
|
}
|
|
|
|
fn build_resource_record_samples(
|
|
bytes: &[u8],
|
|
references: &[WinResourceReference],
|
|
) -> Vec<WinResourceRecordSample> {
|
|
let mut samples = Vec::with_capacity(references.len());
|
|
for (index, reference) in references.iter().enumerate() {
|
|
let previous_offset = index
|
|
.checked_sub(1)
|
|
.and_then(|previous| references.get(previous))
|
|
.map(|previous| previous.offset);
|
|
let delta_from_previous = previous_offset.map(|previous| reference.offset - previous);
|
|
let delta_from_previous_hex = delta_from_previous.map(|delta| format!("0x{delta:x}"));
|
|
|
|
let prelude_words = if reference.offset >= 16 {
|
|
(0..4)
|
|
.map(|index| {
|
|
let offset = reference.offset - 16 + index * 4;
|
|
let value = read_u32_le(bytes, offset).unwrap_or(0);
|
|
WinHeaderWord {
|
|
offset,
|
|
offset_hex: format!("0x{offset:04x}"),
|
|
value,
|
|
value_hex: format!("0x{value:08x}"),
|
|
}
|
|
})
|
|
.collect()
|
|
} else {
|
|
Vec::new()
|
|
};
|
|
|
|
let name_end = reference.offset + reference.name.len();
|
|
let post_name_word_0 = read_u32_le(bytes, name_end + 1).unwrap_or(0);
|
|
let post_name_word_1 = read_u32_le(bytes, name_end + 5).unwrap_or(0);
|
|
let post_name_word_0_high_u16 = ((post_name_word_0 >> 16) & 0xffff) as u16;
|
|
let post_name_word_0_low_u16 = (post_name_word_0 & 0xffff) as u16;
|
|
|
|
samples.push(WinResourceRecordSample {
|
|
offset: reference.offset,
|
|
offset_hex: reference.offset_hex.clone(),
|
|
name: reference.name.clone(),
|
|
delta_from_previous,
|
|
delta_from_previous_hex,
|
|
prelude_words,
|
|
post_name_word_0,
|
|
post_name_word_0_hex: format!("0x{post_name_word_0:08x}"),
|
|
post_name_word_0_high_u16,
|
|
post_name_word_0_high_u16_hex: format!("0x{post_name_word_0_high_u16:04x}"),
|
|
post_name_word_0_low_u16,
|
|
post_name_word_0_low_u16_hex: format!("0x{post_name_word_0_low_u16:04x}"),
|
|
post_name_word_1,
|
|
post_name_word_1_hex: format!("0x{post_name_word_1:08x}"),
|
|
});
|
|
}
|
|
samples
|
|
}
|
|
|
|
fn build_delta_histogram(samples: &[WinResourceRecordSample]) -> Vec<WinReferenceDeltaFrequency> {
|
|
let mut counts = std::collections::BTreeMap::<usize, usize>::new();
|
|
for sample in samples {
|
|
if let Some(delta) = sample.delta_from_previous {
|
|
*counts.entry(delta).or_default() += 1;
|
|
}
|
|
}
|
|
|
|
let mut frequencies = counts
|
|
.into_iter()
|
|
.map(|(delta, count)| WinReferenceDeltaFrequency {
|
|
delta,
|
|
delta_hex: format!("0x{delta:x}"),
|
|
count,
|
|
})
|
|
.collect::<Vec<_>>();
|
|
frequencies.sort_by(|left, right| {
|
|
right
|
|
.count
|
|
.cmp(&left.count)
|
|
.then_with(|| left.delta.cmp(&right.delta))
|
|
});
|
|
frequencies.truncate(12);
|
|
frequencies
|
|
}
|
|
|
|
fn build_resource_selector_records(
|
|
samples: &[WinResourceRecordSample],
|
|
) -> Vec<WinResourceSelectorRecord> {
|
|
samples
|
|
.iter()
|
|
.map(|sample| {
|
|
let post_name_word_1_high_u16 = ((sample.post_name_word_1 >> 16) & 0xffff) as u16;
|
|
let post_name_word_1_middle_u16 = ((sample.post_name_word_1 >> 8) & 0xffff) as u16;
|
|
let post_name_word_1_low_u16 = (sample.post_name_word_1 & 0xffff) as u16;
|
|
WinResourceSelectorRecord {
|
|
offset: sample.offset,
|
|
offset_hex: sample.offset_hex.clone(),
|
|
name: sample.name.clone(),
|
|
post_name_word_0: sample.post_name_word_0,
|
|
post_name_word_0_hex: sample.post_name_word_0_hex.clone(),
|
|
selector_high_u16: sample.post_name_word_0_high_u16,
|
|
selector_high_u16_hex: sample.post_name_word_0_high_u16_hex.clone(),
|
|
selector_low_u16: sample.post_name_word_0_low_u16,
|
|
selector_low_u16_hex: sample.post_name_word_0_low_u16_hex.clone(),
|
|
post_name_word_1: sample.post_name_word_1,
|
|
post_name_word_1_hex: sample.post_name_word_1_hex.clone(),
|
|
post_name_word_1_high_u16,
|
|
post_name_word_1_high_u16_hex: format!("0x{post_name_word_1_high_u16:04x}"),
|
|
post_name_word_1_middle_u16,
|
|
post_name_word_1_middle_u16_hex: format!("0x{post_name_word_1_middle_u16:04x}"),
|
|
post_name_word_1_low_u16,
|
|
post_name_word_1_low_u16_hex: format!("0x{post_name_word_1_low_u16:04x}"),
|
|
}
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
fn collect_anonymous_selector_records(
|
|
bytes: &[u8],
|
|
references: &[WinResourceReference],
|
|
) -> Vec<WinAnonymousSelectorRecord> {
|
|
const PRELUDE: [u8; 12] = [
|
|
0xb8, 0x0b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xb9, 0x0b, 0x00, 0x00,
|
|
];
|
|
|
|
let mut records = Vec::new();
|
|
let mut start = 0usize;
|
|
while let Some(relative) = bytes.get(start..).and_then(|slice| {
|
|
slice
|
|
.windows(PRELUDE.len())
|
|
.position(|window| window == PRELUDE)
|
|
}) {
|
|
let record_offset = start + relative;
|
|
let name_len = read_u32_le(bytes, record_offset + PRELUDE.len()).unwrap_or(0);
|
|
if name_len == 0 {
|
|
let selector_word_0 = read_u32_le(bytes, record_offset + 0x10).unwrap_or(0);
|
|
let selector_word_0_low_u16 = (selector_word_0 & 0xffff) as u16;
|
|
if (0xc352..=0xc39b).contains(&selector_word_0_low_u16) {
|
|
let preceding_named_record = references
|
|
.iter()
|
|
.rev()
|
|
.find(|reference| reference.offset < record_offset);
|
|
let following_named_record = references
|
|
.iter()
|
|
.find(|reference| reference.offset > record_offset);
|
|
let selector_word_1 = read_u32_le(bytes, record_offset + 0x14).unwrap_or(0);
|
|
let selector_word_0_high_u16 = ((selector_word_0 >> 16) & 0xffff) as u16;
|
|
let selector_word_1_middle_u16 = ((selector_word_1 >> 8) & 0xffff) as u16;
|
|
let body_word_0 = read_u32_le(bytes, record_offset + 0x18).unwrap_or(0);
|
|
let body_word_1 = read_u32_le(bytes, record_offset + 0x1c).unwrap_or(0);
|
|
let body_word_2 = read_u32_le(bytes, record_offset + 0x20).unwrap_or(0);
|
|
let body_word_3 = read_u32_le(bytes, record_offset + 0x24).unwrap_or(0);
|
|
let footer_word_0 = read_u32_le(bytes, record_offset + 0x98).unwrap_or(0);
|
|
let footer_word_1 = read_u32_le(bytes, record_offset + 0x9c).unwrap_or(0);
|
|
records.push(WinAnonymousSelectorRecord {
|
|
record_offset,
|
|
record_offset_hex: format!("0x{record_offset:04x}"),
|
|
preceding_named_record_name: preceding_named_record
|
|
.map(|record| record.name.clone()),
|
|
preceding_named_record_offset_hex: preceding_named_record
|
|
.map(|record| record.offset_hex.clone()),
|
|
following_named_record_name: following_named_record
|
|
.map(|record| record.name.clone()),
|
|
following_named_record_offset_hex: following_named_record
|
|
.map(|record| record.offset_hex.clone()),
|
|
selector_word_0,
|
|
selector_word_0_hex: format!("0x{selector_word_0:08x}"),
|
|
selector_word_0_high_u16,
|
|
selector_word_0_high_u16_hex: format!("0x{selector_word_0_high_u16:04x}"),
|
|
selector_word_0_low_u16,
|
|
selector_word_0_low_u16_hex: format!("0x{selector_word_0_low_u16:04x}"),
|
|
selector_word_1,
|
|
selector_word_1_hex: format!("0x{selector_word_1:08x}"),
|
|
selector_word_1_middle_u16,
|
|
selector_word_1_middle_u16_hex: format!("0x{selector_word_1_middle_u16:04x}"),
|
|
body_word_0,
|
|
body_word_0_hex: format!("0x{body_word_0:08x}"),
|
|
body_word_1,
|
|
body_word_1_hex: format!("0x{body_word_1:08x}"),
|
|
body_word_2,
|
|
body_word_2_hex: format!("0x{body_word_2:08x}"),
|
|
body_word_3,
|
|
body_word_3_hex: format!("0x{body_word_3:08x}"),
|
|
footer_word_0,
|
|
footer_word_0_hex: format!("0x{footer_word_0:08x}"),
|
|
footer_word_1,
|
|
footer_word_1_hex: format!("0x{footer_word_1:08x}"),
|
|
});
|
|
}
|
|
}
|
|
start = record_offset + 1;
|
|
}
|
|
records
|
|
}
|
|
|
|
fn shared_prelude_prefix_hex(samples: &[WinResourceRecordSample]) -> Option<Vec<String>> {
|
|
let first = samples.first()?;
|
|
if first.prelude_words.len() < 3 {
|
|
return None;
|
|
}
|
|
let prefix = first.prelude_words[..3]
|
|
.iter()
|
|
.map(|word| word.value)
|
|
.collect::<Vec<_>>();
|
|
if samples.iter().all(|sample| {
|
|
sample.prelude_words.len() >= 3
|
|
&& sample.prelude_words[..3]
|
|
.iter()
|
|
.map(|word| word.value)
|
|
.collect::<Vec<_>>()
|
|
== prefix
|
|
}) {
|
|
return Some(
|
|
prefix
|
|
.into_iter()
|
|
.map(|value| format!("0x{value:08x}"))
|
|
.collect(),
|
|
);
|
|
}
|
|
None
|
|
}
|
|
|
|
fn parse_imb_reference_at(bytes: &[u8], offset: usize) -> Option<String> {
|
|
if offset > 0 {
|
|
let previous = *bytes.get(offset - 1)?;
|
|
if previous != 0 {
|
|
return None;
|
|
}
|
|
}
|
|
let slice = bytes.get(offset..)?;
|
|
let nul = slice.iter().position(|byte| *byte == 0)?;
|
|
let candidate = slice.get(..nul)?;
|
|
if candidate.len() < 5 {
|
|
return None;
|
|
}
|
|
let value = std::str::from_utf8(candidate).ok()?;
|
|
if !value.ends_with(".imb") {
|
|
return None;
|
|
}
|
|
if !value
|
|
.bytes()
|
|
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-' | b'.' | b' '))
|
|
{
|
|
return None;
|
|
}
|
|
Some(value.to_string())
|
|
}
|
|
|
|
fn parse_inline_ascii_name(bytes: &[u8], offset: usize) -> Option<String> {
|
|
let prefix = *bytes.get(offset)?;
|
|
if prefix != 0 {
|
|
return None;
|
|
}
|
|
parse_imb_reference_at(bytes, offset + 1)
|
|
}
|
|
|
|
fn read_u32_le(bytes: &[u8], offset: usize) -> Option<u32> {
|
|
let slice = bytes.get(offset..offset + 4)?;
|
|
Some(u32::from_le_bytes(slice.try_into().ok()?))
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn inspects_synthetic_window_blob() {
|
|
let mut bytes = vec![0u8; 0x90];
|
|
bytes[0x00..0x04].copy_from_slice(&0x0000_07d0u32.to_le_bytes());
|
|
bytes[0x0c..0x10].copy_from_slice(&0x8000_0000u32.to_le_bytes());
|
|
bytes[0x10..0x14].copy_from_slice(&0x8000_003fu32.to_le_bytes());
|
|
bytes[0x14..0x18].copy_from_slice(&0x0000_003fu32.to_le_bytes());
|
|
bytes[0x34..0x38].copy_from_slice(&0x0007_d100u32.to_le_bytes());
|
|
bytes[0x38..0x3c].copy_from_slice(&0x0007_d200u32.to_le_bytes());
|
|
bytes[0x40..0x44].copy_from_slice(&0x000b_b800u32.to_le_bytes());
|
|
bytes[0x48..0x4c].copy_from_slice(&0x000b_b900u32.to_le_bytes());
|
|
bytes[0x50] = 0;
|
|
bytes[0x51..0x51 + "Root.imb".len()].copy_from_slice(b"Root.imb");
|
|
bytes[0x59] = 0;
|
|
bytes.extend_from_slice(b"\0Button.imb\0");
|
|
|
|
let report = inspect_win_bytes(&bytes).expect("inspection should succeed");
|
|
assert!(report.matches_observed_common_signature);
|
|
assert_eq!(
|
|
report.inline_root_resource_name.as_deref(),
|
|
Some("Root.imb")
|
|
);
|
|
assert_eq!(report.imb_reference_count, 2);
|
|
assert_eq!(report.unique_imb_reference_count, 2);
|
|
assert_eq!(report.resource_selector_records.len(), 2);
|
|
assert_eq!(report.resource_selector_records[0].name, "Root.imb");
|
|
assert!(report.anonymous_selector_records.is_empty());
|
|
}
|
|
}
|