diff --git a/LOG.md b/LOG.md index 80bf69e..963538a 100644 --- a/LOG.md +++ b/LOG.md @@ -91,3 +91,29 @@ The original constructor calls `global::.ZuneLibraryExports.CreateProper **File:** `ZuneDBApi/MicrosoftZuneInterop/QueryPropertyBag.cs` (previous `IntPtr`-based draft) The ILSpy decompilation shows `delegate* unmanaged[Cdecl, Cdecl]` for the vtable dispatch (the double `Cdecl` appears to be an ILSpy artifact of C++/CLI compilation). Standard COM uses `__stdcall` on x86 and the platform ABI on x64 (where Cdecl and Stdcall converge). The current implementation uses `[GeneratedComInterface]` which handles this, but if raw vtable dispatch is ever needed (e.g. for the unknown slots), the calling convention should be verified against the native binary. + +--- + +## 2026-07-12 — IMultiSortAttributes vtable slot correction + +**File:** `ZuneDBApi/MicrosoftZuneInterop/QueryPropertyBag.cs` + +The original comment had `GetSortOrders` at slot 3 and `GetSortAttributes` at slot 4. This was wrong. The correct assignment was determined by reading the full ILSpy decompilation of `PackMultiSortAttributes`: + +```csharp +// ptr ← call at *(vtable + 32) (slot 4, assuming IUnknown at slots 0–2) +ptr = ((delegate* unmanaged[Cdecl, Cdecl])(*(ulong*)(*(long*)intPtr + 32)))((nint)intPtr); +// ptr2 ← call at *(vtable + 40) (slot 5) +ptr2 = ((delegate* unmanaged[Cdecl, Cdecl])(*(ulong*)(*(long*)intPtr2 + 40)))((nint)intPtr2); +``` + +Then in the fill loop: +- `*ptr4 = (int)eQuerySortType` where `ptr4` starts at `ptr2` and advances by 4 each iteration → `ptr2` receives **EQuerySortType** values → slot 5 = `GetSortOrders()` +- `*(int*)((byte*)ptr5 + (nuint)ptr4) = num5` where `ptr5 = ptr - ptr2`, which simplifies to writing `num5` (the schema index from `CSchemaMap.GetIndex`) into `ptr[i]` → `ptr` receives **schema indices** → slot 4 = `GetSortAttributes()` + +Slot 3 (offset 24) is not called in `PackMultiSortAttributes` and remains unknown. + +**Corrected layout:** +- `[3]` unknown +- `[4]` `GetSortAttributes()` → `int*` (parallel array of SchemaMap indices) +- `[5]` `GetSortOrders()` → `int*` (parallel array of EQuerySortType values) diff --git a/ZuneDBApi/MicrosoftZuneInterop/QueryPropertyBag.cs b/ZuneDBApi/MicrosoftZuneInterop/QueryPropertyBag.cs index 832b097..9921333 100644 --- a/ZuneDBApi/MicrosoftZuneInterop/QueryPropertyBag.cs +++ b/ZuneDBApi/MicrosoftZuneInterop/QueryPropertyBag.cs @@ -6,8 +6,9 @@ using System.Runtime.InteropServices.Marshalling; namespace MicrosoftZuneInterop; // Native COM interface IMultiSortAttributes — vtable layout (x64, IUnknown = slots 0–2): -// [3] GetSortOrders() -> EQuerySortType* (parallel array of sort directions) -// [4] GetSortAttributes() -> int* (parallel array of SchemaMap indices) +// [3] unknown +// [4] GetSortAttributes() -> int* (parallel array of SchemaMap indices) +// [5] GetSortOrders() -> int* (parallel array of EQuerySortType values) // // Native struct IDList (16 bytes): // int Count @@ -71,11 +72,25 @@ public class QueryPropertyBag : IDisposable return StrategyBasedComWrappers.Instance.GetOrCreateComInterfaceForObject(_bag, CreateComInterfaceFlags.None); } - // Packs multiIds into a native IDList: { int Count; int* Ids }. - // The returned IntPtr is unmanaged heap memory; the native caller is responsible for freeing it. - public IntPtr PackIDList(IList multiIds) + // Packs multiIds into a native IDList: { int Count; (4 pad); int* Ids }. + // The returned IntPtr points to native heap memory; the native caller is responsible for freeing it. + public unsafe IntPtr PackIDList(IList multiIds) { - throw new NotImplementedException(); + int count = multiIds.Count; + + // Allocate the 16-byte IDList struct and zero the Ids pointer slot before filling it, + // so the struct is never in a half-initialized state if AllocHGlobal below throws. + IntPtr listPtr = Marshal.AllocHGlobal(16); + *(int*)listPtr = count; + *(long*)(listPtr + 8) = 0L; + + IntPtr idsPtr = Marshal.AllocHGlobal(count * sizeof(int)); + *(IntPtr*)(listPtr + 8) = idsPtr; + + for (int i = 0; i < count; i++) + *(int*)(idsPtr + i * sizeof(int)) = (int)multiIds[i]!; + + return listPtr; } // Packs parallel sort-direction/attribute arrays into a native IMultiSortAttributes COM object. diff --git a/logs/MicrosoftZuneInterop/IQueryPropertyBag.md b/logs/MicrosoftZuneInterop/IQueryPropertyBag.md new file mode 100644 index 0000000..6d40d61 --- /dev/null +++ b/logs/MicrosoftZuneInterop/IQueryPropertyBag.md @@ -0,0 +1,94 @@ +# IQueryPropertyBag — decompilation log + +Append-only. Do not edit previous entries. + +--- + +## 2026-07-12 — Interface reconstruction + +**Source:** ILSpy decompilation of `ZuneShell/lib/ZuneDBApi.dll` (mixed-mode C++/CLI assembly). + +ILSpy cannot see the native side of a C++/CLI assembly, so `IQueryPropertyBag` is only +visible indirectly — through the managed `QueryPropertyBag` class that wraps it. The +vtable offsets were read from the raw `delegate* unmanaged[Cdecl, Cdecl]` call sites +that ILSpy emits for native virtual dispatch. + +### Vtable layout (x64, pointer size = 8 bytes) + +Offsets confirmed by reading the decompiled call sites in `QueryPropertyBag`: + +| Slot | Byte offset | Signature | Source | +|------|------------|-----------|--------| +| 0 | 0 | `QueryInterface` (IUnknown) | assumed; standard COM | +| 1 | 8 | `AddRef` (IUnknown) | assumed; standard COM | +| 2 | 16 | `Release` (IUnknown) | confirmed — `!QueryPropertyBag()` destructor calls `*(vtable + 16)` | +| 3 | 24 | unknown | not observed in any managed call site | +| 4 | 32 | unknown | not observed in any managed call site | +| 5 | 40 | `SetString(EQueryPropertyBagProp prop, wchar_t* value) -> HRESULT` | `SetValue` with `string` branch: `*(vtable + 40)` | +| 6 | 48 | unknown | not observed in any managed call site | +| 7 | 56 | `SetInt(EQueryPropertyBagProp prop, int value) -> HRESULT` | `SetValue` with `int`/`bool` branch: `*(vtable + 56)` | +| 8–12 | 64–96 | unknown | not observed in any managed call site | +| 13 | 104 | `IsSet(EQueryPropertyBagProp prop, int* out) -> HRESULT` | `IsSet`: `*(vtable + 104)` | + +Unknown slots (3, 4, 6, 8–12) are declared as `void _ReservedN()` placeholders in +`IQueryPropertyBag.cs` to preserve vtable ordering without blocking compilation. +These placeholders must not be called, and their signatures must be verified against +the native binary before the CCW/RCW is used in production. + +**Needs:** Ghidra analysis of the native `IQueryPropertyBag` vtable definition to +fill in slots 3, 4, 6, and 8–12. + +--- + +## 2026-07-12 — GUID: initial search + +**Source:** ILSpy search for `IQueryPropertyBag` as a type in `ZuneDBApi.dll`. + +The `[GeneratedComInterface]` attribute requires a GUID. ILSpy finds no managed type +named `IQueryPropertyBag` (only the global-namespace native struct and the +`CComPtrNtv` wrapper). A placeholder GUID +`3D8A1F2B-6C4E-4A5D-9B7F-2E0C1A8D3F4E` was used. + +--- + +## 2026-07-12 — GUID: follow-up search in managed metadata + +**Task:** Check whether the real GUID is encoded anywhere accessible to ILSpy. +Specifically examined: the global-namespace `IQueryPropertyBag` struct, ``, +and all call sites in `QueryPropertyBag`. + +**Findings:** + +1. The global-namespace struct `IQueryPropertyBag` carries only `[NativeCppClass]`, + `[DebugInfoInPDB]`, and `[MiscellaneousBits(65)]` — no `[Guid]` attribute. +2. `` could not be decompiled by ILSpy (tool returned an error); its fields + are not inspectable through the managed surface. +3. Full decompilation of `MicrosoftZuneInterop.QueryPropertyBag` shows no IID + reference in managed code. `GetIQueryPropertyBag()` returns the raw pointer + directly; there is no `QueryInterface` call visible. +4. The destructor (`!QueryPropertyBag`) calls `*(vtable + 16)` with just `this`, + confirming `Release` is at slot 2 — consistent with IUnknown, but this reveals + no IID. + +**Conclusion:** The GUID is not present in the managed metadata of `ZuneDBApi.dll`. +If one exists it is in the native segment only (`__declspec(uuid(...))` or +`DEFINE_GUID`). Recovery requires Ghidra. Placeholder GUID stands. + +It is also possible the interface has no GUID at all: the user confirmed that +`IQueryPropertyBag` objects are not `CoCreateInstance`-registered, and no +`QueryInterface` call was observed, so a GUID may simply never be needed at runtime. + +--- + +## 2026-07-12 — Calling convention + +The ILSpy decompilation emits `delegate* unmanaged[Cdecl, Cdecl]` for the vtable +dispatch. The duplicate `Cdecl` is a known ILSpy artifact of C++/CLI compilation and +does not mean the calling convention is Cdecl. On x64 Windows, Stdcall and Cdecl +are identical (both use the Microsoft x64 ABI), so the distinction does not affect +correctness there. + +The current implementation uses `[GeneratedComInterface]`, which selects the correct +calling convention automatically. If raw vtable dispatch is ever needed (e.g. to call +the placeholder-reserved slots), the convention should be verified against the native +binary before assuming Stdcall. diff --git a/logs/MicrosoftZuneInterop/QueryPropertyBag.md b/logs/MicrosoftZuneInterop/QueryPropertyBag.md new file mode 100644 index 0000000..addf4e3 --- /dev/null +++ b/logs/MicrosoftZuneInterop/QueryPropertyBag.md @@ -0,0 +1,138 @@ +# QueryPropertyBag — decompilation log + +Append-only. Do not edit previous entries. + +--- + +## 2026-07-12 — Decompilation + +**Source:** ILSpy decompilation of `MicrosoftZuneInterop.QueryPropertyBag` in +`ZuneShell/lib/ZuneDBApi.dll` (mixed-mode C++/CLI assembly). + +`QueryPropertyBag` is a managed wrapper around a native `IQueryPropertyBag*` COM +pointer created by the native export `ZuneLibraryExports.CreatePropertyBag`. The +class is not itself a COM object — it simply holds the pointer and delegates to it. + +### Constructor + +Calls `global::.ZuneLibraryExports.CreatePropertyBag(&pPropertyBag)`. +This is a native export. The DLL name, exact symbol, and calling convention are +unknown and cannot be determined from the managed metadata alone. + +**Unknown (Q5):** P/Invoke signature for `CreatePropertyBag`. Needs native binary +inspection or documentation. Constructor is stubbed with a `// TODO` comment. + +### SetValue / IsSet + +Dispatch to `IQueryPropertyBag` vtable slots 5, 7, 13 (see +`../MicrosoftZuneInterop/IQueryPropertyBag.md`). Logic is fully implementable once +`MapNameToProp` is implemented. + +### MapNameToProp + +Walks a static 37-entry native array `MicrosoftZuneInterop.?A0x52c37a46.kPropIdMap` +of `PropIdMapEntry` structs (each 16 bytes: `wchar_t*` name at [0..7], +`EQueryPropertyBagProp` id at [8..11], 4 bytes padding at [12..15]) using +`_wcsicmp` for case-insensitive comparison. + +The name strings and enum values are in native data — not visible through ILSpy. +Without them, `MapNameToProp` (and by extension `SetValue` and `IsSet`) cannot be +implemented. + +**Unknown (Q3):** The 37 name strings and their `EQueryPropertyBagProp` values. +Needs Ghidra analysis of the native data segment. + +**Unknown (Q4):** `EQueryPropertyBagProp` enum definition. Lives in the global +(unnamed) namespace; ILSpy queries with a namespace prefix fail. Resolving Q3 and +Q4 together in Ghidra would be efficient since the map table embeds the enum values +directly. + +### GetIQueryPropertyBag + +Returns `m_pPropertyBag` directly. No `QueryInterface`, no IID involved. + +### PackIDList — implemented 2026-07-12 + +**Source:** Full ILSpy decompilation of `PackIDList` in `ZuneDBApi.dll`. + +The original allocates a 16-byte `IDList` struct on the native heap and fills it: + +```csharp +// Original (ILSpy output, simplified): +IDList* ptr = (IDList*)global::.@new(16uL); +*(int*)ptr = count; // Count at offset 0 +*(long*)((ulong)(nint)ptr + 8uL) = 0L; // zero Ids ptr at offset 8 +*(long*)((ulong)(nint)ptr + 8uL) = (nint)global::.new[](count * 4uL); // heap int[] +for (int i = 0; i < count; i++) + *(int*)(i * 4 + *(long*)((ulong)(nint)ptr + 8uL)) = (int)multiIds[i]; +``` + +**Translation decisions:** + +- `global::.@new(16)` → `Marshal.AllocHGlobal(16)`. + `operator new` and `Marshal.AllocHGlobal` both allocate unmanaged heap memory. + The key difference: `operator new` can return null on failure; `Marshal.AllocHGlobal` + throws `OutOfMemoryException` instead. The original null-check / try-catch is + therefore not needed — if allocation fails the exception propagates naturally. + +- `global::.new[](count * 4)` → `Marshal.AllocHGlobal(count * sizeof(int))`. + Same allocation family; same reasoning. `sizeof(int)` is 4 on all platforms. + +- The original overflow guard `(count > 4611686018427387903L ? ulong.MaxValue : count * 4)` + is omitted. `IList.Count` returns a 32-bit `int` (max ~2 billion). Multiplying by + 4 yields at most ~8 GB, which fits in a 64-bit `nint` and would result in an + `OutOfMemoryException` from the allocator long before arithmetic overflow. + +- The Ids pointer slot at offset 8 is zeroed before the second `AllocHGlobal` call, + matching the original's initialization order. This ensures the struct is never in + a half-initialized state if the second allocation throws. + +- Pointer arithmetic is done with `unsafe` pointer casts, directly mirroring the + original byte-level layout. + +**Reference:** The `IDList` struct layout (16 bytes: `int Count` + 4 pad + `int* Ids`) +is confirmed by the ILSpy output for this method and consistent with the comment +at the top of `QueryPropertyBag.cs`. + +--- + +## 2026-07-12 — IMultiSortAttributes vtable layout + +**Source:** ILSpy decompilation of `PackMultiSortAttributes` in `ZuneDBApi.dll`. + +The original `PackMultiSortAttributes` calls two vtable slots on the native +`IMultiSortAttributes*` returned by `CreateMultiSortAttributes`: + +```csharp +// ptr ← vtable slot at byte offset 32 +ptr = ((delegate* unmanaged[Cdecl, Cdecl])(*(ulong*)(*(long*)intPtr + 32)))((nint)intPtr); +// ptr2 ← vtable slot at byte offset 40 +ptr2 = ((delegate* unmanaged[Cdecl, Cdecl])(*(ulong*)(*(long*)intPtr2 + 40)))((nint)intPtr2); +``` + +In the fill loop: +- `ptr4` starts at `ptr2` and advances by 4; `*ptr4 = (int)eQuerySortType` → `ptr2` + receives `EQuerySortType` values → offset 40 (slot 5) = **GetSortOrders()** +- `ptr5 = ptr - ptr2`; `*(int*)((byte*)ptr5 + ptr4) = num5` simplifies to writing + the schema index (`CSchemaMap.GetIndex` result) into `ptr[i]` → offset 32 (slot 4) + = **GetSortAttributes()** + +Assuming IUnknown at slots 0–2 (offsets 0, 8, 16), slot 3 (offset 24) is not called +in this method and remains unknown. The original comment had these two methods +transposed; the code comment and this log both reflect the corrected order. + +| Slot | Byte offset | Signature | +|------|------------|-----------| +| 0–2 | 0–16 | IUnknown (assumed) | +| 3 | 24 | unknown | +| 4 | 32 | `GetSortAttributes() -> int*` (schema index array) | +| 5 | 40 | `GetSortOrders() -> int*` (EQuerySortType array) | + +### PackMultiSortAttributes — still a stub + +Requires two native P/Invokes that cannot yet be resolved from managed metadata: +- `ZuneLibraryExports.CreateMultiSortAttributes(int count, IMultiSortAttributes** out)` +- `CSchemaMap.GetIndex(wchar_t* name) -> int` + +Both the DLL names and calling conventions are unknown. Stubbed with a `// TODO` +comment listing the full algorithm derived from the decompilation.