diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/ClrDataModule.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/ClrDataModule.cs index 5467735eaa0319..6172fe6fdf295c 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/ClrDataModule.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/ClrDataModule.cs @@ -34,6 +34,8 @@ public sealed unsafe partial class ClrDataModule : ICustomQueryInterface, IXCLRD // This is an IUnknown pointer for the legacy implementation private readonly nint _legacyModulePointer; + private MetaDataImportImpl? _metaDataImportImpl; + public ClrDataModule(TargetPointer address, Target target, IXCLRDataModule? legacyImpl) { _address = address; @@ -49,19 +51,70 @@ public ClrDataModule(TargetPointer address, Target target, IXCLRDataModule? lega private const uint CORDEBUG_JIT_DEFAULT = 0x1; private const uint CORDEBUG_JIT_DISABLE_OPTIMIZATION = 0x3; - private static readonly Guid IID_IMetaDataImport = Guid.Parse("7DAC8207-D3AE-4c75-9B67-92801A497D44"); CustomQueryInterfaceResult ICustomQueryInterface.GetInterface(ref Guid iid, out nint ppv) { ppv = default; - if (!LegacyFallbackHelper.CanFallback() || _legacyModulePointer == 0) - return CustomQueryInterfaceResult.NotHandled; // Legacy DAC implementation of IXCLRDataModule handles QIs for IMetaDataImport by creating and // passing out an implementation of IMetaDataImport. Note that it does not do COM aggregation. // It simply returns a completely separate object. See ClrDataModule::QueryInterface in task.cpp - if (iid == IID_IMetaDataImport && Marshal.QueryInterface(_legacyModulePointer, iid, out ppv) >= 0) + // The returned MetaDataImportImpl also implements IMetaDataImport2 and IMetaDataAssemblyImport, + // so consumers can QI the returned object for those interfaces as well. + // + // IMPORTANT: Some consumers (e.g. ClrMD) QI for IMetaDataImport but then access IMetaDataImport2 + // vtable slots beyond the IMetaDataImport vtable boundary. This works with native C++ COM objects + // (where the vtable for IMetaDataImport and IMetaDataImport2 is unified) but breaks with managed + // [GeneratedComInterface] CCWs which create separate vtables per interface. To handle this, we + // always return the IMetaDataImport2 vtable pointer when asked for IMetaDataImport. Since + // IMetaDataImport2 inherits from IMetaDataImport, the first slots are identical. + if (iid == typeof(IMetaDataImport).GUID) + { + MetaDataImportImpl? wrapper = _metaDataImportImpl; + if (wrapper is null) + { + MetadataReader? reader = null; + IMetaDataImport? legacyImport = null; + + try + { + ILoader loader = _target.Contracts.Loader; + Contracts.ModuleHandle moduleHandle = loader.GetModuleHandleFromModulePtr(_address); + reader = _target.Contracts.EcmaMetadata.GetMetadata(moduleHandle); + } + catch + { + } + + try + { + Guid iidMetaDataImport = typeof(IMetaDataImport).GUID; + if (_legacyModulePointer != 0 && Marshal.QueryInterface(_legacyModulePointer, iidMetaDataImport, out nint ppMdi) >= 0) + { + legacyImport = ComInterfaceMarshaller.ConvertToManaged((void*)ppMdi); + Marshal.Release(ppMdi); + } + } + catch + { + } + + if (reader is null) + return CustomQueryInterfaceResult.NotHandled; + + wrapper = new MetaDataImportImpl(reader, legacyImport); + _metaDataImportImpl ??= wrapper; + wrapper = _metaDataImportImpl; + } + + nint pUnk = (nint)ComInterfaceMarshaller.ConvertToUnmanaged(wrapper); + + // ConvertToUnmanaged returns a COM pointer for IMetaDataImport2. + // We return this directly as ppv so that consumers (e.g. ClrMD) that QI for + // IMetaDataImport but access IMetaDataImport2 vtable slots get the full vtable. + ppv = pUnk; return CustomQueryInterfaceResult.Handled; + } return CustomQueryInterfaceResult.NotHandled; } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/IMetaDataImport.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/IMetaDataImport.cs new file mode 100644 index 00000000000000..2924ca87e5512f --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/IMetaDataImport.cs @@ -0,0 +1,326 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Runtime.InteropServices; +using System.Runtime.InteropServices.Marshalling; + +namespace Microsoft.Diagnostics.DataContractReader.Legacy; + +// Managed declarations for IMetaDataImport and IMetaDataImport2. +// See src/coreclr/inc/cor.h +// Vtable ordering must exactly match the native interface. + +[GeneratedComInterface] +[Guid("7DAC8207-D3AE-4c75-9B67-92801A497D44")] +public unsafe partial interface IMetaDataImport +{ + [PreserveSig] + void CloseEnum(nint hEnum); + + [PreserveSig] + int CountEnum(nint hEnum, uint* pulCount); + + [PreserveSig] + int ResetEnum(nint hEnum, uint ulPos); + + [PreserveSig] + int EnumTypeDefs(nint* phEnum, uint* rTypeDefs, uint cMax, uint* pcTypeDefs); + + [PreserveSig] + int EnumInterfaceImpls(nint* phEnum, uint td, uint* rImpls, uint cMax, uint* pcImpls); + + [PreserveSig] + int EnumTypeRefs(nint* phEnum, uint* rTypeRefs, uint cMax, uint* pcTypeRefs); + + [PreserveSig] + int FindTypeDefByName(char* szTypeDef, uint tkEnclosingClass, uint* ptd); + + [PreserveSig] + int GetScopeProps(char* szName, uint cchName, uint* pchName, Guid* pmvid); + + [PreserveSig] + int GetModuleFromScope(uint* pmd); + + [PreserveSig] + int GetTypeDefProps(uint td, char* szTypeDef, uint cchTypeDef, uint* pchTypeDef, uint* pdwTypeDefFlags, uint* ptkExtends); + + [PreserveSig] + int GetInterfaceImplProps(uint iiImpl, uint* pClass, uint* ptkIface); + + [PreserveSig] + int GetTypeRefProps(uint tr, uint* ptkResolutionScope, char* szName, uint cchName, uint* pchName); + + [PreserveSig] + int ResolveTypeRef(uint tr, Guid* riid, void** ppIScope, uint* ptd); + + [PreserveSig] + int EnumMembers(nint* phEnum, uint cl, uint* rMembers, uint cMax, uint* pcTokens); + + [PreserveSig] + int EnumMembersWithName(nint* phEnum, uint cl, char* szName, uint* rMembers, uint cMax, uint* pcTokens); + + [PreserveSig] + int EnumMethods(nint* phEnum, uint cl, uint* rMethods, uint cMax, uint* pcTokens); + + [PreserveSig] + int EnumMethodsWithName(nint* phEnum, uint cl, char* szName, uint* rMethods, uint cMax, uint* pcTokens); + + [PreserveSig] + int EnumFields(nint* phEnum, uint cl, uint* rFields, uint cMax, uint* pcTokens); + + [PreserveSig] + int EnumFieldsWithName(nint* phEnum, uint cl, char* szName, uint* rFields, uint cMax, uint* pcTokens); + + [PreserveSig] + int EnumParams(nint* phEnum, uint mb, uint* rParams, uint cMax, uint* pcTokens); + + [PreserveSig] + int EnumMemberRefs(nint* phEnum, uint tkParent, uint* rMemberRefs, uint cMax, uint* pcTokens); + + [PreserveSig] + int EnumMethodImpls(nint* phEnum, uint td, uint* rMethodBody, uint* rMethodDecl, uint cMax, uint* pcTokens); + + [PreserveSig] + int EnumPermissionSets(nint* phEnum, uint tk, uint dwActions, uint* rPermission, uint cMax, uint* pcTokens); + + [PreserveSig] + int FindMember(uint td, char* szName, byte* pvSigBlob, uint cbSigBlob, uint* pmb); + + [PreserveSig] + int FindMethod(uint td, char* szName, byte* pvSigBlob, uint cbSigBlob, uint* pmb); + + [PreserveSig] + int FindField(uint td, char* szName, byte* pvSigBlob, uint cbSigBlob, uint* pmb); + + [PreserveSig] + int FindMemberRef(uint td, char* szName, byte* pvSigBlob, uint cbSigBlob, uint* pmr); + + [PreserveSig] + int GetMethodProps(uint mb, uint* pClass, char* szMethod, uint cchMethod, uint* pchMethod, + uint* pdwAttr, byte** ppvSigBlob, uint* pcbSigBlob, uint* pulCodeRVA, uint* pdwImplFlags); + + [PreserveSig] + int GetMemberRefProps(uint mr, uint* ptk, char* szMember, uint cchMember, uint* pchMember, + byte** ppvSigBlob, uint* pbSig); + + [PreserveSig] + int EnumProperties(nint* phEnum, uint td, uint* rProperties, uint cMax, uint* pcProperties); + + [PreserveSig] + int EnumEvents(nint* phEnum, uint td, uint* rEvents, uint cMax, uint* pcEvents); + + [PreserveSig] + int GetEventProps(uint ev, uint* pClass, char* szEvent, uint cchEvent, uint* pchEvent, + uint* pdwEventFlags, uint* ptkEventType, uint* pmdAddOn, uint* pmdRemoveOn, uint* pmdFire, + uint* rmdOtherMethod, uint cMax, uint* pcOtherMethod); + + [PreserveSig] + int EnumMethodSemantics(nint* phEnum, uint mb, uint* rEventProp, uint cMax, uint* pcEventProp); + + [PreserveSig] + int GetMethodSemantics(uint mb, uint tkEventProp, uint* pdwSemanticsFlags); + + [PreserveSig] + int GetClassLayout(uint td, uint* pdwPackSize, void* rFieldOffset, uint cMax, uint* pcFieldOffset, uint* pulClassSize); + + [PreserveSig] + int GetFieldMarshal(uint tk, byte** ppvNativeType, uint* pcbNativeType); + + [PreserveSig] + int GetRVA(uint tk, uint* pulCodeRVA, uint* pdwImplFlags); + + [PreserveSig] + int GetPermissionSetProps(uint pm, uint* pdwAction, void** ppvPermission, uint* pcbPermission); + + [PreserveSig] + int GetSigFromToken(uint mdSig, byte** ppvSig, uint* pcbSig); + + [PreserveSig] + int GetModuleRefProps(uint mur, char* szName, uint cchName, uint* pchName); + + [PreserveSig] + int EnumModuleRefs(nint* phEnum, uint* rModuleRefs, uint cmax, uint* pcModuleRefs); + + [PreserveSig] + int GetTypeSpecFromToken(uint typespec, byte** ppvSig, uint* pcbSig); + + [PreserveSig] + int GetNameFromToken(uint tk, byte** pszUtf8NamePtr); + + [PreserveSig] + int EnumUnresolvedMethods(nint* phEnum, uint* rMethods, uint cMax, uint* pcTokens); + + [PreserveSig] + int GetUserString(uint stk, char* szString, uint cchString, uint* pchString); + + [PreserveSig] + int GetPinvokeMap(uint tk, uint* pdwMappingFlags, char* szImportName, uint cchImportName, + uint* pchImportName, uint* pmrImportDLL); + + [PreserveSig] + int EnumSignatures(nint* phEnum, uint* rSignatures, uint cmax, uint* pcSignatures); + + [PreserveSig] + int EnumTypeSpecs(nint* phEnum, uint* rTypeSpecs, uint cmax, uint* pcTypeSpecs); + + [PreserveSig] + int EnumUserStrings(nint* phEnum, uint* rStrings, uint cmax, uint* pcStrings); + + [PreserveSig] + int GetParamForMethodIndex(uint md, uint ulParamSeq, uint* ppd); + + [PreserveSig] + int EnumCustomAttributes(nint* phEnum, uint tk, uint tkType, uint* rCustomAttributes, uint cMax, uint* pcCustomAttributes); + + [PreserveSig] + int GetCustomAttributeProps(uint cv, uint* ptkObj, uint* ptkType, void** ppBlob, uint* pcbSize); + + [PreserveSig] + int FindTypeRef(uint tkResolutionScope, char* szName, uint* ptr); + + [PreserveSig] + int GetMemberProps(uint mb, uint* pClass, char* szMember, uint cchMember, uint* pchMember, + uint* pdwAttr, byte** ppvSigBlob, uint* pcbSigBlob, uint* pulCodeRVA, uint* pdwImplFlags, + uint* pdwCPlusTypeFlag, void** ppValue, uint* pcchValue); + + [PreserveSig] + int GetFieldProps(uint mb, uint* pClass, char* szField, uint cchField, uint* pchField, + uint* pdwAttr, byte** ppvSigBlob, uint* pcbSigBlob, uint* pdwCPlusTypeFlag, + void** ppValue, uint* pcchValue); + + [PreserveSig] + int GetPropertyProps(uint prop, uint* pClass, char* szProperty, uint cchProperty, uint* pchProperty, + uint* pdwPropFlags, byte** ppvSig, uint* pbSig, uint* pdwCPlusTypeFlag, + void** ppDefaultValue, uint* pcchDefaultValue, uint* pmdSetter, uint* pmdGetter, + uint* rmdOtherMethod, uint cMax, uint* pcOtherMethod); + + [PreserveSig] + int GetParamProps(uint tk, uint* pmd, uint* pulSequence, char* szName, uint cchName, uint* pchName, + uint* pdwAttr, uint* pdwCPlusTypeFlag, void** ppValue, uint* pcchValue); + + [PreserveSig] + int GetCustomAttributeByName(uint tkObj, char* szName, void** ppData, uint* pcbData); + + [PreserveSig] + int IsValidToken(uint tk); + + [PreserveSig] + int GetNestedClassProps(uint tdNestedClass, uint* ptdEnclosingClass); + + [PreserveSig] + int GetNativeCallConvFromSig(void* pvSig, uint cbSig, uint* pCallConv); + + [PreserveSig] + int IsGlobal(uint pd, int* pbGlobal); +} + +[GeneratedComInterface] +[Guid("FCE5EFA0-8BBA-4f8e-A036-8F2022B08466")] +public unsafe partial interface IMetaDataImport2 : IMetaDataImport +{ + [PreserveSig] + int EnumGenericParams(nint* phEnum, uint tk, uint* rGenericParams, uint cMax, uint* pcGenericParams); + + [PreserveSig] + int GetGenericParamProps(uint gp, uint* pulParamSeq, uint* pdwParamFlags, uint* ptOwner, + uint* reserved, char* wzname, uint cchName, uint* pchName); + + [PreserveSig] + int GetMethodSpecProps(uint mi, uint* tkParent, byte** ppvSigBlob, uint* pcbSigBlob); + + [PreserveSig] + int EnumGenericParamConstraints(nint* phEnum, uint tk, uint* rGenericParamConstraints, uint cMax, uint* pcGenericParamConstraints); + + [PreserveSig] + int GetGenericParamConstraintProps(uint gpc, uint* ptGenericParam, uint* ptkConstraintType); + + [PreserveSig] + int GetPEKind(uint* pdwPEKind, uint* pdwMachine); + + [PreserveSig] + int GetVersionString(char* pwzBuf, uint ccBufSize, uint* pccBufSize); + + [PreserveSig] + int EnumMethodSpecs(nint* phEnum, uint tk, uint* rMethodSpecs, uint cMax, uint* pcMethodSpecs); +} + +// ASSEMBLYMETADATA from cor.h — version + locale info for assemblies. +// rProcessor and rOS are deprecated and always null/0 in modern usage. +// This struct contains pointer-sized fields (szLocale, rProcessor, rOS) which is safe because +// it is only used in same-process COM interop where bitness always matches the caller. +[StructLayout(LayoutKind.Sequential)] +public unsafe struct ASSEMBLYMETADATA +{ + public ushort usMajorVersion; + public ushort usMinorVersion; + public ushort usBuildNumber; + public ushort usRevisionNumber; + public char* szLocale; + public uint cbLocale; + public uint* rProcessor; + public uint ulProcessor; + public nint rOS; + public uint ulOS; +} + +[GeneratedComInterface] +[Guid("EE62470B-E94B-424E-9B7C-2F00C9249F93")] +public unsafe partial interface IMetaDataAssemblyImport +{ + [PreserveSig] + int GetAssemblyProps(uint mda, byte** ppbPublicKey, uint* pcbPublicKey, uint* pulHashAlgId, + char* szName, uint cchName, uint* pchName, ASSEMBLYMETADATA* pMetaData, uint* pdwAssemblyFlags); + + [PreserveSig] + int GetAssemblyRefProps(uint mdar, byte** ppbPublicKeyOrToken, uint* pcbPublicKeyOrToken, + char* szName, uint cchName, uint* pchName, ASSEMBLYMETADATA* pMetaData, + byte** ppbHashValue, uint* pcbHashValue, uint* pdwAssemblyRefFlags); + + [PreserveSig] + int GetFileProps(uint mdf, char* szName, uint cchName, uint* pchName, + byte** ppbHashValue, uint* pcbHashValue, uint* pdwFileFlags); + + [PreserveSig] + int GetExportedTypeProps(uint mdct, char* szName, uint cchName, uint* pchName, + uint* ptkImplementation, uint* ptkTypeDef, uint* pdwExportedTypeFlags); + + [PreserveSig] + int GetManifestResourceProps(uint mdmr, char* szName, uint cchName, uint* pchName, + uint* ptkImplementation, uint* pdwOffset, uint* pdwResourceFlags); + + [PreserveSig] + int EnumAssemblyRefs(nint* phEnum, uint* rAssemblyRefs, uint cMax, uint* pcTokens); + + [PreserveSig] + int EnumFiles(nint* phEnum, uint* rFiles, uint cMax, uint* pcTokens); + + [PreserveSig] + int EnumExportedTypes(nint* phEnum, uint* rExportedTypes, uint cMax, uint* pcTokens); + + [PreserveSig] + int EnumManifestResources(nint* phEnum, uint* rManifestResources, uint cMax, uint* pcTokens); + + [PreserveSig] + int GetAssemblyFromScope(uint* ptkAssembly); + + [PreserveSig] + int FindExportedTypeByName(char* szName, uint mdtExportedType, uint* ptkExportedType); + + [PreserveSig] + int FindManifestResourceByName(char* szName, uint* ptkManifestResource); + + [PreserveSig] + void CloseEnum(nint hEnum); + + [PreserveSig] + int FindAssembliesByName(char* szAppBase, char* szPrivateBin, char* szAssemblyName, + nint* ppIUnk, uint cMax, uint* pcAssemblies); +} + +internal static class CldbHResults +{ + public const int CLDB_E_RECORD_NOTFOUND = unchecked((int)0x80131130); + public const int CLDB_E_FILE_CORRUPT = unchecked((int)0x8013110E); + public const int CLDB_S_TRUNCATION = 0x00131106; +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/LegacyFallbackHelper.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/LegacyFallbackHelper.cs index 7e7fbac013a022..bbf9b89f87df82 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/LegacyFallbackHelper.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/LegacyFallbackHelper.cs @@ -26,8 +26,8 @@ internal static class LegacyFallbackHelper // Dump creation — the cDAC does not implement memory enumeration. nameof(ICLRDataEnumMemoryRegions.EnumMemoryRegions), - // IMetaDataImport QI — needed until managed MetadataReader wrapper lands (PR #127028). - nameof(ICustomQueryInterface.GetInterface), + // IXCLRDataModule — not yet implemented in the cDAC. + nameof(IXCLRDataModule.GetMethodDefinitionByToken), // GC heap analysis — not yet implemented in the cDAC (PR #125895). nameof(ISOSDacInterface11.IsTrackedType), diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/MetaDataImportImpl.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/MetaDataImportImpl.cs new file mode 100644 index 00000000000000..2a2711751d6efa --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/MetaDataImportImpl.cs @@ -0,0 +1,1754 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.Reflection.Metadata; +using System.Reflection.Metadata.Ecma335; +using System.Runtime.InteropServices; +using System.Runtime.InteropServices.Marshalling; +using Microsoft.Diagnostics.DataContractReader.Contracts; + +namespace Microsoft.Diagnostics.DataContractReader.Legacy; + +[GeneratedComClass] +internal sealed unsafe partial class MetaDataImportImpl : ICustomQueryInterface, IMetaDataImport2, IMetaDataAssemblyImport +{ + private readonly MetadataReader _reader; + private readonly IMetaDataImport? _legacyImport; + private readonly IMetaDataImport2? _legacyImport2; + private readonly IMetaDataAssemblyImport? _legacyAssemblyImport; + private Dictionary? _interfaceImplToTypeDef; + private Dictionary? _paramToMethod; + + // Tracks GCHandle values allocated by AllocEnum so that CountEnum, ResetEnum, + // and CloseEnum can distinguish cDAC-created enum handles from legacy HENUMInternal*. + // ConcurrentDictionary is used because COM objects may be called from multiple threads. + private readonly ConcurrentDictionary _cdacEnumHandles = new(); + + public MetaDataImportImpl(MetadataReader reader, IMetaDataImport? legacyImport = null) + { + _reader = reader; + _legacyImport = legacyImport; + _legacyImport2 = legacyImport as IMetaDataImport2; + _legacyAssemblyImport = legacyImport as IMetaDataAssemblyImport; + } + + CustomQueryInterfaceResult ICustomQueryInterface.GetInterface(ref Guid iid, out nint ppv) + { + ppv = default; + + if (iid == typeof(IMetaDataImport).GUID) + { + // ConvertToUnmanaged returns an already-AddRef'd IMetaDataImport2 COM pointer. + // Return it directly so consumers get the full IMetaDataImport2 vtable. + ppv = (nint)ComInterfaceMarshaller.ConvertToUnmanaged(this); + return CustomQueryInterfaceResult.Handled; + } + + return CustomQueryInterfaceResult.NotHandled; + } + + + private sealed class MetadataEnum + { + public List Tokens { get; } + public int Position { get; set; } + + public MetadataEnum(List tokens) + { + Tokens = tokens; + } + } + + private nint AllocEnum(List tokens) + { + MetadataEnum e = new(tokens); + GCHandle handle = GCHandle.Alloc(e); + nint ptr = GCHandle.ToIntPtr(handle); + _cdacEnumHandles.TryAdd(ptr, 0); + return ptr; + } + + private MetadataEnum GetEnum(nint hEnum) + { + if (hEnum == 0 || !_cdacEnumHandles.ContainsKey(hEnum)) + throw new ArgumentException("Invalid enum handle.", nameof(hEnum)); + GCHandle handle = GCHandle.FromIntPtr(hEnum); + return (MetadataEnum)(handle.Target ?? throw new ArgumentException("Enum handle target is null.", nameof(hEnum))); + } + + private int FillEnum(nint* phEnum, List tokens, uint* rTokens, uint cMax, uint* pcTokens) + { + if (phEnum is null) + return HResults.E_INVALIDARG; + + if (*phEnum == 0) + *phEnum = AllocEnum(tokens); + + MetadataEnum e = GetEnum(*phEnum); + uint count = 0; + while (count < cMax && e.Position < e.Tokens.Count) + { + if (rTokens is not null) + rTokens[count] = e.Tokens[e.Position]; + e.Position++; + count++; + } + + if (pcTokens is not null) + *pcTokens = count; + + return count > 0 ? HResults.S_OK : HResults.S_FALSE; + } + + void IMetaDataImport.CloseEnum(nint hEnum) + { + if (hEnum == 0) + return; + + if (_cdacEnumHandles.TryRemove(hEnum, out _)) + { + GCHandle handle = GCHandle.FromIntPtr(hEnum); + handle.Free(); + } + else + { + _legacyImport?.CloseEnum(hEnum); + } + } + + int IMetaDataImport.CountEnum(nint hEnum, uint* pulCount) + { + if (hEnum == 0) + { + if (pulCount is not null) + *pulCount = 0; + return HResults.S_OK; + } + + if (_cdacEnumHandles.ContainsKey(hEnum)) + { + MetadataEnum e = GetEnum(hEnum); + if (pulCount is not null) + *pulCount = (uint)e.Tokens.Count; + return HResults.S_OK; + } + + return _legacyImport is not null ? _legacyImport.CountEnum(hEnum, pulCount) : HResults.E_NOTIMPL; + } + + int IMetaDataImport.ResetEnum(nint hEnum, uint ulPos) + { + if (hEnum == 0) + return HResults.S_OK; + + if (_cdacEnumHandles.ContainsKey(hEnum)) + { + MetadataEnum e = GetEnum(hEnum); + e.Position = (int)Math.Min(ulPos, (uint)e.Tokens.Count); + return HResults.S_OK; + } + + return _legacyImport is not null ? _legacyImport.ResetEnum(hEnum, ulPos) : HResults.E_NOTIMPL; + } + + int IMetaDataImport.EnumTypeDefs(nint* phEnum, uint* rTypeDefs, uint cMax, uint* pcTypeDefs) + => _legacyImport is not null ? _legacyImport.EnumTypeDefs(phEnum, rTypeDefs, cMax, pcTypeDefs) : HResults.E_NOTIMPL; + + int IMetaDataImport.EnumInterfaceImpls(nint* phEnum, uint td, uint* rImpls, uint cMax, uint* pcImpls) + { + int hr = HResults.S_OK; + try + { + if (phEnum is not null && *phEnum != 0) + { + hr = FillEnum(phEnum, GetEnum(*phEnum).Tokens, rImpls, cMax, pcImpls); + } + else + { + TypeDefinitionHandle typeHandle = MetadataTokens.TypeDefinitionHandle((int)(td & 0x00FFFFFF)); + TypeDefinition typeDef = _reader.GetTypeDefinition(typeHandle); + List tokens = new(); + foreach (InterfaceImplementationHandle h in typeDef.GetInterfaceImplementations()) + tokens.Add((uint)MetadataTokens.GetToken(h)); + hr = FillEnum(phEnum, tokens, rImpls, cMax, pcImpls); + } + } + catch (System.Exception ex) + { + hr = ex.HResult; + } + + return hr; + } + + int IMetaDataImport.EnumTypeRefs(nint* phEnum, uint* rTypeRefs, uint cMax, uint* pcTypeRefs) + => _legacyImport is not null ? _legacyImport.EnumTypeRefs(phEnum, rTypeRefs, cMax, pcTypeRefs) : HResults.E_NOTIMPL; + + int IMetaDataImport.EnumMembers(nint* phEnum, uint cl, uint* rMembers, uint cMax, uint* pcTokens) + => _legacyImport is not null ? _legacyImport.EnumMembers(phEnum, cl, rMembers, cMax, pcTokens) : HResults.E_NOTIMPL; + + int IMetaDataImport.EnumMethods(nint* phEnum, uint cl, uint* rMethods, uint cMax, uint* pcTokens) + => _legacyImport is not null ? _legacyImport.EnumMethods(phEnum, cl, rMethods, cMax, pcTokens) : HResults.E_NOTIMPL; + + int IMetaDataImport.EnumFields(nint* phEnum, uint cl, uint* rFields, uint cMax, uint* pcTokens) + { + int hr = HResults.S_OK; + try + { + if (phEnum is not null && *phEnum != 0) + { + hr = FillEnum(phEnum, GetEnum(*phEnum).Tokens, rFields, cMax, pcTokens); + } + else + { + TypeDefinitionHandle typeHandle = MetadataTokens.TypeDefinitionHandle((int)(cl & 0x00FFFFFF)); + TypeDefinition typeDef = _reader.GetTypeDefinition(typeHandle); + List tokens = new(); + foreach (FieldDefinitionHandle h in typeDef.GetFields()) + tokens.Add((uint)MetadataTokens.GetToken(h)); + hr = FillEnum(phEnum, tokens, rFields, cMax, pcTokens); + } + } + catch (System.Exception ex) + { + hr = ex.HResult; + } + + return hr; + } + + int IMetaDataImport.EnumCustomAttributes(nint* phEnum, uint tk, uint tkType, uint* rCustomAttributes, uint cMax, uint* pcCustomAttributes) + => _legacyImport is not null ? _legacyImport.EnumCustomAttributes(phEnum, tk, tkType, rCustomAttributes, cMax, pcCustomAttributes) : HResults.E_NOTIMPL; + + int IMetaDataImport2.EnumGenericParams(nint* phEnum, uint tk, uint* rGenericParams, uint cMax, uint* pcGenericParams) + { + int hr = HResults.S_OK; + try + { + if (phEnum is not null && *phEnum != 0) + { + hr = FillEnum(phEnum, GetEnum(*phEnum).Tokens, rGenericParams, cMax, pcGenericParams); + } + else + { + EntityHandle owner = MetadataTokens.EntityHandle((int)tk); + GenericParameterHandleCollection genericParams; + + if (owner.Kind == HandleKind.TypeDefinition) + genericParams = _reader.GetTypeDefinition((TypeDefinitionHandle)owner).GetGenericParameters(); + else if (owner.Kind == HandleKind.MethodDefinition) + genericParams = _reader.GetMethodDefinition((MethodDefinitionHandle)owner).GetGenericParameters(); + else + { + throw new ArgumentException(null, nameof(tk)); + } + + List tokens = new(); + foreach (GenericParameterHandle h in genericParams) + tokens.Add((uint)MetadataTokens.GetToken(h)); + hr = FillEnum(phEnum, tokens, rGenericParams, cMax, pcGenericParams); + } + } + catch (System.Exception ex) + { + hr = ex.HResult; + } + + return hr; + } + + int IMetaDataImport.GetTypeDefProps(uint td, char* szTypeDef, uint cchTypeDef, uint* pchTypeDef, uint* pdwTypeDefFlags, uint* ptkExtends) + { + int hr = HResults.S_OK; + try + { + TypeDefinitionHandle typeHandle = MetadataTokens.TypeDefinitionHandle((int)(td & 0x00FFFFFF)); + TypeDefinition typeDef = _reader.GetTypeDefinition(typeHandle); + + string fullName = GetTypeDefFullName(typeDef); + OutputBufferHelpers.CopyStringToBuffer(szTypeDef, cchTypeDef, pchTypeDef, fullName, out bool truncated); + + if (pdwTypeDefFlags is not null) + *pdwTypeDefFlags = (uint)typeDef.Attributes; + + if (ptkExtends is not null) + { + EntityHandle baseType = typeDef.BaseType; + *ptkExtends = baseType.IsNil ? 0 : (uint)MetadataTokens.GetToken(baseType); + } + + hr = truncated ? CldbHResults.CLDB_S_TRUNCATION : HResults.S_OK; + } + catch (System.Exception ex) + { + hr = ex.HResult; + } + +#if DEBUG + if (_legacyImport is not null) + { + uint flagsLocal = 0, extendsLocal = 0, pchLocal = 0; + int hrLegacy = _legacyImport.GetTypeDefProps(td, null, 0, &pchLocal, &flagsLocal, &extendsLocal); + Debug.ValidateHResult(hr, hrLegacy); + if (hr >= 0 && hrLegacy >= 0) + { + if (pdwTypeDefFlags is not null) + Debug.Assert(*pdwTypeDefFlags == flagsLocal, $"TypeDefFlags mismatch: cDAC=0x{*pdwTypeDefFlags:X}, DAC=0x{flagsLocal:X}"); + if (ptkExtends is not null) + Debug.Assert(*ptkExtends == extendsLocal, $"Extends mismatch: cDAC=0x{*ptkExtends:X}, DAC=0x{extendsLocal:X}"); + if (pchTypeDef is not null) + Debug.Assert(*pchTypeDef == pchLocal, $"Name length mismatch: cDAC={*pchTypeDef}, DAC={pchLocal}"); + } + } +#endif + return hr; + } + + int IMetaDataImport.GetTypeRefProps(uint tr, uint* ptkResolutionScope, char* szName, uint cchName, uint* pchName) + { + int hr = HResults.S_OK; + try + { + TypeReferenceHandle refHandle = MetadataTokens.TypeReferenceHandle((int)(tr & 0x00FFFFFF)); + TypeReference typeRef = _reader.GetTypeReference(refHandle); + + string fullName = GetTypeRefFullName(typeRef); + OutputBufferHelpers.CopyStringToBuffer(szName, cchName, pchName, fullName, out bool truncated); + + if (ptkResolutionScope is not null) + { + EntityHandle scope = typeRef.ResolutionScope; + *ptkResolutionScope = scope.IsNil ? 0 : (uint)MetadataTokens.GetToken(scope); + } + + hr = truncated ? CldbHResults.CLDB_S_TRUNCATION : HResults.S_OK; + } + catch (System.Exception ex) + { + hr = ex.HResult; + } + +#if DEBUG + if (_legacyImport is not null) + { + uint scopeLocal = 0, pchLocal = 0; + int hrLegacy = _legacyImport.GetTypeRefProps(tr, &scopeLocal, null, 0, &pchLocal); + Debug.ValidateHResult(hr, hrLegacy); + if (hr >= 0 && hrLegacy >= 0) + { + if (ptkResolutionScope is not null) + Debug.Assert(*ptkResolutionScope == scopeLocal, $"ResolutionScope mismatch: cDAC=0x{*ptkResolutionScope:X}, DAC=0x{scopeLocal:X}"); + if (pchName is not null) + Debug.Assert(*pchName == pchLocal, $"Name length mismatch: cDAC={*pchName}, DAC={pchLocal}"); + } + } +#endif + return hr; + } + + int IMetaDataImport.GetMethodProps(uint mb, uint* pClass, char* szMethod, uint cchMethod, uint* pchMethod, + uint* pdwAttr, byte** ppvSigBlob, uint* pcbSigBlob, uint* pulCodeRVA, uint* pdwImplFlags) + { + int hr = HResults.S_OK; + try + { + MethodDefinitionHandle methodHandle = MetadataTokens.MethodDefinitionHandle((int)(mb & 0x00FFFFFF)); + MethodDefinition methodDef = _reader.GetMethodDefinition(methodHandle); + + string name = _reader.GetString(methodDef.Name); + OutputBufferHelpers.CopyStringToBuffer(szMethod, cchMethod, pchMethod, name, out bool truncated); + + if (pClass is not null) + *pClass = MapGlobalParentToken((uint)MetadataTokens.GetToken(methodDef.GetDeclaringType())); + + if (pdwAttr is not null) + *pdwAttr = (uint)methodDef.Attributes; + + if (ppvSigBlob is not null || pcbSigBlob is not null) + { + BlobHandle sigHandle = methodDef.Signature; + BlobReader blobReader = _reader.GetBlobReader(sigHandle); + if (ppvSigBlob is not null) + *ppvSigBlob = blobReader.StartPointer; + if (pcbSigBlob is not null) + *pcbSigBlob = (uint)blobReader.Length; + } + + if (pulCodeRVA is not null) + *pulCodeRVA = (uint)methodDef.RelativeVirtualAddress; + + if (pdwImplFlags is not null) + *pdwImplFlags = (uint)methodDef.ImplAttributes; + + hr = truncated ? CldbHResults.CLDB_S_TRUNCATION : HResults.S_OK; + } + catch (System.Exception ex) + { + hr = ex.HResult; + } + +#if DEBUG + if (_legacyImport is not null) + { + uint classLocal = 0, attrLocal = 0, rvaLocal = 0, implLocal = 0, pchLocal = 0, cbSigLocal = 0; + byte* sigLocal = null; + int hrLegacy = _legacyImport.GetMethodProps(mb, &classLocal, null, 0, &pchLocal, &attrLocal, &sigLocal, &cbSigLocal, &rvaLocal, &implLocal); + Debug.ValidateHResult(hr, hrLegacy); + if (hr >= 0 && hrLegacy >= 0) + { + if (pClass is not null) + Debug.Assert(*pClass == classLocal, $"Class mismatch: cDAC=0x{*pClass:X}, DAC=0x{classLocal:X}"); + if (pdwAttr is not null) + Debug.Assert(*pdwAttr == attrLocal, $"Attr mismatch: cDAC=0x{*pdwAttr:X}, DAC=0x{attrLocal:X}"); + if (pchMethod is not null) + Debug.Assert(*pchMethod == pchLocal, $"Name length mismatch: cDAC={*pchMethod}, DAC={pchLocal}"); + if (pulCodeRVA is not null) + Debug.Assert(*pulCodeRVA == rvaLocal, $"RVA mismatch: cDAC=0x{*pulCodeRVA:X}, DAC=0x{rvaLocal:X}"); + if (pdwImplFlags is not null) + Debug.Assert(*pdwImplFlags == implLocal, $"ImplFlags mismatch: cDAC=0x{*pdwImplFlags:X}, DAC=0x{implLocal:X}"); + if (ppvSigBlob is not null) + ValidateBlobsEqual(*ppvSigBlob, pcbSigBlob is not null ? *pcbSigBlob : cbSigLocal, sigLocal, cbSigLocal, "MethodSig"); + else if (pcbSigBlob is not null) + Debug.Assert(*pcbSigBlob == cbSigLocal, $"SigBlob length mismatch: cDAC={*pcbSigBlob}, DAC={cbSigLocal}"); + } + } +#endif + return hr; + } + + int IMetaDataImport.GetFieldProps(uint mb, uint* pClass, char* szField, uint cchField, uint* pchField, + uint* pdwAttr, byte** ppvSigBlob, uint* pcbSigBlob, uint* pdwCPlusTypeFlag, + void** ppValue, uint* pcchValue) + { + int hr = HResults.S_OK; + try + { + FieldDefinitionHandle fieldHandle = MetadataTokens.FieldDefinitionHandle((int)(mb & 0x00FFFFFF)); + FieldDefinition fieldDef = _reader.GetFieldDefinition(fieldHandle); + + string name = _reader.GetString(fieldDef.Name); + OutputBufferHelpers.CopyStringToBuffer(szField, cchField, pchField, name, out bool truncated); + + if (pClass is not null) + *pClass = MapGlobalParentToken((uint)MetadataTokens.GetToken(fieldDef.GetDeclaringType())); + + if (pdwAttr is not null) + *pdwAttr = (uint)fieldDef.Attributes; + + if (ppvSigBlob is not null || pcbSigBlob is not null) + { + BlobHandle sigHandle = fieldDef.Signature; + BlobReader blobReader = _reader.GetBlobReader(sigHandle); + if (ppvSigBlob is not null) + *ppvSigBlob = blobReader.StartPointer; + if (pcbSigBlob is not null) + *pcbSigBlob = (uint)blobReader.Length; + } + + if (pdwCPlusTypeFlag is not null) + *pdwCPlusTypeFlag = (uint)CorElementType.Void; + if (ppValue is not null) + *ppValue = null; + if (pcchValue is not null) + *pcchValue = 0; + + ConstantHandle constHandle = fieldDef.GetDefaultValue(); + if (!constHandle.IsNil && (pdwCPlusTypeFlag is not null || ppValue is not null)) + { + Constant constant = _reader.GetConstant(constHandle); + if (pdwCPlusTypeFlag is not null) + *pdwCPlusTypeFlag = (uint)constant.TypeCode; + if (ppValue is not null || pcchValue is not null) + { + BlobReader valueReader = _reader.GetBlobReader(constant.Value); + if (ppValue is not null) + *ppValue = valueReader.StartPointer; + if (pcchValue is not null) + *pcchValue = (uint)constant.TypeCode == (uint)CorElementType.String ? (uint)valueReader.Length / sizeof(char) : (uint)valueReader.Length; + } + } + + hr = truncated ? CldbHResults.CLDB_S_TRUNCATION : HResults.S_OK; + } + catch (System.Exception ex) + { + hr = ex.HResult; + } + +#if DEBUG + if (_legacyImport is not null) + { + uint classLocal = 0, attrLocal = 0, pchLocal = 0, cbSigLocal = 0, cpTypeLocal = 0, cchValueLocal = 0; + byte* sigLocal = null; + void* valueLocal = null; + int hrLegacy = _legacyImport.GetFieldProps(mb, &classLocal, null, 0, &pchLocal, &attrLocal, &sigLocal, &cbSigLocal, &cpTypeLocal, &valueLocal, &cchValueLocal); + Debug.ValidateHResult(hr, hrLegacy); + if (hr >= 0 && hrLegacy >= 0) + { + if (pClass is not null) + Debug.Assert(*pClass == classLocal, $"Class mismatch: cDAC=0x{*pClass:X}, DAC=0x{classLocal:X}"); + if (pdwAttr is not null) + Debug.Assert(*pdwAttr == attrLocal, $"Attr mismatch: cDAC=0x{*pdwAttr:X}, DAC=0x{attrLocal:X}"); + if (pchField is not null) + Debug.Assert(*pchField == pchLocal, $"Name length mismatch: cDAC={*pchField}, DAC={pchLocal}"); + if (pdwCPlusTypeFlag is not null) + Debug.Assert(*pdwCPlusTypeFlag == cpTypeLocal, $"CPlusTypeFlag mismatch: cDAC=0x{*pdwCPlusTypeFlag:X}, DAC=0x{cpTypeLocal:X}"); + if (ppvSigBlob is not null) + ValidateBlobsEqual(*ppvSigBlob, pcbSigBlob is not null ? *pcbSigBlob : cbSigLocal, sigLocal, cbSigLocal, "FieldSig"); + else if (pcbSigBlob is not null) + Debug.Assert(*pcbSigBlob == cbSigLocal, $"SigBlob length mismatch: cDAC={*pcbSigBlob}, DAC={cbSigLocal}"); + if (ppValue is not null) + ValidateBlobsEqual((byte*)*ppValue, pcchValue is not null ? *pcchValue : cchValueLocal, (byte*)valueLocal, cchValueLocal, "FieldConstant"); + else if (pcchValue is not null) + Debug.Assert(*pcchValue == cchValueLocal, $"Constant length mismatch: cDAC={*pcchValue}, DAC={cchValueLocal}"); + } + } +#endif + return hr; + } + + int IMetaDataImport.GetMemberProps(uint mb, uint* pClass, char* szMember, uint cchMember, uint* pchMember, + uint* pdwAttr, byte** ppvSigBlob, uint* pcbSigBlob, uint* pulCodeRVA, uint* pdwImplFlags, + uint* pdwCPlusTypeFlag, void** ppValue, uint* pcchValue) + { + uint tableIndex = mb >> 24; + if (tableIndex == 0x06) // MethodDef + { + int hr = ((IMetaDataImport)this).GetMethodProps(mb, pClass, szMember, cchMember, pchMember, pdwAttr, ppvSigBlob, pcbSigBlob, pulCodeRVA, pdwImplFlags); + if (pdwCPlusTypeFlag is not null) + *pdwCPlusTypeFlag = 0; + if (ppValue is not null) + *ppValue = null; + if (pcchValue is not null) + *pcchValue = 0; + return hr; + } + + if (tableIndex == 0x04) // FieldDef + { + int hr = ((IMetaDataImport)this).GetFieldProps(mb, pClass, szMember, cchMember, pchMember, pdwAttr, ppvSigBlob, pcbSigBlob, pdwCPlusTypeFlag, ppValue, pcchValue); + if (pulCodeRVA is not null) + *pulCodeRVA = 0; + if (pdwImplFlags is not null) + *pdwImplFlags = 0; + return hr; + } + + return HResults.E_INVALIDARG; + } + + int IMetaDataImport.GetInterfaceImplProps(uint iiImpl, uint* pClass, uint* ptkIface) + { + int hr = HResults.S_OK; + try + { + InterfaceImplementationHandle implHandle = MetadataTokens.InterfaceImplementationHandle((int)(iiImpl & 0x00FFFFFF)); + InterfaceImplementation impl = _reader.GetInterfaceImplementation(implHandle); + + if (pClass is not null) + { + _interfaceImplToTypeDef ??= BuildInterfaceImplLookup(); + *pClass = _interfaceImplToTypeDef.TryGetValue((int)(iiImpl & 0x00FFFFFF), out uint ownerToken) + ? ownerToken : 0; + } + + if (ptkIface is not null) + *ptkIface = (uint)MetadataTokens.GetToken(impl.Interface); + + hr = HResults.S_OK; + } + catch (System.Exception ex) + { + hr = ex.HResult; + } + +#if DEBUG + if (_legacyImport is not null) + { + uint classLocal = 0, ifaceLocal = 0; + int hrLegacy = _legacyImport.GetInterfaceImplProps(iiImpl, &classLocal, &ifaceLocal); + Debug.ValidateHResult(hr, hrLegacy); + if (hr >= 0 && hrLegacy >= 0) + { + if (pClass is not null) + Debug.Assert(*pClass == classLocal, $"Class mismatch: cDAC=0x{*pClass:X}, DAC=0x{classLocal:X}"); + if (ptkIface is not null) + Debug.Assert(*ptkIface == ifaceLocal, $"Interface mismatch: cDAC=0x{*ptkIface:X}, DAC=0x{ifaceLocal:X}"); + } + } +#endif + return hr; + } + + int IMetaDataImport.GetNestedClassProps(uint tdNestedClass, uint* ptdEnclosingClass) + { + int hr = HResults.S_OK; + try + { + TypeDefinitionHandle typeHandle = MetadataTokens.TypeDefinitionHandle((int)(tdNestedClass & 0x00FFFFFF)); + TypeDefinition typeDef = _reader.GetTypeDefinition(typeHandle); + TypeDefinitionHandle declaringType = typeDef.GetDeclaringType(); + + if (ptdEnclosingClass is not null) + *ptdEnclosingClass = declaringType.IsNil ? 0 : (uint)MetadataTokens.GetToken(declaringType); + + hr = declaringType.IsNil ? CldbHResults.CLDB_E_RECORD_NOTFOUND : HResults.S_OK; + } + catch (System.Exception ex) + { + hr = ex.HResult; + } + +#if DEBUG + if (_legacyImport is not null) + { + uint enclosingLocal = 0; + int hrLegacy = _legacyImport.GetNestedClassProps(tdNestedClass, &enclosingLocal); + Debug.ValidateHResult(hr, hrLegacy); + if (hr >= 0 && hrLegacy >= 0 && ptdEnclosingClass is not null) + Debug.Assert(*ptdEnclosingClass == enclosingLocal, $"Enclosing class mismatch: cDAC=0x{*ptdEnclosingClass:X}, DAC=0x{enclosingLocal:X}"); + } +#endif + return hr; + } + + int IMetaDataImport2.GetGenericParamProps(uint gp, uint* pulParamSeq, uint* pdwParamFlags, uint* ptOwner, + uint* reserved, char* wzname, uint cchName, uint* pchName) + { + int hr = HResults.S_OK; + try + { + GenericParameterHandle gpHandle = MetadataTokens.GenericParameterHandle((int)(gp & 0x00FFFFFF)); + GenericParameter genericParam = _reader.GetGenericParameter(gpHandle); + + if (pulParamSeq is not null) + *pulParamSeq = (uint)genericParam.Index; + + if (pdwParamFlags is not null) + *pdwParamFlags = (uint)genericParam.Attributes; + + if (ptOwner is not null) + *ptOwner = (uint)MetadataTokens.GetToken(genericParam.Parent); + + if (reserved is not null) + *reserved = 0; + + string name = _reader.GetString(genericParam.Name); + OutputBufferHelpers.CopyStringToBuffer(wzname, cchName, pchName, name, out bool truncated); + + hr = truncated ? CldbHResults.CLDB_S_TRUNCATION : HResults.S_OK; + } + catch (System.Exception ex) + { + hr = ex.HResult; + } + +#if DEBUG + if (_legacyImport2 is not null) + { + uint seqLocal = 0, flagsLocal = 0, ownerLocal = 0; + int hrLegacy = _legacyImport2.GetGenericParamProps(gp, &seqLocal, &flagsLocal, &ownerLocal, null, null, 0, null); + Debug.ValidateHResult(hr, hrLegacy); + if (hr >= 0 && hrLegacy >= 0) + { + if (pulParamSeq is not null) + Debug.Assert(*pulParamSeq == seqLocal, $"ParamSeq mismatch: cDAC={*pulParamSeq}, DAC={seqLocal}"); + if (pdwParamFlags is not null) + Debug.Assert(*pdwParamFlags == flagsLocal, $"ParamFlags mismatch: cDAC=0x{*pdwParamFlags:X}, DAC=0x{flagsLocal:X}"); + if (ptOwner is not null) + Debug.Assert(*ptOwner == ownerLocal, $"Owner mismatch: cDAC=0x{*ptOwner:X}, DAC=0x{ownerLocal:X}"); + } + } +#endif + return hr; + } + + int IMetaDataImport.GetRVA(uint tk, uint* pulCodeRVA, uint* pdwImplFlags) + { + int hr = HResults.S_OK; + try + { + uint tableIndex = tk >> 24; + if (tableIndex == 0x06) // MethodDef + { + MethodDefinitionHandle methodHandle = MetadataTokens.MethodDefinitionHandle((int)(tk & 0x00FFFFFF)); + MethodDefinition methodDef = _reader.GetMethodDefinition(methodHandle); + if (pulCodeRVA is not null) + *pulCodeRVA = (uint)methodDef.RelativeVirtualAddress; + if (pdwImplFlags is not null) + *pdwImplFlags = (uint)methodDef.ImplAttributes; + } + else if (tableIndex == 0x04) // FieldDef + { + FieldDefinitionHandle fieldHandle = MetadataTokens.FieldDefinitionHandle((int)(tk & 0x00FFFFFF)); + FieldDefinition fieldDef = _reader.GetFieldDefinition(fieldHandle); + if (pulCodeRVA is not null) + *pulCodeRVA = (uint)fieldDef.GetRelativeVirtualAddress(); + if (pdwImplFlags is not null) + *pdwImplFlags = 0; + } + else + { + hr = HResults.E_INVALIDARG; + } + } + catch (System.Exception ex) + { + hr = ex.HResult; + } + +#if DEBUG + if (_legacyImport is not null) + { + uint rvaLocal = 0, implLocal = 0; + int hrLegacy = _legacyImport.GetRVA(tk, &rvaLocal, &implLocal); + Debug.ValidateHResult(hr, hrLegacy); + if (hr >= 0 && hrLegacy >= 0) + { + if (pulCodeRVA is not null) + Debug.Assert(*pulCodeRVA == rvaLocal, $"RVA mismatch: cDAC=0x{*pulCodeRVA:X}, DAC=0x{rvaLocal:X}"); + if (pdwImplFlags is not null) + Debug.Assert(*pdwImplFlags == implLocal, $"ImplFlags mismatch: cDAC=0x{*pdwImplFlags:X}, DAC=0x{implLocal:X}"); + } + } +#endif + return hr; + } + + int IMetaDataImport.GetSigFromToken(uint mdSig, byte** ppvSig, uint* pcbSig) + { + int hr = HResults.S_OK; + try + { + StandaloneSignatureHandle sigHandle = MetadataTokens.StandaloneSignatureHandle((int)(mdSig & 0x00FFFFFF)); + StandaloneSignature sig = _reader.GetStandaloneSignature(sigHandle); + BlobReader blobReader = _reader.GetBlobReader(sig.Signature); + + if (ppvSig is not null) + *ppvSig = blobReader.StartPointer; + if (pcbSig is not null) + *pcbSig = (uint)blobReader.Length; + + hr = HResults.S_OK; + } + catch (System.Exception ex) + { + hr = ex.HResult; + } + +#if DEBUG + if (_legacyImport is not null) + { + uint cbLocal = 0; + byte* sigLocal = null; + int hrLegacy = _legacyImport.GetSigFromToken(mdSig, &sigLocal, &cbLocal); + Debug.ValidateHResult(hr, hrLegacy); + if (hr >= 0 && hrLegacy >= 0) + { + if (ppvSig is not null) + ValidateBlobsEqual(*ppvSig, pcbSig is not null ? *pcbSig : cbLocal, sigLocal, cbLocal, "StandaloneSig"); + else if (pcbSig is not null) + Debug.Assert(*pcbSig == cbLocal, $"Sig length mismatch: cDAC={*pcbSig}, DAC={cbLocal}"); + } + } +#endif + return hr; + } + + int IMetaDataImport.GetCustomAttributeByName(uint tkObj, char* szName, void** ppData, uint* pcbData) + { + int hr = HResults.S_OK; + try + { + if (ppData is not null) + *ppData = null; + if (pcbData is not null) + *pcbData = 0; + + string targetName = new string(szName); + EntityHandle parent = MetadataTokens.EntityHandle((int)tkObj); + bool found = false; + + foreach (CustomAttributeHandle caHandle in _reader.GetCustomAttributes(parent)) + { + CustomAttribute ca = _reader.GetCustomAttribute(caHandle); + string attrTypeName = GetCustomAttributeTypeName(ca.Constructor); + if (string.Equals(attrTypeName, targetName, StringComparison.Ordinal)) + { + BlobReader blobReader = _reader.GetBlobReader(ca.Value); + if (ppData is not null) + *ppData = blobReader.StartPointer; + if (pcbData is not null) + *pcbData = (uint)blobReader.Length; + found = true; + break; + } + } + + if (!found) + hr = HResults.S_FALSE; + } + catch (System.Exception ex) + { + hr = ex.HResult; + } + +#if DEBUG + if (_legacyImport is not null) + { + uint cbLocal = 0; + void* dataLocal = null; + int hrLegacy = _legacyImport.GetCustomAttributeByName(tkObj, szName, &dataLocal, &cbLocal); + Debug.ValidateHResult(hr, hrLegacy); + if (hr >= 0 && hrLegacy >= 0) + { + if (ppData is not null) + ValidateBlobsEqual((byte*)*ppData, pcbData is not null ? *pcbData : cbLocal, (byte*)dataLocal, cbLocal, "CustomAttribute"); + else if (pcbData is not null) + Debug.Assert(*pcbData == cbLocal, $"CustomAttribute length mismatch: cDAC={*pcbData}, DAC={cbLocal}"); + } + } +#endif + return hr; + } + + int IMetaDataImport.IsValidToken(uint tk) + { + int rid = (int)(tk & 0x00FFFFFF); + int tokenType = (int)(tk >> 24); + + if (rid == 0) + return 0; // FALSE + + const int UserStringTokenType = 0x70; + if (tokenType == UserStringTokenType) + { + int heapSize = _reader.GetHeapSize(HeapIndex.UserString); + return rid < heapSize ? 1 : 0; + } + + if (tokenType < 0 || tokenType > (int)TableIndex.CustomDebugInformation) + return 0; // FALSE + + int rowCount = _reader.GetTableRowCount((TableIndex)tokenType); + return rid <= rowCount ? 1 : 0; // TRUE or FALSE + } + + private string GetCustomAttributeTypeName(EntityHandle constructor) + { + if (constructor.Kind == HandleKind.MethodDefinition) + { + MethodDefinition method = _reader.GetMethodDefinition((MethodDefinitionHandle)constructor); + TypeDefinition typeDef = _reader.GetTypeDefinition(method.GetDeclaringType()); + return GetTypeDefFullName(typeDef); + } + if (constructor.Kind == HandleKind.MemberReference) + { + MemberReference memberRef = _reader.GetMemberReference((MemberReferenceHandle)constructor); + EntityHandle parent = memberRef.Parent; + if (parent.Kind == HandleKind.TypeReference) + { + TypeReference typeRef = _reader.GetTypeReference((TypeReferenceHandle)parent); + return GetTypeRefFullName(typeRef); + } + if (parent.Kind == HandleKind.TypeDefinition) + { + TypeDefinition typeDef = _reader.GetTypeDefinition((TypeDefinitionHandle)parent); + return GetTypeDefFullName(typeDef); + } + } + return string.Empty; + } + + int IMetaDataImport.FindTypeDefByName(char* szTypeDef, uint tkEnclosingClass, uint* ptd) + { + int hr = HResults.S_OK; + try + { + if (ptd is not null) + *ptd = 0; + + string targetName = new string(szTypeDef); + + bool found = false; + foreach (TypeDefinitionHandle tdh in _reader.TypeDefinitions) + { + TypeDefinition typeDef = _reader.GetTypeDefinition(tdh); + string fullName = GetTypeDefFullName(typeDef); + + if (!string.Equals(fullName, targetName, StringComparison.Ordinal)) + continue; + + if (tkEnclosingClass != 0) + { + TypeDefinitionHandle declaringType = typeDef.GetDeclaringType(); + if (declaringType.IsNil || (uint)MetadataTokens.GetToken(declaringType) != tkEnclosingClass) + continue; + } + + if (ptd is not null) + *ptd = (uint)MetadataTokens.GetToken(tdh); + + found = true; + break; + } + + if (!found) + hr = CldbHResults.CLDB_E_RECORD_NOTFOUND; + } + catch (System.Exception ex) + { + hr = ex.HResult; + } + +#if DEBUG + if (_legacyImport is not null) + { + uint tdLocal = 0; + int hrLegacy = _legacyImport.FindTypeDefByName(szTypeDef, tkEnclosingClass, &tdLocal); + Debug.ValidateHResult(hr, hrLegacy); + if (hr >= 0 && hrLegacy >= 0 && ptd is not null) + Debug.Assert(*ptd == tdLocal, $"TypeDef mismatch: cDAC=0x{*ptd:X}, DAC=0x{tdLocal:X}"); + } +#endif + return hr; + } + + int IMetaDataImport.GetScopeProps(char* szName, uint cchName, uint* pchName, Guid* pmvid) + => _legacyImport is not null ? _legacyImport.GetScopeProps(szName, cchName, pchName, pmvid) : HResults.E_NOTIMPL; + + int IMetaDataImport.GetModuleFromScope(uint* pmd) + => _legacyImport is not null ? _legacyImport.GetModuleFromScope(pmd) : HResults.E_NOTIMPL; + + int IMetaDataImport.ResolveTypeRef(uint tr, Guid* riid, void** ppIScope, uint* ptd) + => _legacyImport is not null ? _legacyImport.ResolveTypeRef(tr, riid, ppIScope, ptd) : HResults.E_NOTIMPL; + + int IMetaDataImport.EnumMembersWithName(nint* phEnum, uint cl, char* szName, uint* rMembers, uint cMax, uint* pcTokens) + => _legacyImport is not null ? _legacyImport.EnumMembersWithName(phEnum, cl, szName, rMembers, cMax, pcTokens) : HResults.E_NOTIMPL; + + int IMetaDataImport.EnumMethodsWithName(nint* phEnum, uint cl, char* szName, uint* rMethods, uint cMax, uint* pcTokens) + => _legacyImport is not null ? _legacyImport.EnumMethodsWithName(phEnum, cl, szName, rMethods, cMax, pcTokens) : HResults.E_NOTIMPL; + + int IMetaDataImport.EnumFieldsWithName(nint* phEnum, uint cl, char* szName, uint* rFields, uint cMax, uint* pcTokens) + => _legacyImport is not null ? _legacyImport.EnumFieldsWithName(phEnum, cl, szName, rFields, cMax, pcTokens) : HResults.E_NOTIMPL; + + int IMetaDataImport.EnumParams(nint* phEnum, uint mb, uint* rParams, uint cMax, uint* pcTokens) + => _legacyImport is not null ? _legacyImport.EnumParams(phEnum, mb, rParams, cMax, pcTokens) : HResults.E_NOTIMPL; + + int IMetaDataImport.EnumMemberRefs(nint* phEnum, uint tkParent, uint* rMemberRefs, uint cMax, uint* pcTokens) + => _legacyImport is not null ? _legacyImport.EnumMemberRefs(phEnum, tkParent, rMemberRefs, cMax, pcTokens) : HResults.E_NOTIMPL; + + int IMetaDataImport.EnumMethodImpls(nint* phEnum, uint td, uint* rMethodBody, uint* rMethodDecl, uint cMax, uint* pcTokens) + => _legacyImport is not null ? _legacyImport.EnumMethodImpls(phEnum, td, rMethodBody, rMethodDecl, cMax, pcTokens) : HResults.E_NOTIMPL; + + int IMetaDataImport.EnumPermissionSets(nint* phEnum, uint tk, uint dwActions, uint* rPermission, uint cMax, uint* pcTokens) + => _legacyImport is not null ? _legacyImport.EnumPermissionSets(phEnum, tk, dwActions, rPermission, cMax, pcTokens) : HResults.E_NOTIMPL; + + int IMetaDataImport.FindMember(uint td, char* szName, byte* pvSigBlob, uint cbSigBlob, uint* pmb) + => _legacyImport is not null ? _legacyImport.FindMember(td, szName, pvSigBlob, cbSigBlob, pmb) : HResults.E_NOTIMPL; + + int IMetaDataImport.FindMethod(uint td, char* szName, byte* pvSigBlob, uint cbSigBlob, uint* pmb) + => _legacyImport is not null ? _legacyImport.FindMethod(td, szName, pvSigBlob, cbSigBlob, pmb) : HResults.E_NOTIMPL; + + int IMetaDataImport.FindField(uint td, char* szName, byte* pvSigBlob, uint cbSigBlob, uint* pmb) + => _legacyImport is not null ? _legacyImport.FindField(td, szName, pvSigBlob, cbSigBlob, pmb) : HResults.E_NOTIMPL; + + int IMetaDataImport.FindMemberRef(uint td, char* szName, byte* pvSigBlob, uint cbSigBlob, uint* pmr) + => _legacyImport is not null ? _legacyImport.FindMemberRef(td, szName, pvSigBlob, cbSigBlob, pmr) : HResults.E_NOTIMPL; + + int IMetaDataImport.GetMemberRefProps(uint mr, uint* ptk, char* szMember, uint cchMember, uint* pchMember, + byte** ppvSigBlob, uint* pbSig) + { + int hr = HResults.S_OK; + try + { + MemberReferenceHandle refHandle = MetadataTokens.MemberReferenceHandle((int)(mr & 0x00FFFFFF)); + MemberReference memberRef = _reader.GetMemberReference(refHandle); + + string name = _reader.GetString(memberRef.Name); + OutputBufferHelpers.CopyStringToBuffer(szMember, cchMember, pchMember, name, out bool truncated); + + if (ptk is not null) + *ptk = MapGlobalParentToken((uint)MetadataTokens.GetToken(memberRef.Parent)); + + if (ppvSigBlob is not null || pbSig is not null) + { + BlobReader blobReader = _reader.GetBlobReader(memberRef.Signature); + if (ppvSigBlob is not null) + *ppvSigBlob = blobReader.StartPointer; + if (pbSig is not null) + *pbSig = (uint)blobReader.Length; + } + + hr = truncated ? CldbHResults.CLDB_S_TRUNCATION : HResults.S_OK; + } + catch (System.Exception ex) + { + hr = ex.HResult; + } + +#if DEBUG + if (_legacyImport is not null) + { + uint tkLocal = 0, pchLocal = 0, cbSigLocal = 0; + byte* sigLocal = null; + int hrLegacy = _legacyImport.GetMemberRefProps(mr, &tkLocal, null, 0, &pchLocal, &sigLocal, &cbSigLocal); + Debug.ValidateHResult(hr, hrLegacy); + if (hr >= 0 && hrLegacy >= 0) + { + if (ptk is not null) + Debug.Assert(*ptk == tkLocal, $"Parent mismatch: cDAC=0x{*ptk:X}, DAC=0x{tkLocal:X}"); + if (pchMember is not null) + Debug.Assert(*pchMember == pchLocal, $"Name length mismatch: cDAC={*pchMember}, DAC={pchLocal}"); + if (ppvSigBlob is not null) + ValidateBlobsEqual(*ppvSigBlob, pbSig is not null ? *pbSig : cbSigLocal, sigLocal, cbSigLocal, "MemberRefSig"); + else if (pbSig is not null) + Debug.Assert(*pbSig == cbSigLocal, $"SigBlob length mismatch: cDAC={*pbSig}, DAC={cbSigLocal}"); + } + } +#endif + return hr; + } + + int IMetaDataImport.EnumProperties(nint* phEnum, uint td, uint* rProperties, uint cMax, uint* pcProperties) + => _legacyImport is not null ? _legacyImport.EnumProperties(phEnum, td, rProperties, cMax, pcProperties) : HResults.E_NOTIMPL; + + int IMetaDataImport.EnumEvents(nint* phEnum, uint td, uint* rEvents, uint cMax, uint* pcEvents) + => _legacyImport is not null ? _legacyImport.EnumEvents(phEnum, td, rEvents, cMax, pcEvents) : HResults.E_NOTIMPL; + + int IMetaDataImport.GetEventProps(uint ev, uint* pClass, char* szEvent, uint cchEvent, uint* pchEvent, + uint* pdwEventFlags, uint* ptkEventType, uint* pmdAddOn, uint* pmdRemoveOn, uint* pmdFire, + uint* rmdOtherMethod, uint cMax, uint* pcOtherMethod) + => _legacyImport is not null ? _legacyImport.GetEventProps(ev, pClass, szEvent, cchEvent, pchEvent, pdwEventFlags, ptkEventType, pmdAddOn, pmdRemoveOn, pmdFire, rmdOtherMethod, cMax, pcOtherMethod) : HResults.E_NOTIMPL; + + int IMetaDataImport.EnumMethodSemantics(nint* phEnum, uint mb, uint* rEventProp, uint cMax, uint* pcEventProp) + => _legacyImport is not null ? _legacyImport.EnumMethodSemantics(phEnum, mb, rEventProp, cMax, pcEventProp) : HResults.E_NOTIMPL; + + int IMetaDataImport.GetMethodSemantics(uint mb, uint tkEventProp, uint* pdwSemanticsFlags) + => _legacyImport is not null ? _legacyImport.GetMethodSemantics(mb, tkEventProp, pdwSemanticsFlags) : HResults.E_NOTIMPL; + + int IMetaDataImport.GetClassLayout(uint td, uint* pdwPackSize, void* rFieldOffset, uint cMax, uint* pcFieldOffset, uint* pulClassSize) + { + int hr = HResults.S_OK; + try + { + TypeDefinitionHandle typeHandle = MetadataTokens.TypeDefinitionHandle((int)(td & 0x00FFFFFF)); + TypeDefinition typeDef = _reader.GetTypeDefinition(typeHandle); + TypeLayout layout = typeDef.GetLayout(); + + if (layout.IsDefault) + { + hr = CldbHResults.CLDB_E_RECORD_NOTFOUND; + } + else + { + if (pdwPackSize is not null) + *pdwPackSize = (uint)layout.PackingSize; + + if (pulClassSize is not null) + *pulClassSize = (uint)layout.Size; + + if (rFieldOffset is not null || pcFieldOffset is not null) + { + uint* fieldOffsets = (uint*)rFieldOffset; + uint count = 0; + foreach (FieldDefinitionHandle fh in typeDef.GetFields()) + { + if (fieldOffsets is not null && count < cMax) + { + // Each entry is {FieldDef token (uint), ulOffset (uint)} + fieldOffsets[count * 2] = (uint)MetadataTokens.GetToken(fh); + int offset = _reader.GetFieldDefinition(fh).GetOffset(); + fieldOffsets[count * 2 + 1] = offset >= 0 ? (uint)offset : 0xFFFFFFFF; + } + count++; + } + if (pcFieldOffset is not null) + *pcFieldOffset = count; + } + + hr = HResults.S_OK; + } + } + catch (System.Exception ex) + { + hr = ex.HResult; + } + +#if DEBUG + if (_legacyImport is not null) + { + uint packLocal = 0, sizeLocal = 0, fieldCountLocal = 0; + int hrLegacy = _legacyImport.GetClassLayout(td, &packLocal, null, 0, &fieldCountLocal, &sizeLocal); + Debug.ValidateHResult(hr, hrLegacy); + if (hr >= 0 && hrLegacy >= 0) + { + if (pdwPackSize is not null) + Debug.Assert(*pdwPackSize == packLocal, $"PackSize mismatch: cDAC={*pdwPackSize}, DAC={packLocal}"); + if (pulClassSize is not null) + Debug.Assert(*pulClassSize == sizeLocal, $"ClassSize mismatch: cDAC={*pulClassSize}, DAC={sizeLocal}"); + if (pcFieldOffset is not null) + Debug.Assert(*pcFieldOffset == fieldCountLocal, $"FieldOffset count mismatch: cDAC={*pcFieldOffset}, DAC={fieldCountLocal}"); + } + } +#endif + return hr; + } + + int IMetaDataImport.GetFieldMarshal(uint tk, byte** ppvNativeType, uint* pcbNativeType) + => _legacyImport is not null ? _legacyImport.GetFieldMarshal(tk, ppvNativeType, pcbNativeType) : HResults.E_NOTIMPL; + + int IMetaDataImport.GetPermissionSetProps(uint pm, uint* pdwAction, void** ppvPermission, uint* pcbPermission) + => _legacyImport is not null ? _legacyImport.GetPermissionSetProps(pm, pdwAction, ppvPermission, pcbPermission) : HResults.E_NOTIMPL; + + int IMetaDataImport.GetModuleRefProps(uint mur, char* szName, uint cchName, uint* pchName) + { + int hr = HResults.S_OK; + try + { + ModuleReferenceHandle modRefHandle = MetadataTokens.ModuleReferenceHandle((int)(mur & 0x00FFFFFF)); + ModuleReference modRef = _reader.GetModuleReference(modRefHandle); + + string name = _reader.GetString(modRef.Name); + OutputBufferHelpers.CopyStringToBuffer(szName, cchName, pchName, name, out bool truncated); + + hr = truncated ? CldbHResults.CLDB_S_TRUNCATION : HResults.S_OK; + } + catch (System.Exception ex) + { + hr = ex.HResult; + } + +#if DEBUG + if (_legacyImport is not null) + { + uint pchLocal = 0; + int hrLegacy = _legacyImport.GetModuleRefProps(mur, null, 0, &pchLocal); + Debug.ValidateHResult(hr, hrLegacy); + if (hr >= 0 && hrLegacy >= 0 && pchName is not null) + Debug.Assert(*pchName == pchLocal, $"Name length mismatch: cDAC={*pchName}, DAC={pchLocal}"); + } +#endif + return hr; + } + + int IMetaDataImport.EnumModuleRefs(nint* phEnum, uint* rModuleRefs, uint cmax, uint* pcModuleRefs) + => _legacyImport is not null ? _legacyImport.EnumModuleRefs(phEnum, rModuleRefs, cmax, pcModuleRefs) : HResults.E_NOTIMPL; + + int IMetaDataImport.GetTypeSpecFromToken(uint typespec, byte** ppvSig, uint* pcbSig) + { + int hr = HResults.S_OK; + try + { + TypeSpecificationHandle tsHandle = MetadataTokens.TypeSpecificationHandle((int)(typespec & 0x00FFFFFF)); + TypeSpecification typeSpec = _reader.GetTypeSpecification(tsHandle); + BlobReader blobReader = _reader.GetBlobReader(typeSpec.Signature); + + if (ppvSig is not null) + *ppvSig = blobReader.StartPointer; + if (pcbSig is not null) + *pcbSig = (uint)blobReader.Length; + + hr = HResults.S_OK; + } + catch (System.Exception ex) + { + hr = ex.HResult; + } + +#if DEBUG + if (_legacyImport is not null) + { + uint cbLocal = 0; + byte* sigLocal = null; + int hrLegacy = _legacyImport.GetTypeSpecFromToken(typespec, &sigLocal, &cbLocal); + Debug.ValidateHResult(hr, hrLegacy); + if (hr >= 0 && hrLegacy >= 0) + { + if (ppvSig is not null) + ValidateBlobsEqual(*ppvSig, pcbSig is not null ? *pcbSig : cbLocal, sigLocal, cbLocal, "TypeSpec"); + else if (pcbSig is not null) + Debug.Assert(*pcbSig == cbLocal, $"Sig length mismatch: cDAC={*pcbSig}, DAC={cbLocal}"); + } + } +#endif + return hr; + } + + int IMetaDataImport.GetNameFromToken(uint tk, byte** pszUtf8NamePtr) + => _legacyImport is not null ? _legacyImport.GetNameFromToken(tk, pszUtf8NamePtr) : HResults.E_NOTIMPL; + + int IMetaDataImport.EnumUnresolvedMethods(nint* phEnum, uint* rMethods, uint cMax, uint* pcTokens) + => _legacyImport is not null ? _legacyImport.EnumUnresolvedMethods(phEnum, rMethods, cMax, pcTokens) : HResults.E_NOTIMPL; + + int IMetaDataImport.GetUserString(uint stk, char* szString, uint cchString, uint* pchString) + { + int hr = HResults.S_OK; + try + { + // Read the user string from raw #US heap bytes to match native behavior exactly. + // Native does: GetUserString → check size is odd → TruncateBySize(1) → GetSize()/sizeof(WCHAR). + // Using raw bytes avoids potential discrepancies with MetadataReader.GetUserString(). + int heapMetadataOffset = _reader.GetHeapMetadataOffset(HeapIndex.UserString); + int heapSize = _reader.GetHeapSize(HeapIndex.UserString); + int handleOffset = (int)(stk & 0x00FFFFFF); + + byte* heapBase = _reader.MetadataPointer + heapMetadataOffset; + int remaining = heapSize - handleOffset; + if (remaining <= 0) + throw Marshal.GetExceptionForHR(CldbHResults.CLDB_E_FILE_CORRUPT)!; + + BlobReader blobReader = new BlobReader(heapBase + handleOffset, remaining); + int blobSize = blobReader.ReadCompressedInteger(); + + // Validate blob fits within the remaining heap to prevent out-of-bounds reads. + if (blobSize > blobReader.RemainingBytes) + throw Marshal.GetExceptionForHR(CldbHResults.CLDB_E_FILE_CORRUPT)!; + + // Native rejects even-sized blobs (missing terminal byte) as corrupt. + if ((blobSize % sizeof(char)) == 0) + throw Marshal.GetExceptionForHR(CldbHResults.CLDB_E_FILE_CORRUPT)!; + + int charCount = (blobSize - 1) / sizeof(char); + + if (pchString is not null) + *pchString = (uint)charCount; + + if (szString is not null && cchString > 0) + { + char* dataPtr = (char*)blobReader.CurrentPointer; + int copyChars = Math.Min(charCount, (int)cchString); + new ReadOnlySpan(dataPtr, copyChars).CopyTo(new Span(szString, copyChars)); + + if ((uint)charCount > cchString) + { + szString[cchString - 1] = '\0'; + hr = CldbHResults.CLDB_S_TRUNCATION; + } + } + } + catch (System.Exception ex) + { + hr = ex.HResult; + } + +#if DEBUG + if (_legacyImport is not null) + { + uint pchLocal = 0; + int hrLegacy = _legacyImport.GetUserString(stk, null, 0, &pchLocal); + Debug.ValidateHResult(hr, hrLegacy); + if (hr >= 0 && hrLegacy >= 0 && pchString is not null) + Debug.Assert(*pchString == pchLocal, $"String length mismatch: cDAC={*pchString}, DAC={pchLocal}"); + } +#endif + return hr; + } + + int IMetaDataImport.GetPinvokeMap(uint tk, uint* pdwMappingFlags, char* szImportName, uint cchImportName, + uint* pchImportName, uint* pmrImportDLL) + => _legacyImport is not null ? _legacyImport.GetPinvokeMap(tk, pdwMappingFlags, szImportName, cchImportName, pchImportName, pmrImportDLL) : HResults.E_NOTIMPL; + + int IMetaDataImport.EnumSignatures(nint* phEnum, uint* rSignatures, uint cmax, uint* pcSignatures) + => _legacyImport is not null ? _legacyImport.EnumSignatures(phEnum, rSignatures, cmax, pcSignatures) : HResults.E_NOTIMPL; + + int IMetaDataImport.EnumTypeSpecs(nint* phEnum, uint* rTypeSpecs, uint cmax, uint* pcTypeSpecs) + => _legacyImport is not null ? _legacyImport.EnumTypeSpecs(phEnum, rTypeSpecs, cmax, pcTypeSpecs) : HResults.E_NOTIMPL; + + int IMetaDataImport.EnumUserStrings(nint* phEnum, uint* rStrings, uint cmax, uint* pcStrings) + => _legacyImport is not null ? _legacyImport.EnumUserStrings(phEnum, rStrings, cmax, pcStrings) : HResults.E_NOTIMPL; + + int IMetaDataImport.GetParamForMethodIndex(uint md, uint ulParamSeq, uint* ppd) + { + int hr = HResults.S_OK; + try + { + if (ppd is not null) + *ppd = 0; + + MethodDefinitionHandle methodHandle = MetadataTokens.MethodDefinitionHandle((int)(md & 0x00FFFFFF)); + MethodDefinition methodDef = _reader.GetMethodDefinition(methodHandle); + + bool found = false; + foreach (ParameterHandle ph in methodDef.GetParameters()) + { + Parameter param = _reader.GetParameter(ph); + if (param.SequenceNumber == (int)ulParamSeq) + { + if (ppd is not null) + *ppd = (uint)MetadataTokens.GetToken(ph); + found = true; + break; + } + } + + if (!found) + hr = CldbHResults.CLDB_E_RECORD_NOTFOUND; + } + catch (System.Exception ex) + { + hr = ex.HResult; + } + +#if DEBUG + if (_legacyImport is not null) + { + uint pdLocal = 0; + int hrLegacy = _legacyImport.GetParamForMethodIndex(md, ulParamSeq, &pdLocal); + Debug.ValidateHResult(hr, hrLegacy); + if (hr >= 0 && hrLegacy >= 0 && ppd is not null) + Debug.Assert(*ppd == pdLocal, $"Param token mismatch: cDAC=0x{*ppd:X}, DAC=0x{pdLocal:X}"); + } +#endif + return hr; + } + + int IMetaDataImport.GetCustomAttributeProps(uint cv, uint* ptkObj, uint* ptkType, void** ppBlob, uint* pcbSize) + => _legacyImport is not null ? _legacyImport.GetCustomAttributeProps(cv, ptkObj, ptkType, ppBlob, pcbSize) : HResults.E_NOTIMPL; + + int IMetaDataImport.FindTypeRef(uint tkResolutionScope, char* szName, uint* ptr) + => _legacyImport is not null ? _legacyImport.FindTypeRef(tkResolutionScope, szName, ptr) : HResults.E_NOTIMPL; + + int IMetaDataImport.GetPropertyProps(uint prop, uint* pClass, char* szProperty, uint cchProperty, uint* pchProperty, + uint* pdwPropFlags, byte** ppvSig, uint* pbSig, uint* pdwCPlusTypeFlag, + void** ppDefaultValue, uint* pcchDefaultValue, uint* pmdSetter, uint* pmdGetter, + uint* rmdOtherMethod, uint cMax, uint* pcOtherMethod) + => _legacyImport is not null ? _legacyImport.GetPropertyProps(prop, pClass, szProperty, cchProperty, pchProperty, pdwPropFlags, ppvSig, pbSig, pdwCPlusTypeFlag, ppDefaultValue, pcchDefaultValue, pmdSetter, pmdGetter, rmdOtherMethod, cMax, pcOtherMethod) : HResults.E_NOTIMPL; + + int IMetaDataImport.GetParamProps(uint tk, uint* pmd, uint* pulSequence, char* szName, uint cchName, uint* pchName, + uint* pdwAttr, uint* pdwCPlusTypeFlag, void** ppValue, uint* pcchValue) + { + int hr = HResults.S_OK; + try + { + ParameterHandle paramHandle = MetadataTokens.ParameterHandle((int)(tk & 0x00FFFFFF)); + Parameter param = _reader.GetParameter(paramHandle); + + string name = _reader.GetString(param.Name); + OutputBufferHelpers.CopyStringToBuffer(szName, cchName, pchName, name, out bool truncated); + + if (pmd is not null) + { + _paramToMethod ??= BuildParamToMethodLookup(); + *pmd = _paramToMethod.TryGetValue(MetadataTokens.GetRowNumber(paramHandle), out uint methodToken) ? methodToken : 0; + } + + if (pulSequence is not null) + *pulSequence = (uint)param.SequenceNumber; + + if (pdwAttr is not null) + *pdwAttr = (uint)param.Attributes; + + if (pdwCPlusTypeFlag is not null) + *pdwCPlusTypeFlag = (uint)CorElementType.Void; + if (ppValue is not null) + *ppValue = null; + if (pcchValue is not null) + *pcchValue = 0; + + ConstantHandle constHandle = param.GetDefaultValue(); + if (!constHandle.IsNil && (pdwCPlusTypeFlag is not null || ppValue is not null)) + { + Constant constant = _reader.GetConstant(constHandle); + if (pdwCPlusTypeFlag is not null) + *pdwCPlusTypeFlag = (uint)constant.TypeCode; + if (ppValue is not null || pcchValue is not null) + { + BlobReader valueReader = _reader.GetBlobReader(constant.Value); + if (ppValue is not null) + *ppValue = valueReader.StartPointer; + if (pcchValue is not null) + *pcchValue = (uint)constant.TypeCode == (uint)CorElementType.String ? (uint)valueReader.Length / sizeof(char) : (uint)valueReader.Length; + } + } + + hr = truncated ? CldbHResults.CLDB_S_TRUNCATION : HResults.S_OK; + } + catch (System.Exception ex) + { + hr = ex.HResult; + } + +#if DEBUG + if (_legacyImport is not null) + { + uint mdLocal = 0, seqLocal = 0, attrLocal = 0; + int hrLegacy = _legacyImport.GetParamProps(tk, &mdLocal, &seqLocal, null, 0, null, &attrLocal, null, null, null); + Debug.ValidateHResult(hr, hrLegacy); + if (hr >= 0 && hrLegacy >= 0) + { + if (pmd is not null) + Debug.Assert(*pmd == mdLocal, $"Method mismatch: cDAC=0x{*pmd:X}, DAC=0x{mdLocal:X}"); + if (pulSequence is not null) + Debug.Assert(*pulSequence == seqLocal, $"Sequence mismatch: cDAC={*pulSequence}, DAC={seqLocal}"); + if (pdwAttr is not null) + Debug.Assert(*pdwAttr == attrLocal, $"Attr mismatch: cDAC=0x{*pdwAttr:X}, DAC=0x{attrLocal:X}"); + } + } +#endif + return hr; + } + + int IMetaDataImport.GetNativeCallConvFromSig(void* pvSig, uint cbSig, uint* pCallConv) + => _legacyImport is not null ? _legacyImport.GetNativeCallConvFromSig(pvSig, cbSig, pCallConv) : HResults.E_NOTIMPL; + + int IMetaDataImport.IsGlobal(uint pd, int* pbGlobal) + => _legacyImport is not null ? _legacyImport.IsGlobal(pd, pbGlobal) : HResults.E_NOTIMPL; + + // IMetaDataImport2 methods — delegate to legacy via _legacyImport2 + int IMetaDataImport2.GetMethodSpecProps(uint mi, uint* tkParent, byte** ppvSigBlob, uint* pcbSigBlob) + => _legacyImport2 is not null ? _legacyImport2.GetMethodSpecProps(mi, tkParent, ppvSigBlob, pcbSigBlob) : HResults.E_NOTIMPL; + + int IMetaDataImport2.EnumGenericParamConstraints(nint* phEnum, uint tk, uint* rGenericParamConstraints, uint cMax, uint* pcGenericParamConstraints) + => _legacyImport2 is not null ? _legacyImport2.EnumGenericParamConstraints(phEnum, tk, rGenericParamConstraints, cMax, pcGenericParamConstraints) : HResults.E_NOTIMPL; + + int IMetaDataImport2.GetGenericParamConstraintProps(uint gpc, uint* ptGenericParam, uint* ptkConstraintType) + => _legacyImport2 is not null ? _legacyImport2.GetGenericParamConstraintProps(gpc, ptGenericParam, ptkConstraintType) : HResults.E_NOTIMPL; + + int IMetaDataImport2.GetPEKind(uint* pdwPEKind, uint* pdwMachine) + => _legacyImport2 is not null ? _legacyImport2.GetPEKind(pdwPEKind, pdwMachine) : HResults.E_NOTIMPL; + + int IMetaDataImport2.GetVersionString(char* pwzBuf, uint ccBufSize, uint* pccBufSize) + => _legacyImport2 is not null ? _legacyImport2.GetVersionString(pwzBuf, ccBufSize, pccBufSize) : HResults.E_NOTIMPL; + + int IMetaDataImport2.EnumMethodSpecs(nint* phEnum, uint tk, uint* rMethodSpecs, uint cMax, uint* pcMethodSpecs) + => _legacyImport2 is not null ? _legacyImport2.EnumMethodSpecs(phEnum, tk, rMethodSpecs, cMax, pcMethodSpecs) : HResults.E_NOTIMPL; + + // ============================================= + // IMetaDataAssemblyImport + // ============================================= + + int IMetaDataAssemblyImport.GetAssemblyProps(uint mda, byte** ppbPublicKey, uint* pcbPublicKey, + uint* pulHashAlgId, char* szName, uint cchName, uint* pchName, + ASSEMBLYMETADATA* pMetaData, uint* pdwAssemblyFlags) + { + int hr = HResults.S_OK; + try + { + // Validate that the token is the assembly definition token + if (mda != 0x20000001) + throw Marshal.GetExceptionForHR(CldbHResults.CLDB_E_RECORD_NOTFOUND)!; + + AssemblyDefinition assemblyDef = _reader.GetAssemblyDefinition(); + string name = _reader.GetString(assemblyDef.Name); + + OutputBufferHelpers.CopyStringToBuffer(szName, cchName, pchName, name, out bool truncated); + + if (!assemblyDef.PublicKey.IsNil) + { + BlobReader publicKeyReader = _reader.GetBlobReader(assemblyDef.PublicKey); + if (ppbPublicKey is not null) + *ppbPublicKey = publicKeyReader.CurrentPointer; + if (pcbPublicKey is not null) + *pcbPublicKey = (uint)publicKeyReader.Length; + } + else + { + if (ppbPublicKey is not null) + *ppbPublicKey = null; + if (pcbPublicKey is not null) + *pcbPublicKey = 0; + } + if (pulHashAlgId is not null) + *pulHashAlgId = (uint)assemblyDef.HashAlgorithm; + + if (pMetaData is not null) + { + System.Version version = assemblyDef.Version; + pMetaData->usMajorVersion = (ushort)version.Major; + pMetaData->usMinorVersion = (ushort)version.Minor; + pMetaData->usBuildNumber = (ushort)version.Build; + pMetaData->usRevisionNumber = (ushort)version.Revision; + + string culture = _reader.GetString(assemblyDef.Culture); + OutputBufferHelpers.CopyStringToBuffer(pMetaData->szLocale, pMetaData->cbLocale, null, culture, out bool localTruncated); + truncated |= localTruncated; + pMetaData->cbLocale = (uint)(culture.Length + 1); + pMetaData->ulProcessor = 0; + pMetaData->ulOS = 0; + } + + if (pdwAssemblyFlags is not null) + { + uint flags = (uint)assemblyDef.Flags; + // Native RegMeta ORs afPublicKey (0x0001) into flags when public key blob is non-empty + if (!assemblyDef.PublicKey.IsNil && _reader.GetBlobReader(assemblyDef.PublicKey).Length > 0) + flags |= 0x0001; + *pdwAssemblyFlags = flags; + } + + hr = truncated ? CldbHResults.CLDB_S_TRUNCATION : HResults.S_OK; + } + catch (System.Exception ex) + { + hr = ex.HResult; + } + +#if DEBUG + if (_legacyAssemblyImport is not null) + { + uint pchLocal = 0, hashAlgLocal = 0, flagsLocal = 0, cbPublicKeyLocal = 0; + byte* publicKeyLocal = null; + ASSEMBLYMETADATA metaLocal = default; + int hrLegacy = _legacyAssemblyImport.GetAssemblyProps(mda, &publicKeyLocal, &cbPublicKeyLocal, &hashAlgLocal, null, 0, &pchLocal, &metaLocal, &flagsLocal); + Debug.ValidateHResult(hr, hrLegacy); + if (hr >= 0 && hrLegacy >= 0) + { + if (pchName is not null) + Debug.Assert(*pchName == pchLocal, $"Name length mismatch: cDAC={*pchName}, DAC={pchLocal}"); + if (pulHashAlgId is not null) + Debug.Assert(*pulHashAlgId == hashAlgLocal, $"HashAlgId mismatch: cDAC=0x{*pulHashAlgId:X}, DAC=0x{hashAlgLocal:X}"); + if (pdwAssemblyFlags is not null) + Debug.Assert(*pdwAssemblyFlags == flagsLocal, $"Flags mismatch: cDAC=0x{*pdwAssemblyFlags:X}, DAC=0x{flagsLocal:X}"); + if (ppbPublicKey is not null) + ValidateBlobsEqual(*ppbPublicKey, pcbPublicKey is not null ? *pcbPublicKey : cbPublicKeyLocal, publicKeyLocal, cbPublicKeyLocal, "AssemblyPublicKey"); + else if (pcbPublicKey is not null) + Debug.Assert(*pcbPublicKey == cbPublicKeyLocal, $"PublicKey length mismatch: cDAC={*pcbPublicKey}, DAC={cbPublicKeyLocal}"); + if (pMetaData is not null) + { + Debug.Assert(pMetaData->usMajorVersion == metaLocal.usMajorVersion, $"MajorVersion mismatch: cDAC={pMetaData->usMajorVersion}, DAC={metaLocal.usMajorVersion}"); + Debug.Assert(pMetaData->usMinorVersion == metaLocal.usMinorVersion, $"MinorVersion mismatch: cDAC={pMetaData->usMinorVersion}, DAC={metaLocal.usMinorVersion}"); + Debug.Assert(pMetaData->usBuildNumber == metaLocal.usBuildNumber, $"BuildNumber mismatch: cDAC={pMetaData->usBuildNumber}, DAC={metaLocal.usBuildNumber}"); + Debug.Assert(pMetaData->usRevisionNumber == metaLocal.usRevisionNumber, $"RevisionNumber mismatch: cDAC={pMetaData->usRevisionNumber}, DAC={metaLocal.usRevisionNumber}"); + } + } + } +#endif + return hr; + } + + int IMetaDataAssemblyImport.GetAssemblyRefProps(uint mdar, byte** ppbPublicKeyOrToken, uint* pcbPublicKeyOrToken, + char* szName, uint cchName, uint* pchName, ASSEMBLYMETADATA* pMetaData, + byte** ppbHashValue, uint* pcbHashValue, uint* pdwAssemblyRefFlags) + { + int hr = HResults.S_OK; + try + { + AssemblyReferenceHandle refHandle = MetadataTokens.AssemblyReferenceHandle((int)(mdar & 0x00FFFFFF)); + AssemblyReference assemblyRef = _reader.GetAssemblyReference(refHandle); + string name = _reader.GetString(assemblyRef.Name); + + OutputBufferHelpers.CopyStringToBuffer(szName, cchName, pchName, name, out bool truncated); + + if (!assemblyRef.PublicKeyOrToken.IsNil) + { + BlobReader publicKeyReader = _reader.GetBlobReader(assemblyRef.PublicKeyOrToken); + if (ppbPublicKeyOrToken is not null) + *ppbPublicKeyOrToken = publicKeyReader.CurrentPointer; + if (pcbPublicKeyOrToken is not null) + *pcbPublicKeyOrToken = (uint)publicKeyReader.Length; + } + else + { + if (ppbPublicKeyOrToken is not null) + *ppbPublicKeyOrToken = null; + if (pcbPublicKeyOrToken is not null) + *pcbPublicKeyOrToken = 0; + } + + if (pMetaData is not null) + { + System.Version version = assemblyRef.Version; + pMetaData->usMajorVersion = (ushort)version.Major; + pMetaData->usMinorVersion = (ushort)version.Minor; + pMetaData->usBuildNumber = (ushort)version.Build; + pMetaData->usRevisionNumber = (ushort)version.Revision; + + string culture = _reader.GetString(assemblyRef.Culture); + OutputBufferHelpers.CopyStringToBuffer(pMetaData->szLocale, pMetaData->cbLocale, null, culture, out bool localTruncated); + truncated |= localTruncated; + pMetaData->cbLocale = (uint)(culture.Length + 1); + pMetaData->ulProcessor = 0; + pMetaData->ulOS = 0; + } + + if (!assemblyRef.HashValue.IsNil) + { + BlobReader hashReader = _reader.GetBlobReader(assemblyRef.HashValue); + if (ppbHashValue is not null) + *ppbHashValue = hashReader.CurrentPointer; + if (pcbHashValue is not null) + *pcbHashValue = (uint)hashReader.Length; + } + else + { + if (ppbHashValue is not null) + *ppbHashValue = null; + if (pcbHashValue is not null) + *pcbHashValue = 0; + } + + if (pdwAssemblyRefFlags is not null) + *pdwAssemblyRefFlags = (uint)assemblyRef.Flags; + + hr = truncated ? CldbHResults.CLDB_S_TRUNCATION : HResults.S_OK; + } + catch (System.Exception ex) + { + hr = ex.HResult; + } + +#if DEBUG + if (_legacyAssemblyImport is not null) + { + uint pchLocal = 0, flagsLocal = 0, cbPublicKeyLocal = 0, cbHashLocal = 0; + byte* publicKeyLocal = null, hashLocal = null; + ASSEMBLYMETADATA metaLocal = default; + int hrLegacy = _legacyAssemblyImport.GetAssemblyRefProps(mdar, &publicKeyLocal, &cbPublicKeyLocal, null, 0, &pchLocal, &metaLocal, &hashLocal, &cbHashLocal, &flagsLocal); + Debug.ValidateHResult(hr, hrLegacy); + if (hr >= 0 && hrLegacy >= 0) + { + if (pchName is not null) + Debug.Assert(*pchName == pchLocal, $"Name length mismatch: cDAC={*pchName}, DAC={pchLocal}"); + if (pdwAssemblyRefFlags is not null) + Debug.Assert(*pdwAssemblyRefFlags == flagsLocal, $"Flags mismatch: cDAC=0x{*pdwAssemblyRefFlags:X}, DAC=0x{flagsLocal:X}"); + if (ppbPublicKeyOrToken is not null) + ValidateBlobsEqual(*ppbPublicKeyOrToken, pcbPublicKeyOrToken is not null ? *pcbPublicKeyOrToken : cbPublicKeyLocal, publicKeyLocal, cbPublicKeyLocal, "AssemblyRefPublicKey"); + else if (pcbPublicKeyOrToken is not null) + Debug.Assert(*pcbPublicKeyOrToken == cbPublicKeyLocal, $"PublicKey length mismatch: cDAC={*pcbPublicKeyOrToken}, DAC={cbPublicKeyLocal}"); + if (ppbHashValue is not null) + ValidateBlobsEqual(*ppbHashValue, pcbHashValue is not null ? *pcbHashValue : cbHashLocal, hashLocal, cbHashLocal, "AssemblyRefHash"); + else if (pcbHashValue is not null) + Debug.Assert(*pcbHashValue == cbHashLocal, $"Hash length mismatch: cDAC={*pcbHashValue}, DAC={cbHashLocal}"); + if (pMetaData is not null) + { + Debug.Assert(pMetaData->usMajorVersion == metaLocal.usMajorVersion, $"MajorVersion mismatch: cDAC={pMetaData->usMajorVersion}, DAC={metaLocal.usMajorVersion}"); + Debug.Assert(pMetaData->usMinorVersion == metaLocal.usMinorVersion, $"MinorVersion mismatch: cDAC={pMetaData->usMinorVersion}, DAC={metaLocal.usMinorVersion}"); + Debug.Assert(pMetaData->usBuildNumber == metaLocal.usBuildNumber, $"BuildNumber mismatch: cDAC={pMetaData->usBuildNumber}, DAC={metaLocal.usBuildNumber}"); + Debug.Assert(pMetaData->usRevisionNumber == metaLocal.usRevisionNumber, $"RevisionNumber mismatch: cDAC={pMetaData->usRevisionNumber}, DAC={metaLocal.usRevisionNumber}"); + } + } + } +#endif + return hr; + } + + int IMetaDataAssemblyImport.GetFileProps(uint mdf, char* szName, uint cchName, uint* pchName, + byte** ppbHashValue, uint* pcbHashValue, uint* pdwFileFlags) + => _legacyAssemblyImport is not null ? _legacyAssemblyImport.GetFileProps(mdf, szName, cchName, pchName, ppbHashValue, pcbHashValue, pdwFileFlags) : HResults.E_NOTIMPL; + + int IMetaDataAssemblyImport.GetExportedTypeProps(uint mdct, char* szName, uint cchName, uint* pchName, + uint* ptkImplementation, uint* ptkTypeDef, uint* pdwExportedTypeFlags) + => _legacyAssemblyImport is not null ? _legacyAssemblyImport.GetExportedTypeProps(mdct, szName, cchName, pchName, ptkImplementation, ptkTypeDef, pdwExportedTypeFlags) : HResults.E_NOTIMPL; + + int IMetaDataAssemblyImport.GetManifestResourceProps(uint mdmr, char* szName, uint cchName, uint* pchName, + uint* ptkImplementation, uint* pdwOffset, uint* pdwResourceFlags) + => _legacyAssemblyImport is not null ? _legacyAssemblyImport.GetManifestResourceProps(mdmr, szName, cchName, pchName, ptkImplementation, pdwOffset, pdwResourceFlags) : HResults.E_NOTIMPL; + + int IMetaDataAssemblyImport.EnumAssemblyRefs(nint* phEnum, uint* rAssemblyRefs, uint cMax, uint* pcTokens) + => _legacyAssemblyImport is not null ? _legacyAssemblyImport.EnumAssemblyRefs(phEnum, rAssemblyRefs, cMax, pcTokens) : HResults.E_NOTIMPL; + + int IMetaDataAssemblyImport.EnumFiles(nint* phEnum, uint* rFiles, uint cMax, uint* pcTokens) + => _legacyAssemblyImport is not null ? _legacyAssemblyImport.EnumFiles(phEnum, rFiles, cMax, pcTokens) : HResults.E_NOTIMPL; + + int IMetaDataAssemblyImport.EnumExportedTypes(nint* phEnum, uint* rExportedTypes, uint cMax, uint* pcTokens) + => _legacyAssemblyImport is not null ? _legacyAssemblyImport.EnumExportedTypes(phEnum, rExportedTypes, cMax, pcTokens) : HResults.E_NOTIMPL; + + int IMetaDataAssemblyImport.EnumManifestResources(nint* phEnum, uint* rManifestResources, uint cMax, uint* pcTokens) + => _legacyAssemblyImport is not null ? _legacyAssemblyImport.EnumManifestResources(phEnum, rManifestResources, cMax, pcTokens) : HResults.E_NOTIMPL; + + int IMetaDataAssemblyImport.GetAssemblyFromScope(uint* ptkAssembly) + { + if (ptkAssembly is not null) + *ptkAssembly = 0x20000001; // TokenFromRid(1, mdtAssembly) + + int hr = HResults.S_OK; +#if DEBUG + if (_legacyAssemblyImport is not null) + { + uint tkLocal = 0; + int hrLegacy = _legacyAssemblyImport.GetAssemblyFromScope(&tkLocal); + Debug.ValidateHResult(hr, hrLegacy); + if (hr >= 0 && hrLegacy >= 0 && ptkAssembly is not null) + Debug.Assert(*ptkAssembly == tkLocal, $"Assembly token mismatch: cDAC=0x{*ptkAssembly:X}, DAC=0x{tkLocal:X}"); + } +#endif + return hr; + } + + int IMetaDataAssemblyImport.FindExportedTypeByName(char* szName, uint mdtExportedType, uint* ptkExportedType) + => _legacyAssemblyImport is not null ? _legacyAssemblyImport.FindExportedTypeByName(szName, mdtExportedType, ptkExportedType) : HResults.E_NOTIMPL; + + int IMetaDataAssemblyImport.FindManifestResourceByName(char* szName, uint* ptkManifestResource) + => _legacyAssemblyImport is not null ? _legacyAssemblyImport.FindManifestResourceByName(szName, ptkManifestResource) : HResults.E_NOTIMPL; + + void IMetaDataAssemblyImport.CloseEnum(nint hEnum) + => ((IMetaDataImport)this).CloseEnum(hEnum); + + int IMetaDataAssemblyImport.FindAssembliesByName(char* szAppBase, char* szPrivateBin, char* szAssemblyName, + nint* ppIUnk, uint cMax, uint* pcAssemblies) + => _legacyAssemblyImport is not null ? _legacyAssemblyImport.FindAssembliesByName(szAppBase, szPrivateBin, szAssemblyName, ppIUnk, cMax, pcAssemblies) : HResults.E_NOTIMPL; + + // Helpers and lookup builders + + private string GetTypeDefFullName(TypeDefinition typeDef) + { + string name = _reader.GetString(typeDef.Name); + string ns = _reader.GetString(typeDef.Namespace); + return string.IsNullOrEmpty(ns) ? name : $"{ns}.{name}"; + } + + private string GetTypeRefFullName(TypeReference typeRef) + { + string name = _reader.GetString(typeRef.Name); + string ns = _reader.GetString(typeRef.Namespace); + return string.IsNullOrEmpty(ns) ? name : $"{ns}.{name}"; + } + + // Native RegMeta maps the global type (TypeDef RID 1) to mdTypeDefNil (0x00000000) + // when returning parent tokens from GetMethodProps, GetFieldProps, and GetMemberRefProps. + private static uint MapGlobalParentToken(uint token) + { + // TypeDef RID 1 has token 0x02000001 + return token == 0x02000001 ? 0 : token; + } + +#if DEBUG + private static void ValidateBlobsEqual(byte* cdacBlob, uint cdacLen, byte* dacBlob, uint dacLen, string name) + { + Debug.Assert(cdacLen == dacLen, $"{name} length mismatch: cDAC={cdacLen}, DAC={dacLen}"); + if (cdacLen == dacLen && cdacLen > 0 && cdacBlob is not null && dacBlob is not null) + { + ReadOnlySpan cdacSpan = new(cdacBlob, (int)cdacLen); + ReadOnlySpan dacSpan = new(dacBlob, (int)dacLen); + Debug.Assert(cdacSpan.SequenceEqual(dacSpan), $"{name} content mismatch (length={cdacLen})"); + } + } +#endif + + private Dictionary BuildInterfaceImplLookup() + { + Dictionary lookup = new(); + foreach (TypeDefinitionHandle tdh in _reader.TypeDefinitions) + { + uint typeToken = (uint)MetadataTokens.GetToken(tdh); + foreach (InterfaceImplementationHandle ih in _reader.GetTypeDefinition(tdh).GetInterfaceImplementations()) + { + lookup[MetadataTokens.GetRowNumber(ih)] = typeToken; + } + } + return lookup; + } + + private Dictionary BuildParamToMethodLookup() + { + Dictionary lookup = new(); + foreach (TypeDefinitionHandle tdh in _reader.TypeDefinitions) + { + foreach (MethodDefinitionHandle mdh in _reader.GetTypeDefinition(tdh).GetMethods()) + { + uint methodToken = (uint)MetadataTokens.GetToken(mdh); + foreach (ParameterHandle ph in _reader.GetMethodDefinition(mdh).GetParameters()) + { + lookup[MetadataTokens.GetRowNumber(ph)] = methodToken; + } + } + } + return lookup; + } + +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Microsoft.Diagnostics.DataContractReader.Legacy.csproj b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Microsoft.Diagnostics.DataContractReader.Legacy.csproj index 237437772770ab..3424ce84e341c6 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Microsoft.Diagnostics.DataContractReader.Legacy.csproj +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Microsoft.Diagnostics.DataContractReader.Legacy.csproj @@ -20,5 +20,6 @@ + diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/OutputBufferHelpers.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/OutputBufferHelpers.cs index 6c044447446cdd..a2b7c3665c8f72 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/OutputBufferHelpers.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/OutputBufferHelpers.cs @@ -9,15 +9,22 @@ namespace Microsoft.Diagnostics.DataContractReader.Legacy; public static class OutputBufferHelpers { public static unsafe void CopyStringToBuffer(char* stringBuf, uint bufferSize, uint* neededBufferSize, string str) + { + CopyStringToBuffer(stringBuf, bufferSize, neededBufferSize, str, out _); + } + + public static unsafe void CopyStringToBuffer(char* stringBuf, uint bufferSize, uint* neededBufferSize, string str, out bool truncated) { ReadOnlySpan strSpan = str.AsSpan(); if (neededBufferSize != null) *neededBufferSize = checked((uint)(strSpan.Length + 1)); + truncated = false; if (stringBuf != null && bufferSize > 0) { Span target = new Span(stringBuf, checked((int)bufferSize)); int nullTerminatorLocation = strSpan.Length > bufferSize - 1 ? checked((int)(bufferSize - 1)) : strSpan.Length; + truncated = strSpan.Length + 1 > bufferSize; strSpan = strSpan.Slice(0, nullTerminatorLocation); strSpan.CopyTo(target); target[nullTerminatorLocation] = '\0'; diff --git a/src/native/managed/cdac/tests/DumpTests/Debuggees/MultiModule/Program.cs b/src/native/managed/cdac/tests/DumpTests/Debuggees/MultiModule/Program.cs index d803f40e8d39ea..927c2dce9c257f 100644 --- a/src/native/managed/cdac/tests/DumpTests/Debuggees/MultiModule/Program.cs +++ b/src/native/managed/cdac/tests/DumpTests/Debuggees/MultiModule/Program.cs @@ -8,11 +8,18 @@ /// /// Debuggee for cDAC dump tests — exercises the Loader contract. /// Loads assemblies from multiple AssemblyLoadContexts then crashes. +/// Also includes metadata features for MetaDataImport dump tests: +/// - Non-const fields (for ELEMENT_TYPE_VOID default value testing) +/// - String literals (user strings in #US heap) /// internal static class Program { public const int CustomAlcCount = 3; + // Non-const field — used by MetaDataImport dump tests to verify + // ELEMENT_TYPE_VOID is returned for fields without default constants. + private static int s_nonConstField; + private static void Main() { // Create multiple AssemblyLoadContexts and load the runtime assembly in each @@ -33,10 +40,19 @@ private static void Main() // Also load System.Xml to have another module present var xmlAssembly = Assembly.Load("System.Private.Xml"); + // Use the non-const field so it doesn't get optimized away + s_nonConstField = contexts.Length; + + // Use a string literal to ensure the #US heap has a user string entry + // (MetaDataImport dump tests verify GetUserString char count semantics) + string userString = "cDAC dump test marker string"; + // Keep references alive GC.KeepAlive(contexts); GC.KeepAlive(loadedAssemblies); GC.KeepAlive(xmlAssembly); + GC.KeepAlive(userString); + GC.KeepAlive(s_nonConstField); Environment.FailFast("cDAC dump test: MultiModule debuggee intentional crash"); } diff --git a/src/native/managed/cdac/tests/DumpTests/MetaDataImportDumpTests.cs b/src/native/managed/cdac/tests/DumpTests/MetaDataImportDumpTests.cs new file mode 100644 index 00000000000000..715f13c1a6069e --- /dev/null +++ b/src/native/managed/cdac/tests/DumpTests/MetaDataImportDumpTests.cs @@ -0,0 +1,146 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Reflection.Metadata; +using System.Reflection.Metadata.Ecma335; +using Microsoft.Diagnostics.DataContractReader.Contracts; +using Microsoft.Diagnostics.DataContractReader.Legacy; +using Xunit; + +namespace Microsoft.Diagnostics.DataContractReader.DumpTests; + +/// +/// Dump-based integration tests for MetaDataImportImpl semantic parity. +/// Verifies our managed IMetaDataImport implementation against real metadata from dumps. +/// Uses the MultiModule debuggee which contains non-const fields, user strings, and +/// methods on named types. +/// +public class MetaDataImportDumpTests : DumpTestBase +{ + protected override string DebuggeeName => "MultiModule"; + protected override string DumpType => "full"; + + private (MetadataReader reader, IMetaDataImport mdi) GetRootModuleImport() + { + ILoader loader = Target.Contracts.Loader; + IEcmaMetadata ecmaMetadata = Target.Contracts.EcmaMetadata; + + TargetPointer rootAssembly = loader.GetRootAssembly(); + Contracts.ModuleHandle moduleHandle = loader.GetModuleHandleFromAssemblyPtr(rootAssembly); + + MetadataReader? reader = ecmaMetadata.GetMetadata(moduleHandle); + Assert.NotNull(reader); + + return (reader, new MetaDataImportImpl(reader)); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnVersion("net10.0", "Assembly type does not include IsDynamic/IsLoaded fields in .NET 10")] + public unsafe void GetFieldProps_NoConstant_ReturnsElementTypeVoid(TestConfiguration config) + { + InitializeDumpTest(config); + var (reader, mdi) = GetRootModuleImport(); + + // Find a field without a constant value. + // The MultiModule debuggee has s_nonConstField which has no default constant. + uint fieldToken = 0; + foreach (TypeDefinitionHandle tdh in reader.TypeDefinitions) + { + TypeDefinition td = reader.GetTypeDefinition(tdh); + foreach (FieldDefinitionHandle fdh in td.GetFields()) + { + FieldDefinition fd = reader.GetFieldDefinition(fdh); + if (fd.GetDefaultValue().IsNil) + { + fieldToken = (uint)MetadataTokens.GetToken(fdh); + break; + } + } + if (fieldToken != 0) + break; + } + + Assert.True(fieldToken != 0, "Expected at least one field without a constant in MultiModule debuggee"); + + uint dwCPlusTypeFlag; + void* pValue; + uint cchValue; + int hr = mdi.GetFieldProps(fieldToken, null, null, 0, null, null, null, null, &dwCPlusTypeFlag, &pValue, &cchValue); + + Assert.Equal(HResults.S_OK, hr); + // Native RegMeta returns ELEMENT_TYPE_VOID (1) when field has no constant + Assert.Equal(1u, dwCPlusTypeFlag); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnVersion("net10.0", "Assembly type does not include IsDynamic/IsLoaded fields in .NET 10")] + public unsafe void GetMethodProps_NonGlobalMethod_ReturnsParentClass(TestConfiguration config) + { + InitializeDumpTest(config); + var (reader, mdi) = GetRootModuleImport(); + + // Find a method on a non- type (TypeDef RID > 1) + uint methodToken = 0; + uint expectedParentToken = 0; + foreach (TypeDefinitionHandle tdh in reader.TypeDefinitions) + { + int typeRid = MetadataTokens.GetRowNumber(tdh); + if (typeRid == 1) continue; // skip + + TypeDefinition td = reader.GetTypeDefinition(tdh); + foreach (MethodDefinitionHandle mdh in td.GetMethods()) + { + methodToken = (uint)MetadataTokens.GetToken(mdh); + expectedParentToken = (uint)MetadataTokens.GetToken(tdh); + break; + } + if (methodToken != 0) break; + } + + Assert.True(methodToken != 0, "Expected at least one method on a non-global type in MultiModule debuggee"); + + uint pClass; + int hr = mdi.GetMethodProps(methodToken, &pClass, null, 0, null, null, null, null, null, null); + + Assert.Equal(HResults.S_OK, hr); + Assert.Equal(expectedParentToken, pClass); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnVersion("net10.0", "Assembly type does not include IsDynamic/IsLoaded fields in .NET 10")] + public unsafe void GetUserString_ReturnsCharCountWithoutNull(TestConfiguration config) + { + InitializeDumpTest(config); + var (reader, mdi) = GetRootModuleImport(); + + // Walk the user string heap for a non-empty string. + // The MultiModule debuggee has string literals that populate the #US heap. + uint userStringToken = 0; + int expectedCharCount = 0; + UserStringHandle ush = MetadataTokens.UserStringHandle(1); + while (!ush.IsNil) + { + string value = reader.GetUserString(ush); + if (value.Length > 0) + { + userStringToken = (uint)MetadataTokens.GetToken(ush); + expectedCharCount = value.Length; + break; + } + ush = reader.GetNextHandle(ush); + } + + Assert.True(userStringToken != 0, "Expected at least one user string in MultiModule debuggee"); + + uint pchString; + int hr = mdi.GetUserString(userStringToken, null, 0, &pchString); + + Assert.Equal(HResults.S_OK, hr); + // Native returns character count WITHOUT null terminator + Assert.Equal((uint)expectedCharCount, pchString); + } +} diff --git a/src/native/managed/cdac/tests/DumpTests/Microsoft.Diagnostics.DataContractReader.DumpTests.csproj b/src/native/managed/cdac/tests/DumpTests/Microsoft.Diagnostics.DataContractReader.DumpTests.csproj index 034f33e100abe8..f458c4f674d55a 100644 --- a/src/native/managed/cdac/tests/DumpTests/Microsoft.Diagnostics.DataContractReader.DumpTests.csproj +++ b/src/native/managed/cdac/tests/DumpTests/Microsoft.Diagnostics.DataContractReader.DumpTests.csproj @@ -26,7 +26,6 @@ - diff --git a/src/native/managed/cdac/tests/MetaDataImportImplTests.cs b/src/native/managed/cdac/tests/MetaDataImportImplTests.cs new file mode 100644 index 00000000000000..9983512542c64f --- /dev/null +++ b/src/native/managed/cdac/tests/MetaDataImportImplTests.cs @@ -0,0 +1,1102 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Immutable; +using System.Reflection; +using System.Reflection.Metadata; +using System.Reflection.Metadata.Ecma335; +using System.Runtime.InteropServices; +using System.Runtime.InteropServices.Marshalling; +using Microsoft.Diagnostics.DataContractReader.Legacy; +using Xunit; + +namespace Microsoft.Diagnostics.DataContractReader.Tests; + +public unsafe class MetaDataImportImplTests +{ + // Build a minimal assembly metadata with types, methods, and fields. + private static (MetadataReader reader, MetadataReaderProvider provider) CreateTestMetadata() + { + MetadataBuilder mb = new(); + + // Module + mb.AddModule(0, mb.GetOrAddString("TestModule"), mb.GetOrAddGuid(Guid.NewGuid()), default, default); + + // Assembly with a public key blob (to test afPublicKey flag OR) + byte[] publicKey = [0x00, 0x24, 0x00, 0x00, 0x04, 0x80, 0x00, 0x00]; + mb.AddAssembly(mb.GetOrAddString("TestAssembly"), new Version(1, 0, 0, 0), + default, mb.GetOrAddBlob(publicKey), AssemblyFlags.PublicKey, AssemblyHashAlgorithm.Sha1); + + // mscorlib assembly ref (for System.Object base type) + AssemblyReferenceHandle mscorlibRef = mb.AddAssemblyReference( + mb.GetOrAddString("mscorlib"), new Version(4, 0, 0, 0), + default, mb.GetOrAddBlob(new byte[] { 0xB7, 0x7A, 0x5C, 0x56, 0x19, 0x34, 0xE0, 0x89 }), + default, default); + + // System.Object type ref + TypeReferenceHandle objectRef = mb.AddTypeReference(mscorlibRef, + mb.GetOrAddString("System"), mb.GetOrAddString("Object")); + + // IDisposable type ref (for interface impl testing) + TypeReferenceHandle disposableRef = mb.AddTypeReference(mscorlibRef, + mb.GetOrAddString("System"), mb.GetOrAddString("IDisposable")); + + // Create a method signature blob: void () + BlobBuilder sigBlob = new(); + new BlobEncoder(sigBlob).MethodSignature().Parameters(0, returnType => returnType.Void(), parameters => { }); + BlobHandle voidMethodSig = mb.GetOrAddBlob(sigBlob); + + // Create a field signature blob: int + BlobBuilder fieldSigBlob = new(); + new BlobEncoder(fieldSigBlob).Field().Type().Int32(); + BlobHandle intFieldSig = mb.GetOrAddBlob(fieldSigBlob); + + // Create a field signature blob: string + BlobBuilder stringFieldSigBlob = new(); + new BlobEncoder(stringFieldSigBlob).Field().Type().String(); + BlobHandle stringFieldSig = mb.GetOrAddBlob(stringFieldSigBlob); + + // TypeDef: (required, row 1) — owns the global method (method 1) and no fields + mb.AddTypeDefinition(default, default, mb.GetOrAddString(""), default, + MetadataTokens.FieldDefinitionHandle(1), MetadataTokens.MethodDefinitionHandle(1)); + + // Global method on : GlobalHelper (void) — method row 1 + mb.AddMethodDefinition(MethodAttributes.Static | MethodAttributes.Public, MethodImplAttributes.IL, + mb.GetOrAddString("GlobalHelper"), voidMethodSig, + -1, MetadataTokens.ParameterHandle(1)); + + // TypeDef: TestNamespace.TestClass (row 2) — owns field 1+, method 2+ + TypeDefinitionHandle testClassHandle = mb.AddTypeDefinition( + TypeAttributes.Public | TypeAttributes.Class, + mb.GetOrAddString("TestNamespace"), + mb.GetOrAddString("TestClass"), + objectRef, + MetadataTokens.FieldDefinitionHandle(1), + MetadataTokens.MethodDefinitionHandle(2)); + + // FieldDef: _value (int) — field row 1, no constant + mb.AddFieldDefinition(FieldAttributes.Private, mb.GetOrAddString("_value"), intFieldSig); + + // FieldDef: StringConst (string) — field row 2, with string constant + FieldDefinitionHandle stringConstField = mb.AddFieldDefinition( + FieldAttributes.Public | FieldAttributes.Static | FieldAttributes.Literal | FieldAttributes.HasDefault, + mb.GetOrAddString("StringConst"), stringFieldSig); + + // Add a string constant value for StringConst — MetadataBuilder encodes as UTF-16LE + mb.AddConstant(stringConstField, "test"); + + // MethodDef: DoWork (void) with no parameters — method row 2 + mb.AddMethodDefinition(MethodAttributes.Public, MethodImplAttributes.IL, + mb.GetOrAddString("DoWork"), voidMethodSig, + -1, MetadataTokens.ParameterHandle(1)); + + // Parameter: "arg0" at sequence 1 (associated with DoWork for parameter enumeration testing) + mb.AddParameter(ParameterAttributes.None, mb.GetOrAddString("arg0"), 1); + + // Interface implementation: TestClass : IDisposable + mb.AddInterfaceImplementation(testClassHandle, disposableRef); + + // TypeDef: TestNamespace.TestClass+NestedType (row 3) + TypeDefinitionHandle nestedHandle = mb.AddTypeDefinition( + TypeAttributes.NestedPublic | TypeAttributes.Class, + default, + mb.GetOrAddString("NestedType"), + objectRef, + MetadataTokens.FieldDefinitionHandle(3), + MetadataTokens.MethodDefinitionHandle(3)); + + // Nested class relationship + mb.AddNestedType(nestedHandle, testClassHandle); + + // Generic parameter on TestClass + mb.AddGenericParameter(testClassHandle, GenericParameterAttributes.None, mb.GetOrAddString("T"), 0); + + // MemberRef: Object.ToString() on objectRef + BlobBuilder memberRefSig = new(); + new BlobEncoder(memberRefSig).MethodSignature().Parameters(0, returnType => returnType.Type().String(), parameters => { }); + mb.AddMemberReference(objectRef, mb.GetOrAddString("ToString"), mb.GetOrAddBlob(memberRefSig)); + + // ModuleRef: "NativeLib" + mb.AddModuleReference(mb.GetOrAddString("NativeLib")); + + // TypeSpec: a simple type spec (int[]) + BlobBuilder typeSpecSig = new(); + new BlobEncoder(typeSpecSig).TypeSpecificationSignature().SZArray().Int32(); + mb.AddTypeSpecification(mb.GetOrAddBlob(typeSpecSig)); + + // UserString: "Hello, World!" + _ = mb.GetOrAddUserString("Hello, World!"); + + // TypeDef with explicit layout for GetClassLayout testing (row 4) + mb.AddTypeDefinition( + TypeAttributes.Public | TypeAttributes.SequentialLayout | TypeAttributes.Class, + mb.GetOrAddString("TestNamespace"), + mb.GetOrAddString("LayoutClass"), + objectRef, + MetadataTokens.FieldDefinitionHandle(3), + MetadataTokens.MethodDefinitionHandle(3)); + mb.AddTypeLayout(MetadataTokens.TypeDefinitionHandle(4), 8, 32); + + // Custom attribute on TestClass: [System.ObsoleteAttribute("test message")] + MemberReferenceHandle obsoleteCtor = mb.AddMemberReference( + mb.AddTypeReference(mscorlibRef, mb.GetOrAddString("System"), mb.GetOrAddString("ObsoleteAttribute")), + mb.GetOrAddString(".ctor"), + mb.GetOrAddBlob(new byte[] { 0x20, 0x01, 0x0E, 0x00 })); // instance void(string) + mb.AddCustomAttribute(testClassHandle, obsoleteCtor, + mb.GetOrAddBlob(new byte[] { 0x01, 0x00, 0x0C, 0x74, 0x65, 0x73, 0x74, 0x20, 0x6D, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x00, 0x00 })); + + // Serialize + BlobBuilder metadataBlob = new(); + MetadataRootBuilder root = new(mb); + root.Serialize(metadataBlob, 0, 0); + byte[] bytes = metadataBlob.ToArray(); + + // Create provider and reader — provider must stay alive as long as the reader is used + MetadataReaderProvider provider = MetadataReaderProvider.FromMetadataImage(ImmutableArray.Create(bytes)); + return (provider.GetMetadataReader(), provider); + } + + // Provider is stored alongside wrapper to prevent GC from collecting pinned metadata memory. + private static MetadataReaderProvider? _testProvider; + + private static IMetaDataImport2 CreateWrapper() + { + (MetadataReader reader, MetadataReaderProvider provider) = CreateTestMetadata(); + _testProvider = provider; + return new MetaDataImportImpl(reader); + } + + [Fact] + public void EnumFields_Pagination() + { + IMetaDataImport2 wrapper = CreateWrapper(); + + nint hEnum = 0; + uint token; + uint count; + + int hr = wrapper.EnumFields(&hEnum, 0x02000002, &token, 1, &count); + Assert.Equal(HResults.S_OK, hr); + Assert.Equal(1u, count); + Assert.Equal(0x04000001u, token); + + wrapper.CloseEnum(hEnum); + } + + [Fact] + public void GetTypeDefProps_ReturnsNameAndFlags() + { + IMetaDataImport2 wrapper = CreateWrapper(); + + // Find TestClass token (should be row 2 = 0x02000002) + uint testClassToken = 0x02000002; + char* nameBuf = stackalloc char[256]; + uint nameLen; + uint flags; + uint extends; + + int hr = wrapper.GetTypeDefProps(testClassToken, nameBuf, 256, &nameLen, &flags, &extends); + Assert.Equal(HResults.S_OK, hr); + + string name = new string(nameBuf, 0, (int)nameLen - 1); // minus null terminator + Assert.Equal("TestNamespace.TestClass", name); + Assert.True((flags & (uint)TypeAttributes.Public) != 0); + Assert.NotEqual(0u, extends); // Should have a base type (Object) + } + + [Fact] + public void GetTypeRefProps_ReturnsName() + { + IMetaDataImport2 wrapper = CreateWrapper(); + + // System.Object TypeRef should be row 1 = 0x01000001 + uint objectRefToken = 0x01000001; + char* nameBuf = stackalloc char[256]; + uint nameLen; + uint scope; + + int hr = wrapper.GetTypeRefProps(objectRefToken, &scope, nameBuf, 256, &nameLen); + Assert.Equal(HResults.S_OK, hr); + + string name = new string(nameBuf, 0, (int)nameLen - 1); + Assert.Equal("System.Object", name); + } + + [Fact] + public void GetMethodProps_ReturnsNameAndSignature() + { + IMetaDataImport2 wrapper = CreateWrapper(); + + // DoWork should be MethodDef row 2 = 0x06000002 + uint methodToken = 0x06000002; + uint parentClass; + char* nameBuf = stackalloc char[256]; + uint nameLen; + uint attrs; + byte* sigBlob; + uint sigLen; + uint rva; + uint implFlags; + + int hr = wrapper.GetMethodProps(methodToken, &parentClass, nameBuf, 256, &nameLen, + &attrs, &sigBlob, &sigLen, &rva, &implFlags); + Assert.Equal(HResults.S_OK, hr); + + string name = new string(nameBuf, 0, (int)nameLen - 1); + Assert.Equal("DoWork", name); + Assert.True(sigLen > 0); + Assert.True(sigBlob is not null); + } + + [Fact] + public void GetFieldProps_ReturnsNameAndSignature() + { + IMetaDataImport2 wrapper = CreateWrapper(); + + // _value should be FieldDef row 1 = 0x04000001 + uint fieldToken = 0x04000001; + uint parentClass; + char* nameBuf = stackalloc char[256]; + uint nameLen; + uint attrs; + byte* sigBlob; + uint sigLen; + + int hr = wrapper.GetFieldProps(fieldToken, &parentClass, nameBuf, 256, &nameLen, + &attrs, &sigBlob, &sigLen, null, null, null); + Assert.Equal(HResults.S_OK, hr); + + string name = new string(nameBuf, 0, (int)nameLen - 1); + Assert.Equal("_value", name); + Assert.True(sigLen > 0); + } + + [Fact] + public void GetMemberProps_DispatchesToMethodOrField() + { + IMetaDataImport2 wrapper = CreateWrapper(); + + uint parentClass; + char* nameBuf = stackalloc char[256]; + uint nameLen; + + // Method token (DoWork = 0x06000002) + int hr = wrapper.GetMemberProps(0x06000002, &parentClass, nameBuf, 256, &nameLen, + null, null, null, null, null, null, null, null); + Assert.Equal(HResults.S_OK, hr); + Assert.Equal("DoWork", new string(nameBuf, 0, (int)nameLen - 1)); + + // Field token + hr = wrapper.GetMemberProps(0x04000001, &parentClass, nameBuf, 256, &nameLen, + null, null, null, null, null, null, null, null); + Assert.Equal(HResults.S_OK, hr); + Assert.Equal("_value", new string(nameBuf, 0, (int)nameLen - 1)); + + // Invalid table + hr = wrapper.GetMemberProps(0xFF000001, null, null, 0, null, + null, null, null, null, null, null, null, null); + Assert.Equal(HResults.E_INVALIDARG, hr); + } + + [Fact] + public void EnumFields_ReturnsFieldsForType() + { + IMetaDataImport2 wrapper = CreateWrapper(); + + nint hEnum = 0; + uint* tokens = stackalloc uint[10]; + uint count; + int hr = wrapper.EnumFields(&hEnum, 0x02000002, tokens, 10, &count); + Assert.Equal(HResults.S_OK, hr); + Assert.True(count >= 1); + Assert.Contains(0x04000001u, new ReadOnlySpan(tokens, (int)count).ToArray()); // _value + + wrapper.CloseEnum(hEnum); + } + + [Fact] + public void EnumInterfaceImpls_ReturnsImplementations() + { + IMetaDataImport2 wrapper = CreateWrapper(); + + nint hEnum = 0; + uint* tokens = stackalloc uint[10]; + uint count; + int hr = wrapper.EnumInterfaceImpls(&hEnum, 0x02000002, tokens, 10, &count); + Assert.Equal(HResults.S_OK, hr); + Assert.Equal(1u, count); + + wrapper.CloseEnum(hEnum); + } + + [Fact] + public void GetInterfaceImplProps_ReturnsClassAndInterface() + { + IMetaDataImport2 wrapper = CreateWrapper(); + + // Get the interface impl token first + nint hEnum = 0; + uint implToken; + uint count; + wrapper.EnumInterfaceImpls(&hEnum, 0x02000002, &implToken, 1, &count); + wrapper.CloseEnum(hEnum); + + uint parentClass; + uint interfaceToken; + int hr = wrapper.GetInterfaceImplProps(implToken, &parentClass, &interfaceToken); + Assert.Equal(HResults.S_OK, hr); + Assert.Equal(0x02000002u, parentClass); // TestClass + Assert.NotEqual(0u, interfaceToken); + } + + [Fact] + public void GetNestedClassProps_ReturnsEnclosingClass() + { + IMetaDataImport2 wrapper = CreateWrapper(); + + // NestedType is TypeDef row 3 = 0x02000003 + uint enclosingClass; + int hr = wrapper.GetNestedClassProps(0x02000003, &enclosingClass); + Assert.Equal(HResults.S_OK, hr); + Assert.Equal(0x02000002u, enclosingClass); // TestClass + } + + [Fact] + public void GetNestedClassProps_NonNestedReturnsRecordNotFound() + { + IMetaDataImport2 wrapper = CreateWrapper(); + + // TestClass (row 2) is not nested + uint enclosingClass; + int hr = wrapper.GetNestedClassProps(0x02000002, &enclosingClass); + Assert.True(hr < 0); // CLDB_E_RECORD_NOTFOUND + } + + [Fact] + public void EnumGenericParams_ReturnsParams() + { + IMetaDataImport2 wrapper = CreateWrapper(); + + nint hEnum = 0; + uint* tokens = stackalloc uint[10]; + uint count; + int hr = wrapper.EnumGenericParams(&hEnum, 0x02000002, tokens, 10, &count); + Assert.Equal(HResults.S_OK, hr); + Assert.Equal(1u, count); + + wrapper.CloseEnum(hEnum); + } + + [Fact] + public void GetGenericParamProps_ReturnsNameAndOwner() + { + IMetaDataImport2 wrapper = CreateWrapper(); + + // Get generic param token + nint hEnum = 0; + uint gpToken; + uint count; + wrapper.EnumGenericParams(&hEnum, 0x02000002, &gpToken, 1, &count); + wrapper.CloseEnum(hEnum); + + uint seq; + uint flags; + uint owner; + char* nameBuf = stackalloc char[256]; + uint nameLen; + + int hr = wrapper.GetGenericParamProps(gpToken, &seq, &flags, &owner, null, nameBuf, 256, &nameLen); + Assert.Equal(HResults.S_OK, hr); + Assert.Equal(0u, seq); + Assert.Equal(0x02000002u, owner); // TestClass + Assert.Equal("T", new string(nameBuf, 0, (int)nameLen - 1)); + } + + [Fact] + public void IsValidToken_ValidAndInvalid() + { + IMetaDataImport2 wrapper = CreateWrapper(); + + // Valid TypeDef token + Assert.Equal(1, wrapper.IsValidToken(0x02000001)); + Assert.Equal(1, wrapper.IsValidToken(0x02000002)); + + // Invalid - RID 0 + Assert.Equal(0, wrapper.IsValidToken(0x02000000)); + + // Invalid - RID too high + Assert.Equal(0, wrapper.IsValidToken(0x020000FF)); + } + + [Fact] + public void InvalidToken_ReturnsError() + { + IMetaDataImport2 wrapper = CreateWrapper(); + + // Invalid TypeDef token (way out of range) + char* nameBuf = stackalloc char[256]; + uint nameLen; + int hr = wrapper.GetTypeDefProps(0x020000FF, nameBuf, 256, &nameLen, null, null); + Assert.True(hr < 0); // Should return an error HRESULT + } + + [Fact] + public void NotImplementedMethods_ReturnENotImpl() + { + IMetaDataImport2 wrapper = CreateWrapper(); + + Assert.Equal(HResults.E_NOTIMPL, wrapper.GetScopeProps(null, 0, null, null)); + Assert.Equal(HResults.E_NOTIMPL, wrapper.ResolveTypeRef(0, null, null, null)); + Assert.Equal(HResults.E_NOTIMPL, wrapper.EnumTypeDefs(null, null, 0, null)); + Assert.Equal(HResults.E_NOTIMPL, wrapper.EnumTypeRefs(null, null, 0, null)); + } + + [Fact] + public void FindTypeDefByName_FindsType() + { + IMetaDataImport2 wrapper = CreateWrapper(); + + uint td; + fixed (char* name = "TestNamespace.TestClass") + { + int hr = wrapper.FindTypeDefByName(name, 0, &td); + Assert.Equal(HResults.S_OK, hr); + Assert.Equal(0x02000002u, td); + } + } + + [Fact] + public void FindTypeDefByName_NotFound() + { + IMetaDataImport2 wrapper = CreateWrapper(); + + uint td; + fixed (char* name = "DoesNotExist") + { + int hr = wrapper.FindTypeDefByName(name, 0, &td); + Assert.True(hr < 0); // CLDB_E_RECORD_NOTFOUND + } + } + + [Fact] + public void GetMemberRefProps_ReturnsNameAndParent() + { + IMetaDataImport2 wrapper = CreateWrapper(); + + uint memberRefToken = 0x0A000001; // MemberRef row 1 + uint parentToken; + char* nameBuf = stackalloc char[256]; + uint nameLen; + byte* sigBlob; + uint sigLen; + + int hr = wrapper.GetMemberRefProps(memberRefToken, &parentToken, nameBuf, 256, &nameLen, &sigBlob, &sigLen); + Assert.Equal(HResults.S_OK, hr); + + string name = new string(nameBuf, 0, (int)nameLen - 1); + Assert.Equal("ToString", name); + Assert.NotEqual(0u, parentToken); + Assert.True(sigLen > 0); + } + + [Fact] + public void GetModuleRefProps_ReturnsName() + { + IMetaDataImport2 wrapper = CreateWrapper(); + + uint moduleRefToken = 0x1A000001; // ModuleRef row 1 + char* nameBuf = stackalloc char[256]; + uint nameLen; + + int hr = wrapper.GetModuleRefProps(moduleRefToken, nameBuf, 256, &nameLen); + Assert.Equal(HResults.S_OK, hr); + + string name = new string(nameBuf, 0, (int)nameLen - 1); + Assert.Equal("NativeLib", name); + } + + [Fact] + public void GetTypeSpecFromToken_ReturnsSig() + { + IMetaDataImport2 wrapper = CreateWrapper(); + + uint typeSpecToken = 0x1B000001; // TypeSpec row 1 + byte* sigBlob; + uint sigLen; + + int hr = wrapper.GetTypeSpecFromToken(typeSpecToken, &sigBlob, &sigLen); + Assert.Equal(HResults.S_OK, hr); + Assert.True(sigLen > 0); + Assert.True(sigBlob is not null); + } + + [Fact] + public void GetUserString_ReturnsString() + { + IMetaDataImport2 wrapper = CreateWrapper(); + + uint userStringToken = 0x70000001; // UserString heap offset 1 + char* strBuf = stackalloc char[256]; + uint strLen; + + int hr = wrapper.GetUserString(userStringToken, strBuf, 256, &strLen); + Assert.Equal(HResults.S_OK, hr); + + string value = new string(strBuf, 0, (int)strLen); + Assert.Equal("Hello, World!", value); + } + + [Fact] + public void GetParamProps_ReturnsNameAndSequence() + { + IMetaDataImport2 wrapper = CreateWrapper(); + + uint paramToken = 0x08000001; // Param row 1 + uint parentMethod; + uint sequence; + char* nameBuf = stackalloc char[256]; + uint nameLen; + uint attrs; + + int hr = wrapper.GetParamProps(paramToken, &parentMethod, &sequence, nameBuf, 256, &nameLen, + &attrs, null, null, null); + Assert.Equal(HResults.S_OK, hr); + + string name = new string(nameBuf, 0, (int)nameLen - 1); + Assert.Equal("arg0", name); + Assert.Equal(1u, sequence); + Assert.Equal(0x06000002u, parentMethod); // DoWork + } + + [Fact] + public void GetParamForMethodIndex_FindsParam() + { + IMetaDataImport2 wrapper = CreateWrapper(); + + uint paramToken; + int hr = wrapper.GetParamForMethodIndex(0x06000002, 1, ¶mToken); + Assert.Equal(HResults.S_OK, hr); + Assert.Equal(0x08000001u, paramToken); + } + + [Fact] + public void GetParamForMethodIndex_NotFound() + { + IMetaDataImport2 wrapper = CreateWrapper(); + + uint paramToken; + int hr = wrapper.GetParamForMethodIndex(0x06000002, 99, ¶mToken); + Assert.True(hr < 0); // CLDB_E_RECORD_NOTFOUND + } + + [Fact] + public void GetClassLayout_ReturnsLayout() + { + IMetaDataImport2 wrapper = CreateWrapper(); + + uint packSize; + uint classSize; + int hr = wrapper.GetClassLayout(0x02000004, &packSize, null, 0, null, &classSize); + Assert.Equal(HResults.S_OK, hr); + Assert.Equal(8u, packSize); + Assert.Equal(32u, classSize); + } + + [Fact] + public void GetClassLayout_NoLayout_ReturnsRecordNotFound() + { + IMetaDataImport2 wrapper = CreateWrapper(); + + uint packSize; + uint classSize; + int hr = wrapper.GetClassLayout(0x02000002, &packSize, null, 0, null, &classSize); + Assert.True(hr < 0); // CLDB_E_RECORD_NOTFOUND + } + + [Fact] + public void ReaderOnly_MethodsWork() + { + // When only reader is available (no legacy), implemented methods should still work + (MetadataReader reader, MetadataReaderProvider provider) = CreateTestMetadata(); + _testProvider = provider; + IMetaDataImport2 wrapper = new MetaDataImportImpl(reader, legacyImport: null); + + uint flags; + char* nameBuf = stackalloc char[256]; + uint nameLen; + + int hr = wrapper.GetTypeDefProps(0x02000002, nameBuf, 256, &nameLen, &flags, null); + Assert.Equal(HResults.S_OK, hr); + + string name = new string(nameBuf, 0, (int)nameLen - 1); + Assert.Equal("TestNamespace.TestClass", name); + } + + [Fact] + public void GetRVA_MethodDef_ReturnsRVA() + { + (MetadataReader reader, MetadataReaderProvider provider) = CreateTestMetadata(); + _testProvider = provider; + IMetaDataImport2 wrapper = new MetaDataImportImpl(reader, legacyImport: null); + + uint rva, implFlags; + // DoWork is MethodDef token 0x06000002 + int hr = wrapper.GetRVA(0x06000002, &rva, &implFlags); + Assert.Equal(HResults.S_OK, hr); + Assert.Equal((uint)MethodImplAttributes.IL, implFlags); + } + + [Fact] + public void GetRVA_InvalidTable_ReturnsEInvalidArg() + { + (MetadataReader reader, MetadataReaderProvider provider) = CreateTestMetadata(); + _testProvider = provider; + IMetaDataImport2 wrapper = new MetaDataImportImpl(reader, legacyImport: null); + + uint rva; + // TypeDef token (0x02) is not MethodDef or FieldDef + int hr = wrapper.GetRVA(0x02000001, &rva, null); + Assert.Equal(HResults.E_INVALIDARG, hr); + } + + [Fact] + public void GetCustomAttributeByName_Found_ReturnsSok() + { + (MetadataReader reader, MetadataReaderProvider provider) = CreateTestMetadata(); + _testProvider = provider; + IMetaDataImport2 wrapper = new MetaDataImportImpl(reader, legacyImport: null); + + void* pData; + uint cbData; + fixed (char* attrName = "System.ObsoleteAttribute") + { + // TestClass (0x02000002) has [Obsolete("test message")] + int hr = wrapper.GetCustomAttributeByName(0x02000002, attrName, &pData, &cbData); + Assert.Equal(HResults.S_OK, hr); + Assert.True(pData is not null); + Assert.True(cbData > 0); + } + } + + [Fact] + public void GetCustomAttributeByName_NotFound_ReturnsSFalse() + { + (MetadataReader reader, MetadataReaderProvider provider) = CreateTestMetadata(); + _testProvider = provider; + IMetaDataImport2 wrapper = new MetaDataImportImpl(reader, legacyImport: null); + + void* pData; + uint cbData; + fixed (char* attrName = "System.NonExistentAttribute") + { + int hr = wrapper.GetCustomAttributeByName(0x02000002, attrName, &pData, &cbData); + Assert.Equal(HResults.S_FALSE, hr); + Assert.True(pData is null); + Assert.Equal(0u, cbData); + } + } + + [Fact] + public void GetAssemblyFromScope_ReturnsAssemblyToken() + { + (MetadataReader reader, MetadataReaderProvider provider) = CreateTestMetadata(); + _testProvider = provider; + IMetaDataImport2 wrapper = new MetaDataImportImpl(reader, legacyImport: null); + IMetaDataAssemblyImport assemblyImport = (IMetaDataAssemblyImport)wrapper; + + uint tkAssembly; + int hr = assemblyImport.GetAssemblyFromScope(&tkAssembly); + Assert.Equal(HResults.S_OK, hr); + Assert.Equal(0x20000001u, tkAssembly); + } + + [Fact] + public void GetAssemblyProps_ReturnsNameAndVersion() + { + (MetadataReader reader, MetadataReaderProvider provider) = CreateTestMetadata(); + _testProvider = provider; + IMetaDataImport2 wrapper = new MetaDataImportImpl(reader, legacyImport: null); + IMetaDataAssemblyImport assemblyImport = (IMetaDataAssemblyImport)wrapper; + + char* nameBuf = stackalloc char[256]; + uint nameLen; + ASSEMBLYMETADATA metadata = default; + uint flags; + + int hr = assemblyImport.GetAssemblyProps(0x20000001, null, null, null, nameBuf, 256, &nameLen, &metadata, &flags); + Assert.Equal(HResults.S_OK, hr); + + string name = new string(nameBuf, 0, (int)nameLen - 1); + Assert.Equal("TestAssembly", name); + Assert.Equal(1, metadata.usMajorVersion); + Assert.Equal(0, metadata.usMinorVersion); + Assert.Equal(0, metadata.usBuildNumber); + Assert.Equal(0, metadata.usRevisionNumber); + Assert.Equal((uint)AssemblyFlags.PublicKey, flags); + } + + [Fact] + public void GetAssemblyRefProps_ReturnsRefNameAndVersion() + { + (MetadataReader reader, MetadataReaderProvider provider) = CreateTestMetadata(); + _testProvider = provider; + IMetaDataImport2 wrapper = new MetaDataImportImpl(reader, legacyImport: null); + IMetaDataAssemblyImport assemblyImport = (IMetaDataAssemblyImport)wrapper; + + // mscorlib assembly ref is token 0x23000001 + char* nameBuf = stackalloc char[256]; + uint nameLen; + ASSEMBLYMETADATA metadata = default; + uint flags; + + int hr = assemblyImport.GetAssemblyRefProps(0x23000001, null, null, nameBuf, 256, &nameLen, &metadata, null, null, &flags); + Assert.Equal(HResults.S_OK, hr); + + string name = new string(nameBuf, 0, (int)nameLen - 1); + Assert.Equal("mscorlib", name); + Assert.Equal(4, metadata.usMajorVersion); + Assert.Equal(0, metadata.usMinorVersion); + Assert.Equal(0, metadata.usBuildNumber); + Assert.Equal(0, metadata.usRevisionNumber); + } + + [Fact] + public void GetAssemblyProps_SmallBuffer_Truncates() + { + (MetadataReader reader, MetadataReaderProvider provider) = CreateTestMetadata(); + _testProvider = provider; + IMetaDataImport2 wrapper = new MetaDataImportImpl(reader, legacyImport: null); + IMetaDataAssemblyImport assemblyImport = (IMetaDataAssemblyImport)wrapper; + + char* nameBuf = stackalloc char[5]; + uint nameLen; + + int hr = assemblyImport.GetAssemblyProps(0x20000001, null, null, null, nameBuf, 5, &nameLen, null, null); + Assert.Equal(0x00131106, hr); // CLDB_S_TRUNCATION + // Full name is "TestAssembly" (12 chars + null = 13) + Assert.Equal(13u, nameLen); + // Buffer should contain "Test\0" + string truncated = new string(nameBuf, 0, 4); + Assert.Equal("Test", truncated); + } + + [Fact] + public void GetAssemblyProps_InvalidToken_ReturnsRecordNotFound() + { + var builder = new MetadataBuilder(); + builder.AddModule(0, builder.GetOrAddString("TestModule"), builder.GetOrAddGuid(Guid.NewGuid()), default, default); + builder.AddAssembly( + builder.GetOrAddString("TestAssembly"), + new Version(1, 0, 0, 0), + default, + default, + default, + default); + + var metadataBuilder = new BlobBuilder(); + new MetadataRootBuilder(builder).Serialize(metadataBuilder, 0, 0); + var metadata = metadataBuilder.ToImmutableArray(); + + fixed (byte* ptr = metadata.AsSpan()) + { + var reader = new MetadataReader(ptr, metadata.Length); + IMetaDataImport2 impl = new MetaDataImportImpl(reader); + var assemblyImport = (IMetaDataAssemblyImport)impl; + + // Pass an invalid assembly token (wrong RID) + int hr = assemblyImport.GetAssemblyProps(0x20000002, null, null, null, null, 0, null, null, null); + Assert.Equal(unchecked((int)0x80131130), hr); // CLDB_E_RECORD_NOTFOUND + } + } + + [Fact] + public void GetFieldProps_NoConstant_ReturnsElementTypeVoid() + { + IMetaDataImport2 wrapper = CreateWrapper(); + + // _value (field row 1) has no constant + uint fieldToken = 0x04000001; + uint cplusTypeFlag; + void* pValue; + uint cchValue; + + int hr = wrapper.GetFieldProps(fieldToken, null, null, 0, null, null, null, null, &cplusTypeFlag, &pValue, &cchValue); + Assert.Equal(HResults.S_OK, hr); + Assert.Equal(1u, cplusTypeFlag); // ELEMENT_TYPE_VOID + Assert.True(pValue is null); + Assert.Equal(0u, cchValue); + } + + [Fact] + public void GetFieldProps_StringConstant_ReturnsCharCount() + { + IMetaDataImport2 wrapper = CreateWrapper(); + + // StringConst (field row 2) has a string constant "test" + uint fieldToken = 0x04000002; + uint cplusTypeFlag; + void* pValue; + uint cchValue; + + int hr = wrapper.GetFieldProps(fieldToken, null, null, 0, null, null, null, null, &cplusTypeFlag, &pValue, &cchValue); + Assert.Equal(HResults.S_OK, hr); + Assert.Equal(0x0Eu, cplusTypeFlag); // ELEMENT_TYPE_STRING + Assert.True(pValue is not null); + Assert.Equal(4u, cchValue); // "test" = 4 characters, not 8 bytes + } + + [Fact] + public void GetMethodProps_GlobalMethod_ReturnsMdTypeDefNil() + { + IMetaDataImport2 wrapper = CreateWrapper(); + + // GlobalHelper (method row 1) is on — parent should be mdTypeDefNil (0) + uint methodToken = 0x06000001; + uint parentClass; + + int hr = wrapper.GetMethodProps(methodToken, &parentClass, null, 0, null, null, null, null, null, null); + Assert.Equal(HResults.S_OK, hr); + Assert.Equal(0u, parentClass); // mdTypeDefNil, not 0x02000001 + } + + [Fact] + public void GetMethodProps_NonGlobalMethod_ReturnsParentClass() + { + IMetaDataImport2 wrapper = CreateWrapper(); + + // DoWork (method row 2) is on TestClass — parent should be TestClass token + uint methodToken = 0x06000002; + uint parentClass; + + int hr = wrapper.GetMethodProps(methodToken, &parentClass, null, 0, null, null, null, null, null, null); + Assert.Equal(HResults.S_OK, hr); + Assert.Equal(0x02000002u, parentClass); // TestClass + } + + [Fact] + public void GetFieldProps_GlobalField_ReturnsMdTypeDefNil() + { + // Create metadata with a global field on + MetadataBuilder mb = new(); + mb.AddModule(0, mb.GetOrAddString("Mod"), mb.GetOrAddGuid(Guid.NewGuid()), default, default); + mb.AddAssembly(mb.GetOrAddString("Asm"), new Version(1, 0, 0, 0), default, default, default, default); + + BlobBuilder fieldSig = new(); + new BlobEncoder(fieldSig).Field().Type().Int32(); + BlobHandle intFieldSig = mb.GetOrAddBlob(fieldSig); + + // owns field 1 + mb.AddTypeDefinition(default, default, mb.GetOrAddString(""), default, + MetadataTokens.FieldDefinitionHandle(1), MetadataTokens.MethodDefinitionHandle(1)); + mb.AddFieldDefinition(FieldAttributes.Static | FieldAttributes.Public, mb.GetOrAddString("GlobalField"), intFieldSig); + + BlobBuilder blob = new(); + new MetadataRootBuilder(mb).Serialize(blob, 0, 0); + var bytes = blob.ToImmutableArray(); + + fixed (byte* ptr = bytes.AsSpan()) + { + var reader = new MetadataReader(ptr, bytes.Length); + IMetaDataImport2 impl = new MetaDataImportImpl(reader); + + uint parentClass; + int hr = impl.GetFieldProps(0x04000001, &parentClass, null, 0, null, null, null, null, null, null, null); + Assert.Equal(HResults.S_OK, hr); + Assert.Equal(0u, parentClass); // mdTypeDefNil + } + } + + [Fact] + public void GetUserString_ReturnsCharCountWithoutNull() + { + IMetaDataImport2 wrapper = CreateWrapper(); + + uint userStringToken = 0x70000001; + uint pchString; + + // Query length only (no buffer) + int hr = wrapper.GetUserString(userStringToken, null, 0, &pchString); + Assert.Equal(HResults.S_OK, hr); + Assert.Equal(13u, pchString); // "Hello, World!" = 13 chars, NO null terminator + } + + [Fact] + public void GetAssemblyProps_IncludesAfPublicKeyFlag() + { + IMetaDataImport2 wrapper = CreateWrapper(); + var assemblyImport = (IMetaDataAssemblyImport)wrapper; + + uint flags; + byte* pubKey; + uint pubKeyLen; + + int hr = assemblyImport.GetAssemblyProps(0x20000001, &pubKey, &pubKeyLen, null, null, 0, null, null, &flags); + Assert.Equal(HResults.S_OK, hr); + Assert.True(pubKeyLen > 0); + Assert.True((flags & 0x0001) != 0, "afPublicKey flag should be set when public key blob is non-empty"); + } + + [Fact] + public void GetParamProps_NoConstant_ReturnsElementTypeVoid() + { + IMetaDataImport2 wrapper = CreateWrapper(); + + // arg0 (param row 1) has no constant + uint paramToken = 0x08000001; + uint cplusTypeFlag; + void* pValue; + uint cchValue; + + int hr = wrapper.GetParamProps(paramToken, null, null, null, 0, null, null, &cplusTypeFlag, &pValue, &cchValue); + Assert.Equal(HResults.S_OK, hr); + Assert.Equal(1u, cplusTypeFlag); // ELEMENT_TYPE_VOID + Assert.True(pValue is null); + Assert.Equal(0u, cchValue); + } + + // Regression test: ClrMD QIs for IMetaDataImport but accesses IMetaDataImport2 vtable + // slots beyond the IMetaDataImport boundary (EnumGenericParams is slot 65, past the + // 65-slot IMetaDataImport vtable). With [GeneratedComInterface] CCWs, each interface + // gets its own vtable. The ICustomQueryInterface on MetaDataImportImpl must redirect + // IMetaDataImport QIs to IMetaDataImport2 so all 73 slots are accessible. + [Fact] + public void QueryInterfaceForIMetaDataImport_ReturnsIMetaDataImport2VtableWithExtendedSlots() + { + var (reader, provider) = CreateTestMetadata(); + using var _ = provider; + + IMetaDataImport2 wrapper = new MetaDataImportImpl(reader); + + nint pUnk = (nint)ComInterfaceMarshaller.ConvertToUnmanaged(wrapper); + + try + { + // Simulate ClrMD: QI for IMetaDataImport + Guid iidImport = typeof(IMetaDataImport).GUID; + int hr = Marshal.QueryInterface(pUnk, in iidImport, out nint pImport); + Assert.Equal(0, hr); + Assert.NotEqual(nint.Zero, pImport); + + try + { + // Simulate ClrMD's CallableCOMWrapper: QI the returned pointer AGAIN for IMetaDataImport. + // This second QI goes to MetaDataImportImpl's CCW. Without the ICustomQueryInterface + // redirect, this would return the shorter IMetaDataImport vtable (65 slots) and accessing + // slot 65 (EnumGenericParams) would AV. + Guid iidImportAgain = typeof(IMetaDataImport).GUID; + hr = Marshal.QueryInterface(pImport, in iidImportAgain, out nint pImportAgain); + Assert.Equal(0, hr); + Assert.NotEqual(nint.Zero, pImportAgain); + + try + { + // Verify the returned pointer has IMetaDataImport2 slots accessible. + // QI the result for IMetaDataImport2 to verify COM identity is correct. + Guid iidImport2 = typeof(IMetaDataImport2).GUID; + hr = Marshal.QueryInterface(pImportAgain, in iidImport2, out nint pImport2); + Assert.Equal(0, hr); + Assert.NotEqual(nint.Zero, pImport2); + Marshal.Release(pImport2); + + // The critical check: read the vtable and verify EnumGenericParams (slot 65) + // is a valid function pointer, not garbage from reading past the vtable end. + // IMetaDataImport has 62 methods, IMetaDataImport2 adds 8 = 70 total. + // Vtable: [0]=QI, [1]=AddRef, [2]=Release, [3..64]=IMetaDataImport, [65..72]=IMetaDataImport2 + nint* vtable = *(nint**)pImportAgain; + nint enumGenericParams = vtable[65]; // EnumGenericParams + Assert.NotEqual(nint.Zero, enumGenericParams); + + // Call EnumGenericParams with a null enumerator to verify it doesn't AV. + // Use the COM function pointer directly, like ClrMD does. + nint hEnum = 0; + uint count = 0; + // delegate* unmanaged[Stdcall] + var fn = (delegate* unmanaged[Stdcall])enumGenericParams; + hr = fn(pImportAgain, &hEnum, 0x02000002, null, 0, &count); + // Should succeed (or return no results) without AV + Assert.True(hr >= 0 || hr == unchecked((int)0x80131130)); // S_OK or CLDB_E_RECORD_NOTFOUND + } + finally + { + Marshal.Release(pImportAgain); + } + } + finally + { + Marshal.Release(pImport); + } + } + finally + { + ComInterfaceMarshaller.Free((void*)pUnk); + } + } + + [Fact] + public void CountEnum_ReturnsCountForCdacEnum() + { + IMetaDataImport2 wrapper = CreateWrapper(); + + nint hEnum = 0; + uint* tokens = stackalloc uint[10]; + uint count; + + int hr = wrapper.EnumFields(&hEnum, 0x02000002, tokens, 10, &count); + Assert.Equal(HResults.S_OK, hr); + Assert.True(count > 0); + uint expectedCount = count; + + uint enumCount; + hr = wrapper.CountEnum(hEnum, &enumCount); + Assert.Equal(HResults.S_OK, hr); + Assert.Equal(expectedCount, enumCount); + + wrapper.CloseEnum(hEnum); + } + + [Fact] + public void CountEnum_NullHandle_ReturnsZero() + { + IMetaDataImport2 wrapper = CreateWrapper(); + + uint count = 42; + int hr = wrapper.CountEnum(0, &count); + Assert.Equal(HResults.S_OK, hr); + Assert.Equal(0u, count); + } + + [Fact] + public void ResetEnum_ResetsPositionForCdacEnum() + { + IMetaDataImport2 wrapper = CreateWrapper(); + + nint hEnum = 0; + uint token; + uint count; + + int hr = wrapper.EnumFields(&hEnum, 0x02000002, &token, 1, &count); + Assert.Equal(HResults.S_OK, hr); + Assert.Equal(1u, count); + uint firstToken = token; + + hr = wrapper.ResetEnum(hEnum, 0); + Assert.Equal(HResults.S_OK, hr); + + hr = wrapper.EnumFields(&hEnum, 0x02000002, &token, 1, &count); + Assert.Equal(HResults.S_OK, hr); + Assert.Equal(1u, count); + Assert.Equal(firstToken, token); + + wrapper.CloseEnum(hEnum); + } + + [Fact] + public void ResetEnum_NullHandle_ReturnsOk() + { + IMetaDataImport2 wrapper = CreateWrapper(); + + int hr = wrapper.ResetEnum(0, 0); + Assert.Equal(HResults.S_OK, hr); + } +}