use crate::{
metadata::resources::{ResourceType, RESOURCE_MAGIC},
utils::{compressed_uint_size, to_u32, write_7bit_encoded_int, write_compressed_uint},
Error, Result,
};
use std::collections::BTreeMap;
fn compute_resource_hash(key: &str) -> u32 {
let mut hash = 5381u32;
for ch in key.chars() {
hash = hash.wrapping_mul(33).wrapping_add(ch as u32);
}
hash
}
#[derive(Debug, Clone)]
pub struct DotNetResourceEncoder {
resources: Vec<(String, ResourceType)>,
version: u32,
}
impl DotNetResourceEncoder {
#[must_use]
pub fn new() -> Self {
DotNetResourceEncoder {
resources: Vec::new(),
version: 2, }
}
pub fn add_string(&mut self, name: &str, value: &str) -> Result<()> {
self.resources
.push((name.to_string(), ResourceType::String(value.to_string())));
Ok(())
}
pub fn add_int32(&mut self, name: &str, value: i32) -> Result<()> {
self.resources
.push((name.to_string(), ResourceType::Int32(value)));
Ok(())
}
pub fn add_boolean(&mut self, name: &str, value: bool) -> Result<()> {
self.resources
.push((name.to_string(), ResourceType::Boolean(value)));
Ok(())
}
pub fn add_byte_array(&mut self, name: &str, data: &[u8]) -> Result<()> {
self.resources
.push((name.to_string(), ResourceType::ByteArray(data.to_vec())));
Ok(())
}
pub fn add_stream(&mut self, name: &str, data: &[u8]) -> Result<()> {
self.resources
.push((name.to_string(), ResourceType::Stream(data.to_vec())));
Ok(())
}
pub fn add_byte(&mut self, name: &str, value: u8) -> Result<()> {
self.resources
.push((name.to_string(), ResourceType::Byte(value)));
Ok(())
}
pub fn add_sbyte(&mut self, name: &str, value: i8) -> Result<()> {
self.resources
.push((name.to_string(), ResourceType::SByte(value)));
Ok(())
}
pub fn add_char(&mut self, name: &str, value: char) -> Result<()> {
self.resources
.push((name.to_string(), ResourceType::Char(value)));
Ok(())
}
pub fn add_int16(&mut self, name: &str, value: i16) -> Result<()> {
self.resources
.push((name.to_string(), ResourceType::Int16(value)));
Ok(())
}
pub fn add_uint16(&mut self, name: &str, value: u16) -> Result<()> {
self.resources
.push((name.to_string(), ResourceType::UInt16(value)));
Ok(())
}
pub fn add_uint32(&mut self, name: &str, value: u32) -> Result<()> {
self.resources
.push((name.to_string(), ResourceType::UInt32(value)));
Ok(())
}
pub fn add_int64(&mut self, name: &str, value: i64) -> Result<()> {
self.resources
.push((name.to_string(), ResourceType::Int64(value)));
Ok(())
}
pub fn add_uint64(&mut self, name: &str, value: u64) -> Result<()> {
self.resources
.push((name.to_string(), ResourceType::UInt64(value)));
Ok(())
}
pub fn add_single(&mut self, name: &str, value: f32) -> Result<()> {
self.resources
.push((name.to_string(), ResourceType::Single(value)));
Ok(())
}
pub fn add_double(&mut self, name: &str, value: f64) -> Result<()> {
self.resources
.push((name.to_string(), ResourceType::Double(value)));
Ok(())
}
pub fn add_decimal(
&mut self,
name: &str,
lo: i32,
mid: i32,
hi: i32,
flags: i32,
) -> Result<()> {
self.resources.push((
name.to_string(),
ResourceType::Decimal { lo, mid, hi, flags },
));
Ok(())
}
pub fn add_datetime(&mut self, name: &str, binary_value: i64) -> Result<()> {
self.resources
.push((name.to_string(), ResourceType::DateTime(binary_value)));
Ok(())
}
pub fn add_timespan(&mut self, name: &str, ticks: i64) -> Result<()> {
self.resources
.push((name.to_string(), ResourceType::TimeSpan(ticks)));
Ok(())
}
#[must_use]
pub fn resource_count(&self) -> usize {
self.resources.len()
}
pub fn encode_dotnet_format(&self) -> Result<Vec<u8>> {
let mut buffer = Vec::new();
let size_placeholder_pos = buffer.len();
buffer.extend_from_slice(&0u32.to_le_bytes());
buffer.extend_from_slice(&RESOURCE_MAGIC.to_le_bytes());
buffer.extend_from_slice(&self.version.to_le_bytes());
let header_size_pos = buffer.len();
buffer.extend_from_slice(&0u32.to_le_bytes());
let reader_type = "System.Resources.ResourceReader, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089";
write_compressed_uint(to_u32(reader_type.len())?, &mut buffer);
buffer.extend_from_slice(reader_type.as_bytes());
let resource_set_type = "System.Resources.RuntimeResourceSet";
write_compressed_uint(to_u32(resource_set_type.len())?, &mut buffer);
buffer.extend_from_slice(resource_set_type.as_bytes());
let header_size = buffer.len() - header_size_pos - 4;
let header_size_bytes = to_u32(header_size)?.to_le_bytes();
buffer[header_size_pos..header_size_pos + 4].copy_from_slice(&header_size_bytes);
buffer.extend_from_slice(&self.version.to_le_bytes());
buffer.extend_from_slice(&to_u32(self.resources.len())?.to_le_bytes());
Self::write_type_table(&mut buffer)?;
while buffer.len() % 8 != 0 {
buffer.push(b'P'); }
let mut name_hashes: Vec<(u32, usize)> = self
.resources
.iter()
.enumerate()
.map(|(i, (name, _))| (compute_resource_hash(name), i))
.collect();
name_hashes.sort_by_key(|(hash, _)| *hash);
for (hash, _) in &name_hashes {
buffer.extend_from_slice(&hash.to_le_bytes());
}
let mut name_section_layout = Vec::new();
let mut name_offset = 0u32;
for (_, resource_index) in &name_hashes {
let (name, _) = &self.resources[*resource_index];
let name_utf16: Vec<u16> = name.encode_utf16().collect();
let byte_count = name_utf16.len() * 2;
#[allow(clippy::cast_possible_truncation)] let len_size = compressed_uint_size(byte_count) as u32;
let entry_size = len_size + to_u32(byte_count)? + 4;
name_section_layout.push(name_offset);
name_offset += entry_size;
}
for name_position in &name_section_layout {
buffer.extend_from_slice(&name_position.to_le_bytes());
}
let mut data_offsets = Vec::new();
let mut data_offset = 0u32;
for (_, resource_index) in &name_hashes {
let (_, resource_type) = &self.resources[*resource_index];
data_offsets.push(data_offset);
let type_code_size = if let Some(type_code) = resource_type.type_code() {
u32::try_from(compressed_uint_size(type_code as usize))
.map_err(|_| Error::NotSupported)?
} else {
return Err(Error::NotSupported);
};
let data_size = resource_type.data_size().ok_or(Error::NotSupported)?;
data_offset += type_code_size + data_size;
}
let data_section_offset_pos = buffer.len();
buffer.extend_from_slice(&0u32.to_le_bytes());
for (i, (_, resource_index)) in name_hashes.iter().enumerate() {
let (name, _) = &self.resources[*resource_index];
let name_utf16: Vec<u16> = name.encode_utf16().collect();
let byte_count = name_utf16.len() * 2;
write_compressed_uint(to_u32(byte_count)?, &mut buffer);
for utf16_char in name_utf16 {
buffer.extend_from_slice(&utf16_char.to_le_bytes());
}
buffer.extend_from_slice(&data_offsets[i].to_le_bytes());
}
let actual_data_section_offset = buffer.len() - 4; let data_section_offset_value = to_u32(actual_data_section_offset)?.to_le_bytes();
buffer[data_section_offset_pos..data_section_offset_pos + 4]
.copy_from_slice(&data_section_offset_value);
self.write_resource_data_sorted(&mut buffer, &name_hashes)?;
let total_size = buffer.len() - 4; let size_bytes = to_u32(total_size)?.to_le_bytes();
buffer[size_placeholder_pos..size_placeholder_pos + 4].copy_from_slice(&size_bytes);
Ok(buffer)
}
fn get_used_types(&self) -> Vec<(&'static str, u32)> {
let mut used_types = BTreeMap::new();
for (_, resource_type) in &self.resources {
if let (Some(type_name), Some(type_index)) =
(resource_type.as_str(), resource_type.index())
{
used_types.insert(type_index, type_name);
}
}
used_types
.into_iter()
.map(|(index, name)| (name, index))
.collect()
}
#[allow(clippy::unnecessary_wraps)]
fn write_type_table(buffer: &mut Vec<u8>) -> Result<()> {
buffer.extend_from_slice(&0u32.to_le_bytes());
Ok(())
}
fn write_resource_data_sorted(
&self,
buffer: &mut Vec<u8>,
name_hashes: &[(u32, usize)],
) -> Result<()> {
for (_, resource_index) in name_hashes {
let (_, resource_type) = &self.resources[*resource_index];
let type_code = match resource_type {
ResourceType::Null => 0u32, ResourceType::String(_) => 1u32, ResourceType::Boolean(_) => 2u32, ResourceType::Char(_) => 3u32, ResourceType::Byte(_) => 4u32, ResourceType::SByte(_) => 5u32, ResourceType::Int16(_) => 6u32, ResourceType::UInt16(_) => 7u32, ResourceType::Int32(_) => 8u32, ResourceType::UInt32(_) => 9u32, ResourceType::Int64(_) => 10u32, ResourceType::UInt64(_) => 11u32, ResourceType::Single(_) => 12u32, ResourceType::Double(_) => 13u32, ResourceType::Decimal { .. } => 14u32, ResourceType::DateTime(_) => 15u32, ResourceType::TimeSpan(_) => 16u32, ResourceType::ByteArray(_) => 32u32, ResourceType::Stream(_) => 33u32, ResourceType::StartOfUserTypes => return Err(Error::NotSupported),
};
write_compressed_uint(type_code, buffer);
match resource_type {
ResourceType::Null => {
}
ResourceType::String(s) => {
let utf8_bytes = s.as_bytes();
write_7bit_encoded_int(to_u32(utf8_bytes.len())?, buffer);
buffer.extend_from_slice(utf8_bytes);
}
ResourceType::Boolean(b) => {
buffer.push(u8::from(*b));
}
ResourceType::Char(c) => {
let utf16_char = *c as u16;
buffer.extend_from_slice(&utf16_char.to_le_bytes());
}
ResourceType::Byte(b) => {
buffer.push(*b);
}
ResourceType::SByte(sb) => {
#[allow(clippy::cast_sign_loss)]
{
buffer.push(*sb as u8);
}
}
ResourceType::Int16(i) => {
buffer.extend_from_slice(&i.to_le_bytes());
}
ResourceType::UInt16(u) => {
buffer.extend_from_slice(&u.to_le_bytes());
}
ResourceType::Int32(i) => {
buffer.extend_from_slice(&i.to_le_bytes());
}
ResourceType::UInt32(u) => {
buffer.extend_from_slice(&u.to_le_bytes());
}
ResourceType::Int64(i) => {
buffer.extend_from_slice(&i.to_le_bytes());
}
ResourceType::UInt64(u) => {
buffer.extend_from_slice(&u.to_le_bytes());
}
ResourceType::Single(f) => {
buffer.extend_from_slice(&f.to_le_bytes());
}
ResourceType::Double(d) => {
buffer.extend_from_slice(&d.to_le_bytes());
}
ResourceType::Decimal { lo, mid, hi, flags } => {
buffer.extend_from_slice(&lo.to_le_bytes());
buffer.extend_from_slice(&mid.to_le_bytes());
buffer.extend_from_slice(&hi.to_le_bytes());
buffer.extend_from_slice(&flags.to_le_bytes());
}
ResourceType::DateTime(binary_value) => {
buffer.extend_from_slice(&binary_value.to_le_bytes());
}
ResourceType::TimeSpan(ticks) => {
buffer.extend_from_slice(&ticks.to_le_bytes());
}
ResourceType::ByteArray(data) | ResourceType::Stream(data) => {
buffer.extend_from_slice(&to_u32(data.len())?.to_le_bytes());
buffer.extend_from_slice(data);
}
ResourceType::StartOfUserTypes => {
return Err(Error::NotSupported);
}
}
}
Ok(())
}
}
impl Default for DotNetResourceEncoder {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::metadata::resources::parser::parse_dotnet_resource;
#[test]
fn test_dotnet_resource_encoder_basic() {
let mut encoder = DotNetResourceEncoder::new();
assert_eq!(encoder.resource_count(), 0);
encoder
.add_string("AppName", "Test App")
.expect("Should add string");
encoder.add_int32("Version", 1).expect("Should add integer");
encoder
.add_boolean("Debug", true)
.expect("Should add boolean");
assert_eq!(encoder.resource_count(), 3);
}
#[test]
fn test_dotnet_resource_encoder_encoding() {
let mut encoder = DotNetResourceEncoder::new();
encoder
.add_string("test", "value")
.expect("Should add string resource");
let encoded = encoder
.encode_dotnet_format()
.expect("Should encode .NET format");
assert!(!encoded.is_empty());
assert!(encoded.len() >= 8);
let _size = u32::from_le_bytes([encoded[0], encoded[1], encoded[2], encoded[3]]);
let magic = u32::from_le_bytes([encoded[4], encoded[5], encoded[6], encoded[7]]);
assert_eq!(magic, RESOURCE_MAGIC);
assert!(encoded.len() > 20); }
#[test]
fn test_comprehensive_resource_encoder_api() {
let mut encoder = DotNetResourceEncoder::new();
encoder.add_string("AppName", "My Application").unwrap();
encoder.add_boolean("DebugMode", true).unwrap();
encoder.add_char("Separator", ',').unwrap();
encoder.add_byte("MaxRetries", 5).unwrap();
encoder.add_sbyte("Offset", -10).unwrap();
encoder.add_int16("Port", 8080).unwrap();
encoder.add_uint16("MaxConnections", 65535).unwrap();
encoder.add_int32("Version", 42).unwrap();
encoder.add_uint32("FileSize", 1024000).unwrap();
encoder
.add_int64("TimestampTicks", 637500000000000000)
.unwrap();
encoder
.add_uint64("MaxFileSize", 18446744073709551615)
.unwrap();
encoder.add_single("ScaleFactor", 1.5).unwrap();
encoder.add_double("Pi", std::f64::consts::PI).unwrap();
encoder
.add_byte_array("ConfigData", &[1, 2, 3, 4, 5])
.unwrap();
assert_eq!(encoder.resource_count(), 14);
let encoded_data = encoder.encode_dotnet_format().unwrap();
assert!(!encoded_data.is_empty());
assert!(encoded_data.len() > 100);
let magic = u32::from_le_bytes([
encoded_data[4],
encoded_data[5],
encoded_data[6],
encoded_data[7],
]);
assert_eq!(magic, RESOURCE_MAGIC);
assert_eq!(encoder.resource_count(), 14);
assert!(encoded_data.len() > 100);
}
#[test]
fn test_debug_encoder_format() {
let mut encoder = DotNetResourceEncoder::new();
encoder.add_string("TestResource", "Hello World").unwrap();
let buffer = encoder.encode_dotnet_format().unwrap();
let mut resource = crate::metadata::resources::Resource::parse(&buffer).unwrap();
assert_eq!(resource.rr_version, 2);
assert_eq!(resource.resource_count, 1);
resource
.read_resources(&buffer)
.expect("Should be able to parse generated resources");
}
#[test]
fn test_roundtrip_edge_values() {
let mut encoder = DotNetResourceEncoder::new();
encoder.add_string("EmptyString", "").unwrap();
encoder
.add_string("UnicodeString", "🦀 Rust rocks! ä½ å¥½ä¸–ç•Œ")
.unwrap();
encoder.add_byte_array("EmptyByteArray", &[]).unwrap();
encoder.add_single("NaN", f32::NAN).unwrap();
encoder.add_single("Infinity", f32::INFINITY).unwrap();
encoder
.add_single("NegInfinity", f32::NEG_INFINITY)
.unwrap();
encoder.add_double("DoubleNaN", f64::NAN).unwrap();
encoder.add_double("DoubleInfinity", f64::INFINITY).unwrap();
encoder
.add_double("DoubleNegInfinity", f64::NEG_INFINITY)
.unwrap();
let encoded_data = encoder.encode_dotnet_format().unwrap();
let parsed_resources = parse_dotnet_resource(&encoded_data).unwrap();
assert_eq!(parsed_resources.len(), 9);
let empty_string = parsed_resources.get("EmptyString").unwrap();
if let crate::metadata::resources::ResourceType::String(ref s) = empty_string.data {
assert_eq!(s, "");
} else {
panic!("Expected String resource type");
}
let unicode_string = parsed_resources.get("UnicodeString").unwrap();
if let crate::metadata::resources::ResourceType::String(ref s) = unicode_string.data {
assert_eq!(s, "🦀 Rust rocks! ä½ å¥½ä¸–ç•Œ");
} else {
panic!("Expected String resource type");
}
let empty_bytes = parsed_resources.get("EmptyByteArray").unwrap();
if let crate::metadata::resources::ResourceType::ByteArray(ref ba) = empty_bytes.data {
assert_eq!(ba, &Vec::<u8>::new());
} else {
panic!("Expected ByteArray resource type");
}
let nan_val = parsed_resources.get("NaN").unwrap();
if let crate::metadata::resources::ResourceType::Single(f) = nan_val.data {
assert!(f.is_nan());
} else {
panic!("Expected Single resource type");
}
let inf_val = parsed_resources.get("Infinity").unwrap();
if let crate::metadata::resources::ResourceType::Single(f) = inf_val.data {
assert_eq!(f, f32::INFINITY);
} else {
panic!("Expected Single resource type");
}
let neg_inf_val = parsed_resources.get("NegInfinity").unwrap();
if let crate::metadata::resources::ResourceType::Single(f) = neg_inf_val.data {
assert_eq!(f, f32::NEG_INFINITY);
} else {
panic!("Expected Single resource type");
}
}
#[test]
fn test_large_resource_data() {
let mut encoder = DotNetResourceEncoder::new();
let large_string = "x".repeat(10000);
encoder.add_string("LargeString", &large_string).unwrap();
let large_bytes: Vec<u8> = (0..5000).map(|i| (i % 256) as u8).collect();
encoder
.add_byte_array("LargeByteArray", &large_bytes)
.unwrap();
let encoded_data = encoder.encode_dotnet_format().unwrap();
let parsed_resources = parse_dotnet_resource(&encoded_data).unwrap();
assert_eq!(parsed_resources.len(), 2);
let parsed_string = parsed_resources.get("LargeString").unwrap();
if let crate::metadata::resources::ResourceType::String(ref s) = parsed_string.data {
assert_eq!(s.len(), 10000);
assert_eq!(s, &large_string);
} else {
panic!("Expected String resource type");
}
let parsed_bytes = parsed_resources.get("LargeByteArray").unwrap();
if let crate::metadata::resources::ResourceType::ByteArray(ref ba) = parsed_bytes.data {
assert_eq!(ba.len(), 5000);
assert_eq!(ba, &large_bytes);
} else {
panic!("Expected ByteArray resource type");
}
}
}