use std::collections::BTreeMap;
use crate::{
file::parser::Parser,
metadata::resources::{
ResourceEntry, ResourceEntryRef, ResourceType, ResourceTypeRef, RESOURCE_MAGIC,
},
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(malformed_error!("Resource data too small"));
}
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;
if size > (data.len() - 4) || 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() - 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 = 0;
let align_bytes = parser.pos() & 7;
if align_bytes != 0 {
let padding_to_skip = 8 - align_bytes;
padding_count += padding_to_skip;
parser.advance_by(padding_to_skip)?;
}
padding_count += Self::skip_pad_patterns(parser, data)?;
Ok(padding_count)
}
fn skip_pad_patterns(parser: &mut Parser, data: &[u8]) -> Result<usize> {
let mut padding_count = 0;
while parser.pos() + 4 <= data.len() {
let pos = parser.pos();
let remaining = data.len() - pos;
if remaining < 3 {
break;
}
if data[pos] == b'P' && data[pos + 1] == b'A' && data[pos + 2] == b'D' {
parser.advance_by(3)?;
padding_count += 3;
if parser.pos() < data.len() {
let next_byte = data[parser.pos()];
if next_byte == b'P' || next_byte == 0 {
parser.advance()?;
padding_count += 1;
}
}
} 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 = self.name_section_offset + self.name_positions[i] as usize;
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 + type_offset as usize + 4
} else {
self.data_section_offset + type_offset as usize
};
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 (type_index as usize) < self.type_names.len() {
let type_name = &self.type_names[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 (type_code as usize) < self.type_names.len() {
let type_name = &self.type_names[type_code as usize];
ResourceType::from_type_name(type_name, &mut parser)?
} else {
return Err(malformed_error!("Invalid type index: {}", type_code));
}
}
};
let result = ResourceEntry {
name: name.clone(),
name_hash: self.name_hashes[i],
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 = self.name_section_offset + self.name_positions[i] as usize;
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 + type_offset as usize + 4
} else {
self.data_section_offset + type_offset as usize
};
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 (type_index as usize) < self.type_names.len() {
let type_name = &self.type_names[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 (type_code as usize) < self.type_names.len() {
let type_name = &self.type_names[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 result = ResourceEntryRef {
name: name.clone(),
name_hash: self.name_hashes[i],
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);
}
}