rrt/crates/rrt-runtime/src/win.rs

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());
}
}