use crate::{
metadata::{
identity::Identity,
validation::{
context::{OwnedValidationContext, ValidationContext},
traits::OwnedValidator,
},
},
Error, Result,
};
use std::sync::Arc;
pub struct OwnedAssemblyValidator;
impl OwnedAssemblyValidator {
#[must_use]
pub fn new() -> Self {
Self
}
}
impl OwnedAssemblyValidator {
fn validate_assembly_metadata_consistency(
&self,
context: &OwnedValidationContext,
) -> Result<()> {
let assembly_info = context.object().assembly();
if let Some(assembly) = assembly_info {
if assembly.name.is_empty() {
return Err(Error::ValidationOwnedFailed {
validator: self.name().to_string(),
message: "Assembly has empty name".to_string(),
});
}
if !Self::is_valid_assembly_name(&assembly.name) {
return Err(Error::ValidationOwnedFailed {
validator: self.name().to_string(),
message: format!("Assembly has invalid name format: '{}'", assembly.name),
});
}
self.validate_assembly_version(assembly)?;
if let Some(culture) = &assembly.culture {
if !Self::is_valid_culture_format(culture) {
return Err(Error::ValidationOwnedFailed {
validator: self.name().to_string(),
message: format!("Assembly has invalid culture format: '{culture}'"),
});
}
}
if let Some(public_key) = &assembly.public_key {
self.validate_assembly_public_key(public_key)?;
}
self.validate_assembly_custom_attributes(assembly)?;
}
Ok(())
}
fn is_valid_assembly_name(name: &str) -> bool {
if name.is_empty() || name.len() > 260 {
return false;
}
let invalid_chars = ['/', '\\', ':', '*', '?', '"', '<', '>', '|'];
if name.chars().any(|c| invalid_chars.contains(&c)) {
return false;
}
if name.starts_with(' ') || name.starts_with('.') {
return false;
}
true
}
fn validate_assembly_version(
&self,
assembly: &Arc<crate::metadata::tables::Assembly>,
) -> Result<()> {
if assembly.major_version > 65535
|| assembly.minor_version > 65535
|| assembly.build_number > 65535
|| assembly.revision_number > 65535
{
return Err(Error::ValidationOwnedFailed {
validator: self.name().to_string(),
message: format!(
"Assembly '{}' has invalid version components: {}.{}.{}.{}",
assembly.name,
assembly.major_version,
assembly.minor_version,
assembly.build_number,
assembly.revision_number
),
});
}
Ok(())
}
fn is_valid_culture_format(culture: &str) -> bool {
if culture.is_empty() || culture == "neutral" {
return true;
}
let parts: Vec<&str> = culture.split('-').collect();
match parts.len() {
1 => {
parts[0].len() == 2 && parts[0].chars().all(|c| c.is_ascii_lowercase())
}
2 => {
parts[0].len() == 2
&& parts[0].chars().all(|c| c.is_ascii_lowercase())
&& parts[1].len() == 2
&& parts[1].chars().all(|c| c.is_ascii_uppercase())
}
_ => false,
}
}
fn validate_assembly_public_key(&self, public_key: &[u8]) -> Result<()> {
if public_key.is_empty() {
return Ok(()); }
if public_key.len() != 16 && (public_key.len() < 160 || public_key.len() > 2048) {
return Err(Error::ValidationOwnedFailed {
validator: self.name().to_string(),
message: format!(
"Assembly public key has invalid size: {} bytes. Expected 16 bytes (ECMA key) or 160-2048 bytes (RSA key)",
public_key.len()
),
});
}
if public_key.iter().all(|&b| b == 0) {
return Err(Error::ValidationOwnedFailed {
validator: self.name().to_string(),
message: "Assembly public key consists entirely of zero bytes".to_string(),
});
}
if public_key.iter().all(|&b| b == 0xFF) {
return Err(Error::ValidationOwnedFailed {
validator: self.name().to_string(),
message: "Assembly public key consists entirely of 0xFF bytes".to_string(),
});
}
Ok(())
}
fn validate_assembly_custom_attributes(
&self,
assembly: &Arc<crate::metadata::tables::Assembly>,
) -> Result<()> {
for (_, custom_attr) in assembly.custom_attributes.iter() {
if custom_attr.fixed_args.len() > 20 {
return Err(Error::ValidationOwnedFailed {
validator: self.name().to_string(),
message: format!(
"Assembly '{}' has custom attribute with excessive fixed arguments ({})",
assembly.name,
custom_attr.fixed_args.len()
),
});
}
if custom_attr.named_args.len() > 50 {
return Err(Error::ValidationOwnedFailed {
validator: self.name().to_string(),
message: format!(
"Assembly '{}' has custom attribute with excessive named arguments ({})",
assembly.name,
custom_attr.named_args.len()
),
});
}
}
Ok(())
}
fn validate_cross_assembly_references(&self, context: &OwnedValidationContext) -> Result<()> {
if let Some(assembly) = context.object().assembly() {
if assembly.name.len() > 1024 {
return Err(Error::ValidationOwnedFailed {
validator: self.name().to_string(),
message: format!(
"Assembly name is excessively long: {} characters",
assembly.name.len()
),
});
}
if let Some(culture) = &assembly.culture {
if !Self::is_valid_culture_format(culture) {
return Err(Error::ValidationOwnedFailed {
validator: self.name().to_string(),
message: format!("Assembly has invalid culture format: '{culture}'"),
});
}
}
}
let assembly_refs = context.object().refs_assembly();
for (index, entry) in assembly_refs.iter().enumerate() {
let assembly_ref = entry.value();
if assembly_ref.name.is_empty() {
return Err(Error::ValidationOwnedFailed {
validator: self.name().to_string(),
message: format!("Assembly reference {index} has empty name"),
});
}
if assembly_ref.name.len() > 1024 {
return Err(Error::ValidationOwnedFailed {
validator: self.name().to_string(),
message: format!(
"Assembly reference '{}' has excessively long name: {} characters",
assembly_ref.name,
assembly_ref.name.len()
),
});
}
if let Some(culture) = &assembly_ref.culture {
if !Self::is_valid_culture_format(culture) {
return Err(Error::ValidationOwnedFailed {
validator: self.name().to_string(),
message: format!(
"Assembly reference '{}' has invalid culture format: '{}'",
assembly_ref.name, culture
),
});
}
}
if let Some(identity) = &assembly_ref.identifier {
match identity {
Identity::Token(token) => {
if *token == 0 {
return Err(Error::ValidationOwnedFailed {
validator: self.name().to_string(),
message: format!(
"Assembly reference '{}' has empty public key token",
assembly_ref.name
),
});
}
}
Identity::PubKey(public_key) => {
if public_key.is_empty() {
return Err(Error::ValidationOwnedFailed {
validator: self.name().to_string(),
message: format!(
"Assembly reference '{}' has empty public key",
assembly_ref.name
),
});
}
if public_key.len() != 16
&& (public_key.len() < 160 || public_key.len() > 2048)
{
return Err(Error::ValidationOwnedFailed {
validator: self.name().to_string(),
message: format!(
"Assembly reference '{}' has invalid public key size: {} bytes. Expected 16 bytes (ECMA key) or 160-2048 bytes (RSA key)",
assembly_ref.name,
public_key.len()
),
});
}
}
Identity::EcmaKey(ecma_key) => {
if ecma_key.len() != 16 {
return Err(Error::ValidationOwnedFailed {
validator: self.name().to_string(),
message: format!(
"Assembly reference '{}' has invalid ECMA key size: {} bytes. Expected exactly 16 bytes",
assembly_ref.name,
ecma_key.len()
),
});
}
if ecma_key.is_empty() || ecma_key.iter().all(|&b| b == 0) {
return Err(Error::ValidationOwnedFailed {
validator: self.name().to_string(),
message: format!(
"Assembly reference '{}' has empty or invalid ECMA key",
assembly_ref.name
),
});
}
}
}
}
}
for type_entry in context.target_assembly_types() {
let type_ref = type_entry.as_ref();
if let Some(_external) = type_ref.get_external() {
if type_ref.name.is_empty() {
return Err(Error::ValidationOwnedFailed {
validator: self.name().to_string(),
message: "Cross-assembly type reference has empty name".to_string(),
});
}
if type_ref.namespace.len() > 512 {
return Err(Error::ValidationOwnedFailed {
validator: self.name().to_string(),
message: format!(
"Type reference '{}' has excessively long namespace: {} characters",
type_ref.name,
type_ref.namespace.len()
),
});
}
}
}
let member_refs = context.object().refs_members();
for entry in member_refs {
let member_ref = entry.value();
if member_ref.name.is_empty() {
return Err(Error::ValidationOwnedFailed {
validator: self.name().to_string(),
message: "Cross-assembly member reference has empty name".to_string(),
});
}
if member_ref.name.len() > 512 {
return Err(Error::ValidationOwnedFailed {
validator: self.name().to_string(),
message: format!(
"Member reference '{}' has excessively long name: {} characters",
member_ref.name,
member_ref.name.len()
),
});
}
}
Ok(())
}
fn validate_assembly_version_compatibility(
&self,
context: &OwnedValidationContext,
) -> Result<()> {
if let Some(assembly) = context.object().assembly() {
if assembly.major_version > 999
|| assembly.minor_version > 999
|| assembly.build_number > 65535
|| assembly.revision_number > 65535
{
return Err(Error::ValidationOwnedFailed {
validator: self.name().to_string(),
message: format!(
"Assembly '{}' has suspicious version numbers: {}.{}.{}.{}",
assembly.name,
assembly.major_version,
assembly.minor_version,
assembly.build_number,
assembly.revision_number
),
});
}
}
let assembly_refs = context.object().refs_assembly();
for entry in assembly_refs {
let assembly_ref = entry.value();
if assembly_ref.major_version > 999
|| assembly_ref.minor_version > 999
|| assembly_ref.build_number > 65535
|| assembly_ref.revision_number > 65535
{
return Err(Error::ValidationOwnedFailed {
validator: self.name().to_string(),
message: format!(
"Assembly reference '{}' has suspicious version numbers: {}.{}.{}.{}",
assembly_ref.name,
assembly_ref.major_version,
assembly_ref.minor_version,
assembly_ref.build_number,
assembly_ref.revision_number
),
});
}
if let Some(identity) = &assembly_ref.identifier {
match identity {
Identity::Token(token) => {
if *token == 0 {
return Err(Error::ValidationOwnedFailed {
validator: self.name().to_string(),
message: format!(
"Assembly reference '{}' has zero public key token",
assembly_ref.name
),
});
}
}
Identity::PubKey(public_key) => {
if public_key.iter().all(|&b| b == 0) {
return Err(Error::ValidationOwnedFailed {
validator: self.name().to_string(),
message: format!(
"Assembly reference '{}' public key consists entirely of zero bytes",
assembly_ref.name
),
});
}
}
Identity::EcmaKey(ecma_key) => {
if ecma_key.iter().all(|&b| b == 0) {
return Err(Error::ValidationOwnedFailed {
validator: self.name().to_string(),
message: format!(
"Assembly reference '{}' ECMA key consists entirely of zero bytes",
assembly_ref.name
),
});
}
}
}
}
if assembly_ref.flags > 0x0001 {
return Err(Error::ValidationOwnedFailed {
validator: self.name().to_string(),
message: format!(
"Assembly reference '{}' has unknown flags: 0x{:08X}",
assembly_ref.name, assembly_ref.flags
),
});
}
}
Ok(())
}
fn validate_module_file_consistency(&self, context: &OwnedValidationContext) -> Result<()> {
if let Some(assembly) = context.object().assembly() {
if assembly.flags > 0x0001 {
return Err(Error::ValidationOwnedFailed {
validator: self.name().to_string(),
message: format!(
"Assembly '{}' has unknown flags: 0x{:08X}",
assembly.name, assembly.flags
),
});
}
if assembly.hash_alg_id != 0
&& assembly.hash_alg_id != 0x8003
&& assembly.hash_alg_id != 0x8004
&& assembly.hash_alg_id != 0x800C
{
return Err(Error::ValidationOwnedFailed {
validator: self.name().to_string(),
message: format!(
"Assembly '{}' has unknown hash algorithm: 0x{:08X}",
assembly.name, assembly.hash_alg_id
),
});
}
}
if let Some(module) = context.object().module() {
let index = 0;
if module.name.is_empty() {
return Err(Error::ValidationOwnedFailed {
validator: self.name().to_string(),
message: format!("Module {index} has empty name"),
});
}
if module.name.len() > 260 {
return Err(Error::ValidationOwnedFailed {
validator: self.name().to_string(),
message: format!(
"Module '{}' has excessively long name: {} characters",
module.name,
module.name.len()
),
});
}
if module.generation > 65535 {
return Err(Error::ValidationOwnedFailed {
validator: self.name().to_string(),
message: format!(
"Module '{}' has suspicious generation number: {}",
module.name, module.generation
),
});
}
if module.mvid.to_bytes().iter().all(|&b| b == 0) {
return Err(Error::ValidationOwnedFailed {
validator: self.name().to_string(),
message: format!(
"Module '{}' has all-zero MVID (Module Version ID)",
module.name
),
});
}
}
let file = context.object().file();
let file_data = file.data();
if file_data.len() < 1024 {
return Err(Error::ValidationOwnedFailed {
validator: self.name().to_string(),
message: "Assembly file is suspiciously small (< 1024 bytes)".to_string(),
});
}
if file_data.len() > 100_000_000 {
return Err(Error::ValidationOwnedFailed {
validator: self.name().to_string(),
message: format!(
"Assembly file is excessively large: {} bytes",
file_data.len()
),
});
}
Ok(())
}
}
impl OwnedValidator for OwnedAssemblyValidator {
fn validate_owned(&self, context: &OwnedValidationContext) -> Result<()> {
self.validate_assembly_metadata_consistency(context)?;
self.validate_cross_assembly_references(context)?;
self.validate_assembly_version_compatibility(context)?;
self.validate_module_file_consistency(context)?;
Ok(())
}
fn name(&self) -> &'static str {
"OwnedAssemblyValidator"
}
fn priority(&self) -> u32 {
110
}
fn should_run(&self, context: &OwnedValidationContext) -> bool {
context.config().enable_semantic_validation
}
}
impl Default for OwnedAssemblyValidator {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
#[cfg_attr(feature = "skip-expensive-tests", allow(unused_imports))]
mod tests {
use super::*;
use crate::{
metadata::validation::ValidationConfig,
test::{
factories::validation::system_assembly::owned_assembly_validator_file_factory,
owned_validator_test,
},
};
#[test]
#[cfg(not(feature = "skip-expensive-tests"))]
fn test_owned_assembly_validator() -> Result<()> {
let validator = OwnedAssemblyValidator::new();
let config = ValidationConfig {
enable_semantic_validation: true,
..Default::default()
};
owned_validator_test(
owned_assembly_validator_file_factory,
"OwnedAssemblyValidator",
"ValidationOwnedFailed",
config,
|context| validator.validate_owned(context),
)
}
}