use std::{fmt, fmt::Write as _, str::FromStr, sync::atomic::Ordering};
use crate::{
metadata::{
identity::cryptographic::Identity,
tables::{Assembly, AssemblyHashAlgorithm, AssemblyRef},
},
Error, Result,
};
#[derive(Debug, Clone)]
pub struct AssemblyIdentity {
pub name: String,
pub version: AssemblyVersion,
pub culture: Option<String>,
pub strong_name: Option<Identity>,
pub processor_architecture: Option<ProcessorArchitecture>,
}
impl PartialEq for AssemblyIdentity {
fn eq(&self, other: &Self) -> bool {
self.name == other.name
&& self.version == other.version
&& self.culture == other.culture
&& self.processor_architecture == other.processor_architecture
}
}
impl Eq for AssemblyIdentity {}
impl std::hash::Hash for AssemblyIdentity {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.name.hash(state);
self.version.hash(state);
self.culture.hash(state);
self.processor_architecture.hash(state);
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct AssemblyVersion {
pub major: u16,
pub minor: u16,
pub build: u16,
pub revision: u16,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ProcessorArchitecture {
MSIL,
X86,
IA64,
AMD64,
ARM,
ARM64,
}
impl AssemblyIdentity {
pub fn new(
name: impl Into<String>,
version: AssemblyVersion,
culture: Option<String>,
strong_name: Option<Identity>,
processor_architecture: Option<ProcessorArchitecture>,
) -> Self {
Self {
name: name.into(),
version,
culture,
strong_name,
processor_architecture,
}
}
pub fn from_assembly_ref(assembly_ref: &AssemblyRef) -> Self {
let processor_value = assembly_ref.processor.load(Ordering::Relaxed);
let processor_architecture = if processor_value != 0 {
ProcessorArchitecture::try_from(processor_value).ok()
} else {
None
};
Self {
name: assembly_ref.name.clone(),
version: Self::version_from_u32(
assembly_ref.major_version,
assembly_ref.minor_version,
assembly_ref.build_number,
assembly_ref.revision_number,
),
culture: assembly_ref.culture.clone(),
strong_name: assembly_ref.identifier.clone(),
processor_architecture,
}
}
pub fn from_assembly(assembly: &Assembly) -> Self {
Self {
name: assembly.name.clone(),
version: Self::version_from_u32(
assembly.major_version,
assembly.minor_version,
assembly.build_number,
assembly.revision_number,
),
culture: assembly.culture.clone(),
strong_name: assembly
.public_key
.as_ref()
.and_then(|key| Identity::from(key, true).ok()),
processor_architecture: None,
}
}
#[inline]
fn version_from_u32(major: u32, minor: u32, build: u32, revision: u32) -> AssemblyVersion {
AssemblyVersion::new(
u16::try_from(major).unwrap_or(u16::MAX),
u16::try_from(minor).unwrap_or(u16::MAX),
u16::try_from(build).unwrap_or(u16::MAX),
u16::try_from(revision).unwrap_or(u16::MAX),
)
}
pub fn parse(display_name: &str) -> Result<Self> {
let mut version = AssemblyVersion::new(0, 0, 0, 0);
let mut culture = None;
let mut strong_name = None;
let mut processor_architecture = None;
let parts: Vec<&str> = display_name.split(',').map(str::trim).collect();
if parts.is_empty() {
return Err(malformed_error!("Empty assembly display name"));
}
let name = parts[0].to_string();
if name.is_empty() {
return Err(malformed_error!("Assembly name cannot be empty"));
}
for part in parts.iter().skip(1) {
if let Some(value) = part.strip_prefix("Version=") {
version = AssemblyVersion::parse(value)?;
} else if let Some(value) = part.strip_prefix("Culture=") {
if value != "neutral" {
culture = Some(value.to_string());
}
} else if let Some(value) = part.strip_prefix("PublicKeyToken=") {
if value != "null" && !value.is_empty() {
let token_bytes = hex::decode(value).map_err(|e| {
malformed_error!("Invalid hex in PublicKeyToken '{}': {}", value, e)
})?;
if token_bytes.len() != 8 {
return Err(malformed_error!(
"PublicKeyToken must be exactly 8 bytes (16 hex characters), got {} bytes from '{}'",
token_bytes.len(),
value
));
}
let mut token_array = [0u8; 8];
token_array.copy_from_slice(&token_bytes);
let token = u64::from_le_bytes(token_array);
strong_name = Some(Identity::Token(token));
}
} else if let Some(value) = part.strip_prefix("ProcessorArchitecture=") {
processor_architecture = Some(ProcessorArchitecture::parse(value)?);
}
}
Ok(Self {
name,
version,
culture,
strong_name,
processor_architecture,
})
}
#[must_use]
pub fn display_name(&self) -> String {
let mut result = String::with_capacity(self.name.len() + 80);
result.push_str(&self.name);
let _ = write!(result, ", Version={}", self.version);
let culture_str = self.culture.as_deref().unwrap_or("neutral");
let _ = write!(result, ", Culture={culture_str}");
result.push_str(", PublicKeyToken=");
match &self.strong_name {
Some(Identity::Token(token)) => {
let bytes = token.to_le_bytes();
let _ = write!(
result,
"{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7]
);
}
Some(identity @ (Identity::PubKey(_) | Identity::EcmaKey(_))) => {
match identity.to_token(AssemblyHashAlgorithm::SHA1) {
Ok(token) => {
let bytes = token.to_le_bytes();
let _ = write!(
result,
"{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
bytes[0],
bytes[1],
bytes[2],
bytes[3],
bytes[4],
bytes[5],
bytes[6],
bytes[7]
);
}
Err(_) => result.push_str("null"),
}
}
None => result.push_str("null"),
}
if let Some(arch) = &self.processor_architecture {
let _ = write!(result, ", ProcessorArchitecture={arch}");
}
result
}
#[must_use]
pub fn simple_name(&self) -> &str {
&self.name
}
#[must_use]
pub fn is_strong_named(&self) -> bool {
self.strong_name.is_some()
}
#[must_use]
pub fn is_culture_neutral(&self) -> bool {
self.culture.is_none()
}
#[must_use]
pub fn satisfies(&self, required: &AssemblyIdentity) -> bool {
if !self.name.eq_ignore_ascii_case(&required.name) {
return false;
}
if self.culture != required.culture {
return false;
}
self.version.is_compatible_with(&required.version)
}
}
impl AssemblyVersion {
pub const UNKNOWN: Self = Self {
major: 0,
minor: 0,
build: 0,
revision: 0,
};
#[must_use]
pub const fn new(major: u16, minor: u16, build: u16, revision: u16) -> Self {
Self {
major,
minor,
build,
revision,
}
}
#[must_use]
pub const fn is_unknown(&self) -> bool {
self.major == 0 && self.minor == 0 && self.build == 0 && self.revision == 0
}
#[must_use]
pub fn is_compatible_with(&self, required: &AssemblyVersion) -> bool {
if required.is_unknown() {
return true;
}
self.major == required.major && *self >= *required
}
#[must_use]
pub fn is_closer_to(&self, other: &AssemblyVersion, target: &AssemblyVersion) -> bool {
let self_same_major = self.major == target.major;
let other_same_major = other.major == target.major;
match (self_same_major, other_same_major) {
(true, false) => true,
(false, true) => false,
(true, true) => {
self > other
}
(false, false) => {
let self_dist = self.major.abs_diff(target.major);
let other_dist = other.major.abs_diff(target.major);
self_dist < other_dist
}
}
}
pub fn parse(version_str: &str) -> Result<Self> {
let parts: Vec<&str> = version_str.split('.').collect();
if parts.is_empty() || parts.len() > 4 {
return Err(malformed_error!("Invalid version format: {}", version_str));
}
let mut components = [0u16; 4];
for (i, part) in parts.iter().enumerate() {
components[i] = part
.parse::<u16>()
.map_err(|_| malformed_error!("Invalid version component: {}", part))?;
}
Ok(Self::new(
components[0],
components[1],
components[2],
components[3],
))
}
}
impl ProcessorArchitecture {
pub fn parse(arch_str: &str) -> Result<Self> {
match arch_str.trim().to_lowercase().as_str() {
"msil" => Ok(Self::MSIL),
"x86" => Ok(Self::X86),
"ia64" => Ok(Self::IA64),
"amd64" | "x64" => Ok(Self::AMD64),
"arm" => Ok(Self::ARM),
"arm64" => Ok(Self::ARM64),
_ => Err(malformed_error!(
"Unknown processor architecture: '{}'",
arch_str.trim()
)),
}
}
}
impl fmt::Display for AssemblyVersion {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{}.{}.{}.{}",
self.major, self.minor, self.build, self.revision
)
}
}
impl fmt::Display for ProcessorArchitecture {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let arch_str = match self {
Self::MSIL => "MSIL",
Self::X86 => "x86",
Self::IA64 => "IA64",
Self::AMD64 => "AMD64",
Self::ARM => "ARM",
Self::ARM64 => "ARM64",
};
write!(f, "{arch_str}")
}
}
impl fmt::Display for AssemblyIdentity {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.display_name())
}
}
impl FromStr for AssemblyVersion {
type Err = Error;
fn from_str(s: &str) -> Result<Self> {
Self::parse(s)
}
}
impl FromStr for AssemblyIdentity {
type Err = Error;
fn from_str(s: &str) -> Result<Self> {
Self::parse(s)
}
}
impl FromStr for ProcessorArchitecture {
type Err = Error;
fn from_str(s: &str) -> Result<Self> {
Self::parse(s)
}
}
impl TryFrom<u32> for ProcessorArchitecture {
type Error = Error;
fn try_from(value: u32) -> Result<Self> {
match value {
0x0000 => Ok(Self::MSIL),
0x014C => Ok(Self::X86),
0x0200 => Ok(Self::IA64),
0x8664 => Ok(Self::AMD64),
0x01C0 => Ok(Self::ARM),
0xAA64 => Ok(Self::ARM64),
_ => Err(malformed_error!(
"Unknown processor architecture ID: 0x{:04X}",
value
)),
}
}
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use super::*;
#[test]
fn test_assembly_version_new() {
let version = AssemblyVersion::new(1, 2, 3, 4);
assert_eq!(version.major, 1);
assert_eq!(version.minor, 2);
assert_eq!(version.build, 3);
assert_eq!(version.revision, 4);
}
#[test]
fn test_assembly_version_parse_full() {
let version = AssemblyVersion::parse("4.0.0.0").unwrap();
assert_eq!(version.major, 4);
assert_eq!(version.minor, 0);
assert_eq!(version.build, 0);
assert_eq!(version.revision, 0);
}
#[test]
fn test_assembly_version_parse_partial() {
let v3 = AssemblyVersion::parse("1.2.3").unwrap();
assert_eq!(v3, AssemblyVersion::new(1, 2, 3, 0));
let v2 = AssemblyVersion::parse("1.2").unwrap();
assert_eq!(v2, AssemblyVersion::new(1, 2, 0, 0));
let v1 = AssemblyVersion::parse("1").unwrap();
assert_eq!(v1, AssemblyVersion::new(1, 0, 0, 0));
}
#[test]
fn test_assembly_version_parse_invalid() {
assert!(AssemblyVersion::parse("").is_err());
assert!(AssemblyVersion::parse("1.2.3.4.5").is_err());
assert!(AssemblyVersion::parse("1.2.abc.4").is_err());
assert!(AssemblyVersion::parse("1.2.99999.4").is_err());
}
#[test]
fn test_assembly_version_display() {
let version = AssemblyVersion::new(4, 0, 0, 0);
assert_eq!(version.to_string(), "4.0.0.0");
let version = AssemblyVersion::new(1, 2, 3, 4);
assert_eq!(version.to_string(), "1.2.3.4");
}
#[test]
fn test_assembly_version_ordering() {
let v1 = AssemblyVersion::new(1, 0, 0, 0);
let v2 = AssemblyVersion::new(2, 0, 0, 0);
let v1_1 = AssemblyVersion::new(1, 1, 0, 0);
assert!(v1 < v2);
assert!(v1 < v1_1);
assert!(v1_1 < v2);
}
#[test]
fn test_assembly_version_from_str() {
let version: AssemblyVersion = "4.0.0.0".parse().unwrap();
assert_eq!(version, AssemblyVersion::new(4, 0, 0, 0));
}
#[test]
fn test_processor_architecture_parse() {
assert_eq!(
ProcessorArchitecture::parse("MSIL").unwrap(),
ProcessorArchitecture::MSIL
);
assert_eq!(
ProcessorArchitecture::parse("msil").unwrap(),
ProcessorArchitecture::MSIL
);
assert_eq!(
ProcessorArchitecture::parse("x86").unwrap(),
ProcessorArchitecture::X86
);
assert_eq!(
ProcessorArchitecture::parse("X86").unwrap(),
ProcessorArchitecture::X86
);
assert_eq!(
ProcessorArchitecture::parse("AMD64").unwrap(),
ProcessorArchitecture::AMD64
);
assert_eq!(
ProcessorArchitecture::parse("amd64").unwrap(),
ProcessorArchitecture::AMD64
);
assert_eq!(
ProcessorArchitecture::parse("x64").unwrap(),
ProcessorArchitecture::AMD64
);
assert_eq!(
ProcessorArchitecture::parse("IA64").unwrap(),
ProcessorArchitecture::IA64
);
assert_eq!(
ProcessorArchitecture::parse("ARM").unwrap(),
ProcessorArchitecture::ARM
);
assert_eq!(
ProcessorArchitecture::parse("arm").unwrap(),
ProcessorArchitecture::ARM
);
assert_eq!(
ProcessorArchitecture::parse("ARM64").unwrap(),
ProcessorArchitecture::ARM64
);
assert_eq!(
ProcessorArchitecture::parse("arm64").unwrap(),
ProcessorArchitecture::ARM64
);
}
#[test]
fn test_processor_architecture_parse_invalid() {
assert!(ProcessorArchitecture::parse("unknown").is_err());
assert!(ProcessorArchitecture::parse("").is_err());
assert!(ProcessorArchitecture::parse("PowerPC").is_err());
}
#[test]
fn test_processor_architecture_parse_whitespace() {
assert_eq!(
ProcessorArchitecture::parse(" x86 ").unwrap(),
ProcessorArchitecture::X86
);
assert_eq!(
ProcessorArchitecture::parse("\tAMD64\n").unwrap(),
ProcessorArchitecture::AMD64
);
assert!(ProcessorArchitecture::parse(" ").is_err());
}
#[test]
fn test_processor_architecture_display() {
assert_eq!(ProcessorArchitecture::MSIL.to_string(), "MSIL");
assert_eq!(ProcessorArchitecture::X86.to_string(), "x86");
assert_eq!(ProcessorArchitecture::AMD64.to_string(), "AMD64");
assert_eq!(ProcessorArchitecture::IA64.to_string(), "IA64");
assert_eq!(ProcessorArchitecture::ARM.to_string(), "ARM");
assert_eq!(ProcessorArchitecture::ARM64.to_string(), "ARM64");
}
#[test]
fn test_processor_architecture_from_str() {
let arch: ProcessorArchitecture = "x86".parse().unwrap();
assert_eq!(arch, ProcessorArchitecture::X86);
}
#[test]
fn test_assembly_identity_new() {
let identity = AssemblyIdentity::new(
"TestAssembly",
AssemblyVersion::new(1, 0, 0, 0),
None,
None,
None,
);
assert_eq!(identity.name, "TestAssembly");
assert_eq!(identity.version, AssemblyVersion::new(1, 0, 0, 0));
assert!(identity.culture.is_none());
assert!(identity.strong_name.is_none());
assert!(identity.processor_architecture.is_none());
}
#[test]
fn test_assembly_identity_parse_simple_name() {
let identity = AssemblyIdentity::parse("MyLibrary").unwrap();
assert_eq!(identity.name, "MyLibrary");
assert_eq!(identity.version, AssemblyVersion::new(0, 0, 0, 0));
assert!(identity.culture.is_none());
assert!(identity.strong_name.is_none());
}
#[test]
fn test_assembly_identity_parse_with_version() {
let identity = AssemblyIdentity::parse("MyLibrary, Version=1.2.3.4").unwrap();
assert_eq!(identity.name, "MyLibrary");
assert_eq!(identity.version, AssemblyVersion::new(1, 2, 3, 4));
}
#[test]
fn test_assembly_identity_parse_full_mscorlib() {
let identity = AssemblyIdentity::parse(
"mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089",
)
.unwrap();
assert_eq!(identity.name, "mscorlib");
assert_eq!(identity.version, AssemblyVersion::new(4, 0, 0, 0));
assert!(identity.culture.is_none()); assert!(identity.strong_name.is_some());
if let Some(Identity::Token(token)) = identity.strong_name {
let expected = u64::from_le_bytes([0xb7, 0x7a, 0x5c, 0x56, 0x19, 0x34, 0xe0, 0x89]);
assert_eq!(token, expected);
} else {
panic!("Expected Token identity");
}
}
#[test]
fn test_assembly_identity_parse_with_culture() {
let identity = AssemblyIdentity::parse(
"Resources, Version=1.0.0.0, Culture=en-US, PublicKeyToken=null",
)
.unwrap();
assert_eq!(identity.name, "Resources");
assert_eq!(identity.culture, Some("en-US".to_string()));
assert!(identity.strong_name.is_none());
}
#[test]
fn test_assembly_identity_parse_with_architecture() {
let identity =
AssemblyIdentity::parse("NativeLib, Version=1.0.0.0, ProcessorArchitecture=x86")
.unwrap();
assert_eq!(identity.name, "NativeLib");
assert_eq!(
identity.processor_architecture,
Some(ProcessorArchitecture::X86)
);
}
#[test]
fn test_assembly_identity_parse_empty_returns_error() {
let result = AssemblyIdentity::parse("");
assert!(result.is_err());
let err_msg = result.unwrap_err().to_string();
assert!(
err_msg.contains("cannot be empty"),
"Error message should mention empty name: {}",
err_msg
);
}
#[test]
fn test_assembly_identity_parse_whitespace_only_returns_error() {
let result = AssemblyIdentity::parse(" ");
assert!(result.is_err());
}
#[test]
fn test_assembly_identity_parse_invalid_hex_token() {
let result =
AssemblyIdentity::parse("MyLib, Version=1.0.0.0, PublicKeyToken=xyz_not_hex_123");
assert!(result.is_err());
let err_msg = result.unwrap_err().to_string();
assert!(
err_msg.contains("Invalid hex"),
"Error message should mention invalid hex: {}",
err_msg
);
}
#[test]
fn test_assembly_identity_parse_wrong_length_token() {
let result = AssemblyIdentity::parse(
"MyLib, Version=1.0.0.0, PublicKeyToken=b77a5c56", );
assert!(result.is_err());
let err_msg = result.unwrap_err().to_string();
assert!(
err_msg.contains("8 bytes"),
"Error message should mention expected length: {}",
err_msg
);
}
#[test]
fn test_assembly_identity_parse_too_long_token() {
let result = AssemblyIdentity::parse(
"MyLib, Version=1.0.0.0, PublicKeyToken=b77a5c561934e089aabbccdd", );
assert!(result.is_err());
let err_msg = result.unwrap_err().to_string();
assert!(
err_msg.contains("8 bytes"),
"Error message should mention expected length: {}",
err_msg
);
}
#[test]
fn test_assembly_identity_parse_invalid_processor_architecture() {
let result =
AssemblyIdentity::parse("MyLib, Version=1.0.0.0, ProcessorArchitecture=PowerPC");
assert!(result.is_err());
let err_msg = result.unwrap_err().to_string();
assert!(
err_msg.contains("Unknown processor architecture"),
"Error message should mention unknown architecture: {}",
err_msg
);
}
#[test]
fn test_assembly_identity_display_name_simple() {
let identity = AssemblyIdentity::new(
"MyLibrary",
AssemblyVersion::new(1, 0, 0, 0),
None,
None,
None,
);
let display = identity.display_name();
assert!(display.contains("MyLibrary"));
assert!(display.contains("Version=1.0.0.0"));
assert!(display.contains("Culture=neutral"));
assert!(display.contains("PublicKeyToken=null"));
}
#[test]
fn test_assembly_identity_display_name_with_culture() {
let identity = AssemblyIdentity::new(
"Resources",
AssemblyVersion::new(1, 0, 0, 0),
Some("fr-FR".to_string()),
None,
None,
);
let display = identity.display_name();
assert!(display.contains("Culture=fr-FR"));
}
#[test]
#[cfg(feature = "legacy-crypto")]
fn test_assembly_identity_display_name_with_pubkey() {
let pubkey_data = vec![
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20,
];
let identity = AssemblyIdentity::new(
"StrongLib",
AssemblyVersion::new(1, 0, 0, 0),
None,
Some(Identity::PubKey(pubkey_data)),
None,
);
let display = identity.display_name();
assert!(
!display.contains("PublicKeyToken=null"),
"PubKey should compute a token, not show null: {}",
display
);
assert!(
display.contains("PublicKeyToken="),
"Should have PublicKeyToken in display: {}",
display
);
}
#[test]
#[cfg(feature = "legacy-crypto")]
fn test_assembly_identity_display_name_with_ecma_key() {
let ecma_data = vec![
0x06, 0x28, 0xAC, 0x03, 0x00, 0x06, 0x7A, 0x06, 0x6F, 0xAB, 0x02, 0x00, 0x0A, 0x0B,
0x17, 0x6A,
];
let identity = AssemblyIdentity::new(
"FrameworkLib",
AssemblyVersion::new(4, 0, 0, 0),
None,
Some(Identity::EcmaKey(ecma_data)),
None,
);
let display = identity.display_name();
assert!(
!display.contains("PublicKeyToken=null"),
"EcmaKey should compute a token, not show null: {}",
display
);
}
#[test]
fn test_assembly_identity_simple_name() {
let identity = AssemblyIdentity::new(
"System.Core",
AssemblyVersion::new(4, 0, 0, 0),
None,
None,
None,
);
assert_eq!(identity.simple_name(), "System.Core");
}
#[test]
fn test_assembly_identity_is_strong_named() {
let weak = AssemblyIdentity::new(
"WeakAssembly",
AssemblyVersion::new(1, 0, 0, 0),
None,
None,
None,
);
assert!(!weak.is_strong_named());
let strong = AssemblyIdentity::new(
"StrongAssembly",
AssemblyVersion::new(1, 0, 0, 0),
None,
Some(Identity::Token(0x1234567890ABCDEF)),
None,
);
assert!(strong.is_strong_named());
}
#[test]
fn test_assembly_identity_is_culture_neutral() {
let neutral = AssemblyIdentity::new(
"MainAssembly",
AssemblyVersion::new(1, 0, 0, 0),
None,
None,
None,
);
assert!(neutral.is_culture_neutral());
let localized = AssemblyIdentity::new(
"Resources",
AssemblyVersion::new(1, 0, 0, 0),
Some("de-DE".to_string()),
None,
None,
);
assert!(!localized.is_culture_neutral());
}
#[test]
fn test_assembly_identity_equality() {
let id1 = AssemblyIdentity::new(
"TestAssembly",
AssemblyVersion::new(1, 0, 0, 0),
None,
None,
None,
);
let id2 = AssemblyIdentity::new(
"TestAssembly",
AssemblyVersion::new(1, 0, 0, 0),
None,
None,
None,
);
let id_different_version = AssemblyIdentity::new(
"TestAssembly",
AssemblyVersion::new(2, 0, 0, 0),
None,
None,
None,
);
let id_different_name = AssemblyIdentity::new(
"OtherAssembly",
AssemblyVersion::new(1, 0, 0, 0),
None,
None,
None,
);
assert_eq!(id1, id2);
assert_ne!(id1, id_different_version);
assert_ne!(id1, id_different_name);
}
#[test]
fn test_assembly_identity_equality_ignores_strong_name_difference() {
let id_with_token = AssemblyIdentity::new(
"TestAssembly",
AssemblyVersion::new(1, 0, 0, 0),
None,
Some(Identity::Token(0x1234567890ABCDEF)),
None,
);
let id_without_token = AssemblyIdentity::new(
"TestAssembly",
AssemblyVersion::new(1, 0, 0, 0),
None,
None,
None,
);
assert_eq!(id_with_token, id_without_token);
}
#[test]
fn test_assembly_identity_hash_consistency() {
let id1 = AssemblyIdentity::new(
"TestAssembly",
AssemblyVersion::new(1, 0, 0, 0),
None,
Some(Identity::Token(0x1234567890ABCDEF)),
None,
);
let id2 = AssemblyIdentity::new(
"TestAssembly",
AssemblyVersion::new(1, 0, 0, 0),
None,
None, None,
);
let mut map = HashMap::new();
map.insert(id1.clone(), "value1");
assert!(map.contains_key(&id2));
}
#[test]
fn test_assembly_identity_from_str() {
let identity: AssemblyIdentity = "System.Core, Version=3.5.0.0".parse().unwrap();
assert_eq!(identity.name, "System.Core");
assert_eq!(identity.version, AssemblyVersion::new(3, 5, 0, 0));
}
#[test]
fn test_assembly_identity_roundtrip_parse_display() {
let original = AssemblyIdentity::new(
"TestLib",
AssemblyVersion::new(2, 1, 3, 4),
None,
None,
Some(ProcessorArchitecture::AMD64),
);
let display = original.display_name();
let parsed = AssemblyIdentity::parse(&display).unwrap();
assert_eq!(original.name, parsed.name);
assert_eq!(original.version, parsed.version);
assert_eq!(original.culture, parsed.culture);
assert_eq!(
original.processor_architecture,
parsed.processor_architecture
);
}
#[test]
fn test_assembly_identity_roundtrip_with_token() {
let original = AssemblyIdentity::new(
"StrongLib",
AssemblyVersion::new(1, 2, 3, 4),
Some("en-US".to_string()),
Some(Identity::Token(0xb77a5c561934e089)),
Some(ProcessorArchitecture::X86),
);
let display = original.display_name();
let parsed = AssemblyIdentity::parse(&display).unwrap();
assert_eq!(original.name, parsed.name);
assert_eq!(original.version, parsed.version);
assert_eq!(original.culture, parsed.culture);
assert_eq!(
original.processor_architecture,
parsed.processor_architecture
);
assert_eq!(original.strong_name, parsed.strong_name);
let display2 = parsed.display_name();
assert_eq!(
display, display2,
"Display string should be stable across roundtrips"
);
let parsed2 = AssemblyIdentity::parse(&display2).unwrap();
assert_eq!(parsed.strong_name, parsed2.strong_name);
}
#[test]
fn test_assembly_identity_token_format_consistency() {
let identity = AssemblyIdentity::parse(
"mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089",
)
.unwrap();
assert!(identity.strong_name.is_some());
if let Some(Identity::Token(token)) = identity.strong_name {
let expected = u64::from_le_bytes([0xb7, 0x7a, 0x5c, 0x56, 0x19, 0x34, 0xe0, 0x89]);
assert_eq!(token, expected);
}
}
#[test]
fn test_assembly_identity_parse_extra_whitespace() {
let identity = AssemblyIdentity::parse(
"MyLib , Version=1.0.0.0 , Culture=neutral , PublicKeyToken=null",
)
.unwrap();
assert_eq!(identity.name, "MyLib");
assert_eq!(identity.version, AssemblyVersion::new(1, 0, 0, 0));
}
#[test]
fn test_assembly_identity_parse_case_insensitive_culture() {
let identity = AssemblyIdentity::parse("MyLib, Version=1.0.0.0, Culture=EN-us").unwrap();
assert_eq!(identity.culture, Some("EN-us".to_string()));
}
#[test]
fn test_assembly_identity_parse_unknown_fields_ignored() {
let identity =
AssemblyIdentity::parse("MyLib, Version=1.0.0.0, UnknownField=value, Culture=neutral")
.unwrap();
assert_eq!(identity.name, "MyLib");
assert_eq!(identity.version, AssemblyVersion::new(1, 0, 0, 0));
assert!(identity.culture.is_none());
}
#[test]
fn test_assembly_version_unknown_sentinel() {
let unknown = AssemblyVersion::UNKNOWN;
assert!(unknown.is_unknown());
assert_eq!(unknown.major, 0);
assert_eq!(unknown.minor, 0);
assert_eq!(unknown.build, 0);
assert_eq!(unknown.revision, 0);
let not_unknown = AssemblyVersion::new(0, 0, 0, 1);
assert!(!not_unknown.is_unknown());
}
#[test]
fn test_processor_architecture_full_coverage() {
let all_variants = [
(ProcessorArchitecture::MSIL, "MSIL", 0x0000_u32),
(ProcessorArchitecture::X86, "x86", 0x014C),
(ProcessorArchitecture::IA64, "IA64", 0x0200),
(ProcessorArchitecture::AMD64, "AMD64", 0x8664),
(ProcessorArchitecture::ARM, "ARM", 0x01C0),
(ProcessorArchitecture::ARM64, "ARM64", 0xAA64),
];
for (expected_arch, display_name, machine_code) in all_variants {
let parsed = ProcessorArchitecture::parse(display_name)
.unwrap_or_else(|_| panic!("Failed to parse '{}'", display_name));
assert_eq!(
parsed, expected_arch,
"parse('{}') returned wrong variant",
display_name
);
let parsed_lower = ProcessorArchitecture::parse(&display_name.to_lowercase())
.unwrap_or_else(|_| panic!("Failed to parse lowercase '{}'", display_name));
assert_eq!(
parsed_lower, expected_arch,
"parse('{}') lowercase returned wrong variant",
display_name
);
let from_code = ProcessorArchitecture::try_from(machine_code)
.unwrap_or_else(|_| panic!("Failed try_from(0x{:04X})", machine_code));
assert_eq!(
from_code, expected_arch,
"try_from(0x{:04X}) returned wrong variant",
machine_code
);
let displayed = expected_arch.to_string();
let roundtrip = ProcessorArchitecture::parse(&displayed)
.unwrap_or_else(|_| panic!("Failed to roundtrip '{}'", displayed));
assert_eq!(
roundtrip, expected_arch,
"Display roundtrip failed for {:?}",
expected_arch
);
}
let x64_parsed = ProcessorArchitecture::parse("x64").unwrap();
assert_eq!(x64_parsed, ProcessorArchitecture::AMD64);
}
#[test]
fn test_assembly_version_is_compatible_with() {
let v4_0 = AssemblyVersion::new(4, 0, 0, 0);
let v4_5 = AssemblyVersion::new(4, 5, 0, 0);
let v4_5_1 = AssemblyVersion::new(4, 5, 1, 0);
let v5_0 = AssemblyVersion::new(5, 0, 0, 0);
let v_unknown = AssemblyVersion::UNKNOWN;
assert!(v4_0.is_compatible_with(&v4_0));
assert!(v4_5.is_compatible_with(&v4_5));
assert!(v4_5.is_compatible_with(&v4_0));
assert!(v4_5_1.is_compatible_with(&v4_0));
assert!(v4_5_1.is_compatible_with(&v4_5));
assert!(!v4_0.is_compatible_with(&v4_5));
assert!(!v4_5.is_compatible_with(&v4_5_1));
assert!(!v5_0.is_compatible_with(&v4_0));
assert!(!v4_0.is_compatible_with(&v5_0));
assert!(!v5_0.is_compatible_with(&v4_5));
assert!(v4_0.is_compatible_with(&v_unknown));
assert!(v4_5.is_compatible_with(&v_unknown));
assert!(v5_0.is_compatible_with(&v_unknown));
assert!(v_unknown.is_compatible_with(&v_unknown));
}
#[test]
fn test_assembly_identity_satisfies() {
let system_core_v4_0 = AssemblyIdentity::new(
"System.Core".to_string(),
AssemblyVersion::new(4, 0, 0, 0),
None,
None,
None,
);
let system_core_v4_5 = AssemblyIdentity::new(
"System.Core".to_string(),
AssemblyVersion::new(4, 5, 0, 0),
None,
None,
None,
);
let system_core_v5_0 = AssemblyIdentity::new(
"System.Core".to_string(),
AssemblyVersion::new(5, 0, 0, 0),
None,
None,
None,
);
let system_v4_0 = AssemblyIdentity::new(
"System".to_string(),
AssemblyVersion::new(4, 0, 0, 0),
None,
None,
None,
);
assert!(system_core_v4_0.satisfies(&system_core_v4_0));
assert!(system_core_v4_5.satisfies(&system_core_v4_0));
assert!(!system_core_v4_0.satisfies(&system_core_v4_5));
assert!(!system_core_v5_0.satisfies(&system_core_v4_0));
assert!(!system_v4_0.satisfies(&system_core_v4_0));
assert!(!system_core_v4_0.satisfies(&system_v4_0));
}
#[test]
fn test_assembly_identity_satisfies_case_insensitive() {
let lower = AssemblyIdentity::new(
"system.core".to_string(),
AssemblyVersion::new(4, 0, 0, 0),
None,
None,
None,
);
let upper = AssemblyIdentity::new(
"System.Core".to_string(),
AssemblyVersion::new(4, 0, 0, 0),
None,
None,
None,
);
let mixed = AssemblyIdentity::new(
"SYSTEM.CORE".to_string(),
AssemblyVersion::new(4, 0, 0, 0),
None,
None,
None,
);
assert!(lower.satisfies(&upper));
assert!(upper.satisfies(&lower));
assert!(mixed.satisfies(&lower));
assert!(lower.satisfies(&mixed));
}
#[test]
fn test_assembly_identity_satisfies_culture() {
let neutral = AssemblyIdentity::new(
"MyLib".to_string(),
AssemblyVersion::new(1, 0, 0, 0),
None,
None,
None,
);
let en_us = AssemblyIdentity::new(
"MyLib".to_string(),
AssemblyVersion::new(1, 0, 0, 0),
Some("en-US".to_string()),
None,
None,
);
let fr_fr = AssemblyIdentity::new(
"MyLib".to_string(),
AssemblyVersion::new(1, 0, 0, 0),
Some("fr-FR".to_string()),
None,
None,
);
assert!(neutral.satisfies(&neutral));
assert!(en_us.satisfies(&en_us));
assert!(!neutral.satisfies(&en_us));
assert!(!en_us.satisfies(&neutral));
assert!(!en_us.satisfies(&fr_fr));
}
}