Browse Source

Cap decompiler output length and bridge dead Display options

A member that decompiled to a very large amount of text could hang or
exhaust memory on the UI thread with no escape. Cap display output at five
million characters and, on overflow, show a message offering to display
anyway (at an extended limit) or save to disk.

Also bridge three Display options that were persisted but never reached the
decompiler -- brace folding, debug-symbol info, and the indentation string
-- so toggling them once again affects the generated source.

Assisted-by: Claude:claude-opus-4-8:Claude Code
pull/3755/head
Siegfried Pammer 1 month ago
parent
commit
8a95de41f2
  1. 84
      ILSpy.Tests/TextView/DisplaySettingsBridgeTests.cs
  2. 68
      ILSpy.Tests/TextView/OutputLengthLimitTests.cs
  3. 17
      ILSpy/TextView/AvaloniaEditTextOutput.cs
  4. 101
      ILSpy/TextView/DecompilerTabPageModel.cs
  5. 31
      ILSpy/TextView/OutputLengthExceededException.cs

84
ILSpy.Tests/TextView/DisplaySettingsBridgeTests.cs

@ -0,0 +1,84 @@ @@ -0,0 +1,84 @@
// Copyright (c) 2026 AlphaSierraPapa for the SharpDevelop Team
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
// software and associated documentation files (the "Software"), to deal in the Software
// without restriction, including without limitation the rights to use, copy, modify, merge,
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
// to whom the Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or
// substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using AwesomeAssertions;
using ICSharpCode.Decompiler;
using ILSpy.Options;
using ILSpy.TextView;
using NUnit.Framework;
namespace ICSharpCode.ILSpy.Tests.TextView;
/// <summary>
/// The Display options that affect generated source (brace folding, debug info, indentation, and the
/// fold-expansion flags) must be bridged into the decompiler settings used for a decompile. WPF wired
/// these in DecompilationOptions; the Avalonia port previously only bridged the two expand flags, so
/// FoldBraces / ShowDebugInfo / indentation were silently dead.
/// </summary>
[TestFixture]
public class DisplaySettingsBridgeTests
{
[Test]
public void Brace_Folding_And_Debug_Info_Are_Bridged()
{
var display = new DisplaySettings { FoldBraces = true, ShowDebugInfo = true };
var settings = new DecompilerSettings { FoldBraces = false, ShowDebugInfo = false };
DecompilerTabPageModel.ApplyDisplaySettings(settings, display);
settings.FoldBraces.Should().BeTrue();
settings.ShowDebugInfo.Should().BeTrue();
}
[Test]
public void Indentation_Uses_Spaces_When_Tabs_Are_Off()
{
var display = new DisplaySettings { IndentationUseTabs = false, IndentationSize = 2 };
var settings = new DecompilerSettings();
DecompilerTabPageModel.ApplyDisplaySettings(settings, display);
settings.CSharpFormattingOptions.IndentationString.Should().Be(" ", "two spaces");
}
[Test]
public void Indentation_Uses_Tabs_When_Enabled()
{
var display = new DisplaySettings { IndentationUseTabs = true, IndentationSize = 4, IndentationTabSize = 4 };
var settings = new DecompilerSettings();
DecompilerTabPageModel.ApplyDisplaySettings(settings, display);
settings.CSharpFormattingOptions.IndentationString.Should().Be("\t", "one tab");
}
[Test]
public void Expand_Flags_Still_Bridged()
{
var display = new DisplaySettings { ExpandUsingDeclarations = true, ExpandMemberDefinitions = true };
var settings = new DecompilerSettings { ExpandUsingDeclarations = false, ExpandMemberDefinitions = false };
DecompilerTabPageModel.ApplyDisplaySettings(settings, display);
settings.ExpandUsingDeclarations.Should().BeTrue();
settings.ExpandMemberDefinitions.Should().BeTrue();
}
}

68
ILSpy.Tests/TextView/OutputLengthLimitTests.cs

@ -0,0 +1,68 @@ @@ -0,0 +1,68 @@
// Copyright (c) 2026 AlphaSierraPapa for the SharpDevelop Team
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
// software and associated documentation files (the "Software"), to deal in the Software
// without restriction, including without limitation the rights to use, copy, modify, merge,
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
// to whom the Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or
// substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using System;
using AwesomeAssertions;
using ILSpy.TextView;
using NUnit.Framework;
namespace ICSharpCode.ILSpy.Tests.TextView;
/// <summary>
/// The decompiler text output stops once it exceeds its LengthLimit, so a runaway decompile (a huge
/// type/namespace) can't hang or OOM the UI thread. (Regression: the Avalonia port had no limit.)
/// </summary>
[TestFixture]
public class OutputLengthLimitTests
{
[Test]
public void Write_Throws_Once_The_Length_Limit_Is_Exceeded()
{
var output = new AvaloniaEditTextOutput { LengthLimit = 10 };
var act = () => {
for (int i = 0; i < 100; i++)
output.Write("0123456789");
};
act.Should().Throw<OutputLengthExceededException>();
}
[Test]
public void Write_Does_Not_Throw_Below_The_Limit()
{
var output = new AvaloniaEditTextOutput { LengthLimit = 1000 };
var act = () => {
output.Write("short output");
output.WriteLine();
};
act.Should().NotThrow();
}
[Test]
public void Default_Limit_Is_Unlimited()
{
var output = new AvaloniaEditTextOutput();
output.LengthLimit.Should().Be(int.MaxValue);
}
}

