use std::collections::BTreeMap;
use crate::{
file::parser::Parser,
metadata::resources::{
ResourceEntry, ResourceEntryRef, ResourceType, ResourceTypeRef, RESOURCE_MAGIC,
},
ParseFailure, ParseStage, Result,
};
const MAX_RESOURCE_TYPES: u32 = 4096;
const MAX_RESOURCES: u32 = 1_000_000;
pub fn parse_dotnet_resource(data: &[u8]) -> Result<BTreeMap<String, ResourceEntry>> {
let mut resource = Resource::parse(data)?;
resource.read_resources(data)
}
pub fn parse_dotnet_resource_ref(data: &[u8]) -> Result<BTreeMap<String, ResourceEntryRef<'_>>> {
let mut resource = Resource::parse(data)?;
resource.read_resources_ref(data)
}
#[derive(Default)]
pub struct Resource {
pub res_mgr_header_version: u32,
pub header_size: u32,
pub reader_type: String,
pub resource_set_type: String,
pub rr_header_offset: usize,
pub rr_version: u32,
pub resource_count: u32,
pub type_names: Vec<String>,
pub padding: usize,
pub name_hashes: Vec<u32>,
pub name_positions: Vec<u32>,
pub data_section_offset: usize,
pub name_section_offset: usize,
pub is_debug: bool,
pub is_embedded_resource: bool,
}
impl Resource {
pub fn parse(data: &[u8]) -> Result<Self> {
if data.len() < 12 {
return Err(ParseFailure::Truncated {
stage: ParseStage::Resources,
expected: 12,
found: data.len(),
}
.into());
}
let mut parser = Parser::new(data);
let is_embedded_resource = Self::parse_and_validate_magic(&mut parser, data)?;
let (res_mgr_header_version, header_size, reader_type, resource_set_type) =
Self::parse_resource_manager_header(&mut parser)?;
let mut res = Resource {
res_mgr_header_version,
header_size,
reader_type,
resource_set_type,
is_embedded_resource,
rr_header_offset: parser.pos(),
..Default::default()
};
Self::parse_runtime_reader_header(&mut parser, data, &mut res)?;
Self::parse_type_table(&mut parser, &mut res)?;
res.padding = Self::skip_padding(&mut parser, data)?;
Self::parse_lookup_tables(&mut parser, &mut res)?;
res.data_section_offset = parser.read_le::<u32>()? as usize;
res.name_section_offset = parser.pos();
Ok(res)
}
fn parse_and_validate_magic(parser: &mut Parser, data: &[u8]) -> Result<bool> {
let first_u32 = parser.read_le::<u32>()?;
let second_u32 = parser.read_le::<u32>()?;
if second_u32 == RESOURCE_MAGIC {
let size = first_u32 as usize;
let max_size = data.len().saturating_sub(4);
if size > max_size || size < 8 {
return Err(malformed_error!("Invalid embedded resource size: {}", size));
}
Ok(true)
} else if first_u32 == RESOURCE_MAGIC {
parser.seek(4)?; Ok(false)
} else {
Err(malformed_error!(
"Invalid resource format - expected magic 0x{:08X}, found 0x{:08X}/0x{:08X}",
RESOURCE_MAGIC,
first_u32,
second_u32
))
}
}
fn parse_resource_manager_header(parser: &mut Parser) -> Result<(u32, u32, String, String)> {
let version = parser.read_le::<u32>()?;
let num_bytes_to_skip = parser.read_le::<u32>()?;
if version > 1 {
if num_bytes_to_skip > (1 << 30) {
return Err(malformed_error!(
"Invalid skip bytes: {}",
num_bytes_to_skip
));
}
parser.advance_by(num_bytes_to_skip as usize)?;
Ok((version, num_bytes_to_skip, String::new(), String::new()))
} else {
let reader_type = parser.read_prefixed_string_utf8()?;
let resource_set_type = parser.read_prefixed_string_utf8()?;
if !Self::validate_reader_type(&reader_type) {
return Err(malformed_error!("Unsupported reader type: {}", reader_type));
}
Ok((version, num_bytes_to_skip, reader_type, resource_set_type))
}
}
fn parse_runtime_reader_header(
parser: &mut Parser,
data: &[u8],
res: &mut Resource,
) -> Result<()> {
res.rr_version = parser.read_le::<u32>()?;
if res.rr_version != 1 && res.rr_version != 2 {
return Err(malformed_error!(
"Unsupported resource reader version: {}",
res.rr_version
));
}
if res.rr_version == 2 && data.len().saturating_sub(parser.pos()) >= 11 {
res.is_debug = Self::try_parse_debug_marker(parser);
}
res.resource_count = parser.read_le::<u32>()?;
if res.resource_count > MAX_RESOURCES {
return Err(malformed_error!(
"Resource file has too many resources: {} (max: {})",
res.resource_count,
MAX_RESOURCES
));
}
Ok(())
}
fn try_parse_debug_marker(parser: &mut Parser) -> bool {
let result = parser.transactional(|p| {
let s = p.read_prefixed_string_utf8()?;
if s == "***DEBUG***" {
Ok(true)
} else {
Err(malformed_error!("not a debug marker"))
}
});
result.unwrap_or(false)
}
fn parse_type_table(parser: &mut Parser, res: &mut Resource) -> Result<()> {
let type_count = parser.read_le::<u32>()?;
if type_count > MAX_RESOURCE_TYPES {
return Err(malformed_error!(
"Resource file has too many types: {} (max: {})",
type_count,
MAX_RESOURCE_TYPES
));
}
res.type_names.reserve(type_count as usize);
for _ in 0..type_count {
res.type_names.push(parser.read_prefixed_string_utf8()?);
}
Ok(())
}
fn skip_padding(parser: &mut Parser, data: &[u8]) -> Result<usize> {
let mut padding_count: usize = 0;
let align_bytes = parser.pos() & 7;
if align_bytes != 0 {
let padding_to_skip = 8usize.wrapping_sub(align_bytes);
padding_count = padding_count
.checked_add(padding_to_skip)
.ok_or_else(|| malformed_error!("padding count overflow"))?;
parser.advance_by(padding_to_skip)?;
}
let pad_pattern_count = Self::skip_pad_patterns(parser, data)?;
padding_count = padding_count
.checked_add(pad_pattern_count)
.ok_or_else(|| malformed_error!("padding count overflow"))?;
Ok(padding_count)
}
fn skip_pad_patterns(parser: &mut Parser, data: &[u8]) -> Result<usize> {
let mut padding_count: usize = 0;
loop {
let pos = parser.pos();
let Some(end4) = pos.checked_add(4) else {
break;
};
if end4 > data.len() {
break;
}
let remaining = data.len().saturating_sub(pos);
if remaining < 3 {
break;
}
let p0 = data.get(pos).copied();
let p1 = pos.checked_add(1).and_then(|i| data.get(i).copied());
let p2 = pos.checked_add(2).and_then(|i| data.get(i).copied());
if p0 == Some(b'P') && p1 == Some(b'A') && p2 == Some(b'D') {
parser.advance_by(3)?;
padding_count = padding_count
.checked_add(3)
.ok_or_else(|| malformed_error!("PAD pattern padding overflow"))?;
if parser.pos() < data.len() {
let next_byte = data.get(parser.pos()).copied();
if next_byte == Some(b'P') || next_byte == Some(0) {
parser.advance()?;
padding_count = padding_count
.checked_add(1)
.ok_or_else(|| malformed_error!("PAD pattern padding overflow"))?;
}
}
} else {
break;
}
}
Ok(padding_count)
}
fn parse_lookup_tables(parser: &mut Parser, res: &mut Resource) -> Result<()> {
let count = res.resource_count as usize;
res.name_hashes.reserve(count);
for _ in 0..count {
res.name_hashes.push(parser.read_le::<u32>()?);
}
res.name_positions.reserve(count);
for _ in 0..count {
res.name_positions.push(parser.read_le::<u32>()?);
}
Ok(())
}
pub fn read_resources(&mut self, data: &[u8]) -> Result<BTreeMap<String, ResourceEntry>> {
let count = self.resource_count as usize;
if self.name_hashes.len() != count || self.name_positions.len() != count {
return Err(malformed_error!(
"Resource count {} doesn't match hash/position array lengths ({}/{})",
self.resource_count,
self.name_hashes.len(),
self.name_positions.len()
));
}
let mut resources = BTreeMap::new();
let mut parser = Parser::new(data);
for i in 0..count {
let name_pos_offset = *self
.name_positions
.get(i)
.ok_or_else(|| malformed_error!("name_positions index {} out of bounds", i))?
as usize;
let name_pos = self
.name_section_offset
.checked_add(name_pos_offset)
.ok_or_else(|| malformed_error!("name position overflow"))?;
parser.seek(name_pos)?;
let name = parser.read_prefixed_string_utf16()?;
let type_offset = parser.read_le::<u32>()?;
let data_pos = if self.is_embedded_resource {
self.data_section_offset
.checked_add(type_offset as usize)
.and_then(|v| v.checked_add(4))
.ok_or_else(|| malformed_error!("embedded resource data position overflow"))?
} else {
self.data_section_offset
.checked_add(type_offset as usize)
.ok_or_else(|| malformed_error!("standalone resource data position overflow"))?
};
if data_pos >= data.len() {
return Err(malformed_error!(
"Resource data offset {} is beyond file bounds",
data_pos
));
}
parser.seek(data_pos)?;
let resource_data = if self.rr_version == 1 {
let type_index = parser.read_7bit_encoded_int()?;
if type_index == u32::MAX {
ResourceType::Null
} else if let Some(type_name) = self.type_names.get(type_index as usize) {
ResourceType::from_type_name(type_name, &mut parser)?
} else {
return Err(malformed_error!("Invalid type index: {}", type_index));
}
} else {
#[allow(clippy::cast_possible_truncation)]
let type_code = parser.read_7bit_encoded_int()? as u8;
if self.type_names.is_empty() {
ResourceType::from_type_byte(type_code, &mut parser)?
} else if let Some(type_name) = self.type_names.get(type_code as usize) {
ResourceType::from_type_name(type_name, &mut parser)?
} else {
return Err(malformed_error!("Invalid type index: {}", type_code));
}
};
let name_hash = *self
.name_hashes
.get(i)
.ok_or_else(|| malformed_error!("name_hashes index {} out of bounds", i))?;
let result = ResourceEntry {
name: name.clone(),
name_hash,
data: resource_data,
};
resources.insert(name, result);
}
Ok(resources)
}
pub fn read_resources_ref<'a>(
&mut self,
data: &'a [u8],
) -> Result<BTreeMap<String, ResourceEntryRef<'a>>> {
let count = self.resource_count as usize;
if self.name_hashes.len() != count || self.name_positions.len() != count {
return Err(malformed_error!(
"Resource count {} doesn't match hash/position array lengths ({}/{})",
self.resource_count,
self.name_hashes.len(),
self.name_positions.len()
));
}
let mut resources = BTreeMap::new();
let mut parser = Parser::new(data);
for i in 0..count {
let name_pos_offset = *self
.name_positions
.get(i)
.ok_or_else(|| malformed_error!("name_positions index {} out of bounds", i))?
as usize;
let name_pos = self
.name_section_offset
.checked_add(name_pos_offset)
.ok_or_else(|| malformed_error!("name position overflow"))?;
parser.seek(name_pos)?;
let name = parser.read_prefixed_string_utf16()?;
let type_offset = parser.read_le::<u32>()?;
let data_pos = if self.is_embedded_resource {
self.data_section_offset
.checked_add(type_offset as usize)
.and_then(|v| v.checked_add(4))
.ok_or_else(|| malformed_error!("embedded resource data position overflow"))?
} else {
self.data_section_offset
.checked_add(type_offset as usize)
.ok_or_else(|| malformed_error!("standalone resource data position overflow"))?
};
if data_pos >= data.len() {
return Err(malformed_error!(
"Resource data offset {} is beyond file bounds",
data_pos
));
}
parser.seek(data_pos)?;
let resource_data = if self.rr_version == 1 {
let type_index = parser.read_7bit_encoded_int()?;
if type_index == u32::MAX {
ResourceTypeRef::Null
} else if let Some(type_name) = self.type_names.get(type_index as usize) {
ResourceTypeRef::from_type_name_ref(type_name, &mut parser, data)?
} else {
return Err(malformed_error!("Invalid type index: {}", type_index));
}
} else {
#[allow(clippy::cast_possible_truncation)]
let type_code = parser.read_7bit_encoded_int()? as u8;
if self.type_names.is_empty() {
ResourceTypeRef::from_type_byte_ref(type_code, &mut parser, data)?
} else if let Some(type_name) = self.type_names.get(type_code as usize) {
ResourceTypeRef::from_type_name_ref(type_name, &mut parser, data)?
} else {
return Err(malformed_error!("Invalid type index: {}", type_code));
}
};
let name_hash = *self
.name_hashes
.get(i)
.ok_or_else(|| malformed_error!("name_hashes index {} out of bounds", i))?;
let result = ResourceEntryRef {
name: name.clone(),
name_hash,
data: resource_data,
};
resources.insert(name, result);
}
Ok(resources)
}
fn validate_reader_type(reader_type: &str) -> bool {
match reader_type {
"System.Resources.ResourceReader"
| "System.Resources.Extensions.DeserializingResourceReader" => true,
s if s.starts_with("System.Resources.ResourceReader,") => true,
s if s.starts_with("System.Resources.Extensions.DeserializingResourceReader,") => true,
_ => false,
}
}
}
#[cfg(test)]
mod tests {
use crate::test::verify_wbdll_resource_buffer;
#[test]
fn wb_example() {
let data =
include_bytes!("../../../tests/samples/WB_FxResources.WindowsBase.SR.resources.bin");
verify_wbdll_resource_buffer(data);
}
}