use crate::{
file::parser::Parser,
metadata::{
customattributes::types::{
CustomAttributeArgument, CustomAttributeNamedArgument, CustomAttributeValue,
NAMED_ARG_TYPE, SERIALIZATION_TYPE,
},
streams::Blob,
tables::ParamRc,
typesystem::{CilFlavor, CilTypeRef, TypeRegistry},
},
utils::EnumUtils,
Error::DepthLimitExceeded,
Result,
};
use std::sync::Arc;
const MAX_NESTING_DEPTH: usize = 1000;
const MAX_NAMED_ARGS: u16 = 1024;
const MAX_ATTRIBUTE_ARRAY_LENGTH: i32 = 65536;
pub fn parse_custom_attribute_blob(
blob: &Blob,
index: u32,
params: &Arc<boxcar::Vec<ParamRc>>,
) -> Result<CustomAttributeValue> {
if index == 0 {
return Ok(CustomAttributeValue {
fixed_args: vec![],
named_args: vec![],
});
}
let data = blob.get(index as usize)?;
let mut parser = CustomAttributeParser::new(data);
parser.parse_custom_attribute(params)
}
pub fn parse_custom_attribute_data(
data: &[u8],
params: &Arc<boxcar::Vec<ParamRc>>,
) -> Result<CustomAttributeValue> {
let mut parser = CustomAttributeParser::new(data);
parser.parse_custom_attribute(params)
}
pub fn parse_custom_attribute_data_with_registry(
data: &[u8],
params: &Arc<boxcar::Vec<ParamRc>>,
type_registry: &Arc<TypeRegistry>,
) -> Result<CustomAttributeValue> {
let mut parser = CustomAttributeParser::with_registry(data, type_registry.clone());
parser.parse_custom_attribute(params)
}
pub fn parse_custom_attribute_blob_with_registry(
blob: &Blob,
index: u32,
params: &Arc<boxcar::Vec<ParamRc>>,
type_registry: &Arc<TypeRegistry>,
) -> Result<CustomAttributeValue> {
let data = blob.get(index as usize)?;
let mut parser = CustomAttributeParser::with_registry(data, type_registry.clone());
parser.parse_custom_attribute(params)
}
pub struct CustomAttributeParser<'a> {
parser: Parser<'a>,
type_registry: Option<Arc<TypeRegistry>>,
}
impl<'a> CustomAttributeParser<'a> {
#[must_use]
pub fn new(data: &'a [u8]) -> Self {
Self {
parser: Parser::new(data),
type_registry: None,
}
}
#[must_use]
pub fn with_registry(data: &'a [u8], type_registry: Arc<TypeRegistry>) -> Self {
Self {
parser: Parser::new(data),
type_registry: Some(type_registry),
}
}
pub fn parse_custom_attribute(
&mut self,
params: &Arc<boxcar::Vec<ParamRc>>,
) -> Result<CustomAttributeValue> {
let prolog = self.parser.read_le::<u16>()?;
if prolog != 0x0001 {
return Err(malformed_error!(
"Invalid custom attribute prolog - expected 0x0001"
));
}
let fixed_args = self.parse_fixed_arguments(params)?;
let named_args =
if self.parser.has_more_data() && self.parser.len() >= self.parser.pos() + 2 {
let num_named = self.parser.read_le::<u16>()?;
if num_named > MAX_NAMED_ARGS {
return Err(malformed_error!(
"Custom attribute has too many named arguments: {} (max: {})",
num_named,
MAX_NAMED_ARGS
));
}
let mut args = Vec::with_capacity(num_named as usize);
for _ in 0..num_named {
if let Some(arg) = self.parse_named_argument()? {
args.push(arg);
} else {
break;
}
}
args
} else {
vec![]
};
Ok(CustomAttributeValue {
fixed_args,
named_args,
})
}
fn parse_fixed_arguments(
&mut self,
params: &Arc<boxcar::Vec<ParamRc>>,
) -> Result<Vec<CustomAttributeArgument>> {
let mut sorted_params: Vec<_> = params
.iter()
.filter(|(_, param)| param.sequence > 0)
.map(|(_, param)| param)
.collect();
sorted_params.sort_by_key(|param| param.sequence);
let resolved_param_types: Vec<_> = sorted_params
.iter()
.filter_map(|param| param.base.get())
.collect();
if resolved_param_types.is_empty() && !sorted_params.is_empty() {
return Err(malformed_error!(
"Constructor has {} parameters but no resolved types",
sorted_params.len()
));
}
let mut fixed_args = Vec::new();
for param_type in resolved_param_types {
if !self.parser.has_more_data() {
return Err(malformed_error!(
"Not enough data for remaining constructor parameters"
));
}
if let Some(arg) = self.parse_fixed_argument(param_type)? {
fixed_args.push(arg);
} else {
return Err(malformed_error!(
"Unsupported parameter type in custom attribute constructor"
));
}
}
Ok(fixed_args)
}
fn parse_enum(
&mut self,
type_name: String,
underlying_type_size: usize,
) -> Result<Option<CustomAttributeArgument>> {
match underlying_type_size {
0 => Err(malformed_error!(
"Cannot determine enum underlying type size for '{}' - enum fields not loaded yet.",
type_name
)),
1 => {
let enum_value = self.parser.read_le::<u8>()?;
Ok(Some(CustomAttributeArgument::Enum(
type_name,
Box::new(CustomAttributeArgument::U1(enum_value)),
)))
}
2 => {
let enum_value = self.parser.read_le::<u16>()?;
Ok(Some(CustomAttributeArgument::Enum(
type_name,
Box::new(CustomAttributeArgument::U2(enum_value)),
)))
}
4 => {
let enum_value = self.parser.read_le::<i32>()?;
Ok(Some(CustomAttributeArgument::Enum(
type_name,
Box::new(CustomAttributeArgument::I4(enum_value)),
)))
}
8 => {
let enum_value = self.parser.read_le::<i64>()?;
Ok(Some(CustomAttributeArgument::Enum(
type_name,
Box::new(CustomAttributeArgument::I8(enum_value)),
)))
}
_ => Err(malformed_error!(
"Invalid enum underlying type size {} for enum '{}'. Expected 1, 2, 4, or 8 bytes.",
underlying_type_size,
type_name
)),
}
}
fn parse_fixed_argument(
&mut self,
cil_type: &CilTypeRef,
) -> Result<Option<CustomAttributeArgument>> {
let Some(type_ref) = cil_type.upgrade() else {
return Err(malformed_error!("Type reference has been dropped"));
};
let flavor = type_ref.flavor();
if !self.parser.has_more_data() {
return Err(malformed_error!(
"Not enough data for fixed argument type {:?} (pos={}, len={})",
flavor,
self.parser.pos(),
self.parser.len()
));
}
match flavor {
CilFlavor::Boolean => Ok(Some(CustomAttributeArgument::Bool(
self.parser.read_le::<u8>()? != 0,
))),
CilFlavor::Char => {
let val = self.parser.read_le::<u16>()?;
let character = char::from_u32(u32::from(val)).unwrap_or('\u{FFFD}');
Ok(Some(CustomAttributeArgument::Char(character)))
}
CilFlavor::I1 => Ok(Some(CustomAttributeArgument::I1(
self.parser.read_le::<i8>()?,
))),
CilFlavor::U1 => Ok(Some(CustomAttributeArgument::U1(
self.parser.read_le::<u8>()?,
))),
CilFlavor::I2 => Ok(Some(CustomAttributeArgument::I2(
self.parser.read_le::<i16>()?,
))),
CilFlavor::U2 => Ok(Some(CustomAttributeArgument::U2(
self.parser.read_le::<u16>()?,
))),
CilFlavor::I4 => Ok(Some(CustomAttributeArgument::I4(
self.parser.read_le::<i32>()?,
))),
CilFlavor::U4 => Ok(Some(CustomAttributeArgument::U4(
self.parser.read_le::<u32>()?,
))),
CilFlavor::I8 => Ok(Some(CustomAttributeArgument::I8(
self.parser.read_le::<i64>()?,
))),
CilFlavor::U8 => Ok(Some(CustomAttributeArgument::U8(
self.parser.read_le::<u64>()?,
))),
CilFlavor::R4 => Ok(Some(CustomAttributeArgument::R4(
self.parser.read_le::<f32>()?,
))),
CilFlavor::R8 => Ok(Some(CustomAttributeArgument::R8(
self.parser.read_le::<f64>()?,
))),
CilFlavor::I => {
if cfg!(target_pointer_width = "64") {
let val = self.parser.read_le::<i64>()?;
#[allow(clippy::cast_possible_truncation)] Ok(Some(CustomAttributeArgument::I(val as isize)))
} else {
let val = self.parser.read_le::<i32>()?;
Ok(Some(CustomAttributeArgument::I(val as isize)))
}
}
CilFlavor::U => {
if cfg!(target_pointer_width = "64") {
let val = self.parser.read_le::<u64>()?;
#[allow(clippy::cast_possible_truncation)] Ok(Some(CustomAttributeArgument::U(val as usize)))
} else {
let val = self.parser.read_le::<u32>()?;
Ok(Some(CustomAttributeArgument::U(val as usize)))
}
}
CilFlavor::String => {
if self.parser.peek_byte()? == 0xFF {
let _ = self.parser.read_le::<u8>()?; Ok(Some(CustomAttributeArgument::String(String::new())))
} else {
let s = self
.parse_string()
.map_err(|e| malformed_error!("Failed to parse String parameter: {}", e))?;
Ok(Some(CustomAttributeArgument::String(s)))
}
}
CilFlavor::Class => {
let type_name = type_ref.fullname();
if type_name == "System.Type" {
if self.parser.peek_byte()? == 0xFF {
let _ = self.parser.read_le::<u8>()?; Ok(Some(CustomAttributeArgument::Type(String::new())))
} else {
let s = self.parse_string().map_err(|e| {
malformed_error!("Failed to parse System.Type parameter: {}", e)
})?;
Ok(Some(CustomAttributeArgument::Type(s)))
}
} else if type_name == "System.String" {
if self.parser.peek_byte()? == 0xFF {
let _ = self.parser.read_le::<u8>()?; Ok(Some(CustomAttributeArgument::String(String::new())))
} else {
let s = self.parse_string().map_err(|e| {
malformed_error!("Failed to parse System.String parameter: {}", e)
})?;
Ok(Some(CustomAttributeArgument::String(s)))
}
} else if type_name == "System.Object" {
let type_tag = self.parser.read_le::<u8>()?;
let value = self.parse_argument_by_type_tag(type_tag)?;
Ok(Some(value))
} else {
if let Some(registry) = &self.type_registry {
if let Some(resolved_type) = registry.resolve_type_global(&type_name) {
if EnumUtils::is_enum_type(&resolved_type, Some(registry)) {
let underlying_type_size =
EnumUtils::get_enum_underlying_type_size(&resolved_type);
return self.parse_enum(type_name, underlying_type_size);
}
}
}
let is_enum = if let Some(registry) = &self.type_registry {
EnumUtils::is_enum_type_by_name(&type_name, registry)
} else {
EnumUtils::is_enum_type(&type_ref, None)
};
if is_enum {
let underlying_type_size = if let Some(registry) = &self.type_registry {
EnumUtils::get_enum_underlying_type_size_by_name(&type_name, registry)
} else {
EnumUtils::get_enum_underlying_type_size(&type_ref)
};
return self.parse_enum(type_name, underlying_type_size);
}
self.parse_enum(type_name, 4)
}
}
CilFlavor::ValueType => {
let type_name = type_ref.fullname();
if let Some(registry) = &self.type_registry {
if let Some(resolved_type) = registry.resolve_type_global(&type_name) {
if EnumUtils::is_enum_type(&resolved_type, Some(registry)) {
let underlying_type_size =
EnumUtils::get_enum_underlying_type_size(&resolved_type);
return self.parse_enum(type_name, underlying_type_size);
}
}
}
let is_enum = if let Some(registry) = &self.type_registry {
EnumUtils::is_enum_type_by_name(&type_name, registry)
} else {
EnumUtils::is_enum_type(&type_ref, None)
};
if is_enum {
let underlying_type_size = if let Some(registry) = &self.type_registry {
EnumUtils::get_enum_underlying_type_size_by_name(&type_name, registry)
} else {
EnumUtils::get_enum_underlying_type_size(&type_ref)
};
self.parse_enum(type_name, underlying_type_size)
} else {
Err(malformed_error!(
"Cannot resolve ValueType '{}' - type not found in TypeRegistry. This indicates the assembly containing this type is not loaded yet.",
type_name
))
}
}
CilFlavor::Array { rank, .. } => {
if *rank == 1 {
let array_length = self.parser.read_le::<i32>()?;
if array_length == -1 {
Ok(Some(CustomAttributeArgument::Array(vec![]))) } else if array_length < 0 {
Err(malformed_error!("Invalid array length: {}", array_length))
} else if array_length > MAX_ATTRIBUTE_ARRAY_LENGTH {
Err(malformed_error!(
"Custom attribute array too large: {} (max: {})",
array_length,
MAX_ATTRIBUTE_ARRAY_LENGTH
))
} else {
if let Some(base_type) = type_ref.base() {
let base_type_ref = base_type.into();
#[allow(clippy::cast_sign_loss)]
let mut elements = Vec::with_capacity(array_length as usize);
for _ in 0..array_length {
if let Some(element) = self.parse_fixed_argument(&base_type_ref)? {
elements.push(element);
} else {
return Err(malformed_error!("Failed to parse array element"));
}
}
Ok(Some(CustomAttributeArgument::Array(elements)))
} else {
Err(malformed_error!(
"Array type has no base element type information for fixed arguments"
))
}
}
} else {
Err(malformed_error!(
"Multi-dimensional arrays not supported in custom attributes"
))
}
}
CilFlavor::Void => Ok(Some(CustomAttributeArgument::Void)),
CilFlavor::Object => {
let type_tag = self.parser.read_le::<u8>()?;
let value = self.parse_argument_by_type_tag(type_tag)?;
Ok(Some(value))
}
_ => Err(malformed_error!(
"Unsupported type flavor in custom attribute: {:?}",
flavor
)),
}
}
fn parse_named_argument(&mut self) -> Result<Option<CustomAttributeNamedArgument>> {
if !self.parser.has_more_data() {
return Ok(None);
}
let field_or_prop = self.parser.read_le::<u8>()?;
let is_field = match field_or_prop {
NAMED_ARG_TYPE::FIELD => true,
NAMED_ARG_TYPE::PROPERTY => false,
0x00 => {
return Ok(None);
}
_ => {
return Err(malformed_error!(
"Invalid field/property indicator: 0x{:02X} (expected 0x{:02X} for FIELD or 0x{:02X} for PROPERTY)",
field_or_prop,
NAMED_ARG_TYPE::FIELD,
NAMED_ARG_TYPE::PROPERTY
))
}
};
let type_info = self.parser.read_le::<u8>()?;
let arg_type = match type_info {
SERIALIZATION_TYPE::BOOLEAN => "Boolean".to_string(),
SERIALIZATION_TYPE::CHAR => "Char".to_string(),
SERIALIZATION_TYPE::I1 => "I1".to_string(),
SERIALIZATION_TYPE::U1 => "U1".to_string(),
SERIALIZATION_TYPE::I2 => "I2".to_string(),
SERIALIZATION_TYPE::U2 => "U2".to_string(),
SERIALIZATION_TYPE::I4 => "I4".to_string(),
SERIALIZATION_TYPE::U4 => "U4".to_string(),
SERIALIZATION_TYPE::I8 => "I8".to_string(),
SERIALIZATION_TYPE::U8 => "U8".to_string(),
SERIALIZATION_TYPE::R4 => "R4".to_string(),
SERIALIZATION_TYPE::R8 => "R8".to_string(),
SERIALIZATION_TYPE::STRING => "String".to_string(),
SERIALIZATION_TYPE::TYPE => "Type".to_string(),
SERIALIZATION_TYPE::TAGGED_OBJECT => "TaggedObject".to_string(),
SERIALIZATION_TYPE::ENUM => "Enum".to_string(),
_ => {
return Err(malformed_error!(
"Unsupported named argument type: 0x{:02X}",
type_info
))
}
};
let name_length = self.parser.read_compressed_uint()?;
let mut name = String::with_capacity(name_length as usize);
for _ in 0..name_length {
name.push(char::from(self.parser.read_le::<u8>()?));
}
let value = self.parse_argument_by_type_tag(type_info)?;
Ok(Some(CustomAttributeNamedArgument {
is_field,
name,
arg_type,
value,
}))
}
fn parse_argument_by_type_tag(&mut self, type_tag: u8) -> Result<CustomAttributeArgument> {
enum WorkItem {
ParseTag(u8),
BuildArray(i32),
TaggedObject,
}
let mut work_stack: Vec<WorkItem> = Vec::new();
let mut result_stack: Vec<CustomAttributeArgument> = Vec::new();
work_stack.push(WorkItem::ParseTag(type_tag));
while let Some(work) = work_stack.pop() {
if work_stack.len() + result_stack.len() > MAX_NESTING_DEPTH {
return Err(DepthLimitExceeded(MAX_NESTING_DEPTH));
}
match work {
WorkItem::ParseTag(tag) => {
match tag {
SERIALIZATION_TYPE::BOOLEAN => {
let val = self.parser.read_le::<u8>()?;
result_stack.push(CustomAttributeArgument::Bool(val != 0));
}
SERIALIZATION_TYPE::CHAR => {
let val = self.parser.read_le::<u16>()?;
let character = char::from_u32(u32::from(val)).unwrap_or('\u{FFFD}');
result_stack.push(CustomAttributeArgument::Char(character));
}
SERIALIZATION_TYPE::I1 => {
result_stack
.push(CustomAttributeArgument::I1(self.parser.read_le::<i8>()?));
}
SERIALIZATION_TYPE::U1 => {
result_stack
.push(CustomAttributeArgument::U1(self.parser.read_le::<u8>()?));
}
SERIALIZATION_TYPE::I2 => {
result_stack
.push(CustomAttributeArgument::I2(self.parser.read_le::<i16>()?));
}
SERIALIZATION_TYPE::U2 => {
result_stack
.push(CustomAttributeArgument::U2(self.parser.read_le::<u16>()?));
}
SERIALIZATION_TYPE::I4 => {
result_stack
.push(CustomAttributeArgument::I4(self.parser.read_le::<i32>()?));
}
SERIALIZATION_TYPE::U4 => {
result_stack
.push(CustomAttributeArgument::U4(self.parser.read_le::<u32>()?));
}
SERIALIZATION_TYPE::I8 => {
result_stack
.push(CustomAttributeArgument::I8(self.parser.read_le::<i64>()?));
}
SERIALIZATION_TYPE::U8 => {
result_stack
.push(CustomAttributeArgument::U8(self.parser.read_le::<u64>()?));
}
SERIALIZATION_TYPE::R4 => {
result_stack
.push(CustomAttributeArgument::R4(self.parser.read_le::<f32>()?));
}
SERIALIZATION_TYPE::R8 => {
result_stack
.push(CustomAttributeArgument::R8(self.parser.read_le::<f64>()?));
}
SERIALIZATION_TYPE::STRING => {
if self.parser.peek_byte()? == 0xFF {
let _ = self.parser.read_le::<u8>()?; result_stack.push(CustomAttributeArgument::String(String::new()));
} else {
let s = self.parse_string()?;
result_stack.push(CustomAttributeArgument::String(s));
}
}
SERIALIZATION_TYPE::TYPE => {
if self.parser.peek_byte()? == 0xFF {
let _ = self.parser.read_le::<u8>()?; result_stack.push(CustomAttributeArgument::Type(String::new()));
} else {
let s = self.parse_string()?;
result_stack.push(CustomAttributeArgument::Type(s));
}
}
SERIALIZATION_TYPE::TAGGED_OBJECT => {
let inner_type_tag = self.parser.read_le::<u8>()?;
work_stack.push(WorkItem::TaggedObject);
work_stack.push(WorkItem::ParseTag(inner_type_tag));
}
SERIALIZATION_TYPE::ENUM => {
let type_name = self.parse_string()?;
let val = self.parser.read_le::<i32>()?; result_stack.push(CustomAttributeArgument::Enum(
type_name,
Box::new(CustomAttributeArgument::I4(val)),
));
}
SERIALIZATION_TYPE::SZARRAY => {
let element_type_tag = self.parser.read_le::<u8>()?;
let array_length = self.parser.read_le::<i32>()?;
if array_length == -1 {
result_stack.push(CustomAttributeArgument::Array(vec![]));
} else if array_length < 0 {
return Err(malformed_error!(
"Invalid array length: {}",
array_length
));
} else {
work_stack.push(WorkItem::BuildArray(array_length));
for _ in 0..array_length {
work_stack.push(WorkItem::ParseTag(element_type_tag));
}
}
}
_ => {
return Err(malformed_error!(
"Unsupported serialization type tag: 0x{:02X}",
tag
));
}
}
}
WorkItem::BuildArray(count) => {
#[allow(clippy::cast_sign_loss)]
let count_usize = count as usize;
if result_stack.len() < count_usize {
return Err(malformed_error!(
"Insufficient elements on stack for array of length {}",
count
));
}
let start_idx = result_stack.len() - count_usize;
let elements = result_stack.drain(start_idx..).collect();
result_stack.push(CustomAttributeArgument::Array(elements));
}
WorkItem::TaggedObject => {
}
}
}
if result_stack.len() != 1 {
return Err(malformed_error!(
"Internal error: expected 1 result, got {}",
result_stack.len()
));
}
result_stack
.pop()
.ok_or_else(|| malformed_error!("Internal error: result stack unexpectedly empty"))
}
fn try_parse_string(&mut self) -> Option<String> {
if self.parser.has_more_data() && self.parser.peek_byte().ok()? == 0xFF {
self.parser.read_le::<u8>().ok()?;
return Some(String::new());
}
self.parser
.transactional(|p| {
let length = p.read_compressed_uint()?;
let remaining_data = p.len() - p.pos();
if length as usize > remaining_data {
return Err(malformed_error!("not enough data"));
}
if length > 1000 {
return Err(malformed_error!("length too large for speculative string"));
}
if length == 0 {
return Ok(String::new());
}
let string_bytes = &p.data()[p.pos()..p.pos() + length as usize];
let s = std::str::from_utf8(string_bytes)
.map_err(|_| malformed_error!("invalid UTF-8"))?;
let result = s.to_string();
p.advance_by(length as usize)?;
Ok(result)
})
.ok()
}
fn parse_string(&mut self) -> Result<String> {
if !self.parser.has_more_data() {
return Err(malformed_error!("No data available for string"));
}
let first_byte = self.parser.peek_byte()?;
if first_byte == 0xFF {
self.parser.read_le::<u8>()?;
return Ok(String::new());
}
let length = self.parser.read_compressed_uint()?;
let available_data = self.parser.len() - self.parser.pos();
if length == 0 {
Ok(String::new())
} else if length as usize <= available_data {
let mut bytes = Vec::with_capacity(length as usize);
for _ in 0..length {
bytes.push(self.parser.read_le::<u8>()?);
}
String::from_utf8(bytes).map_err(|e| {
malformed_error!(
"Invalid UTF-8 in custom attribute string at position {}: {}",
self.parser.pos() - length as usize,
e.utf8_error()
)
})
} else {
Err(malformed_error!(
"String length {} exceeds available data {} (blob context: pos={}, len={}, first_byte=0x{:02X})",
length,
available_data,
self.parser.pos() - 1, self.parser.len(),
first_byte
))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::metadata::{
identity::AssemblyIdentity,
tables::Param,
token::Token,
typesystem::{CilFlavor, CilPrimitiveKind, CilTypeRef, TypeBuilder, TypeRegistry},
};
use crate::test::factories::metadata::customattributes::{
create_constructor_with_params, create_constructor_with_params_and_registry,
create_empty_constructor, get_test_type_registry,
};
use std::{
collections::HashMap,
sync::{
atomic::{AtomicU64, Ordering},
Arc, Mutex, OnceLock,
},
};
#[test]
fn test_parse_empty_blob_with_method() {
let method = create_empty_constructor();
let result = parse_custom_attribute_data(&[0x01, 0x00], &method.params).unwrap();
assert!(result.fixed_args.is_empty());
assert!(result.named_args.is_empty());
}
#[test]
fn test_parse_invalid_prolog_with_method() {
let method = create_empty_constructor();
let result = parse_custom_attribute_data(&[0x00, 0x01], &method.params);
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("Invalid custom attribute prolog"));
}
#[test]
fn test_parse_simple_blob_with_method() {
let method = create_empty_constructor();
let blob_data = &[0x01, 0x00];
let result = parse_custom_attribute_data(blob_data, &method.params).unwrap();
assert_eq!(result.fixed_args.len(), 0);
assert_eq!(result.named_args.len(), 0);
let blob_data = &[
0x01, 0x00, 0x00, 0x00, ];
let result = parse_custom_attribute_data(blob_data, &method.params).unwrap();
assert_eq!(result.fixed_args.len(), 0);
assert_eq!(result.named_args.len(), 0);
}
#[test]
fn test_parse_boolean_argument() {
let method = create_constructor_with_params(vec![CilFlavor::Boolean]);
let blob_data = &[
0x01, 0x00, 0x01, 0x00, 0x00, ];
let result = parse_custom_attribute_data(blob_data, &method.params).unwrap();
assert_eq!(result.fixed_args.len(), 1);
match &result.fixed_args[0] {
CustomAttributeArgument::Bool(val) => assert!(*val),
_ => panic!("Expected Boolean argument"),
}
}
#[test]
fn test_parse_char_argument() {
let method = create_constructor_with_params(vec![CilFlavor::Char]);
let blob_data = &[
0x01, 0x00, 0x41, 0x00, 0x00, 0x00, ];
let result = parse_custom_attribute_data(blob_data, &method.params).unwrap();
assert_eq!(result.fixed_args.len(), 1);
match &result.fixed_args[0] {
CustomAttributeArgument::Char(val) => assert_eq!(*val, 'A'),
_ => panic!("Expected Char argument"),
}
}
#[test]
fn test_parse_integer_arguments() {
let method = create_constructor_with_params(vec![
CilFlavor::I1,
CilFlavor::U1,
CilFlavor::I2,
CilFlavor::U2,
CilFlavor::I4,
CilFlavor::U4,
CilFlavor::I8,
CilFlavor::U8,
]);
let blob_data = &[
0x01, 0x00, 0xFF, 0x42, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, ];
let result = parse_custom_attribute_data(blob_data, &method.params).unwrap();
assert_eq!(result.fixed_args.len(), 8);
match &result.fixed_args[0] {
CustomAttributeArgument::I1(val) => assert_eq!(*val, -1i8),
_ => panic!("Expected I1 argument"),
}
match &result.fixed_args[1] {
CustomAttributeArgument::U1(val) => assert_eq!(*val, 66u8),
_ => panic!("Expected U1 argument"),
}
match &result.fixed_args[2] {
CustomAttributeArgument::I2(val) => assert_eq!(*val, -32768i16),
_ => panic!("Expected I2 argument"),
}
match &result.fixed_args[3] {
CustomAttributeArgument::U2(val) => assert_eq!(*val, 65535u16),
_ => panic!("Expected U2 argument"),
}
match &result.fixed_args[4] {
CustomAttributeArgument::I4(val) => assert_eq!(*val, -2147483648i32),
_ => panic!("Expected I4 argument"),
}
match &result.fixed_args[5] {
CustomAttributeArgument::U4(val) => assert_eq!(*val, 4294967295u32),
_ => panic!("Expected U4 argument"),
}
match &result.fixed_args[6] {
CustomAttributeArgument::I8(val) => assert_eq!(*val, -9223372036854775808i64),
_ => panic!("Expected I8 argument"),
}
match &result.fixed_args[7] {
CustomAttributeArgument::U8(val) => assert_eq!(*val, 18446744073709551615u64),
_ => panic!("Expected U8 argument"),
}
}
#[test]
fn test_parse_floating_point_arguments() {
let method = create_constructor_with_params(vec![CilFlavor::R4, CilFlavor::R8]);
let blob_data = &[
0x01, 0x00, 0x00, 0x00, 0x20, 0x41, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x24, 0x40, 0x00, 0x00, ];
let result = parse_custom_attribute_data(blob_data, &method.params).unwrap();
assert_eq!(result.fixed_args.len(), 2);
match &result.fixed_args[0] {
CustomAttributeArgument::R4(val) => assert_eq!(*val, 10.0f32),
_ => panic!("Expected R4 argument"),
}
match &result.fixed_args[1] {
CustomAttributeArgument::R8(val) => assert_eq!(*val, 10.0f64),
_ => panic!("Expected R8 argument"),
}
}
#[test]
fn test_parse_native_integer_arguments() {
let method = create_constructor_with_params(vec![CilFlavor::I, CilFlavor::U]);
#[cfg(target_pointer_width = "64")]
let blob_data = &[
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x80, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0x00, 0x00, ];
#[cfg(target_pointer_width = "32")]
let blob_data = &[
0x01, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, ];
let result = parse_custom_attribute_data(blob_data, &method.params).unwrap();
assert_eq!(result.fixed_args.len(), 2);
match &result.fixed_args[0] {
CustomAttributeArgument::I(_) => (), _ => panic!("Expected I argument"),
}
match &result.fixed_args[1] {
CustomAttributeArgument::U(_) => (), _ => panic!("Expected U argument"),
}
}
#[test]
fn test_parse_string_argument() {
let method = create_constructor_with_params(vec![CilFlavor::String]);
let blob_data = &[
0x01, 0x00, 0x05, 0x48, 0x65, 0x6C, 0x6C, 0x6F, 0x00, 0x00, ];
let result = parse_custom_attribute_data(blob_data, &method.params).unwrap();
assert_eq!(result.fixed_args.len(), 1);
match &result.fixed_args[0] {
CustomAttributeArgument::String(val) => assert_eq!(val, "Hello"),
_ => panic!("Expected String argument"),
}
}
#[test]
fn test_parse_class_as_type_argument() {
let method = create_constructor_with_params(vec![CilFlavor::Class]);
let blob_data = &[
0x01, 0x00, 0x0C, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6D, 0x2E, 0x49, 0x6E, 0x74, 0x33,
0x32, 0x00, 0x00, ];
let result = parse_custom_attribute_data(blob_data, &method.params).unwrap();
assert_eq!(result.fixed_args.len(), 1);
match &result.fixed_args[0] {
CustomAttributeArgument::Type(val) => assert_eq!(val, "System.Int32"),
CustomAttributeArgument::String(val) => assert_eq!(val, "System.Int32"),
other => panic!("Expected Type or String argument, got: {other:?}"),
}
}
#[test]
fn test_parse_class_argument_scenarios() {
let test_registry = get_test_type_registry();
let method1 =
create_constructor_with_params_and_registry(vec![CilFlavor::Class], &test_registry);
let blob_data1 = &[
0x01, 0x00, 0x00, 0x00, 0x00, ];
let result1 =
parse_custom_attribute_data_with_registry(blob_data1, &method1.params, &test_registry);
match result1 {
Ok(attr) => {
assert_eq!(attr.fixed_args.len(), 1);
match &attr.fixed_args[0] {
CustomAttributeArgument::Type(s) => assert_eq!(s, ""),
CustomAttributeArgument::String(s) => assert_eq!(s, ""),
_ => panic!("Expected empty string or type argument"),
}
}
Err(e) => panic!("Expected success for empty string, got: {e}"),
}
}
#[test]
fn test_parse_valuetype_enum_argument() {
let test_registry = get_test_type_registry();
let method =
create_constructor_with_params_and_registry(vec![CilFlavor::ValueType], &test_registry);
let blob_data = &[
0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, ];
let result =
parse_custom_attribute_data_with_registry(blob_data, &method.params, &test_registry)
.unwrap();
assert_eq!(result.fixed_args.len(), 1);
match &result.fixed_args[0] {
CustomAttributeArgument::Enum(type_name, boxed_val) => {
assert!(type_name == "Unknown" || type_name == "System.TestEnum");
match boxed_val.as_ref() {
CustomAttributeArgument::I4(val) => assert_eq!(*val, 1),
_ => panic!("Expected I4 in enum"),
}
}
_ => panic!("Expected Enum argument"),
}
}
#[test]
fn test_parse_void_argument() {
let method = create_constructor_with_params(vec![CilFlavor::Void]);
let blob_data = &[
0x01, 0x00, 0x00, 0x00, ];
let result = parse_custom_attribute_data(blob_data, &method.params).unwrap();
assert_eq!(result.fixed_args.len(), 1);
match &result.fixed_args[0] {
CustomAttributeArgument::Void => (),
_ => panic!("Expected Void argument"),
}
}
#[test]
fn test_parse_array_argument_error() {
let method = create_constructor_with_params(vec![CilFlavor::Array {
element_type: Box::new(CilFlavor::I4),
rank: 1,
dimensions: vec![],
}]);
let blob_data = &[
0x01, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, ];
let result = parse_custom_attribute_data(blob_data, &method.params);
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("Array type has no base element type information"));
}
#[test]
fn test_parse_simple_array_argument() {
let test_identity = AssemblyIdentity::parse("TestAssembly, Version=1.0.0.0").unwrap();
let type_registry = Arc::new(TypeRegistry::new(test_identity).unwrap());
let array_type = TypeBuilder::new(type_registry.clone())
.primitive(CilPrimitiveKind::I4)
.unwrap()
.array()
.unwrap()
.build()
.unwrap();
let method = create_empty_constructor();
let param = Arc::new(Param {
rid: 1,
token: Token::new(0x08000001),
offset: 0,
flags: 0,
sequence: 1,
name: Some("arrayParam".to_string()),
default: OnceLock::new(),
marshal: OnceLock::new(),
modifiers: Arc::new(boxcar::Vec::new()),
base: OnceLock::new(),
is_by_ref: std::sync::atomic::AtomicBool::new(false),
custom_attributes: Arc::new(boxcar::Vec::new()),
});
param.base.set(CilTypeRef::from(array_type)).ok();
method.params.push(param);
let blob_data = &[
0x01, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, ];
let result = parse_custom_attribute_data(blob_data, &method.params).unwrap();
assert_eq!(result.fixed_args.len(), 1);
match &result.fixed_args[0] {
CustomAttributeArgument::Array(elements) => {
assert_eq!(elements.len(), 3);
match &elements[0] {
CustomAttributeArgument::I4(val) => assert_eq!(*val, 1),
_ => panic!("Expected I4 element"),
}
match &elements[1] {
CustomAttributeArgument::I4(val) => assert_eq!(*val, 2),
_ => panic!("Expected I4 element"),
}
match &elements[2] {
CustomAttributeArgument::I4(val) => assert_eq!(*val, 3),
_ => panic!("Expected I4 element"),
}
}
_ => panic!("Expected Array argument"),
}
static TYPE_REGISTRIES: OnceLock<Mutex<HashMap<u64, Arc<TypeRegistry>>>> = OnceLock::new();
static COUNTER: AtomicU64 = AtomicU64::new(1);
let registries = TYPE_REGISTRIES.get_or_init(|| Mutex::new(HashMap::new()));
let mut registries_lock = registries.lock().unwrap();
let key = COUNTER.fetch_add(1, Ordering::SeqCst);
registries_lock.insert(key, type_registry);
}
#[test]
fn test_parse_multidimensional_array_error() {
let method = create_constructor_with_params(vec![CilFlavor::Array {
element_type: Box::new(CilFlavor::I4),
rank: 2,
dimensions: vec![],
}]);
let blob_data = &[
0x01, 0x00, 0x00, 0x00, ];
let result = parse_custom_attribute_data(blob_data, &method.params);
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("Multi-dimensional arrays not supported"));
}
#[test]
fn test_parse_named_arguments() {
let method = create_empty_constructor();
let blob_data = &[
0x01, 0x00, 0x02, 0x00, 0x53, 0x08, 0x05, 0x56, 0x61, 0x6C, 0x75, 0x65, 0x2A, 0x00, 0x00, 0x00, 0x54, 0x0E, 0x04, 0x4E, 0x61, 0x6D, 0x65, 0x04, 0x54, 0x65, 0x73, 0x74, ];
let result = parse_custom_attribute_data(blob_data, &method.params).unwrap();
assert_eq!(result.fixed_args.len(), 0);
assert_eq!(result.named_args.len(), 2);
let field_arg = &result.named_args[0];
assert!(field_arg.is_field);
assert_eq!(field_arg.name, "Value");
assert_eq!(field_arg.arg_type, "I4");
match &field_arg.value {
CustomAttributeArgument::I4(val) => assert_eq!(*val, 42),
_ => panic!("Expected I4 value"),
}
let prop_arg = &result.named_args[1];
assert!(!prop_arg.is_field);
assert_eq!(prop_arg.name, "Name");
assert_eq!(prop_arg.arg_type, "String");
match &prop_arg.value {
CustomAttributeArgument::String(val) => assert_eq!(val, "Test"),
_ => panic!("Expected String value"),
}
}
#[test]
fn test_parse_named_argument_char_type() {
let method = create_empty_constructor();
let blob_data = &[
0x01, 0x00, 0x01, 0x00, 0x53, 0x03, 0x06, 0x4C, 0x65, 0x74, 0x74, 0x65, 0x72, 0x5A, 0x00, ];
let result = parse_custom_attribute_data(blob_data, &method.params).unwrap();
assert_eq!(result.named_args.len(), 1);
let named_arg = &result.named_args[0];
assert_eq!(named_arg.arg_type, "Char");
match &named_arg.value {
CustomAttributeArgument::Char(val) => assert_eq!(*val, 'Z'),
_ => panic!("Expected Char value"),
}
}
#[test]
fn test_parse_invalid_named_argument_type() {
let method = create_empty_constructor();
let blob_data = &[
0x01, 0x00, 0x01, 0x00, 0x99, 0x08, 0x04, 0x54, 0x65, 0x73, 0x74, ];
let result = parse_custom_attribute_data(blob_data, &method.params);
assert!(result.is_err());
if let Err(e) = result {
assert!(e.to_string().contains("Invalid field/property indicator"));
}
}
#[test]
fn test_parse_malformed_data_errors() {
let method = create_constructor_with_params(vec![CilFlavor::I4]);
let blob_data = &[
0x01, 0x00, 0x00, 0x00, ];
let result = parse_custom_attribute_data(blob_data, &method.params);
assert!(result.is_err());
let error_msg = result.unwrap_err().to_string();
assert!(
error_msg.contains("data")
|| error_msg.contains("I4")
|| error_msg.contains("enough")
|| error_msg.contains("Out of Bound")
|| error_msg.contains("bound"),
"Error should mention data, I4, or bound issue: {error_msg}"
);
let method_string = create_constructor_with_params(vec![CilFlavor::String]);
let blob_data = &[
0x01, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0x0F, ];
let result = parse_custom_attribute_data(blob_data, &method_string.params);
assert!(result.is_err());
}
#[test]
fn test_parse_mixed_fixed_and_named_arguments() {
let method = create_constructor_with_params(vec![CilFlavor::I4, CilFlavor::String]);
let blob_data = &[
0x01, 0x00, 0x2A, 0x00, 0x00, 0x00, 0x05, 0x48, 0x65, 0x6C, 0x6C, 0x6F, 0x01, 0x00, 0x54, 0x02, 0x07, 0x45, 0x6E, 0x61, 0x62, 0x6C, 0x65, 0x64, 0x01, ];
let result = parse_custom_attribute_data(blob_data, &method.params).unwrap();
assert_eq!(result.fixed_args.len(), 2);
assert_eq!(result.named_args.len(), 1);
match &result.fixed_args[0] {
CustomAttributeArgument::I4(val) => assert_eq!(*val, 42),
_ => panic!("Expected I4 argument"),
}
match &result.fixed_args[1] {
CustomAttributeArgument::String(val) => assert_eq!(val, "Hello"),
_ => panic!("Expected String argument"),
}
let named_arg = &result.named_args[0];
assert!(!named_arg.is_field);
assert_eq!(named_arg.name, "Enabled");
assert_eq!(named_arg.arg_type, "Boolean");
match &named_arg.value {
CustomAttributeArgument::Bool(val) => assert!(*val),
_ => panic!("Expected Boolean value"),
}
}
#[test]
fn test_parse_utf16_edge_cases() {
let method = create_constructor_with_params(vec![CilFlavor::Char]);
let blob_data = &[
0x01, 0x00, 0x00, 0xD8, 0x00, 0x00, ];
let result = parse_custom_attribute_data(blob_data, &method.params).unwrap();
assert_eq!(result.fixed_args.len(), 1);
match &result.fixed_args[0] {
CustomAttributeArgument::Char(val) => assert_eq!(*val, '\u{FFFD}'), _ => panic!("Expected Char argument"),
}
}
#[test]
fn test_unsupported_type_flavor_error() {
let method = create_constructor_with_params(vec![CilFlavor::Pointer]);
let blob_data = &[
0x01, 0x00, 0x00, 0x00, ];
let result = parse_custom_attribute_data(blob_data, &method.params);
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("Unsupported type flavor in custom attribute"));
}
#[test]
fn test_empty_string_argument() {
let method = create_constructor_with_params(vec![CilFlavor::String]);
let blob_data = &[
0x01, 0x00, 0x00, 0x00, 0x00, ];
let result = parse_custom_attribute_data(blob_data, &method.params).unwrap();
assert_eq!(result.fixed_args.len(), 1);
match &result.fixed_args[0] {
CustomAttributeArgument::String(val) => assert_eq!(val, ""),
_ => panic!("Expected String argument"),
}
}
#[test]
fn test_parse_unsupported_named_argument_type() {
let method = create_empty_constructor();
let blob_data = &[
0x01, 0x00, 0x01, 0x00, 0x53, 0xFF, 0x04, 0x54, 0x65, 0x73, 0x74, ];
let result = parse_custom_attribute_data(blob_data, &method.params);
assert!(result.is_err());
if let Err(e) = result {
assert!(e
.to_string()
.contains("Unsupported named argument type: 0xFF"));
}
}
}