17
ILSpy/TextView/AvaloniaEditTextOutput.cs

@ -45,6 +45,14 @@ namespace ILSpy.TextView @@ -45,6 +45,14 @@ namespace ILSpy.TextView
public sealed class AvaloniaEditTextOutput : ISmartTextOutput
{
readonly StringBuilder builder = new();
/// <summary>
/// When the accumulated text exceeds this many characters, the next write throws
/// <see cref="OutputLengthExceededException"/> so a runaway decompile is stopped before it
/// hangs/OOMs the UI thread. Defaults to unlimited; the decompiler tab sets a real limit.
/// </summary>
public int LengthLimit { get; set; } = int.MaxValue;
readonly Stack<(int Offset, HighlightingColor Color)> openSpans = new();
readonly Stack<(NewFolding Folding, int StartLine)> openFoldings = new();
readonly List<NewFolding> foldings = new();
@ -127,12 +135,14 @@ namespace ILSpy.TextView @@ -127,12 +135,14 @@ namespace ILSpy.TextView
{
WriteIndentIfNeeded();
builder.Append(ch);
CheckLength();
}
public void Write(string text)
{
WriteIndentIfNeeded();
builder.Append(text);
CheckLength();
}
public void WriteLine()
@ -140,6 +150,13 @@ namespace ILSpy.TextView @@ -140,6 +150,13 @@ namespace ILSpy.TextView
builder.Append('\n');
lineNumber++;
needsIndent = true;
CheckLength();
}
void CheckLength()
{
if (builder.Length > LengthLimit)
throw new OutputLengthExceededException();
}
public void WriteReference(OpCodeInfo opCode, bool omitSuffix = false)

101
ILSpy/TextView/DecompilerTabPageModel.cs

