|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection;
using Microsoft.CodeAnalysis.PooledObjects;
using Roslyn.Utilities;
namespace Microsoft.CodeAnalysis
{
/// <summary>
/// Represents an identity of an assembly as defined by CLI metadata specification.
/// </summary>
/// <remarks>
/// May represent assembly definition or assembly reference identity.
/// </remarks>
[DebuggerDisplay("{GetDebuggerDisplay(), nq}")]
public sealed partial class AssemblyIdentity : IEquatable<AssemblyIdentity>
{
// determines the binding model (how assembly references are matched to assembly definitions)
private readonly AssemblyContentType _contentType;
// not null, not empty:
private readonly string _name;
// no version: 0.0.0.0
private readonly Version _version;
// Invariant culture is represented as empty string.
// The culture might not exist on the system.
private readonly string _cultureName;
// weak name: empty
// strong name: full public key or empty (only token is available)
private readonly ImmutableArray<byte> _publicKey;
// weak name: empty
// strong name: null (to be initialized), or 8 bytes (initialized)
private ImmutableArray<byte> _lazyPublicKeyToken;
private readonly bool _isRetargetable;
// cached display name
private string? _lazyDisplayName;
// cached hash code
private int _lazyHashCode;
internal const int PublicKeyTokenSize = 8;
private AssemblyIdentity(AssemblyIdentity other, Version version)
{
Debug.Assert((object)other != null);
Debug.Assert((object)version != null);
_contentType = other.ContentType;
_name = other._name;
_cultureName = other._cultureName;
_publicKey = other._publicKey;
_lazyPublicKeyToken = other._lazyPublicKeyToken;
_isRetargetable = other._isRetargetable;
_version = version;
_lazyDisplayName = null;
_lazyHashCode = 0;
}
internal AssemblyIdentity WithVersion(Version version) => (version == _version) ? this : new AssemblyIdentity(this, version);
/// <summary>
/// Constructs an <see cref="AssemblyIdentity"/> from its constituent parts.
/// </summary>
/// <param name="name">The simple name of the assembly.</param>
/// <param name="version">The version of the assembly.</param>
/// <param name="cultureName">
/// The name of the culture to associate with the assembly.
/// Specify null, <see cref="string.Empty"/>, or "neutral" (any casing) to represent <see cref="System.Globalization.CultureInfo.InvariantCulture"/>.
/// The name can be an arbitrary string that doesn't contain NUL character, the legality of the culture name is not validated.
/// </param>
/// <param name="publicKeyOrToken">The public key or public key token of the assembly.</param>
/// <param name="hasPublicKey">Indicates whether <paramref name="publicKeyOrToken"/> represents a public key.</param>
/// <param name="isRetargetable">Indicates whether the assembly is retargetable.</param>
/// <param name="contentType">Specifies the binding model for how this object will be treated in comparisons.</param>
/// <exception cref="ArgumentException">If <paramref name="name"/> is null, empty or contains a NUL character.</exception>
/// <exception cref="ArgumentException">If <paramref name="cultureName"/> contains a NUL character.</exception>
/// <exception cref="ArgumentOutOfRangeException"><paramref name="contentType"/> is not a value of the <see cref="AssemblyContentType"/> enumeration.</exception>
/// <exception cref="ArgumentException"><paramref name="version"/> contains values that are not greater than or equal to zero and less than or equal to ushort.MaxValue.</exception>
/// <exception cref="ArgumentException"><paramref name="hasPublicKey"/> is true and <paramref name="publicKeyOrToken"/> is not set.</exception>
/// <exception cref="ArgumentException"><paramref name="hasPublicKey"/> is false and <paramref name="publicKeyOrToken"/>
/// contains a value that is not the size of a public key token, 8 bytes.</exception>
public AssemblyIdentity(
string? name,
Version? version = null,
string? cultureName = null,
ImmutableArray<byte> publicKeyOrToken = default,
bool hasPublicKey = false,
bool isRetargetable = false,
AssemblyContentType contentType = AssemblyContentType.Default)
{
if (!IsValid(contentType))
{
throw new ArgumentOutOfRangeException(nameof(contentType), CodeAnalysisResources.InvalidContentType);
}
if (!IsValidName(name))
{
throw new ArgumentException(string.Format(CodeAnalysisResources.InvalidAssemblyName, name), nameof(name));
}
if (!IsValidCultureName(cultureName))
{
throw new ArgumentException(string.Format(CodeAnalysisResources.InvalidCultureName, cultureName), nameof(cultureName));
}
// Version allows more values then can be encoded in metadata:
if (!IsValid(version))
{
throw new ArgumentOutOfRangeException(nameof(version));
}
if (hasPublicKey)
{
if (!MetadataHelpers.IsValidPublicKey(publicKeyOrToken))
{
throw new ArgumentException(CodeAnalysisResources.InvalidPublicKey, nameof(publicKeyOrToken));
}
}
else
{
if (!publicKeyOrToken.IsDefaultOrEmpty && publicKeyOrToken.Length != PublicKeyTokenSize)
{
throw new ArgumentException(CodeAnalysisResources.InvalidSizeOfPublicKeyToken, nameof(publicKeyOrToken));
}
}
if (isRetargetable && contentType == AssemblyContentType.WindowsRuntime)
{
throw new ArgumentException(CodeAnalysisResources.WinRTIdentityCantBeRetargetable, nameof(isRetargetable));
}
_name = name;
_version = version ?? NullVersion;
_cultureName = NormalizeCultureName(cultureName);
_isRetargetable = isRetargetable;
_contentType = contentType;
InitializeKey(publicKeyOrToken, hasPublicKey, out _publicKey, out _lazyPublicKeyToken);
}
// asserting constructor used by SourceAssemblySymbol:
internal AssemblyIdentity(
string name,
Version version,
string? cultureName,
ImmutableArray<byte> publicKeyOrToken,
bool hasPublicKey,
bool isRetargetable)
{
Debug.Assert(name != null);
Debug.Assert(IsValid(version));
Debug.Assert(IsValidCultureName(cultureName));
Debug.Assert((hasPublicKey && MetadataHelpers.IsValidPublicKey(publicKeyOrToken)) || (!hasPublicKey && (publicKeyOrToken.IsDefaultOrEmpty || publicKeyOrToken.Length == PublicKeyTokenSize)));
_name = name;
_version = version ?? NullVersion;
_cultureName = NormalizeCultureName(cultureName);
_isRetargetable = isRetargetable;
_contentType = AssemblyContentType.Default;
InitializeKey(publicKeyOrToken, hasPublicKey, out _publicKey, out _lazyPublicKeyToken);
}
// constructor used by metadata reader:
internal AssemblyIdentity(
bool noThrow,
string name,
Version? version = null,
string? cultureName = null,
ImmutableArray<byte> publicKeyOrToken = default,
bool hasPublicKey = false,
bool isRetargetable = false,
AssemblyContentType contentType = AssemblyContentType.Default)
{
Debug.Assert(name != null);
Debug.Assert((hasPublicKey && MetadataHelpers.IsValidPublicKey(publicKeyOrToken)) || (!hasPublicKey && (publicKeyOrToken.IsDefaultOrEmpty || publicKeyOrToken.Length == PublicKeyTokenSize)));
Debug.Assert(noThrow);
_name = name;
_version = version ?? NullVersion;
_cultureName = NormalizeCultureName(cultureName);
_contentType = IsValid(contentType) ? contentType : AssemblyContentType.Default;
_isRetargetable = isRetargetable && _contentType != AssemblyContentType.WindowsRuntime;
InitializeKey(publicKeyOrToken, hasPublicKey, out _publicKey, out _lazyPublicKeyToken);
}
private static string NormalizeCultureName(string? cultureName)
{
// Treat "neutral" culture as invariant culture name, although it is technically not a legal culture name.
//
// A few reasons:
// 1) Invariant culture is displayed as "neutral" in the identity display name.
// Thus a) an identity with culture "neutral" wouldn't roundrip serialization to display name.
// b) an identity with culture "neutral" wouldn't compare equal to invariant culture identity,
// yet their display names are the same which is confusing.
//
// 2) The implementation of AssemblyName.CultureName on Mono incorrectly returns "neutral" for invariant culture identities.
return cultureName == null || AssemblyIdentityComparer.CultureComparer.Equals(cultureName, InvariantCultureDisplay) ?
string.Empty : cultureName;
}
private static void InitializeKey(ImmutableArray<byte> publicKeyOrToken, bool hasPublicKey,
out ImmutableArray<byte> publicKey, out ImmutableArray<byte> publicKeyToken)
{
if (hasPublicKey)
{
publicKey = publicKeyOrToken;
publicKeyToken = default;
}
else
{
publicKey = ImmutableArray<byte>.Empty;
publicKeyToken = publicKeyOrToken.NullToEmpty();
}
}
internal static bool IsValidCultureName(string? name)
{
// The native compiler doesn't enforce that the culture be anything in particular.
// AssemblyIdentity should preserve user input even if it is of dubious utility.
// Note: If these checks change, the error messages emitted by the compilers when
// this case is detected will also need to change. They currently directly
// name the presence of the NUL character as the reason that the culture name is invalid.
return name == null || name.IndexOf('\0') < 0;
}
private static bool IsValidName([NotNullWhen(true)] string? name)
{
return !string.IsNullOrEmpty(name) && name.IndexOf('\0') < 0;
}
internal static readonly Version NullVersion = new Version(0, 0, 0, 0);
private static bool IsValid(Version? value)
{
return value == null
|| value.Major >= 0
&& value.Minor >= 0
&& value.Build >= 0
&& value.Revision >= 0
&& value.Major <= ushort.MaxValue
&& value.Minor <= ushort.MaxValue
&& value.Build <= ushort.MaxValue
&& value.Revision <= ushort.MaxValue;
}
private static bool IsValid(AssemblyContentType value)
{
return value >= AssemblyContentType.Default && value <= AssemblyContentType.WindowsRuntime;
}
/// <summary>
/// The simple name of the assembly.
/// </summary>
public string Name { get { return _name; } }
/// <summary>
/// The version of the assembly.
/// </summary>
public Version Version { get { return _version; } }
/// <summary>
/// The culture name of the assembly, or empty if the culture is neutral.
/// </summary>
public string CultureName { get { return _cultureName; } }
/// <summary>
/// The AssemblyNameFlags.
/// </summary>
public AssemblyNameFlags Flags
{
get
{
return (_isRetargetable ? AssemblyNameFlags.Retargetable : AssemblyNameFlags.None) |
(HasPublicKey ? AssemblyNameFlags.PublicKey : AssemblyNameFlags.None);
}
}
/// <summary>
/// Specifies assembly binding model for the assembly definition or reference;
/// that is how assembly references are matched to assembly definitions.
/// </summary>
public AssemblyContentType ContentType
{
get { return _contentType; }
}
/// <summary>
/// True if the assembly identity includes full public key.
/// </summary>
public bool HasPublicKey
{
get { return _publicKey.Length > 0; }
}
/// <summary>
/// Full public key or empty.
/// </summary>
public ImmutableArray<byte> PublicKey
{
get { return _publicKey; }
}
/// <summary>
/// Low 8 bytes of SHA1 hash of the public key, or empty.
/// </summary>
public ImmutableArray<byte> PublicKeyToken
{
get
{
if (_lazyPublicKeyToken.IsDefault)
{
ImmutableInterlocked.InterlockedCompareExchange(ref _lazyPublicKeyToken, CalculatePublicKeyToken(_publicKey), default);
}
return _lazyPublicKeyToken;
}
}
/// <summary>
/// True if the assembly identity has a strong name, ie. either a full public key or a token.
/// </summary>
public bool IsStrongName
{
get
{
// if we don't have public key, the token is either empty or 8-byte value
Debug.Assert(HasPublicKey || !_lazyPublicKeyToken.IsDefault);
return HasPublicKey || _lazyPublicKeyToken.Length > 0;
}
}
/// <summary>
/// Gets the value which specifies if the assembly is retargetable.
/// </summary>
public bool IsRetargetable
{
get { return _isRetargetable; }
}
internal static bool IsFullName(AssemblyIdentityParts parts)
{
const AssemblyIdentityParts nvc = AssemblyIdentityParts.Name | AssemblyIdentityParts.Version | AssemblyIdentityParts.Culture;
return (parts & nvc) == nvc && (parts & AssemblyIdentityParts.PublicKeyOrToken) != 0;
}
#region Equals, GetHashCode
/// <summary>
/// Determines whether two <see cref="AssemblyIdentity"/> instances are equal.
/// </summary>
/// <param name="left">The operand appearing on the left side of the operator.</param>
/// <param name="right">The operand appearing on the right side of the operator.</param>
public static bool operator ==(AssemblyIdentity? left, AssemblyIdentity? right)
{
return EqualityComparer<AssemblyIdentity>.Default.Equals(left, right);
}
/// <summary>
/// Determines whether two <see cref="AssemblyIdentity"/> instances are not equal.
/// </summary>
/// <param name="left">The operand appearing on the left side of the operator.</param>
/// <param name="right">The operand appearing on the right side of the operator.</param>
public static bool operator !=(AssemblyIdentity? left, AssemblyIdentity? right)
{
return !(left == right);
}
/// <summary>
/// Determines whether the specified instance is equal to the current instance.
/// </summary>
/// <param name="obj">The object to be compared with the current instance.</param>
public bool Equals(AssemblyIdentity? obj)
{
return !ReferenceEquals(obj, null)
&& (_lazyHashCode == 0 || obj._lazyHashCode == 0 || _lazyHashCode == obj._lazyHashCode)
&& MemberwiseEqual(this, obj) == true;
}
/// <summary>
/// Determines whether the specified instance is equal to the current instance.
/// </summary>
/// <param name="obj">The object to be compared with the current instance.</param>
public override bool Equals(object? obj)
{
return Equals(obj as AssemblyIdentity);
}
/// <summary>
/// Returns the hash code for the current instance.
/// </summary>
/// <returns></returns>
public override int GetHashCode()
{
if (_lazyHashCode == 0)
{
// Do not include PK/PKT in the hash - collisions on PK/PKT are rare (assembly identities differ only in PKT/PK)
// and we can't calculate hash of PKT if only PK is available
_lazyHashCode =
Hash.Combine(AssemblyIdentityComparer.SimpleNameComparer.GetHashCode(_name),
Hash.Combine(_version.GetHashCode(), GetHashCodeIgnoringNameAndVersion()));
}
return _lazyHashCode;
}
internal int GetHashCodeIgnoringNameAndVersion()
{
return
Hash.Combine((int)_contentType,
Hash.Combine(_isRetargetable,
AssemblyIdentityComparer.CultureComparer.GetHashCode(_cultureName)));
}
// internal for testing
internal static ImmutableArray<byte> CalculatePublicKeyToken(ImmutableArray<byte> publicKey)
{
var hash = CryptographicHashProvider.ComputeSha1(publicKey);
// SHA1 hash is always 160 bits:
Debug.Assert(hash.Length == CryptographicHashProvider.Sha1HashSize);
// PublicKeyToken is the low 64 bits of the SHA-1 hash of the public key.
int l = hash.Length - 1;
var result = ArrayBuilder<byte>.GetInstance(PublicKeyTokenSize);
for (int i = 0; i < PublicKeyTokenSize; i++)
{
result.Add(hash[l - i]);
}
return result.ToImmutableAndFree();
}
/// <summary>
/// Returns true (false) if specified assembly identities are (not) equal
/// regardless of unification, retargeting or other assembly binding policies.
/// Returns null if these policies must be consulted to determine name equivalence.
/// </summary>
internal static bool? MemberwiseEqual(AssemblyIdentity x, AssemblyIdentity y)
{
if (ReferenceEquals(x, y))
{
return true;
}
if (!AssemblyIdentityComparer.SimpleNameComparer.Equals(x._name, y._name))
{
return false;
}
if (x._version.Equals(y._version) && EqualIgnoringNameAndVersion(x, y))
{
return true;
}
return null;
}
internal static bool EqualIgnoringNameAndVersion(AssemblyIdentity x, AssemblyIdentity y)
{
return
x.IsRetargetable == y.IsRetargetable &&
x.ContentType == y.ContentType &&
AssemblyIdentityComparer.CultureComparer.Equals(x.CultureName, y.CultureName) &&
KeysEqual(x, y);
}
internal static bool KeysEqual(AssemblyIdentity x, AssemblyIdentity y)
{
var xToken = x._lazyPublicKeyToken;
var yToken = y._lazyPublicKeyToken;
// weak names or both strong names with initialized PKT - compare tokens:
if (!xToken.IsDefault && !yToken.IsDefault)
{
return xToken.SequenceEqual(yToken);
}
// both are strong names with uninitialized PKT - compare full keys:
if (xToken.IsDefault && yToken.IsDefault)
{
return x._publicKey.SequenceEqual(y._publicKey);
}
// one of the strong names doesn't have PK, other doesn't have PTK initialized.
if (xToken.IsDefault)
{
return x.PublicKeyToken.SequenceEqual(yToken);
}
else
{
return xToken.SequenceEqual(y.PublicKeyToken);
}
}
#endregion
#region AssemblyName conversions
/// <summary>
/// Retrieves assembly definition identity from given runtime assembly.
/// </summary>
/// <param name="assembly">The runtime assembly.</param>
/// <returns>Assembly definition identity.</returns>
/// <exception cref="ArgumentNullException"><paramref name="assembly"/> is null.</exception>
public static AssemblyIdentity FromAssemblyDefinition(Assembly assembly)
{
if (assembly == null)
{
throw new ArgumentNullException(nameof(assembly));
}
return FromAssemblyDefinition(assembly.GetName());
}
// internal for testing
internal static AssemblyIdentity FromAssemblyDefinition(AssemblyName name)
{
// AssemblyDef always has full key or no key:
var publicKeyBytes = name.GetPublicKey();
ImmutableArray<byte> publicKey = (publicKeyBytes != null) ? ImmutableArray.Create(publicKeyBytes) : ImmutableArray<byte>.Empty;
return new AssemblyIdentity(
name.Name,
name.Version,
name.CultureName,
publicKey,
hasPublicKey: publicKey.Length > 0,
isRetargetable: (name.Flags & AssemblyNameFlags.Retargetable) != 0,
contentType: name.ContentType);
}
internal static AssemblyIdentity FromAssemblyReference(AssemblyName name)
{
// AssemblyRef either has PKT or no key:
return new AssemblyIdentity(
name.Name,
name.Version,
name.CultureName,
ImmutableArray.Create(name.GetPublicKeyToken()),
hasPublicKey: false,
isRetargetable: (name.Flags & AssemblyNameFlags.Retargetable) != 0,
contentType: name.ContentType);
}
#endregion
}
}
|