|
// 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.Immutable;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection;
using System.Text;
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>
public partial class AssemblyIdentity
{
internal const string InvariantCultureDisplay = "neutral";
/// <summary>
/// Returns the display name of the assembly identity.
/// </summary>
/// <param name="fullKey">True if the full public key should be included in the name. Otherwise public key token is used.</param>
/// <returns>The display name.</returns>
/// <remarks>
/// Characters ',', '=', '"', '\'', '\' occurring in the simple name are escaped by backslash in the display name.
/// Any character '\t' is replaced by two characters '\' and 't',
/// Any character '\n' is replaced by two characters '\' and 'n',
/// Any character '\r' is replaced by two characters '\' and 'r',
/// The assembly name in the display name is enclosed in double quotes if it starts or ends with
/// a whitespace character (' ', '\t', '\r', '\n').
/// </remarks>
public string GetDisplayName(bool fullKey = false)
{
if (fullKey)
{
return BuildDisplayName(fullKey: true);
}
if (_lazyDisplayName == null)
{
_lazyDisplayName = BuildDisplayName(fullKey: false);
}
return _lazyDisplayName;
}
/// <summary>
/// Returns the display name of the current instance.
/// </summary>
public override string ToString()
{
return GetDisplayName(fullKey: false);
}
internal static string PublicKeyToString(ImmutableArray<byte> key)
{
if (key.IsDefaultOrEmpty)
{
return "";
}
PooledStringBuilder sb = PooledStringBuilder.GetInstance();
StringBuilder builder = sb.Builder;
AppendKey(sb, key);
return sb.ToStringAndFree();
}
private string BuildDisplayName(bool fullKey)
{
PooledStringBuilder pooledBuilder = PooledStringBuilder.GetInstance();
var sb = pooledBuilder.Builder;
EscapeName(sb, Name);
sb.Append(", Version=");
sb.Append(_version.Major);
sb.Append('.');
sb.Append(_version.Minor);
sb.Append('.');
sb.Append(_version.Build);
sb.Append('.');
sb.Append(_version.Revision);
sb.Append(", Culture=");
if (_cultureName.Length == 0)
{
sb.Append(InvariantCultureDisplay);
}
else
{
EscapeName(sb, _cultureName);
}
if (fullKey && HasPublicKey)
{
sb.Append(", PublicKey=");
AppendKey(sb, _publicKey);
}
else
{
sb.Append(", PublicKeyToken=");
if (PublicKeyToken.Length > 0)
{
AppendKey(sb, PublicKeyToken);
}
else
{
sb.Append("null");
}
}
if (IsRetargetable)
{
sb.Append(", Retargetable=Yes");
}
switch (_contentType)
{
case AssemblyContentType.Default:
break;
case AssemblyContentType.WindowsRuntime:
sb.Append(", ContentType=WindowsRuntime");
break;
default:
throw ExceptionUtilities.UnexpectedValue(_contentType);
}
string result = sb.ToString();
pooledBuilder.Free();
return result;
}
private static void AppendKey(StringBuilder sb, ImmutableArray<byte> key)
{
foreach (byte b in key)
{
sb.Append(b.ToString("x2"));
}
}
private string GetDebuggerDisplay()
{
return GetDisplayName(fullKey: true);
}
public static bool TryParseDisplayName(string displayName, [NotNullWhen(true)] out AssemblyIdentity? identity)
{
if (displayName == null)
{
throw new ArgumentNullException(nameof(displayName));
}
return TryParseDisplayName(displayName, out identity, parts: out _);
}
/// <summary>
/// Perf: ETW traces show 2%+ of all allocations parsing assembly identity names. This is due to how large
/// these strings can be (600+ characters in some cases), and how many substrings are continually produced as
/// the string is broken up into the pieces needed by AssemblyIdentity. The capacity of 1024 was picked as
/// around 700 unique strings were found in a solution the size of Roslyn.sln. So this seems like a reasonable
/// starting point for a large solution. This cache takes up around 240k in memory, but ends up saving >80MB of
/// garbage over typing even a few characters. And, of course, that savings just grows over the lifetime of a
/// session this is hosted within.
/// </summary>
private static readonly ConcurrentCache<string, (AssemblyIdentity? identity, AssemblyIdentityParts parts)> s_TryParseDisplayNameCache =
new ConcurrentCache<string, (AssemblyIdentity? identity, AssemblyIdentityParts parts)>(1024, ReferenceEqualityComparer.Instance);
/// <summary>
/// Parses display name filling defaults for any basic properties that are missing.
/// </summary>
/// <param name="displayName">Display name.</param>
/// <param name="identity">A full assembly identity.</param>
/// <param name="parts">
/// Parts of the assembly identity that were specified in the display name,
/// or 0 if the parsing failed.
/// </param>
/// <returns>True if display name parsed correctly.</returns>
/// <remarks>
/// The simple name has to be non-empty.
/// A partially specified version might be missing build and/or revision number. The default value for these is 65535.
/// The default culture is neutral (<see cref="CultureName"/> is <see cref="String.Empty"/>.
/// If neither public key nor token is specified the identity is considered weak.
/// </remarks>
/// <exception cref="ArgumentNullException"><paramref name="displayName"/> is null.</exception>
public static bool TryParseDisplayName(string displayName, [NotNullWhen(true)] out AssemblyIdentity? identity, out AssemblyIdentityParts parts)
{
if (!s_TryParseDisplayNameCache.TryGetValue(displayName, out var identityAndParts))
{
if (tryParseDisplayName(displayName, out var localIdentity, out var localParts))
{
identityAndParts = (localIdentity, localParts);
s_TryParseDisplayNameCache.TryAdd(displayName, identityAndParts);
}
}
identity = identityAndParts.identity;
parts = identityAndParts.parts;
return identity != null;
static bool tryParseDisplayName(string displayName, [NotNullWhen(true)] out AssemblyIdentity? identity, out AssemblyIdentityParts parts)
{
// see ndp\clr\src\Binder\TextualIdentityParser.cpp, ndp\clr\src\Binder\StringLexer.cpp
identity = null;
parts = 0;
if (displayName == null)
{
throw new ArgumentNullException(nameof(displayName));
}
if (displayName.IndexOf('\0') >= 0)
{
return false;
}
int position = 0;
string? simpleName;
if (!TryParseNameToken(displayName, ref position, out simpleName))
{
return false;
}
var parsedParts = AssemblyIdentityParts.Name;
var seen = AssemblyIdentityParts.Name;
Version? version = null;
string? culture = null;
bool isRetargetable = false;
var contentType = AssemblyContentType.Default;
var publicKey = default(ImmutableArray<byte>);
var publicKeyToken = default(ImmutableArray<byte>);
while (position < displayName.Length)
{
// Parse ',' name '=' value
if (displayName[position] != ',')
{
return false;
}
position++;
string? propertyName;
if (!TryParseNameToken(displayName, ref position, out propertyName))
{
return false;
}
if (position >= displayName.Length || displayName[position] != '=')
{
return false;
}
position++;
string? propertyValue;
if (!TryParseNameToken(displayName, ref position, out propertyValue))
{
return false;
}
// Process property
if (string.Equals(propertyName, "Version", StringComparison.OrdinalIgnoreCase))
{
if ((seen & AssemblyIdentityParts.Version) != 0)
{
return false;
}
seen |= AssemblyIdentityParts.Version;
if (propertyValue == "*")
{
continue;
}
ulong versionLong;
AssemblyIdentityParts versionParts;
if (!TryParseVersion(propertyValue, out versionLong, out versionParts))
{
return false;
}
version = ToVersion(versionLong);
parsedParts |= versionParts;
}
else if (string.Equals(propertyName, "Culture", StringComparison.OrdinalIgnoreCase) ||
string.Equals(propertyName, "Language", StringComparison.OrdinalIgnoreCase))
{
if ((seen & AssemblyIdentityParts.Culture) != 0)
{
return false;
}
seen |= AssemblyIdentityParts.Culture;
if (propertyValue == "*")
{
continue;
}
culture = string.Equals(propertyValue, InvariantCultureDisplay, StringComparison.OrdinalIgnoreCase) ? null : propertyValue;
parsedParts |= AssemblyIdentityParts.Culture;
}
else if (string.Equals(propertyName, "PublicKey", StringComparison.OrdinalIgnoreCase))
{
if ((seen & AssemblyIdentityParts.PublicKey) != 0)
{
return false;
}
seen |= AssemblyIdentityParts.PublicKey;
if (propertyValue == "*")
{
continue;
}
ImmutableArray<byte> value;
if (!TryParsePublicKey(propertyValue, out value))
{
return false;
}
// NOTE: Fusion would also set the public key token (as derived from the public key) here.
// We may need to do this as well for error cases, as Fusion would fail to parse the
// assembly name if public key token calculation failed.
publicKey = value;
parsedParts |= AssemblyIdentityParts.PublicKey;
}
else if (string.Equals(propertyName, "PublicKeyToken", StringComparison.OrdinalIgnoreCase))
{
if ((seen & AssemblyIdentityParts.PublicKeyToken) != 0)
{
return false;
}
seen |= AssemblyIdentityParts.PublicKeyToken;
if (propertyValue == "*")
{
continue;
}
ImmutableArray<byte> value;
if (!TryParsePublicKeyToken(propertyValue, out value))
{
return false;
}
publicKeyToken = value;
parsedParts |= AssemblyIdentityParts.PublicKeyToken;
}
else if (string.Equals(propertyName, "Retargetable", StringComparison.OrdinalIgnoreCase))
{
if ((seen & AssemblyIdentityParts.Retargetability) != 0)
{
return false;
}
seen |= AssemblyIdentityParts.Retargetability;
if (propertyValue == "*")
{
continue;
}
if (string.Equals(propertyValue, "Yes", StringComparison.OrdinalIgnoreCase))
{
isRetargetable = true;
}
else if (string.Equals(propertyValue, "No", StringComparison.OrdinalIgnoreCase))
{
isRetargetable = false;
}
else
{
return false;
}
parsedParts |= AssemblyIdentityParts.Retargetability;
}
else if (string.Equals(propertyName, "ContentType", StringComparison.OrdinalIgnoreCase))
{
if ((seen & AssemblyIdentityParts.ContentType) != 0)
{
return false;
}
seen |= AssemblyIdentityParts.ContentType;
if (propertyValue == "*")
{
continue;
}
if (string.Equals(propertyValue, "WindowsRuntime", StringComparison.OrdinalIgnoreCase))
{
contentType = AssemblyContentType.WindowsRuntime;
}
else
{
return false;
}
parsedParts |= AssemblyIdentityParts.ContentType;
}
else
{
parsedParts |= AssemblyIdentityParts.Unknown;
}
}
// incompatible values:
if (isRetargetable && contentType == AssemblyContentType.WindowsRuntime)
{
return false;
}
bool hasPublicKey = !publicKey.IsDefault;
bool hasPublicKeyToken = !publicKeyToken.IsDefault;
identity = new AssemblyIdentity(simpleName, version, culture, hasPublicKey ? publicKey : publicKeyToken, hasPublicKey, isRetargetable, contentType);
if (hasPublicKey && hasPublicKeyToken && !identity.PublicKeyToken.SequenceEqual(publicKeyToken))
{
identity = null;
return false;
}
parts = parsedParts;
return true;
}
}
private static bool TryParseNameToken(string displayName, ref int position, [NotNullWhen(true)] out string? value)
{
Debug.Assert(displayName.IndexOf('\0') == -1);
int i = position;
// skip leading whitespace:
while (true)
{
if (i == displayName.Length)
{
value = null;
return false;
}
else if (!IsWhiteSpace(displayName[i]))
{
break;
}
i++;
}
char quote;
if (IsQuote(displayName[i]))
{
quote = displayName[i++];
}
else
{
quote = '\0';
}
int valueStart = i;
int valueEnd = displayName.Length;
bool containsEscapes = false;
while (true)
{
if (i >= displayName.Length)
{
i = displayName.Length;
break;
}
char c = displayName[i];
if (c == '\\')
{
containsEscapes = true;
i += 2;
continue;
}
if (quote == '\0')
{
if (IsNameTokenTerminator(c))
{
break;
}
else if (IsQuote(c))
{
value = null;
return false;
}
}
else if (c == quote)
{
valueEnd = i;
i++;
break;
}
i++;
}
if (quote == '\0')
{
int j = i - 1;
while (j >= valueStart && IsWhiteSpace(displayName[j]))
{
j--;
}
valueEnd = j + 1;
}
else
{
// skip any whitespace following the quote and check for the terminator
while (i < displayName.Length)
{
char c = displayName[i];
if (!IsWhiteSpace(c))
{
if (!IsNameTokenTerminator(c))
{
value = null;
return false;
}
break;
}
i++;
}
}
Debug.Assert(i == displayName.Length || IsNameTokenTerminator(displayName[i]));
position = i;
// empty
if (valueEnd == valueStart)
{
value = null;
return false;
}
if (!containsEscapes)
{
value = displayName.Substring(valueStart, valueEnd - valueStart);
return true;
}
else
{
return TryUnescape(displayName, valueStart, valueEnd, out value);
}
}
private static bool IsNameTokenTerminator(char c)
{
return c == '=' || c == ',';
}
private static bool IsQuote(char c)
{
return c == '"' || c == '\'';
}
internal static Version ToVersion(ulong version)
{
return new Version(
unchecked((ushort)(version >> 48)),
unchecked((ushort)(version >> 32)),
unchecked((ushort)(version >> 16)),
unchecked((ushort)version));
}
// internal for testing
// Parses version format:
// [version-part]{[.][version-part], 3}
// Where version part is
// [*]|[0-9]*
// The number of dots in the version determines the present parts, i.e.
// "1..2" parses as "1.0.2.0" with Major, Minor and Build parts.
// "1.*" parses as "1.0.0.0" with Major and Minor parts.
internal static bool TryParseVersion(string str, out ulong result, out AssemblyIdentityParts parts)
{
Debug.Assert(str.Length > 0);
Debug.Assert(str.IndexOf('\0') < 0);
const int MaxVersionParts = 4;
const int BitsPerVersionPart = 16;
parts = 0;
result = 0;
int partOffset = BitsPerVersionPart * (MaxVersionParts - 1);
int partIndex = 0;
int partValue = 0;
bool partHasValue = false;
bool partHasWildcard = false;
int i = 0;
while (true)
{
char c = (i < str.Length) ? str[i++] : '\0';
if (c == '.' || c == 0)
{
if (partIndex == MaxVersionParts || partHasValue && partHasWildcard)
{
return false;
}
result |= ((ulong)partValue) << partOffset;
if (partHasValue || partHasWildcard)
{
parts |= (AssemblyIdentityParts)((int)AssemblyIdentityParts.VersionMajor << partIndex);
}
if (c == 0)
{
return true;
}
// next part:
partValue = 0;
partOffset -= BitsPerVersionPart;
partIndex++;
partHasWildcard = partHasValue = false;
}
else if (c >= '0' && c <= '9')
{
partHasValue = true;
partValue = partValue * 10 + c - '0';
if (partValue > ushort.MaxValue)
{
return false;
}
}
else if (c == '*')
{
partHasWildcard = true;
}
else
{
return false;
}
}
}
private static bool TryParsePublicKey(string value, out ImmutableArray<byte> key)
{
if (!TryParseHexBytes(value, out key) ||
!MetadataHelpers.IsValidPublicKey(key))
{
key = default;
return false;
}
return true;
}
private const int PublicKeyTokenBytes = 8;
private static bool TryParsePublicKeyToken(string value, out ImmutableArray<byte> token)
{
if (string.Equals(value, "null", StringComparison.OrdinalIgnoreCase) ||
string.Equals(value, "neutral", StringComparison.OrdinalIgnoreCase))
{
token = ImmutableArray<byte>.Empty;
return true;
}
ImmutableArray<byte> result;
if (value.Length != (PublicKeyTokenBytes * 2) || !TryParseHexBytes(value, out result))
{
token = default;
return false;
}
token = result;
return true;
}
private static bool TryParseHexBytes(string value, out ImmutableArray<byte> result)
{
if (value.Length == 0 || (value.Length % 2) != 0)
{
result = default;
return false;
}
var length = value.Length / 2;
var bytes = ArrayBuilder<byte>.GetInstance(length);
for (int i = 0; i < length; i++)
{
int hi = HexValue(value[i * 2]);
int lo = HexValue(value[i * 2 + 1]);
if (hi < 0 || lo < 0)
{
result = default;
bytes.Free();
return false;
}
bytes.Add((byte)((hi << 4) | lo));
}
result = bytes.ToImmutableAndFree();
return true;
}
internal static int HexValue(char c)
{
if (c >= '0' && c <= '9')
{
return c - '0';
}
if (c >= 'a' && c <= 'f')
{
return c - 'a' + 10;
}
if (c >= 'A' && c <= 'F')
{
return c - 'A' + 10;
}
return -1;
}
private static bool IsWhiteSpace(char c)
{
return c == ' ' || c == '\t' || c == '\r' || c == '\n';
}
private static void EscapeName(StringBuilder result, string? name)
{
if (string.IsNullOrEmpty(name))
{
return;
}
bool quoted = false;
if (IsWhiteSpace(name[0]) || IsWhiteSpace(name[name.Length - 1]))
{
result.Append('"');
quoted = true;
}
for (int i = 0; i < name.Length; i++)
{
char c = name[i];
switch (c)
{
case ',':
case '=':
case '\\':
case '"':
case '\'':
result.Append('\\');
result.Append(c);
break;
case '\t':
result.Append("\\t");
break;
case '\r':
result.Append("\\r");
break;
case '\n':
result.Append("\\n");
break;
default:
result.Append(c);
break;
}
}
if (quoted)
{
result.Append('"');
}
}
private static bool TryUnescape(string str, int start, int end, [NotNullWhen(true)] out string? value)
{
var sb = PooledStringBuilder.GetInstance();
int i = start;
while (i < end)
{
char c = str[i++];
if (c == '\\')
{
if (!Unescape(sb.Builder, str, ref i))
{
value = null;
return false;
}
}
else
{
sb.Builder.Append(c);
}
}
value = sb.ToStringAndFree();
return true;
}
private static bool Unescape(StringBuilder sb, string str, ref int i)
{
if (i == str.Length)
{
return false;
}
char c = str[i++];
switch (c)
{
case ',':
case '=':
case '\\':
case '/':
case '"':
case '\'':
sb.Append(c);
return true;
case 't':
sb.Append('\t');
return true;
case 'n':
sb.Append('\n');
return true;
case 'r':
sb.Append('\r');
return true;
case 'u':
int semicolon = str.IndexOf(';', i);
if (semicolon == -1)
{
return false;
}
try
{
int codepoint = Convert.ToInt32(str.Substring(i, semicolon - i), 16);
// \0 is not valid in an assembly name
if (codepoint == 0)
{
return false;
}
sb.Append(char.ConvertFromUtf32(codepoint));
}
catch
{
return false;
}
i = semicolon + 1;
return true;
default:
return false;
}
}
}
}
|