|
// 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.
#nullable disable
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Reflection;
using System.Runtime.InteropServices;
using Microsoft.CodeAnalysis;
using Roslyn.Utilities;
namespace Microsoft.CodeAnalysis
{
internal sealed class FusionAssemblyIdentity
{
[Flags]
internal enum ASM_DISPLAYF
{
VERSION = 0x01,
CULTURE = 0x02,
PUBLIC_KEY_TOKEN = 0x04,
PUBLIC_KEY = 0x08,
CUSTOM = 0x10,
PROCESSORARCHITECTURE = 0x20,
LANGUAGEID = 0x40,
RETARGET = 0x80,
CONFIG_MASK = 0x100,
MVID = 0x200,
CONTENT_TYPE = 0x400,
FULL = VERSION | CULTURE | PUBLIC_KEY_TOKEN | RETARGET | PROCESSORARCHITECTURE | CONTENT_TYPE
}
internal enum PropertyId
{
PUBLIC_KEY = 0, // 0
PUBLIC_KEY_TOKEN, // 1
HASH_VALUE, // 2
NAME, // 3
MAJOR_VERSION, // 4
MINOR_VERSION, // 5
BUILD_NUMBER, // 6
REVISION_NUMBER, // 7
CULTURE, // 8
PROCESSOR_ID_ARRAY, // 9
OSINFO_ARRAY, // 10
HASH_ALGID, // 11
ALIAS, // 12
CODEBASE_URL, // 13
CODEBASE_LASTMOD, // 14
NULL_PUBLIC_KEY, // 15
NULL_PUBLIC_KEY_TOKEN, // 16
CUSTOM, // 17
NULL_CUSTOM, // 18
MVID, // 19
FILE_MAJOR_VERSION, // 20
FILE_MINOR_VERSION, // 21
FILE_BUILD_NUMBER, // 22
FILE_REVISION_NUMBER, // 23
RETARGET, // 24
SIGNATURE_BLOB, // 25
CONFIG_MASK, // 26
ARCHITECTURE, // 27
CONTENT_TYPE, // 28
MAX_PARAMS // 29
}
private static class CANOF
{
public const uint PARSE_DISPLAY_NAME = 0x1;
public const uint SET_DEFAULT_VALUES = 0x2;
}
[ComImport, InterfaceType(ComInterfaceType.InterfaceIsIUnknown), Guid("CD193BC0-B4BC-11d2-9833-00C04FC31D2E")]
internal unsafe interface IAssemblyName
{
void SetProperty(PropertyId id, void* data, uint size);
[PreserveSig]
int GetProperty(PropertyId id, void* data, ref uint size);
[PreserveSig]
int Finalize();
[PreserveSig]
int GetDisplayName(byte* buffer, ref uint characterCount, ASM_DISPLAYF dwDisplayFlags);
[PreserveSig]
int __BindToObject(/*...*/);
[PreserveSig]
int __GetName(/*...*/);
[PreserveSig]
int GetVersion(out uint versionHi, out uint versionLow);
[PreserveSig]
int IsEqual(IAssemblyName pName, uint dwCmpFlags);
[PreserveSig]
int Clone(out IAssemblyName pName);
}
[ComImport, InterfaceType(ComInterfaceType.InterfaceIsIUnknown), Guid("7c23ff90-33af-11d3-95da-00a024a85b51")]
internal interface IApplicationContext
{
}
// NOTE: The CLR caches assembly identities, but doesn't do so in a threadsafe manner.
// Wrap all calls to this with a lock.
private static readonly object s_assemblyIdentityGate = new object();
private static int CreateAssemblyNameObject(out IAssemblyName ppEnum, string szAssemblyName, uint dwFlags, IntPtr pvReserved)
{
lock (s_assemblyIdentityGate)
{
return RealCreateAssemblyNameObject(out ppEnum, szAssemblyName, dwFlags, pvReserved);
}
}
[DllImport("clr", EntryPoint = "CreateAssemblyNameObject", CharSet = CharSet.Unicode, PreserveSig = true)]
private static extern int RealCreateAssemblyNameObject(out IAssemblyName ppEnum, [MarshalAs(UnmanagedType.LPWStr)] string szAssemblyName, uint dwFlags, IntPtr pvReserved);
private const int ERROR_INSUFFICIENT_BUFFER = unchecked((int)0x8007007A);
private const int FUSION_E_INVALID_NAME = unchecked((int)0x80131047);
internal static unsafe string GetDisplayName(IAssemblyName nameObject, ASM_DISPLAYF displayFlags)
{
int hr;
uint characterCountIncludingTerminator = 0;
hr = nameObject.GetDisplayName(null, ref characterCountIncludingTerminator, displayFlags);
if (hr == 0)
{
return String.Empty;
}
if (hr != ERROR_INSUFFICIENT_BUFFER)
{
throw Marshal.GetExceptionForHR(hr);
}
byte[] data = new byte[(int)characterCountIncludingTerminator * 2];
fixed (byte* p = data)
{
hr = nameObject.GetDisplayName(p, ref characterCountIncludingTerminator, displayFlags);
if (hr != 0)
{
throw Marshal.GetExceptionForHR(hr);
}
return Marshal.PtrToStringUni((IntPtr)p, (int)characterCountIncludingTerminator - 1);
}
}
internal static unsafe byte[] GetPropertyBytes(IAssemblyName nameObject, PropertyId propertyId)
{
int hr;
uint size = 0;
hr = nameObject.GetProperty(propertyId, null, ref size);
if (hr == 0)
{
return null;
}
if (hr != ERROR_INSUFFICIENT_BUFFER)
{
throw Marshal.GetExceptionForHR(hr);
}
byte[] data = new byte[(int)size];
fixed (byte* p = data)
{
hr = nameObject.GetProperty(propertyId, p, ref size);
if (hr != 0)
{
throw Marshal.GetExceptionForHR(hr);
}
}
return data;
}
internal static unsafe string GetPropertyString(IAssemblyName nameObject, PropertyId propertyId)
{
byte[] data = GetPropertyBytes(nameObject, propertyId);
if (data == null)
{
return null;
}
fixed (byte* p = data)
{
return Marshal.PtrToStringUni((IntPtr)p, (data.Length / 2) - 1);
}
}
internal static unsafe bool IsKeyOrTokenEmpty(IAssemblyName nameObject, PropertyId propertyId)
{
Debug.Assert(propertyId == PropertyId.NULL_PUBLIC_KEY_TOKEN || propertyId == PropertyId.NULL_PUBLIC_KEY);
uint size = 0;
int hr = nameObject.GetProperty(propertyId, null, ref size);
return hr == 0;
}
internal static unsafe Version GetVersion(IAssemblyName nameObject)
{
uint hi, lo;
int hr = nameObject.GetVersion(out hi, out lo);
if (hr != 0)
{
Debug.Assert(hr == FUSION_E_INVALID_NAME);
return null;
}
return new Version((int)(hi >> 16), (int)(hi & 0xffff), (int)(lo >> 16), (int)(lo & 0xffff));
}
internal static Version GetVersion(IAssemblyName name, out AssemblyIdentityParts parts)
{
uint? major = GetPropertyWord(name, PropertyId.MAJOR_VERSION);
uint? minor = GetPropertyWord(name, PropertyId.MINOR_VERSION);
uint? build = GetPropertyWord(name, PropertyId.BUILD_NUMBER);
uint? revision = GetPropertyWord(name, PropertyId.REVISION_NUMBER);
parts = 0;
if (major != null)
{
parts |= AssemblyIdentityParts.VersionMajor;
}
if (minor != null)
{
parts |= AssemblyIdentityParts.VersionMinor;
}
if (build != null)
{
parts |= AssemblyIdentityParts.VersionBuild;
}
if (revision != null)
{
parts |= AssemblyIdentityParts.VersionRevision;
}
return new Version((int)(major ?? 0), (int)(minor ?? 0), (int)(build ?? 0), (int)(revision ?? 0));
}
internal static byte[] GetPublicKeyToken(IAssemblyName nameObject)
{
byte[] result = GetPropertyBytes(nameObject, PropertyId.PUBLIC_KEY_TOKEN);
if (result != null)
{
return result;
}
if (IsKeyOrTokenEmpty(nameObject, PropertyId.NULL_PUBLIC_KEY_TOKEN))
{
return Array.Empty<byte>();
}
return null;
}
internal static byte[] GetPublicKey(IAssemblyName nameObject)
{
byte[] result = GetPropertyBytes(nameObject, PropertyId.PUBLIC_KEY);
if (result != null)
{
return result;
}
if (IsKeyOrTokenEmpty(nameObject, PropertyId.NULL_PUBLIC_KEY))
{
return Array.Empty<byte>();
}
return null;
}
internal static unsafe uint? GetPropertyWord(IAssemblyName nameObject, PropertyId propertyId)
{
uint result;
uint size = sizeof(uint);
int hr = nameObject.GetProperty(propertyId, &result, ref size);
if (hr != 0)
{
throw Marshal.GetExceptionForHR(hr);
}
if (size == 0)
{
return null;
}
return result;
}
internal static string GetName(IAssemblyName nameObject)
{
return GetPropertyString(nameObject, PropertyId.NAME);
}
internal static string GetCulture(IAssemblyName nameObject)
{
return GetPropertyString(nameObject, PropertyId.CULTURE);
}
internal static AssemblyContentType GetContentType(IAssemblyName nameObject)
{
return (AssemblyContentType)(GetPropertyWord(nameObject, PropertyId.CONTENT_TYPE) ?? 0);
}
internal static ProcessorArchitecture GetProcessorArchitecture(IAssemblyName nameObject)
{
return (ProcessorArchitecture)(GetPropertyWord(nameObject, PropertyId.ARCHITECTURE) ?? 0);
}
internal static unsafe AssemblyNameFlags GetFlags(IAssemblyName nameObject)
{
AssemblyNameFlags result = 0;
uint retarget = GetPropertyWord(nameObject, PropertyId.RETARGET) ?? 0;
if (retarget != 0)
{
result |= AssemblyNameFlags.Retargetable;
}
return result;
}
private static unsafe void SetProperty(IAssemblyName nameObject, PropertyId propertyId, string data)
{
if (data == null)
{
nameObject.SetProperty(propertyId, null, 0);
}
else
{
Debug.Assert(data.IndexOf('\0') == -1);
fixed (char* p = data)
{
Debug.Assert(p[data.Length] == '\0');
// size is in bytes, include trailing \0 character:
nameObject.SetProperty(propertyId, p, (uint)(data.Length + 1) * 2);
}
}
}
private static unsafe void SetProperty(IAssemblyName nameObject, PropertyId propertyId, byte[] data)
{
if (data == null)
{
nameObject.SetProperty(propertyId, null, 0);
}
else
{
fixed (byte* p = data)
{
nameObject.SetProperty(propertyId, p, (uint)data.Length);
}
}
}
private static unsafe void SetProperty(IAssemblyName nameObject, PropertyId propertyId, ushort data)
{
nameObject.SetProperty(propertyId, &data, sizeof(ushort));
}
private static unsafe void SetProperty(IAssemblyName nameObject, PropertyId propertyId, uint data)
{
nameObject.SetProperty(propertyId, &data, sizeof(uint));
}
private static unsafe void SetPublicKeyToken(IAssemblyName nameObject, byte[] value)
{
// An empty public key token is set via NULL_PUBLIC_KEY_TOKEN property.
if (value != null && value.Length == 0)
{
nameObject.SetProperty(PropertyId.NULL_PUBLIC_KEY_TOKEN, null, 0);
}
else
{
SetProperty(nameObject, PropertyId.PUBLIC_KEY_TOKEN, value);
}
}
/// <summary>
/// Converts <see cref="IAssemblyName"/> to <see cref="AssemblyName"/> with all metadata fields filled.
/// </summary>
/// <returns>
/// Assembly name with Version, Culture and PublicKeyToken components filled in:
/// "SimpleName, Version=#.#.#.#, Culture=XXX, PublicKeyToken=XXXXXXXXXXXXXXXX".
/// In addition Retargetable flag and ContentType are set.
/// </returns>
internal static AssemblyIdentity ToAssemblyIdentity(IAssemblyName nameObject)
{
if (nameObject == null)
{
return null;
}
AssemblyNameFlags flags = GetFlags(nameObject);
byte[] publicKey = GetPublicKey(nameObject);
bool hasPublicKey = publicKey != null && publicKey.Length != 0;
AssemblyIdentityParts versionParts;
return new AssemblyIdentity(
GetName(nameObject),
GetVersion(nameObject, out versionParts),
GetCulture(nameObject) ?? "",
(hasPublicKey ? publicKey : GetPublicKeyToken(nameObject)).AsImmutableOrNull(),
hasPublicKey: hasPublicKey,
isRetargetable: (flags & AssemblyNameFlags.Retargetable) != 0,
contentType: GetContentType(nameObject));
}
/// <summary>
/// Converts <see cref="AssemblyName"/> to an equivalent <see cref="IAssemblyName"/>.
/// </summary>
internal static IAssemblyName ToAssemblyNameObject(AssemblyName name)
{
if (name == null)
{
return null;
}
IAssemblyName result;
Marshal.ThrowExceptionForHR(CreateAssemblyNameObject(out result, null, 0, IntPtr.Zero));
string assemblyName = name.Name;
if (assemblyName != null)
{
if (assemblyName.IndexOf('\0') >= 0)
{
#if SCRIPTING
throw new ArgumentException(Scripting.ScriptingResources.InvalidCharactersInAssemblyName, nameof(name));
#elif EDITOR_FEATURES
throw new ArgumentException(Microsoft.CodeAnalysis.Editor.EditorFeaturesResources.Invalid_characters_in_assembly_name, nameof(name));
#else
throw new ArgumentException(Microsoft.CodeAnalysis.CodeAnalysisResources.InvalidCharactersInAssemblyName, nameof(name));
#endif
}
SetProperty(result, PropertyId.NAME, assemblyName);
}
if (name.Version != null)
{
SetProperty(result, PropertyId.MAJOR_VERSION, unchecked((ushort)name.Version.Major));
SetProperty(result, PropertyId.MINOR_VERSION, unchecked((ushort)name.Version.Minor));
SetProperty(result, PropertyId.BUILD_NUMBER, unchecked((ushort)name.Version.Build));
SetProperty(result, PropertyId.REVISION_NUMBER, unchecked((ushort)name.Version.Revision));
}
string cultureName = name.CultureName;
if (cultureName != null)
{
if (cultureName.IndexOf('\0') >= 0)
{
#if SCRIPTING
throw new ArgumentException(Microsoft.CodeAnalysis.Scripting.ScriptingResources.InvalidCharactersInAssemblyName, nameof(name));
#elif EDITOR_FEATURES
throw new ArgumentException(Microsoft.CodeAnalysis.Editor.EditorFeaturesResources.Invalid_characters_in_assembly_name, nameof(name));
#else
throw new ArgumentException(Microsoft.CodeAnalysis.CodeAnalysisResources.InvalidCharactersInAssemblyName, nameof(name));
#endif
}
SetProperty(result, PropertyId.CULTURE, cultureName);
}
if (name.Flags == AssemblyNameFlags.Retargetable)
{
SetProperty(result, PropertyId.RETARGET, 1U);
}
if (name.ContentType != AssemblyContentType.Default)
{
SetProperty(result, PropertyId.CONTENT_TYPE, (uint)name.ContentType);
}
byte[] token = name.GetPublicKeyToken();
SetPublicKeyToken(result, token);
return result;
}
/// <summary>
/// Creates <see cref="IAssemblyName"/> object by parsing given display name.
/// </summary>
internal static IAssemblyName ToAssemblyNameObject(string displayName)
{
// CLR doesn't handle \0 in the display name well:
if (displayName.IndexOf('\0') >= 0)
{
return null;
}
Debug.Assert(displayName != null);
IAssemblyName result;
int hr = CreateAssemblyNameObject(out result, displayName, CANOF.PARSE_DISPLAY_NAME, IntPtr.Zero);
if (hr != 0)
{
return null;
}
Debug.Assert(result != null);
return result;
}
/// <summary>
/// Selects the candidate assembly with the largest version number. Uses culture as a tie-breaker if it is provided.
/// All candidates are assumed to have the same name and must include versions and cultures.
/// </summary>
internal static IAssemblyName GetBestMatch(IEnumerable<IAssemblyName> candidates, string preferredCultureOpt)
{
IAssemblyName bestCandidate = null;
Version bestVersion = null;
string bestCulture = null;
foreach (var candidate in candidates)
{
if (bestCandidate != null)
{
Version candidateVersion = GetVersion(candidate);
Debug.Assert(candidateVersion != null);
if (bestVersion == null)
{
bestVersion = GetVersion(bestCandidate);
Debug.Assert(bestVersion != null);
}
int cmp = bestVersion.CompareTo(candidateVersion);
if (cmp == 0)
{
if (preferredCultureOpt != null)
{
string candidateCulture = GetCulture(candidate);
Debug.Assert(candidateCulture != null);
if (bestCulture == null)
{
bestCulture = GetCulture(candidate);
Debug.Assert(bestCulture != null);
}
// we have exactly the preferred culture or
// we have neutral culture and the best candidate's culture isn't the preferred one:
if (StringComparer.OrdinalIgnoreCase.Equals(candidateCulture, preferredCultureOpt) ||
candidateCulture.Length == 0 && !StringComparer.OrdinalIgnoreCase.Equals(bestCulture, preferredCultureOpt))
{
bestCandidate = candidate;
bestVersion = candidateVersion;
bestCulture = candidateCulture;
}
}
}
else if (cmp < 0)
{
bestCandidate = candidate;
bestVersion = candidateVersion;
}
}
else
{
bestCandidate = candidate;
}
}
return bestCandidate;
}
}
}
|