@ -37,6 +37,7 @@ using CommunityToolkit.Mvvm.Input; @@ -37,6 +37,7 @@ using CommunityToolkit.Mvvm.Input;
using ICSharpCode.Decompiler;
using ILSpy.Languages;
using ILSpy.Options;
using ILSpy.TreeNodes;
using ILSpy.ViewModels;
@ -299,6 +300,15 @@ namespace ILSpy.TextView @@ -299,6 +300,15 @@ namespace ILSpy.TextView
int pendingStepLimit = int.MaxValue;
bool pendingIsDebug;
/// <summary>
/// Output-length safety limits (characters): a decompile that produces more than the active
/// limit is stopped and replaced with a "too much code" message rather than hanging/OOMing the
/// UI. The user can re-run at the extended limit or save to disk. Mirrors the previous version.
/// </summary>
public const int DefaultOutputLengthLimit = 5_000_000;
public const int ExtendedOutputLengthLimit = 75_000_000;
int pendingOutputLengthLimit = DefaultOutputLengthLimit;
/// <summary>
/// Force a fresh decompile of the same nodes with the IL-transform pipeline halted
/// after <paramref name="stepLimit"/> steps. Used by the Debug Steps pane to render
@ -313,6 +323,53 @@ namespace ILSpy.TextView @@ -313,6 +323,53 @@ namespace ILSpy.TextView
StartDecompile();
}
/// <summary>Re-runs the current decompile with a larger output-length limit (the "Display code
/// anyway" button on the too-much-code message).</summary>
public void RestartDecompileWithOutputLimit(int outputLengthLimit)
{
pendingOutputLengthLimit = outputLengthLimit;
StartDecompile();
}
// Built (off the UI thread) when a decompile blows past its output limit: a short message plus
// a "Display code anyway" button (retry at the extended limit, only offered for the normal
// limit) and a "Save code" button. The button click handlers run back on the UI thread.
AvaloniaEditTextOutput BuildOutputLengthExceededMessage(int outputLengthLimit)
{
var output = new AvaloniaEditTextOutput();
bool wasNormalLimit = outputLengthLimit == DefaultOutputLengthLimit;
output.WriteLine(wasNormalLimit
? "You have selected too much code for it to be displayed automatically."
: "You have selected too much code; it cannot be displayed here.");
output.WriteLine();
if (wasNormalLimit)
{
output.AddButton(Images.Images.ViewCode, ICSharpCode.ILSpy.Properties.Resources.DisplayCode,
(_, _) => RestartDecompileWithOutputLimit(ExtendedOutputLengthLimit));
output.WriteLine();
}
output.AddButton(Images.Images.Save, ICSharpCode.ILSpy.Properties.Resources.SaveCode,
(_, _) => SaveCurrentNode());
output.WriteLine();
return output;
}
void SaveCurrentNode()
{
if (CurrentNode is not { } node)
return;
try
{
var languageService = AppEnv.AppComposition.Current.GetExport<Languages.LanguageService>();
var dockWorkspace = AppEnv.AppComposition.Current.GetExport<Docking.DockWorkspace>();
ILSpy.Commands.SaveCodeHelper.SaveNodeAsync(node, languageService, dockWorkspace).HandleExceptions();
}
catch
{
// Best-effort: no save path available (minimal host).
}
}
// Fire-and-forget wrapper around DecompileAsync that observes the resulting Task.
// Without this, exceptions raised by the dispatched property setters (e.g. the
// PropertyChanged subscribers in DecompilerTextView) become UnobservedTaskException
@ -394,11 +451,13 @@ namespace ILSpy.TextView @@ -394,11 +451,13 @@ namespace ILSpy.TextView
var isDebug = pendingIsDebug;
pendingStepLimit = int.MaxValue;
pendingIsDebug = false;
var outputLengthLimit = pendingOutputLengthLimit;
pendingOutputLengthLimit = DefaultOutputLengthLimit;
AvaloniaEditTextOutput output;
using (ILSpy.AppEnv.AppLog.Phase($"DecompileAsync #{callNumber}: Task.Run decompile body ({nodes.Count} node(s), language={language.Name})"))
{
(output, _) = await Task.Run(() => {
var output = new AvaloniaEditTextOutput();
var output = new AvaloniaEditTextOutput { LengthLimit = outputLengthLimit };
var options = decompilerSettings != null
? new DecompilationOptions(decompilerSettings) {
CancellationToken = cts.Token,
@ -425,6 +484,12 @@ namespace ILSpy.TextView @@ -425,6 +484,12 @@ namespace ILSpy.TextView
{
// expected on cancel — just return whatever we got
}
catch (OutputLengthExceededException)
{
// The decompile produced more text than the limit allows -- replace it with
// a message offering to display anyway (extended limit) or save to disk.
output = BuildOutputLengthExceededMessage(outputLengthLimit);
}
catch (Exception ex)
{
output.WriteLine();
@ -597,13 +662,7 @@ namespace ILSpy.TextView @@ -597,13 +662,7 @@ namespace ILSpy.TextView
{
var settingsService = AppEnv.AppComposition.Current.GetExport<SettingsService>();
var settings = settingsService.DecompilerSettings.Clone();
// Bridge the Display-options fold-expansion flags into the decompiler settings:
// TextTokenWriter reads settings.ExpandUsingDeclarations / ExpandMemberDefinitions
// to set each fold's DefaultClosed. Without this the "Expand using declarations /
// member definitions after decompilation" options would have no effect.
var display = settingsService.DisplaySettings;
settings.ExpandUsingDeclarations = display.ExpandUsingDeclarations;
settings.ExpandMemberDefinitions = display.ExpandMemberDefinitions;
ApplyDisplaySettings(settings, settingsService.DisplaySettings);
try
{
var version = AppEnv.AppComposition.Current.GetExport<Languages.LanguageService>().CurrentVersion;
@ -624,5 +683,31 @@ namespace ILSpy.TextView @@ -624,5 +683,31 @@ namespace ILSpy.TextView
return null;
}
}
/// <summary>
/// Bridges the Display options that affect decompiler output into <paramref name="settings"/>:
/// the fold-expansion flags (TextTokenWriter reads them to set each fold's DefaultClosed),
/// brace folding, debug-symbol info, and the indentation string. Without this these Display
/// options would have no effect on the produced source.
/// </summary>
internal static void ApplyDisplaySettings(ICSharpCode.Decompiler.DecompilerSettings settings, DisplaySettings display)
{
settings.ExpandUsingDeclarations = display.ExpandUsingDeclarations;
settings.ExpandMemberDefinitions = display.ExpandMemberDefinitions;
settings.FoldBraces = display.FoldBraces;
settings.ShowDebugInfo = display.ShowDebugInfo;
settings.CSharpFormattingOptions.IndentationString = GetIndentationString(display);
}
static string GetIndentationString(DisplaySettings display)
{
if (display.IndentationUseTabs)
{
int tabs = display.IndentationSize / display.IndentationTabSize;
int spaces = display.IndentationSize % display.IndentationTabSize;
return new string('\t', tabs) + new string(' ', spaces);
}
return new string(' ', display.IndentationSize);
}
}
}

31
ILSpy/TextView/OutputLengthExceededException.cs

@ -0,0 +1,31 @@ @@ -0,0 +1,31 @@
// Copyright (c) 2026 AlphaSierraPapa for the SharpDevelop Team
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
// software and associated documentation files (the "Software"), to deal in the Software
// without restriction, including without limitation the rights to use, copy, modify, merge,
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
// to whom the Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or
// substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using System;
namespace ILSpy.TextView
{
/// <summary>
/// Thrown by <see cref="AvaloniaEditTextOutput"/> once its accumulated text exceeds
/// <see cref="AvaloniaEditTextOutput.LengthLimit"/>, so a runaway decompile (a huge type or
/// namespace) is stopped before it can hang or exhaust memory on the UI thread.
/// </summary>
public sealed class OutputLengthExceededException : Exception
{
}
}
Loading…
Cancel
Save