use crate::{
cilassembly::{ChangeRefRc, CilAssembly},
metadata::{
signatures::{
encode_field_signature, encode_property_signature, SignatureField, SignatureParameter,
SignatureProperty, TypeSignature,
},
tables::{FieldBuilder, PropertyBuilder as PropertyTableBuilder},
},
Error, Result,
};
use super::method::MethodBuilder;
#[derive(Debug, Clone, PartialEq)]
pub enum PropertyAccessors {
GetterAndSetter,
GetterOnly,
SetterOnly,
None,
}
pub enum PropertyImplementation {
Auto {
backing_field_name: Option<String>,
backing_field_attributes: u32,
},
Computed {
getter: Option<Box<dyn FnOnce(MethodBuilder) -> MethodBuilder + Send>>,
setter: Option<Box<dyn FnOnce(MethodBuilder) -> MethodBuilder + Send>>,
},
Manual,
}
pub struct PropertyBuilder {
name: String,
property_type: TypeSignature,
attributes: u32,
accessors: PropertyAccessors,
getter_attributes: u32,
setter_attributes: u32,
implementation: PropertyImplementation,
is_indexed: bool,
parameters: Vec<(String, TypeSignature)>,
}
impl PropertyBuilder {
#[must_use]
pub fn new(name: &str, property_type: TypeSignature) -> Self {
Self {
name: name.to_string(),
property_type,
attributes: 0x0000, accessors: PropertyAccessors::GetterAndSetter,
getter_attributes: 0x0006, setter_attributes: 0x0006, implementation: PropertyImplementation::Auto {
backing_field_name: None,
backing_field_attributes: 0x0001, },
is_indexed: false,
parameters: Vec::new(),
}
}
#[must_use]
pub fn auto_property(mut self) -> Self {
self.implementation = PropertyImplementation::Auto {
backing_field_name: None,
backing_field_attributes: 0x0001, };
self
}
#[must_use]
pub fn computed(mut self) -> Self {
self.implementation = PropertyImplementation::Computed {
getter: None,
setter: None,
};
self
}
#[must_use]
pub fn manual(mut self) -> Self {
self.implementation = PropertyImplementation::Manual;
self
}
#[must_use]
pub fn backing_field(mut self, field_name: &str) -> Self {
if let PropertyImplementation::Auto {
backing_field_name, ..
} = &mut self.implementation
{
*backing_field_name = Some(field_name.to_string());
}
self
}
#[must_use]
pub fn private_backing_field(mut self) -> Self {
if let PropertyImplementation::Auto {
backing_field_attributes,
..
} = &mut self.implementation
{
*backing_field_attributes = 0x0001; }
self
}
#[must_use]
pub fn public_backing_field(mut self) -> Self {
if let PropertyImplementation::Auto {
backing_field_attributes,
..
} = &mut self.implementation
{
*backing_field_attributes = 0x0006; }
self
}
#[must_use]
pub fn readonly(mut self) -> Self {
self.accessors = PropertyAccessors::GetterOnly;
self
}
#[must_use]
pub fn writeonly(mut self) -> Self {
self.accessors = PropertyAccessors::SetterOnly;
self
}
#[must_use]
pub fn getter_and_setter(mut self) -> Self {
self.accessors = PropertyAccessors::GetterAndSetter;
self
}
#[must_use]
pub fn public_accessors(mut self) -> Self {
self.getter_attributes = 0x0006; self.setter_attributes = 0x0006; self
}
#[must_use]
pub fn private_accessors(mut self) -> Self {
self.getter_attributes = 0x0001; self.setter_attributes = 0x0001; self
}
#[must_use]
pub fn getter_visibility(mut self, attributes: u32) -> Self {
self.getter_attributes = attributes;
self
}
#[must_use]
pub fn setter_visibility(mut self, attributes: u32) -> Self {
self.setter_attributes = attributes;
self
}
#[must_use]
pub fn getter<F>(mut self, implementation: F) -> Self
where
F: FnOnce(MethodBuilder) -> MethodBuilder + Send + 'static,
{
if let PropertyImplementation::Computed { getter, .. } = &mut self.implementation {
*getter = Some(Box::new(implementation));
}
self
}
#[must_use]
pub fn setter<F>(mut self, implementation: F) -> Self
where
F: FnOnce(MethodBuilder) -> MethodBuilder + Send + 'static,
{
if let PropertyImplementation::Computed { setter, .. } = &mut self.implementation {
*setter = Some(Box::new(implementation));
}
self
}
#[must_use]
pub fn indexed(mut self, param_name: &str, param_type: TypeSignature) -> Self {
self.is_indexed = true;
self.parameters.push((param_name.to_string(), param_type));
self
}
#[must_use]
pub fn parameter(mut self, param_name: &str, param_type: TypeSignature) -> Self {
self.parameters.push((param_name.to_string(), param_type));
self
}
#[must_use]
pub fn attributes(mut self, attributes: u32) -> Self {
self.attributes = attributes;
self
}
pub fn build(self, assembly: &mut CilAssembly) -> Result<ChangeRefRc> {
let mut signature_params = Vec::new();
for (_param_name, param_type) in &self.parameters {
signature_params.push(SignatureParameter {
modifiers: Vec::new(),
by_ref: false,
base: param_type.clone(),
});
}
let property_signature = SignatureProperty {
has_this: true, modifiers: Vec::new(),
base: self.property_type.clone(),
params: signature_params,
};
let signature_bytes = encode_property_signature(&property_signature)?;
let property_ref = PropertyTableBuilder::new()
.name(&self.name)
.flags(self.attributes)
.signature(&signature_bytes)
.build(assembly)?;
match self.implementation {
PropertyImplementation::Auto {
backing_field_name,
backing_field_attributes,
} => {
let field_name =
backing_field_name.unwrap_or_else(|| format!("<{}>k__BackingField", self.name));
let field_sig = SignatureField {
modifiers: Vec::new(),
base: self.property_type.clone(),
};
let sig_bytes = encode_field_signature(&field_sig)?;
let backing_field_ref = FieldBuilder::new()
.name(&field_name)
.flags(backing_field_attributes)
.signature(&sig_bytes)
.build(assembly)?;
let backing_field_placeholder =
backing_field_ref.placeholder_token().ok_or_else(|| {
Error::ModificationInvalid(
"Failed to get placeholder token for backing field".to_string(),
)
})?;
if matches!(
self.accessors,
PropertyAccessors::GetterAndSetter | PropertyAccessors::GetterOnly
) {
let getter_field_token = backing_field_placeholder; let getter_name = self.name.clone();
let getter_type = self.property_type.clone();
let getter_visibility = self.getter_attributes;
let getter = MethodBuilder::property_getter(&getter_name, getter_type);
let getter = match getter_visibility {
0x0001 => getter.private(),
_ => getter.public(),
};
getter
.implementation(move |body| {
body.implementation(move |asm| {
asm.ldarg_0()? .ldfld(getter_field_token)? .ret()?;
Ok(())
})
})
.build(assembly)?;
}
if matches!(
self.accessors,
PropertyAccessors::GetterAndSetter | PropertyAccessors::SetterOnly
) {
let setter_field_token = backing_field_placeholder; let setter_name = self.name.clone();
let setter_type = self.property_type.clone();
let setter_visibility = self.setter_attributes;
let setter = MethodBuilder::property_setter(&setter_name, setter_type);
let setter = match setter_visibility {
0x0001 => setter.private(),
_ => setter.public(),
};
setter
.implementation(move |body| {
body.implementation(move |asm| {
asm.ldarg_0()? .ldarg_1()? .stfld(setter_field_token)? .ret()?;
Ok(())
})
})
.build(assembly)?;
}
Ok(property_ref)
}
PropertyImplementation::Computed { getter, setter } => {
if matches!(
self.accessors,
PropertyAccessors::GetterAndSetter | PropertyAccessors::GetterOnly
) {
if let Some(getter_impl) = getter {
let getter_method =
MethodBuilder::property_getter(&self.name, self.property_type.clone());
let getter_method = match self.getter_attributes {
0x0001 => getter_method.private(),
_ => getter_method.public(),
};
let configured_getter = getter_impl(getter_method);
configured_getter.build(assembly)?;
} else {
return Err(Error::ModificationInvalid(
"Computed property requires getter implementation".to_string(),
));
}
}
if matches!(
self.accessors,
PropertyAccessors::GetterAndSetter | PropertyAccessors::SetterOnly
) {
if let Some(setter_impl) = setter {
let setter_method =
MethodBuilder::property_setter(&self.name, self.property_type.clone());
let setter_method = match self.setter_attributes {
0x0001 => setter_method.private(),
_ => setter_method.public(),
};
let configured_setter = setter_impl(setter_method);
configured_setter.build(assembly)?;
} else {
return Err(Error::ModificationInvalid(
"Computed property requires setter implementation".to_string(),
));
}
}
Ok(property_ref)
}
PropertyImplementation::Manual => {
Ok(property_ref)
}
}
}
}
impl Default for PropertyBuilder {
fn default() -> Self {
Self::new("DefaultProperty", TypeSignature::Object)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
cilassembly::{ChangeRefKind, CilAssembly},
metadata::{cilassemblyview::CilAssemblyView, signatures::TypeSignature, tables::TableId},
};
use std::path::PathBuf;
fn get_test_assembly() -> Result<CilAssembly> {
let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/samples/WindowsBase.dll");
let view = CilAssemblyView::from_path(&path)?;
Ok(CilAssembly::new(view))
}
#[test]
fn test_simple_auto_property() -> Result<()> {
let mut assembly = get_test_assembly()?;
let property_ref = PropertyBuilder::new("Name", TypeSignature::String)
.auto_property()
.public_accessors()
.build(&mut assembly)?;
assert_eq!(
property_ref.kind(),
ChangeRefKind::TableRow(TableId::Property)
);
Ok(())
}
#[test]
fn test_readonly_auto_property() -> Result<()> {
let mut assembly = get_test_assembly()?;
let property_ref = PropertyBuilder::new("ReadOnlyValue", TypeSignature::I4)
.auto_property()
.readonly()
.public_accessors()
.build(&mut assembly)?;
assert_eq!(
property_ref.kind(),
ChangeRefKind::TableRow(TableId::Property)
);
Ok(())
}
#[test]
fn test_computed_property() -> Result<()> {
let mut assembly = get_test_assembly()?;
let property_ref = PropertyBuilder::new("ComputedValue", TypeSignature::I4)
.computed()
.getter(|method| {
method.implementation(|body| {
body.implementation(|asm| {
asm.ldc_i4(42)?.ret()?;
Ok(())
})
})
})
.readonly()
.build(&mut assembly)?;
assert_eq!(
property_ref.kind(),
ChangeRefKind::TableRow(TableId::Property)
);
Ok(())
}
#[test]
fn test_custom_backing_field() -> Result<()> {
let mut assembly = get_test_assembly()?;
let property_ref = PropertyBuilder::new("Value", TypeSignature::R8)
.auto_property()
.backing_field("_customValue")
.private_backing_field()
.public_accessors()
.build(&mut assembly)?;
assert_eq!(
property_ref.kind(),
ChangeRefKind::TableRow(TableId::Property)
);
Ok(())
}
#[test]
fn test_indexed_property() -> Result<()> {
let mut assembly = get_test_assembly()?;
let property_ref = PropertyBuilder::new("Item", TypeSignature::String)
.auto_property()
.indexed("index", TypeSignature::I4)
.public_accessors()
.build(&mut assembly)?;
assert_eq!(
property_ref.kind(),
ChangeRefKind::TableRow(TableId::Property)
);
Ok(())
}
#[test]
fn test_multi_parameter_indexed_property() -> Result<()> {
let mut assembly = get_test_assembly()?;
let property_ref = PropertyBuilder::new("Matrix", TypeSignature::I4)
.auto_property()
.indexed("row", TypeSignature::I4)
.parameter("column", TypeSignature::I4)
.public_accessors()
.build(&mut assembly)?;
assert_eq!(
property_ref.kind(),
ChangeRefKind::TableRow(TableId::Property)
);
Ok(())
}
#[test]
fn test_writeonly_property() -> Result<()> {
let mut assembly = get_test_assembly()?;
let property_ref = PropertyBuilder::new("WriteOnly", TypeSignature::String)
.auto_property()
.writeonly()
.public_accessors()
.build(&mut assembly)?;
assert_eq!(
property_ref.kind(),
ChangeRefKind::TableRow(TableId::Property)
);
Ok(())
}
#[test]
fn test_manual_property() -> Result<()> {
let mut assembly = get_test_assembly()?;
let property_ref = PropertyBuilder::new("Manual", TypeSignature::Object)
.manual()
.build(&mut assembly)?;
assert_eq!(
property_ref.kind(),
ChangeRefKind::TableRow(TableId::Property)
);
Ok(())
}
#[test]
fn test_property_with_different_accessor_visibility() -> Result<()> {
let mut assembly = get_test_assembly()?;
let property_ref = PropertyBuilder::new("MixedVisibility", TypeSignature::I4)
.auto_property()
.getter_visibility(0x0006) .setter_visibility(0x0001) .build(&mut assembly)?;
assert_eq!(
property_ref.kind(),
ChangeRefKind::TableRow(TableId::Property)
);
Ok(())
}
#[test]
fn test_computed_property_missing_getter_fails() {
let mut assembly = get_test_assembly().unwrap();
let result = PropertyBuilder::new("InvalidComputed", TypeSignature::I4)
.computed()
.readonly()
.build(&mut assembly);
assert!(result.is_err());
}
#[test]
fn test_computed_property_missing_setter_fails() {
let mut assembly = get_test_assembly().unwrap();
let result = PropertyBuilder::new("InvalidComputed", TypeSignature::I4)
.computed()
.writeonly()
.build(&mut assembly);
assert!(result.is_err());
}
}