mirror of
https://github.com/ZuneDev/ZuneShell.dll.git
synced 2026-07-27 13:11:51 -07:00
Implement IDList
This commit is contained in:
@@ -91,3 +91,29 @@ The original constructor calls `global::<Module>.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]<IntPtr, int*>)(*(ulong*)(*(long*)intPtr + 32)))((nint)intPtr);
|
||||
// ptr2 ← call at *(vtable + 40) (slot 5)
|
||||
ptr2 = ((delegate* unmanaged[Cdecl, Cdecl]<IntPtr, int*>)(*(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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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<IQueryPropertyBag>` 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, `<Module>`,
|
||||
and all call sites in `QueryPropertyBag`.
|
||||
|
||||
**Findings:**
|
||||
|
||||
1. The global-namespace struct `IQueryPropertyBag` carries only `[NativeCppClass]`,
|
||||
`[DebugInfoInPDB]`, and `[MiscellaneousBits(65)]` — no `[Guid]` attribute.
|
||||
2. `<Module>` 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.
|
||||
@@ -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::<Module>.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::<Module>.@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::<Module>.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::<Module>.@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::<Module>.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]<IntPtr, int*>)(*(ulong*)(*(long*)intPtr + 32)))((nint)intPtr);
|
||||
// ptr2 ← vtable slot at byte offset 40
|
||||
ptr2 = ((delegate* unmanaged[Cdecl, Cdecl]<IntPtr, int*>)(*(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.
|
||||
Reference in New Issue
Block a user