mirror of
https://github.com/ZuneDev/ZuneShell.dll.git
synced 2026-07-27 13:11:51 -07:00
Add script to generate C# class from MUI strings
This commit is contained in:
Executable
+83
@@ -0,0 +1,83 @@
|
||||
# Usage: extractMuiStrings.sh [muiFile] [outputCs] [namespace] [className]
|
||||
# Reads the RT_STRING (STRINGTABLE) resources out of a Win32 MUI satellite
|
||||
# resource file and generates a C# class mapping resource ID -> string.
|
||||
|
||||
MUI_FILE=${1:-../../windows/shared/zune-x64/Zune/en-US/ZuneResources.dll.mui}
|
||||
OUT_CS=${2:-../ZuneDBApi/MicrosoftZuneLibrary/ZuneLibraryResources.g.cs}
|
||||
NAMESPACE=${3:-MicrosoftZuneLibrary}
|
||||
CLASS_NAME=${4:-ZuneLibraryResources}
|
||||
|
||||
for cmd in wrestool od dd iconv awk; do
|
||||
command -v "$cmd" >/dev/null 2>&1 || { echo "$cmd is required but was not found on PATH." >&2; exit 1; }
|
||||
done
|
||||
|
||||
if [ ! -f "$MUI_FILE" ]; then
|
||||
echo "MUI file not found: $MUI_FILE" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Extracting RT_STRING resources from $MUI_FILE..."
|
||||
|
||||
TMP_DIR=$(mktemp -d)
|
||||
trap 'rm -rf "$TMP_DIR"' EXIT
|
||||
|
||||
mkdir -p "$(dirname "$OUT_CS")"
|
||||
|
||||
{
|
||||
echo "// <auto-generated>"
|
||||
echo "// Generated by Scripts/extractMuiStrings.sh from $(basename "$MUI_FILE"). Do not edit by hand."
|
||||
echo "// </auto-generated>"
|
||||
echo
|
||||
echo "using System.Collections.Generic;"
|
||||
echo
|
||||
echo "namespace $NAMESPACE;"
|
||||
echo
|
||||
echo "public static class $CLASS_NAME"
|
||||
echo "{"
|
||||
echo " public static readonly IReadOnlyDictionary<uint, string> Strings = new Dictionary<uint, string>"
|
||||
echo " {"
|
||||
} > "$OUT_CS"
|
||||
|
||||
# Each RT_STRING block (resource name N) holds ids (N-1)*16 .. (N-1)*16+15,
|
||||
# each stored as a WORD char-count followed by that many UTF-16LE chars (no
|
||||
# null terminator); a zero count marks an unused slot.
|
||||
while read -r name lang; do
|
||||
raw="$TMP_DIR/$name.bin"
|
||||
wrestool --raw -x --type=6 --name="$name" --language="$lang" -o "$raw" "$MUI_FILE"
|
||||
size=$(stat -c%s "$raw")
|
||||
base_id=$(( (name - 1) * 16 ))
|
||||
offset=0
|
||||
|
||||
for ((i = 0; i < 16; i++)); do
|
||||
(( offset + 2 > size )) && break
|
||||
|
||||
length=$(od -An -tu2 --endian=little -j "$offset" -N 2 -- "$raw" | tr -d ' ')
|
||||
offset=$(( offset + 2 ))
|
||||
bytelen=$(( length * 2 ))
|
||||
|
||||
if (( length > 0 )); then
|
||||
id=$(( base_id + i ))
|
||||
escaped=$(
|
||||
dd if="$raw" iflag=skip_bytes,count_bytes skip="$offset" count="$bytelen" 2>/dev/null \
|
||||
| iconv -f UTF-16LE -t UTF-8 \
|
||||
| awk 'BEGIN { RS = "\0" }
|
||||
{ gsub(/\\/, "\\\\"); gsub(/"/, "\\\""); gsub(/\r/, "\\r"); gsub(/\n/, "\\n"); gsub(/\t/, "\\t"); printf "%s", $0 }'
|
||||
)
|
||||
printf ' [%d] = "%s",\n' "$id" "$escaped" >> "$OUT_CS"
|
||||
fi
|
||||
|
||||
offset=$(( offset + bytelen ))
|
||||
done
|
||||
done < <(
|
||||
wrestool -l --type=6 "$MUI_FILE" \
|
||||
| grep -oE -- '--name=[0-9]+ --language=[0-9]+' \
|
||||
| sed -E 's/--name=([0-9]+) --language=([0-9]+)/\1 \2/' \
|
||||
| sort -n -k1,1
|
||||
)
|
||||
|
||||
{
|
||||
echo " };"
|
||||
echo "}"
|
||||
} >> "$OUT_CS"
|
||||
|
||||
echo "Wrote $(grep -c '] = "' "$OUT_CS") strings to $OUT_CS"
|
||||
+3
-3
@@ -82,7 +82,7 @@ document.
|
||||
**Do not make assumptions**. If you have a question about something,
|
||||
follow this procedure:
|
||||
|
||||
1. **Log your question.** Use `LOG.md` in the repo root to log any questions
|
||||
1. **Log your question.** Use log files to document any questions
|
||||
or confusions. Follow the instructions in *Logging*.
|
||||
2. **Ask the human.** They've been working on Zune longer than you have. They may
|
||||
have an immediate answer for you, or at least can narrow down your search.
|
||||
@@ -92,8 +92,8 @@ http://github.com/ZuneDev/Wiki
|
||||
4. **Move on.** If you still haven't gotten a concrete, verifiable answer,
|
||||
you may make whatever assumption is both likely and has the fewest predicates,
|
||||
**if and only if you document this assumption in the code**, using `// TODO`
|
||||
comments. You are also required to log this assumption and its corresponding
|
||||
rationale in `LOG.md`.
|
||||
comments. You are also required to log this assumption and the rationale behind
|
||||
it.
|
||||
|
||||
### STAGE 1: Decompilation
|
||||
|
||||
|
||||
@@ -20,11 +20,11 @@ namespace ZuneUI
|
||||
|
||||
public List<string> Fonts
|
||||
{
|
||||
get => this._fonts;
|
||||
get => _fonts;
|
||||
set
|
||||
{
|
||||
this._fonts = value;
|
||||
this.QueueFontLoading();
|
||||
_fonts = value;
|
||||
QueueFontLoading();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,31 +32,37 @@ namespace ZuneUI
|
||||
{
|
||||
set
|
||||
{
|
||||
this._resourceDll = value;
|
||||
this.QueueFontLoading();
|
||||
_resourceDll = value;
|
||||
QueueFontLoading();
|
||||
}
|
||||
}
|
||||
|
||||
private void QueueFontLoading()
|
||||
{
|
||||
if (this._loaded)
|
||||
if (_loaded)
|
||||
throw new InvalidOperationException("This is a one trick pony, sorry.");
|
||||
if (this._loadQueued)
|
||||
if (_loadQueued)
|
||||
return;
|
||||
Application.DeferredInvoke(new DeferredInvokeHandler(this.LoadFonts), null);
|
||||
this._loadQueued = true;
|
||||
|
||||
Application.DeferredInvoke(LoadFonts, null);
|
||||
|
||||
_loadQueued = true;
|
||||
}
|
||||
|
||||
private void LoadFonts(object args)
|
||||
{
|
||||
this._loadQueued = false;
|
||||
if (this._fonts == null || this._fonts.Count == 0)
|
||||
_loadQueued = false;
|
||||
if (_fonts == null || _fonts.Count == 0)
|
||||
return;
|
||||
if (this._resourceDll == null)
|
||||
if (_resourceDll == null)
|
||||
throw new InvalidOperationException("Must specify a Resource to retrieve the fonts from.");
|
||||
foreach (string font in this._fonts)
|
||||
MemoryFonts.TryLoadFromResource(this._resourceDll, font);
|
||||
this._loaded = true;
|
||||
|
||||
#if WINDOWS
|
||||
foreach (var font in _fonts)
|
||||
MemoryFonts.TryLoadFromResource(_resourceDll, font);
|
||||
#endif
|
||||
|
||||
_loaded = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,188 @@ Append-only. Do not edit previous entries.
|
||||
|
||||
---
|
||||
|
||||
## 2026-07-27 — Relocated: the question-log entries below
|
||||
|
||||
**Housekeeping note:** the next two entries were originally logged in a standalone
|
||||
`LOG.md` (briefly `logs/LOG.md`), per *Dealing with unknowns and uncertainty*'s old
|
||||
wording, which named one central file for all questions. Per feedback, questions should
|
||||
be categorized the same as every other log entry — filed under the type/namespace they
|
||||
concern, same as research notes — so they're relocated here verbatim, with only their
|
||||
now-redundant `logs/MicrosoftZuneLibrary/ZuneLibrary.md` cross-references (which were
|
||||
pointing at this same file from outside it) reworded to point within this file instead.
|
||||
`CLAUDE.md`'s *Dealing with unknowns* section has been generalized accordingly (no longer
|
||||
names one specific log file).
|
||||
|
||||
---
|
||||
|
||||
## 2026-07-27 — RESOLVED: the question below
|
||||
|
||||
Answered by inspecting the on-disk resource DLLs directly with `wrestool` (icoutils)
|
||||
instead of chasing the native function further in Ghidra: the strings live in
|
||||
`windows/shared/zune-x64/Zune/en-US/ZuneResources.dll.mui`, a real Win32 MUI satellite
|
||||
resource module with 302 `RT_STRING` blocks in the standard `STRINGTABLE` binary format.
|
||||
Full writeup: the "Resolved" entry below. `ZuneDBApi.dll`'s own `GetLocResourceInstance()`
|
||||
native internals (exactly which `LoadLibraryEx` call loads it) remain unconfirmed but are
|
||||
no longer the open question — that was module identity and string format, both now known.
|
||||
|
||||
---
|
||||
|
||||
## 2026-07-27 — `ZuneLibrary.LoadStringFromResource`: what does `GetLocResourceInstance()` return?
|
||||
|
||||
**Context:** researching `MicrosoftZuneLibrary.ZuneLibrary.LoadStringFromResource` (full
|
||||
research notes: the "Researched" entry below). The managed method is fully understood —
|
||||
it's a thin wrapper around Win32 `LoadStringW` — but it depends on a native-only helper,
|
||||
`ZuneLibraryExports.GetLocResourceInstance()`, which returns the `HINSTANCE` of whatever
|
||||
module holds the localized string table.
|
||||
|
||||
**Question:** which physical module does `GetLocResourceInstance()` actually resolve to at
|
||||
runtime — `ZuneDBApi.dll` itself, or a separate satellite resource DLL (e.g.
|
||||
`ZuneResources.dll` or `ZuneMarketplaceResources.dll`, both present in
|
||||
`windows/shared/zune-x64/Zune/` alongside `ZuneDBApi.dll`)? `ZuneDBApi.dll`'s own `.rsrc`
|
||||
section is only ~2 KB (too small for a real string table), which points toward a separate
|
||||
module, but this is circumstantial — the native function itself (VA `0x1800148c2` in
|
||||
`ZuneDBApi.dll`, per Ghidra) isn't disassembling to recognizable code yet (no function
|
||||
defined there, raw bytes look like data), so it hasn't been confirmed by disassembly.
|
||||
|
||||
**Status:** unresolved. Not blocking — `ZuneLibrary.LoadStringFromResource` stays a stub
|
||||
(`ZuneUI` code doesn't yet depend on real localized strings), so no `// TODO` was needed in
|
||||
code for this pass. Logged here per *Dealing with unknowns and uncertainty* in case the human
|
||||
already knows which module holds Zune's loc resource strings, which would save a Ghidra
|
||||
deep-dive.
|
||||
|
||||
---
|
||||
|
||||
## 2026-07-27 — Resolved: where `LoadStringFromResource`'s strings actually live
|
||||
|
||||
**Follow-up to the entry below**, prompted by a direct question ("where do the strings come
|
||||
from, what format are they stored in?"). The `GetLocResourceInstance()` gap noted below is
|
||||
now settled — **not** by further Ghidra work on the native function, but by directly
|
||||
inspecting the candidate satellite resource DLLs on disk with `wrestool` (icoutils).
|
||||
|
||||
**Source module confirmed:** `windows/shared/zune-x64/Zune/en-US/ZuneResources.dll.mui`.
|
||||
`wrestool -l` on it shows **302 `RT_STRING` (`--type=6`) resource blocks**, plus a
|
||||
single `RT_MUI` (`--type='MUI'`) resource — this is a genuine Win32 MUI (Multilingual User
|
||||
Interface) satellite resource-only module, the standard mechanism where a language-neutral
|
||||
binary (here `ZuneResources.dll` itself, sitting directly in `Zune/`) has its actual
|
||||
resources split out into a same-named `.mui` file under a BCP-47 locale subfolder
|
||||
(`en-US\`). This lines up exactly with the earlier finding that `ZuneDBApi.dll`'s own
|
||||
`.rsrc` is too small and that it imports `LoadLibraryW`/`GetProcAddress` — it's loading
|
||||
*this* file (almost certainly via `LoadLibraryExW` with `LOAD_LIBRARY_AS_DATAFILE`, the
|
||||
standard way to open a MUI purely for resource access, though the exact call wasn't
|
||||
re-confirmed in Ghidra this pass). `ZuneResources.dll` (the LN/language-neutral binary
|
||||
next to it) itself carries no `RT_STRING` resources — its own `wrestool -l` shows only
|
||||
`BINARY`/icon/version/`MUI` entries, no `--type=6` — confirming the neutral binary is a
|
||||
stub and the `.mui` is where the real string table lives, exactly as MUI architecture
|
||||
predicts. `ZuneMarketplaceResources.dll` was a red herring — it holds only `RCDATA`
|
||||
(compiled `.uix` markup), no strings.
|
||||
|
||||
**Format confirmed byte-for-byte** by extracting block 1 (`wrestool --raw -x --type=6
|
||||
--name=1 --language=1033`) and reading the raw bytes: this is the plain, well-documented
|
||||
Win32 `STRINGTABLE` binary layout —
|
||||
- Each numbered resource block (`--name=N`) holds up to 16 consecutive string IDs:
|
||||
`blockNumber = (stringId >> 4) + 1`, `indexInBlock = stringId & 0xF`.
|
||||
- Within a block: back-to-back records, each `WORD charCount` immediately followed by
|
||||
`charCount` **UTF-16LE code units with no null terminator** (an unused slot is just a
|
||||
`WORD` value of `0`).
|
||||
- Verified two real entries against the extracted bytes: string id 1 (block 1, index 1) is
|
||||
`0018` (=24) followed by 24 UTF-16LE chars spelling `"Starts the Zune software"`; string
|
||||
id 4 is `002C` (=44) followed by 44 chars spelling
|
||||
`"http://go.microsoft.com/fwlink/?LinkID=97902"`. Both char counts match their strings
|
||||
exactly — format is not in doubt.
|
||||
|
||||
This is exactly what `LoadStringW(hInstance, dwResourceNumber, (LPWSTR)&buf, 0)` (see entry
|
||||
below) is built to read: the OS/CRT loader locates the `(dwResourceNumber >> 4) + 1`-th
|
||||
`RT_STRING` block inside whatever module `hInstance` refers to, walks the length-prefixed
|
||||
records to the `dwResourceNumber & 0xF`-th slot, and — because `cchBufferMax` is `0` — hands
|
||||
back a pointer straight at that slot's inline character data plus its `WORD` length, which
|
||||
is exactly what `ZuneLibrary.LoadStringFromResource` then wraps in
|
||||
`Marshal.PtrToStringUni`.
|
||||
|
||||
**Still open, lower priority:** the exact `LoadLibraryEx`/flags call inside
|
||||
`GetLocResourceInstance()` and how it locates the `en-US` subfolder at runtime (fixed
|
||||
relative path vs. `GetThreadPreferredUILanguages`-style lookup) — not re-investigated this
|
||||
pass since the module/format question (the thing actually asked) is now fully answered.
|
||||
Marking the `LOG.md` question resolved.
|
||||
|
||||
---
|
||||
|
||||
## 2026-07-27 — Researched `ZuneLibrary.LoadStringFromResource`
|
||||
|
||||
**Trigger:** explicit request to research how `MicrosoftZuneLibrary.ZuneLibrary.LoadStringFromResource`
|
||||
works. Current stub (`ZuneLibrary.cs:33`): `public static string LoadStringFromResource(uint dwResourceNumber) => "todo";`.
|
||||
|
||||
**Source:** `mcp__ilspy__decompile_method` on `MicrosoftZuneLibrary.ZuneLibrary.LoadStringFromResource`
|
||||
in the real `windows/shared/zune-x64/Zune/ZuneDBApi.dll` (this method was apparently never
|
||||
actually decompiled/inspected during the original bulk pass below — it was stubbed sight-unseen
|
||||
along with everything else in the file).
|
||||
|
||||
**Finding: `ZuneDBApi.dll` is mixed-mode (C++/CLI), and this specific method is not pure IL.**
|
||||
Its decompiled body:
|
||||
|
||||
```csharp
|
||||
public unsafe static string LoadStringFromResource(uint dwResourceNumber)
|
||||
{
|
||||
HINSTANCE__* ptr = global::<Module>.ZuneLibraryExports.GetLocResourceInstance();
|
||||
if (ptr == null) return null;
|
||||
object result = null;
|
||||
IntPtr intPtr = default(IntPtr);
|
||||
int len = global::<Module>.LoadStringW(ptr, dwResourceNumber, (ushort*)(&intPtr), 0);
|
||||
if (intPtr != IntPtr.Zero)
|
||||
result = Marshal.PtrToStringUni(intPtr, len);
|
||||
return (string)result;
|
||||
}
|
||||
```
|
||||
|
||||
Algorithm:
|
||||
1. Calls the module-internal native export `ZuneLibraryExports.GetLocResourceInstance()` to get
|
||||
an `HINSTANCE` for a localized-resource module. If that's null (resource module not loaded),
|
||||
returns `null`.
|
||||
2. Calls the real Win32 `User32`/`Kernel32` `LoadStringW(hInstance, dwResourceNumber, (LPWSTR)&buf, 0)`
|
||||
— the classic **zero-`cchBufferMax` idiom**: when the buffer-size argument is `0`,
|
||||
`LoadStringW` treats the out-param as `LPWSTR*` and writes back a pointer *directly into the
|
||||
module's own resource section* (no allocation, no copy) instead of copying into a
|
||||
caller-supplied buffer. The return value is the string's character length (no null terminator).
|
||||
3. If that pointer is non-null, `Marshal.PtrToStringUni(ptr, len)` copies exactly `len` UTF-16
|
||||
characters starting at that native pointer into a new managed `string`.
|
||||
4. Returns the result, or `null` on any failure path (module not loaded, or resource ID not found —
|
||||
`LoadStringW` returns `len == 0` and a null/garbage buffer pointer in that case too).
|
||||
|
||||
So this is a genuine (small) resource-string lookup, not something with real "business logic" of
|
||||
its own beyond marshaling.
|
||||
|
||||
**Unresolved — where `GetLocResourceInstance()`'s `HINSTANCE` actually points:** followed the
|
||||
export into Ghidra (`ZuneDBApi.dll` already open in the shared instance). The exported thunk
|
||||
(`ZuneLibraryExports.GetLocResourceInstance @ 180111b98`) is a single tail-jump,
|
||||
`jmp qword ptr [0x180001518]`, through a local function-pointer slot — i.e. the real
|
||||
implementation is a separate internal function, not an imported one (confirmed absent from
|
||||
`mcp__ghidra__list_imports`). The slot's stored value resolves to VA `0x1800148c2`, which falls
|
||||
inside `.text` (`180001000`–`1801155ff` per `list_segments`), but Ghidra has no function defined
|
||||
there, and neither `decompile_function` nor `disassemble_bytes` recognize valid instructions
|
||||
starting exactly at that address — the raw bytes read there (`inspect_memory_content`) look like a
|
||||
data table, not code. Did not push further (would need manual function creation/re-analysis in
|
||||
Ghidra, out of scope for a research-and-log pass) — logging as a genuine gap per
|
||||
*Dealing with unknowns* rather than guessing at the internals.
|
||||
|
||||
**Weak supporting evidence for "separate satellite resource DLL" (not confirmed):**
|
||||
`ZuneDBApi.dll`'s own `.rsrc` section (`180136000`–`1801367ff`, ~2 KB) is far too small to hold a
|
||||
real `RT_STRING` table with many localized entries, and the module imports `LoadLibraryW`/
|
||||
`GetProcAddress` (per `list_imports`). This is consistent with `GetLocResourceInstance()` lazily
|
||||
loading and caching an `HINSTANCE` for a companion resource-only module — e.g. one of
|
||||
`ZuneResources.dll` / `ZuneMarketplaceResources.dll`, both present alongside `ZuneDBApi.dll` in
|
||||
`windows/shared/zune-x64/Zune/` — rather than `ZuneDBApi.dll`'s own module handle. This is
|
||||
circumstantial, not verified by disassembly.
|
||||
|
||||
**Searched `github.com/ZuneDev/Wiki`** (via `gh search code` scoped to that repo) for
|
||||
`LoadStringFromResource` and `GetLocResourceInstance` — no hits, nothing documented there yet.
|
||||
|
||||
**Stub status flagged, not changed this pass:** the current `=> "todo"` body doesn't match this
|
||||
file's own header convention (every other no-op stub returns the same default the original
|
||||
would produce when the native dependency is unavailable — here that's `null`, matching this
|
||||
method's own `ptr == null` early-return path). Left as-is since this pass was research/logging
|
||||
only per the request; worth fixing to `=> null` in a follow-up implementation pass.
|
||||
|
||||
---
|
||||
|
||||
## 2026-07-21 — Bulk reconstruction of the MicrosoftZuneLibrary namespace
|
||||
|
||||
**Trigger:** continuation of "decompile the missing types in ZuneShell.csproj" after
|
||||
|
||||
Reference in New Issue
Block a user