Skip to content

Fix #2362: resolve Xamarin compressed references in ilspycmd - #4107

Open
christophwille wants to merge 1 commit into
masterfrom
fix/2362-xalz-references
Open

Fix #2362: resolve Xamarin compressed references in ilspycmd#4107
christophwille wants to merge 1 commit into
masterfrom
fix/2362-xalz-references

Conversation

@christophwille

Copy link
Copy Markdown
Member

Written by Claude (agent), posted on behalf of @christophwille.

Fixes #2362.

What was actually broken

#2362 stayed open with the note that ilspycmd still lacked the Xamarin compressed-DLL ("XALZ") support the UI has had since #2137. Looking at it after #4101, there are two separate things in play:

  1. Passing an XALZ file as the input already works. Fix #3154: accept single-file bundles as ilspycmd input #4101 made every CLI mode load its input through ILSpyX's FileLoaderRegistry, and XamarinCompressedFileLoader is the first loader in that registry. So ilspycmd Foo.dll with a compressed module decompresses on the fly today. Nothing pinned that with a test, so this PR adds one.

  2. References were the real gap. The stack trace in the issue is inside UniversalAssemblyResolver.Resolve: the referenced sibling DLL was still compressed. A Xamarin app folder holds every assembly compressed, and the CLI builds UniversalAssemblyResolver directly, whose private load step does new PEFile(fileName, stream). With throwOnError: false the resulting BadImageFormatException is swallowed, the reference resolves to null, and the output degrades silently. For example, an enum from the referenced assembly prints as a cast of the raw value:

    return (int)id == 1;          // reference unresolved
    return id == TargetFrameworkIdentifier.NETCoreApp;   // reference resolved

    The UI does not have this problem because LoadedAssembly.MyAssemblyResolver only asks the universal resolver for the file name and then loads that file through the registry.

Approach

Constraints from the request: keep the XALZ code in ILSpyX (no duplication), and do not add detection logic to ilspycmd (the registry order already handles it).

  • UniversalAssemblyResolver gains one protected virtual MetadataFile LoadModuleFromFile(string fileName), extracted from the existing private CreatePEFileFromFileName. Both Resolve and ResolveModule already funnel through that method, so the hook covers assemblies and netmodules. This is an additive public-API change on a non-sealed class; no existing caller changes.
  • ilspycmd adds FileLoaderAssemblyResolver : UniversalAssemblyResolver (15 lines, in InputFileLoader.cs) that overrides the hook to run the same loader loop the input file uses, and falls back to the base PE path for anything no loader claims. A corrupt XALZ or a package (bundle/zip) therefore ends up exactly where it did before: an unresolved reference.
  • The three duplicated resolver-construction blocks in IlspyCmdProgram.cs (GetDecompiler, ExtractResource, DecompileAsProject) collapse into one CreateResolver helper. Net fewer lines there.

No new CLI option, no README regeneration, -d|--dump-package untouched, no packages.lock.json changes (K4os.Compression.LZ4 reaches the test project transitively via ILSpyX, as it already does for ICSharpCode.Decompiler.Tests).

Rejected alternatives

  • Wrap UniversalAssemblyResolver in a CLI-side IAssemblyResolver built on the public FindAssemblyFile. Avoids touching the Decompiler, but means re-implementing Resolve, ResolveModule, and both async variants, plus delegating IsGacAssembly/IsSharedAssembly, because WholeProjectDecompiler and BamlAwareWholeProjectDecompiler take the resolver as the AssemblyReferenceClassifier too. Roughly three times the code of the hook, all of it duplicating logic that already exists.
  • Use ILSpyX's AssemblyList/LoadedAssembly stack in ilspycmd. Gives full parity with the UI (including bundle-sibling resolution and ParentBundle context), but drags in AssemblyListManager, a settings provider, and event plumbing, and changes error semantics for every CLI mode. Out of proportion for the problem.
  • Sniff the XALZ magic in ilspycmd and decompress there. Duplicates XamarinCompressedFileLoader, and does nothing for references, which is where the issue actually fails.
  • Inject a Func<string, MetadataFile?> into the resolver constructor instead of a virtual method. Same effect, more surface: another constructor overload on a class that already has six optional parameters.

Tests

ICSharpCode.ILSpyCmd.Tests/XamarinCompressedInputTests.cs builds a temp folder the way a Xamarin app ships: the test assembly and the ICSharpCode.Decompiler.dll it references, both LZ4-compressed behind the 12-byte XALZ header (same synthesis as the existing XamarinCompressedFileLoaderTests, no binary fixture).

All 44 ilspycmd tests pass; the resolver, error-recovery, XALZ loader, and isolated-decompilation fixtures in ICSharpCode.Decompiler.Tests pass unchanged.

Not in this PR

ICSharpCode.Decompiler.PowerShell also constructs UniversalAssemblyResolver directly and keeps the old behavior for compressed references. Same fix would apply if wanted.

🤖 Generated with Claude Code

Since the input file goes through ILSpyX's FileLoaderRegistry, an XALZ
module passed directly to ilspycmd already decompresses on the fly. The
crash in the issue is in reference resolution: a Xamarin app folder
holds every assembly compressed, and UniversalAssemblyResolver opened
the referenced sibling as a plain PE image. In the CLI that failure was
swallowed, so the reference resolved to null and the output degraded
(enum members printed as casts of raw values, and so on). The UI does
not have this problem because its resolver only asks the universal
resolver for the file name and then loads it through the loaders.

Rather than re-implementing the resolver in the CLI, the universal
resolver gains one overridable step - turning a found file into a
module - and the CLI overrides it to run the same loader loop the input
file uses, falling back to the plain PE path for anything no loader
claims.

Assisted-by: Claude:claude-fable-5-1:Claude Code
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ilspycmd: Add support for Xamarin compressed DLLs

1 participant