diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 526fcaa74..4e36b136b 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -1,12 +1,19 @@
name: ci
+# Build validation. GitHub Actions minutes are limited, so this runs automatically
+# only for PRs into `main` (the develop -> main gate). For PRs into `develop`,
+# day-to-day validation is done with scripts/local-ci.sh; a run here happens only
+# when an admin adds the `run-ci` label (re-add it to trigger each run) or
+# dispatches the workflow manually. See the `if:` on the build job below.
+#
+# This workflow does NOT publish releases — that is handled by release.yml
+# (manual dispatch + a monthly check for new commits on main).
on:
- push:
- branches: [main, develop]
- paths-ignore: ['**.md', 'docs/**', 'LICENSE*']
pull_request:
branches: [main, develop]
+ types: [opened, synchronize, reopened, labeled]
paths-ignore: ['**.md', 'docs/**', 'LICENSE*']
+ workflow_dispatch:
concurrency:
group: ci-${{ github.ref }}
@@ -14,6 +21,12 @@ concurrency:
jobs:
build:
+ # Auto for PRs into main and manual dispatch; for develop PRs only when an
+ # admin adds the `run-ci` label.
+ if: >-
+ github.event_name == 'workflow_dispatch' ||
+ github.base_ref == 'main' ||
+ (github.event.action == 'labeled' && github.event.label.name == 'run-ci')
runs-on: windows-2025
timeout-minutes: 10
env:
@@ -88,101 +101,3 @@ jobs:
ConfuserEx-GUI.zip
ConfuserEx.zip
Confuser.MSBuild.Tasks/bin/Release/*.nupkg
-
- # Dev build: on push to develop branch
- dev-release:
- needs: build
- if: github.event_name == 'push' && github.ref == 'refs/heads/develop'
- runs-on: windows-2025
- timeout-minutes: 5
- permissions:
- contents: write
- steps:
- - uses: actions/checkout@v5
- with:
- fetch-depth: 0
-
- - name: Install nbgv
- run: dotnet tool install -g nbgv
-
- - name: Compute version
- id: version
- shell: pwsh
- run: |
- $ver = nbgv get-version -v NuGetPackageVersion
- echo "VERSION=$ver" >> $env:GITHUB_OUTPUT
-
- - name: Download artifacts
- uses: actions/download-artifact@v5
- with:
- name: confuserex-packages
-
- - name: Create or update dev release
- uses: softprops/action-gh-release@v2
- with:
- tag_name: dev-latest
- name: "Dev build v${{ steps.version.outputs.VERSION }}"
- prerelease: true
- make_latest: false
- body: |
- **Development build** — for testing only, not production use.
-
- Version: `${{ steps.version.outputs.VERSION }}`
- Branch: `develop`
- Commit: ${{ github.sha }}
-
- Download the binaries below to test recent fixes and features before they are included in a stable release.
- files: |
- ConfuserEx-CLI.zip
- ConfuserEx-GUI.zip
- ConfuserEx.zip
- *.nupkg
-
- # Release: only on PR merge to main
- release:
- needs: build
- if: github.event_name == 'push' && github.ref == 'refs/heads/main'
- runs-on: windows-2025
- timeout-minutes: 5
- permissions:
- contents: write
- steps:
- - uses: actions/checkout@v5
- with:
- fetch-depth: 0
-
- - name: Install nbgv
- run: dotnet tool install -g nbgv
-
- - name: Compute version
- id: version
- shell: pwsh
- run: |
- $ver = nbgv get-version -v NuGetPackageVersion
- echo "VERSION=$ver" >> $env:GITHUB_OUTPUT
-
- - name: Configure git identity
- run: |
- git config user.name "github-actions[bot]"
- git config user.email "github-actions[bot]@users.noreply.github.com"
-
- - name: Create release tag
- run: |
- git tag -a "v${{ steps.version.outputs.VERSION }}" -m "Release ${{ steps.version.outputs.VERSION }}"
- git push origin "v${{ steps.version.outputs.VERSION }}"
-
- - name: Download artifacts
- uses: actions/download-artifact@v5
- with:
- name: confuserex-packages
-
- - name: Create GitHub Release
- uses: softprops/action-gh-release@v2
- with:
- tag_name: v${{ steps.version.outputs.VERSION }}
- name: v${{ steps.version.outputs.VERSION }}
- files: |
- ConfuserEx-CLI.zip
- ConfuserEx-GUI.zip
- ConfuserEx.zip
- *.nupkg
diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml
deleted file mode 100644
index 112a45e48..000000000
--- a/.github/workflows/format.yml
+++ /dev/null
@@ -1,35 +0,0 @@
-name: format
-
-on:
- pull_request:
- branches: [master, pre-release]
- paths: ['**.cs', '**.vb', '.editorconfig']
-
-jobs:
- check-format:
- runs-on: windows-2025
- timeout-minutes: 5
- permissions:
- contents: read
- steps:
- - uses: actions/checkout@v5
-
- - name: Check formatting
- run: dotnet format Confuser2.sln --verify-no-changes --verbosity diagnostic
- continue-on-error: true
- id: format-check
-
- - name: Format diff
- if: steps.format-check.outcome == 'failure'
- shell: pwsh
- run: |
- dotnet format Confuser2.sln
- git diff --stat
- $diff = git diff --name-only
- if ($diff) {
- Write-Host "::warning::The following files need formatting:" -ForegroundColor Yellow
- $diff | ForEach-Object { Write-Host " $_" }
- Write-Host ""
- Write-Host "Run 'dotnet format Confuser2.sln' locally and commit the changes."
- exit 1
- }
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index 60a2b6cc4..8204d7727 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -1,11 +1,17 @@
name: lint
+# GitHub Actions minutes are limited. Linting is done locally with
+# scripts/local-ci.sh (dotnet format whitespace/style/analyzers). This workflow
+# runs automatically only for PRs into `main`; for PRs into `develop` it runs only
+# when an admin adds the `run-ci` label (re-add to trigger each run) or dispatches
+# it manually. The previous any-branch `push` trigger was removed to stop a lint
+# run firing on every push. See the `if:` on the lint job below.
on:
- push:
- paths-ignore: ['**.md', 'docs/**', 'LICENSE*']
pull_request:
branches: [main, develop]
+ types: [opened, synchronize, reopened, labeled]
paths-ignore: ['**.md', 'docs/**', 'LICENSE*']
+ workflow_dispatch:
concurrency:
group: lint-${{ github.ref }}
@@ -13,6 +19,12 @@ concurrency:
jobs:
lint:
+ # Auto for PRs into main and manual dispatch; for develop PRs only when an
+ # admin adds the `run-ci` label.
+ if: >-
+ github.event_name == 'workflow_dispatch' ||
+ github.base_ref == 'main' ||
+ (github.event.action == 'labeled' && github.event.label.name == 'run-ci')
runs-on: windows-2025
timeout-minutes: 5
permissions:
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 000000000..abdef97f8
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,161 @@
+name: release
+
+# GitHub Actions minutes are limited, so releases are NOT cut automatically on
+# every push to main. Instead:
+# - Run this workflow manually (Actions tab -> release -> Run workflow) to build
+# and publish a release from main on demand.
+# - On the 1st of each month it checks main for commits since the last release
+# tag and only spends the (expensive) Windows build when there are new changes.
+#
+# The check job runs on a cheap Ubuntu runner, so a monthly run with nothing new
+# costs only a few seconds and publishes nothing.
+on:
+ workflow_dispatch:
+ inputs:
+ force:
+ description: "Release even if there are no new commits since the last release tag"
+ type: boolean
+ default: false
+ schedule:
+ - cron: "0 6 1 * *" # 06:00 UTC on the 1st of every month
+
+concurrency:
+ group: release
+ cancel-in-progress: false
+
+permissions:
+ contents: write
+
+jobs:
+ check:
+ name: Check main for new commits
+ runs-on: ubuntu-latest
+ outputs:
+ should_release: ${{ steps.decide.outputs.should_release }}
+ steps:
+ - uses: actions/checkout@v5
+ with:
+ ref: main
+ fetch-depth: 0
+ fetch-tags: true
+
+ - name: Decide whether to release
+ id: decide
+ shell: bash
+ run: |
+ last_tag=$(git tag --list 'v*' --sort=-v:refname | head -n1)
+ if [ -z "$last_tag" ]; then
+ echo "No release tag found — treating as first release."
+ echo "should_release=true" >> "$GITHUB_OUTPUT"
+ exit 0
+ fi
+
+ behind=$(git rev-list "$last_tag"..HEAD --count)
+ echo "Last release: $last_tag — $behind new commit(s) on main since then."
+
+ if [ "${{ github.event_name }}" = "workflow_dispatch" ] && [ "${{ inputs.force }}" = "true" ]; then
+ echo "Manual dispatch with force=true — releasing."
+ echo "should_release=true" >> "$GITHUB_OUTPUT"
+ elif [ "$behind" -gt 0 ]; then
+ echo "New commits present — releasing."
+ echo "should_release=true" >> "$GITHUB_OUTPUT"
+ else
+ echo "No new commits since $last_tag — skipping. (Use force=true to release anyway.)"
+ echo "should_release=false" >> "$GITHUB_OUTPUT"
+ fi
+
+ release:
+ name: Build and publish release
+ needs: check
+ if: needs.check.outputs.should_release == 'true'
+ runs-on: windows-2025
+ timeout-minutes: 15
+ permissions:
+ contents: write
+ env:
+ NUGET_PACKAGES: ${{ github.workspace }}/.nuget/packages
+ steps:
+ - uses: actions/checkout@v5
+ with:
+ ref: main
+ fetch-depth: 0
+
+ - name: Setup MSBuild
+ uses: microsoft/setup-msbuild@v2
+
+ - name: Cache NuGet packages
+ uses: actions/cache@v4
+ with:
+ path: ${{ github.workspace }}/.nuget/packages
+ key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj', '**/*.vcxproj') }}
+ restore-keys: |
+ ${{ runner.os }}-nuget-
+
+ - name: Install nbgv
+ run: dotnet tool install -g nbgv
+
+ - name: Compute version
+ id: version
+ shell: pwsh
+ run: |
+ $ver = nbgv get-version -v NuGetPackageVersion
+ echo "VERSION=$ver" >> $env:GITHUB_OUTPUT
+ Write-Host "Version: $ver"
+
+ - name: Restore
+ run: msbuild Confuser2.sln -t:Restore -verbosity:minimal
+
+ - name: Build
+ run: msbuild Confuser2.sln -p:Configuration=Release -verbosity:minimal
+
+ - name: Package CLI
+ shell: pwsh
+ run: |
+ $src = 'Confuser.CLI/bin/Release/net10.0'
+ Get-ChildItem $src -Exclude '*.pdb','*.xml' | Compress-Archive -DestinationPath 'ConfuserEx-CLI.zip'
+ Write-Host "Created ConfuserEx-CLI.zip"
+
+ - name: Package GUI
+ shell: pwsh
+ run: |
+ $src = 'ConfuserEx/bin/Release/net10.0-windows'
+ Get-ChildItem $src -Exclude '*.pdb','*.xml' | Compress-Archive -DestinationPath 'ConfuserEx-GUI.zip'
+ Write-Host "Created ConfuserEx-GUI.zip"
+
+ - name: Package combined
+ shell: pwsh
+ run: |
+ $tmp = 'combined'
+ New-Item -ItemType Directory -Path $tmp -Force | Out-Null
+ Copy-Item 'Confuser.CLI/bin/Release/net10.0/*' $tmp -Exclude '*.pdb','*.xml' -Recurse
+ Copy-Item 'ConfuserEx/bin/Release/net10.0-windows/*' $tmp -Exclude '*.pdb','*.xml' -Recurse -Force
+ Get-ChildItem $tmp | Compress-Archive -DestinationPath 'ConfuserEx.zip'
+ Remove-Item $tmp -Recurse -Force
+ Write-Host "Created ConfuserEx.zip"
+
+ - name: Configure git identity
+ run: |
+ git config user.name "github-actions[bot]"
+ git config user.email "github-actions[bot]@users.noreply.github.com"
+
+ - name: Create release tag
+ shell: bash
+ run: |
+ tag="v${{ steps.version.outputs.VERSION }}"
+ if git rev-parse "$tag" >/dev/null 2>&1; then
+ echo "Tag $tag already exists — skipping tag creation."
+ else
+ git tag -a "$tag" -m "Release ${{ steps.version.outputs.VERSION }}"
+ git push origin "$tag"
+ fi
+
+ - name: Create GitHub Release
+ uses: softprops/action-gh-release@v2
+ with:
+ tag_name: v${{ steps.version.outputs.VERSION }}
+ name: v${{ steps.version.outputs.VERSION }}
+ files: |
+ ConfuserEx-CLI.zip
+ ConfuserEx-GUI.zip
+ ConfuserEx.zip
+ Confuser.MSBuild.Tasks/bin/Release/*.nupkg
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 728b57c48..62cd3c286 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -1,9 +1,16 @@
name: test
+# GitHub Actions minutes are limited. This full build+test+coverage run happens
+# automatically only for PRs into `main` (the release gate). For PRs into
+# `develop`, run tests locally with scripts/local-ci.sh; a run here happens only
+# when an admin adds the `run-ci` label (re-add to trigger each run) or dispatches
+# the workflow manually. See the `if:` on the test job below.
on:
pull_request:
branches: [main, develop]
+ types: [opened, synchronize, reopened, labeled]
paths-ignore: ['**.md', 'docs/**', 'LICENSE*']
+ workflow_dispatch:
concurrency:
group: test-${{ github.ref }}
@@ -11,6 +18,12 @@ concurrency:
jobs:
test:
+ # Auto for PRs into main and manual dispatch; for develop PRs only when an
+ # admin adds the `run-ci` label.
+ if: >-
+ github.event_name == 'workflow_dispatch' ||
+ github.base_ref == 'main' ||
+ (github.event.action == 'labeled' && github.event.label.name == 'run-ci')
runs-on: windows-2025
timeout-minutes: 15
permissions:
@@ -61,8 +74,12 @@ jobs:
$name = $proj.BaseName
Write-Host "`nTesting $name..." -ForegroundColor Cyan
+ # Do NOT pass --collect:"XPlat Code Coverage" here. Coverage is driven by each
+ # project's RunSettingsFilePath (Tests/Directory.Build.targets), which enables it
+ # only for .NET (Core) test projects. A command-line --collect overrides that gate
+ # and forces Coverlet to instrument the signed Confuser.* assemblies on net4x,
+ # breaking their strong name so .NET Framework refuses to load them.
dotnet test $proj.FullName -c Release --no-build --verbosity minimal `
- --collect:"XPlat Code Coverage" `
--logger "trx;LogFileName=$name.trx" `
--results-directory "$resultsDir/$name"
diff --git a/.gitignore b/.gitignore
index 9ee6a61e2..beadc33d8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -39,4 +39,11 @@ packages/
gh-pages/
.idea/
-**/*out*
\ No newline at end of file
+**/*out*
+
+# Local CI artifacts
+test-results/
+coverage/
+
+# Release packages (produced by the CI/local-ci package step)
+*.zip
\ No newline at end of file
diff --git a/Confuser.CLI/Confuser.CLI.csproj b/Confuser.CLI/Confuser.CLI.csproj
index d087b4871..df0632989 100644
--- a/Confuser.CLI/Confuser.CLI.csproj
+++ b/Confuser.CLI/Confuser.CLI.csproj
@@ -16,6 +16,9 @@
+
+
+
diff --git a/Confuser.CLI/Program.cs b/Confuser.CLI/Program.cs
index aff59c47c..9d3323c02 100644
--- a/Confuser.CLI/Program.cs
+++ b/Confuser.CLI/Program.cs
@@ -5,8 +5,12 @@
using System.Linq;
using System.Xml;
using Confuser.Core;
+using Confuser.Core.Diagnostics;
using Confuser.Core.Project;
+using Microsoft.Extensions.Logging;
using NDesk.Options;
+using Serilog;
+using Serilog.Events;
namespace Confuser.CLI {
internal class Program {
@@ -21,6 +25,10 @@ static int Main(string[] args) {
try {
bool noPause = false;
bool debug = false;
+ bool quiet = false;
+ bool dumpRequested = false;
+ string dumpPath = null;
+ int verbosity = 0;
string outDir = null;
string snKeyPath = null;
string snKeyPass = null;
@@ -48,6 +56,15 @@ static int Main(string[] args) {
}, {
"snkeypass=", "specifies strong name key password.",
value => { snKeyPass = value; }
+ }, {
+ "v|verbose", "increase verbosity (repeat for more: -v, -vv, -vvv).",
+ value => { verbosity++; }
+ }, {
+ "q|quiet", "only show warnings and errors.",
+ value => { quiet = (value != null); }
+ }, {
+ "dump:", "write a diagnostic report (optionally to the given file).",
+ value => { dumpRequested = true; if (!string.IsNullOrEmpty(value)) dumpPath = value; }
}
};
@@ -130,7 +147,7 @@ static int Main(string[] args) {
parameters.Project = proj;
}
- int retVal = RunProject(parameters);
+ int retVal = RunProject(parameters, quiet, verbosity, dumpRequested, dumpPath);
if (NeedPause() && !noPause) {
Console.WriteLine("Press any key to continue...");
@@ -192,14 +209,61 @@ static void LoadTemplateProject(string templatePath, ConfuserProject proj, List<
templateModules.Add(templateModule);
}
- static int RunProject(ConfuserParameters parameters) {
- var logger = new ConsoleLogger();
- parameters.Logger = logger;
+ static int RunProject(ConfuserParameters parameters, bool quiet, int verbosity, bool dumpRequested, string dumpPath) {
+ var levelSwitch = quiet
+ ? LogEventLevel.Warning
+ : verbosity >= 3 ? LogEventLevel.Verbose
+ : verbosity >= 2 ? LogEventLevel.Verbose
+ : verbosity >= 1 ? LogEventLevel.Debug
+ : LogEventLevel.Information;
+
+ Log.Logger = new LoggerConfiguration()
+ .MinimumLevel.Is(levelSwitch)
+ .WriteTo.Console(
+ outputTemplate: "[{Level:u4}] {Message:lj}{NewLine}{Exception}")
+ .CreateLogger();
+
+ using var loggerFactory = LoggerFactory.Create(builder =>
+ builder.AddSerilog(dispose: false));
+ var melLogger = loggerFactory.CreateLogger("ConfuserEx");
+
+ var progressReporter = new ConsoleProgressReporter();
+
+ // When a diagnostic report is requested, wrap both the logger and the progress reporter
+ // with a collector so the report captures the full transcript, timing and outcome even
+ // when the run fails.
+ DiagnosticCollector collector = null;
+ if (dumpRequested) {
+ collector = new DiagnosticCollector(melLogger, progressReporter) { Project = parameters.Project };
+ parameters.Logger = collector;
+ parameters.ProgressReporter = collector;
+ }
+ else {
+ parameters.Logger = melLogger;
+ parameters.ProgressReporter = progressReporter;
+ }
- Console.Title = "ConfuserEx - Running...";
+ if (OperatingSystem.IsWindows())
+ Console.Title = "ConfuserEx - Running...";
ConfuserEngine.Run(parameters).GetAwaiter().GetResult();
- return logger.ReturnValue;
+ Log.CloseAndFlush();
+
+ if (collector != null)
+ WriteDiagnosticReport(collector, dumpPath);
+
+ return progressReporter.ReturnValue;
+ }
+
+ static void WriteDiagnosticReport(DiagnosticCollector collector, string dumpPath) {
+ string path = string.IsNullOrEmpty(dumpPath) ? "confuser-diagnostic-report.md" : dumpPath;
+ try {
+ File.WriteAllText(path, collector.GenerateReport());
+ WriteLineWithColor(ConsoleColor.Cyan, "Diagnostic report written to: " + Path.GetFullPath(path));
+ }
+ catch (Exception ex) {
+ WriteLineWithColor(ConsoleColor.Red, "Failed to write diagnostic report: " + ex.Message);
+ }
}
static bool NeedPause() {
@@ -217,6 +281,9 @@ static void PrintUsage() {
WriteLine(" -debug : specifies debug symbol generation.");
WriteLine(" -snkey : specifies strong name key file path.");
WriteLine(" -snkeypass : specifies strong name key password.");
+ WriteLine(" -v|verbose : increase verbosity (-v debug, -vv trace).");
+ WriteLine(" -q|quiet : only show warnings and errors.");
+ WriteLine(" -dump : write a diagnostic report (-dump= for a custom path).");
}
static void WriteLineWithColor(ConsoleColor color, string txt) {
@@ -234,57 +301,15 @@ static void WriteLine() {
Console.WriteLine();
}
- class ConsoleLogger : ILogger {
+ class ConsoleProgressReporter : IProgressReporter {
readonly DateTime begin;
- public ConsoleLogger() {
+ public ConsoleProgressReporter() {
begin = DateTime.Now;
}
public int ReturnValue { get; private set; }
- public void Debug(string msg) {
- WriteLineWithColor(ConsoleColor.Gray, "[DEBUG] " + msg);
- }
-
- public void DebugFormat(string format, params object[] args) {
- WriteLineWithColor(ConsoleColor.Gray, "[DEBUG] " + string.Format(format, args));
- }
-
- public void Info(string msg) {
- WriteLineWithColor(ConsoleColor.White, " [INFO] " + msg);
- }
-
- public void InfoFormat(string format, params object[] args) {
- WriteLineWithColor(ConsoleColor.White, " [INFO] " + string.Format(format, args));
- }
-
- public void Warn(string msg) {
- WriteLineWithColor(ConsoleColor.Yellow, " [WARN] " + msg);
- }
-
- public void WarnFormat(string format, params object[] args) {
- WriteLineWithColor(ConsoleColor.Yellow, " [WARN] " + string.Format(format, args));
- }
-
- public void WarnException(string msg, Exception ex) {
- WriteLineWithColor(ConsoleColor.Yellow, " [WARN] " + msg);
- WriteLineWithColor(ConsoleColor.Yellow, "Exception: " + ex);
- }
-
- public void Error(string msg) {
- WriteLineWithColor(ConsoleColor.Red, "[ERROR] " + msg);
- }
-
- public void ErrorFormat(string format, params object[] args) {
- WriteLineWithColor(ConsoleColor.Red, "[ERROR] " + string.Format(format, args));
- }
-
- public void ErrorException(string msg, Exception ex) {
- WriteLineWithColor(ConsoleColor.Red, "[ERROR] " + msg);
- WriteLineWithColor(ConsoleColor.Red, "Exception: " + ex);
- }
-
public void Progress(int progress, int overall) { }
public void EndProgress() { }
diff --git a/Confuser.Core/Confuser.Core.csproj b/Confuser.Core/Confuser.Core.csproj
index 0e13ac77c..2969a4d41 100644
--- a/Confuser.Core/Confuser.Core.csproj
+++ b/Confuser.Core/Confuser.Core.csproj
@@ -15,8 +15,9 @@
-
+
+
diff --git a/Confuser.Core/ConfuserContext.cs b/Confuser.Core/ConfuserContext.cs
index a8c7a1c76..493643745 100644
--- a/Confuser.Core/ConfuserContext.cs
+++ b/Confuser.Core/ConfuserContext.cs
@@ -4,6 +4,7 @@
using Confuser.Core.Project;
using dnlib.DotNet;
using dnlib.DotNet.Writer;
+using Microsoft.Extensions.Logging;
namespace Confuser.Core {
///
@@ -18,7 +19,13 @@ public class ConfuserContext {
/// Gets the logger used for logging events.
///
/// The logger.
- public ILogger Logger { get; internal set; }
+ public Microsoft.Extensions.Logging.ILogger Logger { get; internal set; }
+
+ ///
+ /// Gets the progress reporter used for reporting protection progress.
+ ///
+ /// The progress reporter.
+ public IProgressReporter ProgressReporter { get; internal set; }
///
/// Gets the project being processed.
diff --git a/Confuser.Core/ConfuserEngine.cs b/Confuser.Core/ConfuserEngine.cs
index a4e737cce..68ac9c482 100644
--- a/Confuser.Core/ConfuserEngine.cs
+++ b/Confuser.Core/ConfuserEngine.cs
@@ -10,6 +10,7 @@
using dnlib.DotNet;
using dnlib.DotNet.Emit;
using dnlib.DotNet.Writer;
+using Microsoft.Extensions.Logging;
using Microsoft.Win32;
using CopyrightAttribute = System.Reflection.AssemblyCopyrightAttribute;
using InformationalAttribute = System.Reflection.AssemblyInformationalVersionAttribute;
@@ -80,6 +81,7 @@ static void RunInternal(ConfuserParameters parameters, CancellationToken token)
// 1. Setup context
var context = new ConfuserContext();
context.Logger = parameters.GetLogger();
+ context.ProgressReporter = parameters.GetProgressReporter();
context.Project = parameters.Project.Clone();
context.PackerInitiated = parameters.PackerInitiated;
context.token = token;
@@ -107,7 +109,7 @@ static void RunInternal(ConfuserParameters parameters, CancellationToken token)
var modulePath = Path.Combine(context.BaseDirectory, firstModule.Path);
foreach (var runtimePath in DotNetCorePathResolver.ResolveRuntimePaths(modulePath, context.Logger)) {
asmResolver.PostSearchPaths.Add(runtimePath);
- context.Logger.DebugFormat("Auto-detected .NET runtime path: {0}", runtimePath);
+ context.Logger.LogDebug("Auto-detected .NET runtime path: {0}", runtimePath);
}
}
@@ -116,25 +118,25 @@ static void RunInternal(ConfuserParameters parameters, CancellationToken token)
Marker marker = parameters.GetMarker();
// 2. Discover plugins
- context.Logger.Debug("Discovering plugins...");
+ context.Logger.LogDebug("Discovering plugins...");
IList prots;
IList packers;
IList components;
parameters.GetPluginDiscovery().GetPlugins(context, out prots, out packers, out components);
- context.Logger.InfoFormat("Discovered {0} protections, {1} packers.", prots.Count, packers.Count);
+ context.Logger.LogInformation("Discovered {0} protections, {1} packers.", prots.Count, packers.Count);
context.CheckCancellation();
// 3. Resolve dependency
- context.Logger.Debug("Resolving component dependency...");
+ context.Logger.LogDebug("Resolving component dependency...");
try {
var resolver = new DependencyResolver(prots);
prots = resolver.SortDependency();
}
catch (CircularDependencyException ex) {
- context.Logger.ErrorException("", ex);
+ context.Logger.LogError(ex, "");
throw new ConfuserException(ex);
}
@@ -147,7 +149,7 @@ static void RunInternal(ConfuserParameters parameters, CancellationToken token)
context.CheckCancellation();
// 4. Load modules
- context.Logger.Info("Loading input modules...");
+ context.Logger.LogInformation("Loading input modules...");
marker.Initialize(prots, packers);
MarkerResult markings = marker.MarkProject(context.Project, context);
context.Modules = new ModuleSorter(markings.Modules).Sort().ToList().AsReadOnly();
@@ -162,13 +164,13 @@ static void RunInternal(ConfuserParameters parameters, CancellationToken token)
context.CheckCancellation();
// 5. Initialize components
- context.Logger.Info("Initializing...");
+ context.Logger.LogInformation("Initializing...");
foreach (ConfuserComponent comp in components) {
try {
comp.Initialize(context);
}
catch (Exception ex) {
- context.Logger.ErrorException("Error occurred during initialization of '" + comp.Name + "'.", ex);
+ context.Logger.LogError(ex, "Error occurred during initialization of '" + comp.Name + "'.");
throw new ConfuserException(ex);
}
context.CheckCancellation();
@@ -177,7 +179,7 @@ static void RunInternal(ConfuserParameters parameters, CancellationToken token)
context.CheckCancellation();
// 6. Build pipeline
- context.Logger.Debug("Building pipeline...");
+ context.Logger.LogDebug("Building pipeline...");
var pipeline = new ProtectionPipeline();
context.Pipeline = pipeline;
foreach (ConfuserComponent comp in components) {
@@ -192,33 +194,33 @@ static void RunInternal(ConfuserParameters parameters, CancellationToken token)
ok = true;
}
catch (AssemblyResolveException ex) {
- context.Logger.ErrorException("Failed to resolve an assembly, check if all dependencies are present in the correct version.", ex);
+ context.Logger.LogError(ex, "Failed to resolve an assembly, check if all dependencies are present in the correct version.");
PrintEnvironmentInfo(context);
}
catch (TypeResolveException ex) {
- context.Logger.ErrorException("Failed to resolve a type, check if all dependencies are present in the correct version.", ex);
+ context.Logger.LogError(ex, "Failed to resolve a type, check if all dependencies are present in the correct version.");
PrintEnvironmentInfo(context);
}
catch (MemberRefResolveException ex) {
- context.Logger.ErrorException("Failed to resolve a member, check if all dependencies are present in the correct version.", ex);
+ context.Logger.LogError(ex, "Failed to resolve a member, check if all dependencies are present in the correct version.");
PrintEnvironmentInfo(context);
}
catch (IOException ex) {
- context.Logger.ErrorException("An IO error occurred, check if all input/output locations are readable/writable.", ex);
+ context.Logger.LogError(ex, "An IO error occurred, check if all input/output locations are readable/writable.");
}
catch (OperationCanceledException) {
- context.Logger.Error("Operation cancelled.");
+ context.Logger.LogError("Operation cancelled.");
}
catch (ConfuserException) {
// Exception is already handled/logged, so just ignore and report failure
}
catch (Exception ex) {
- context.Logger.ErrorException("Unknown error occurred.", ex);
+ context.Logger.LogError(ex, "Unknown error occurred.");
}
finally {
if (context.Resolver != null)
context.InternalResolver.Clear();
- context.Logger.Finish(ok);
+ context.ProgressReporter.Finish(ok);
}
}
@@ -268,27 +270,27 @@ static void RunPipeline(ProtectionPipeline pipeline, ConfuserContext context) {
pipeline.ExecuteStage(PipelineStage.SaveModules, SaveModules, () => getAllDefs(), context);
if (!context.PackerInitiated)
- context.Logger.Info("Done.");
+ context.Logger.LogInformation("Done.");
}
static void Inspection(ConfuserContext context) {
- context.Logger.Info("Resolving dependencies...");
+ context.Logger.LogInformation("Resolving dependencies...");
foreach (var dependency in context.Modules
.SelectMany(module => module.GetAssemblyRefs().Select(asmRef => Tuple.Create(asmRef, module)))) {
var resolved = context.Resolver.Resolve(dependency.Item1, dependency.Item2);
if (resolved == null)
- context.Logger.WarnFormat("Failed to resolve dependency '{0}' of '{1}'. Some protections may not work correctly.",
+ context.Logger.LogWarning("Failed to resolve dependency '{0}' of '{1}'. Some protections may not work correctly.",
dependency.Item1.FullName, dependency.Item2.Name);
}
- context.Logger.Debug("Checking Strong Name...");
+ context.Logger.LogDebug("Checking Strong Name...");
foreach (var module in context.Modules) {
CheckStrongName(context, module);
}
var marker = context.Registry.GetService();
- context.Logger.Debug("Creating global .cctors...");
+ context.Logger.LogDebug("Creating global .cctors...");
foreach (ModuleDefMD module in context.Modules) {
TypeDef modType = module.GlobalType;
if (modType == null) {
@@ -316,12 +318,12 @@ static void CheckStrongName(ConfuserContext context, ModuleDef module) {
bool isKeyProvided = snKey != null || (snDelaySign && snPubKeyBytes != null);
if (!isKeyProvided && moduleIsSignedOrDelayedSigned)
- context.Logger.WarnFormat("[{0}] SN Key or SN public Key is not provided for a signed module, the output may not be working.", module.Name);
+ context.Logger.LogWarning("[{0}] SN Key or SN public Key is not provided for a signed module, the output may not be working.", module.Name);
else if (isKeyProvided && !moduleIsSignedOrDelayedSigned)
- context.Logger.WarnFormat("[{0}] SN Key or SN public Key is provided for an unsigned module, the output may not be working.", module.Name);
+ context.Logger.LogWarning("[{0}] SN Key or SN public Key is provided for an unsigned module, the output may not be working.", module.Name);
else if (snPubKeyBytes != null && moduleIsSignedOrDelayedSigned &&
!module.Assembly.PublicKey.Data.SequenceEqual(snPubKeyBytes))
- context.Logger.WarnFormat("[{0}] Provided SN public Key and signed module's public key do not match, the output may not be working.",
+ context.Logger.LogWarning("[{0}] Provided SN public Key and signed module's public key do not match, the output may not be working.",
module.Name);
}
@@ -338,7 +340,7 @@ static void CopyPEHeaders(PEHeadersOptions writerOptions, ModuleDefMD module) {
}
static void BeginModule(ConfuserContext context) {
- context.Logger.InfoFormat("Processing module '{0}'...", context.CurrentModule.Name);
+ context.Logger.LogInformation("Processing module '{0}'...", context.CurrentModule.Name);
context.CurrentModuleWriterOptions = new ModuleWriterOptions(context.CurrentModule);
CopyPEHeaders(context.CurrentModuleWriterOptions.PEHeadersOptions, context.CurrentModule);
@@ -393,7 +395,7 @@ static void EndModule(ConfuserContext context) {
output = Path.Combine(context.BaseDirectory, output);
string relativeOutput = Utils.GetRelativePath(output, context.BaseDirectory);
if (relativeOutput is null) {
- context.Logger.WarnFormat("Input file is not inside the base directory. Relative path can't be created. Placing file into output root." +
+ context.Logger.LogWarning("Input file is not inside the base directory. Relative path can't be created. Placing file into output root." +
Environment.NewLine + "Responsible file is: {0}", output);
output = Path.GetFileName(output);
}
@@ -408,7 +410,7 @@ static void EndModule(ConfuserContext context) {
}
static void WriteModule(ConfuserContext context) {
- context.Logger.InfoFormat("Writing module '{0}'...", context.CurrentModule.Name);
+ context.Logger.LogInformation("Writing module '{0}'...", context.CurrentModule.Name);
MemoryStream pdb = null, output = new MemoryStream();
@@ -430,7 +432,7 @@ static void WriteModule(ConfuserContext context) {
}
static void Debug(ConfuserContext context) {
- context.Logger.Info("Finalizing...");
+ context.Logger.LogInformation("Finalizing...");
if (!context.Project.Debug)
return;
for (int i = 0; i < context.OutputModules.Count; i++) {
@@ -446,7 +448,7 @@ static void Debug(ConfuserContext context) {
static void Pack(ConfuserContext context) {
if (context.Packer != null) {
- context.Logger.Info("Packing...");
+ context.Logger.LogInformation("Packing...");
context.Packer.Pack(context, new ProtectionParameters(context.Packer, context.Modules.OfType().ToList()));
}
}
@@ -458,7 +460,7 @@ static void SaveModules(ConfuserContext context) {
string dir = Path.GetDirectoryName(path);
if (!Directory.Exists(dir))
Directory.CreateDirectory(dir);
- context.Logger.DebugFormat("Saving to '{0}'...", path);
+ context.Logger.LogDebug("Saving to '{0}'...", path);
File.WriteAllBytes(path, context.OutputModules[i]);
}
}
@@ -469,13 +471,13 @@ static void SaveModules(ConfuserContext context) {
/// The working context.
static void PrintInfo(ConfuserContext context) {
if (context.PackerInitiated) {
- context.Logger.Info("Protecting packer stub...");
+ context.Logger.LogInformation("Protecting packer stub...");
}
else {
- context.Logger.InfoFormat("{0} {1}", Version, Copyright);
+ context.Logger.LogInformation("{0} {1}", Version, Copyright);
Type mono = Type.GetType("Mono.Runtime");
- context.Logger.InfoFormat("Running on {0}, {1}, {2} bits",
+ context.Logger.LogInformation("Running on {0}, {1}, {2} bits",
Environment.OSVersion,
mono == null ?
".NET Framework v" + Environment.Version :
@@ -542,27 +544,27 @@ static void PrintEnvironmentInfo(ConfuserContext context) {
if (context.PackerInitiated)
return;
- context.Logger.Error("---BEGIN DEBUG INFO---");
+ context.Logger.LogError("---BEGIN DEBUG INFO---");
- context.Logger.Error("Installed Framework Versions:");
+ context.Logger.LogError("Installed Framework Versions:");
foreach (string ver in GetFrameworkVersions()) {
- context.Logger.ErrorFormat(" {0}", ver.Trim());
+ context.Logger.LogError(" {0}", ver.Trim());
}
- context.Logger.Error("");
+ context.Logger.LogError("");
if (context.Resolver != null) {
- context.Logger.Error("Cached assemblies:");
+ context.Logger.LogError("Cached assemblies:");
foreach (AssemblyDef asm in context.InternalResolver.GetCachedAssemblies()) {
if (string.IsNullOrEmpty(asm.ManifestModule.Location))
- context.Logger.ErrorFormat(" {0}", asm.FullName);
+ context.Logger.LogError(" {0}", asm.FullName);
else
- context.Logger.ErrorFormat(" {0} ({1})", asm.FullName, asm.ManifestModule.Location);
+ context.Logger.LogError(" {0} ({1})", asm.FullName, asm.ManifestModule.Location);
foreach (var reference in asm.Modules.OfType().SelectMany(m => m.GetAssemblyRefs()))
- context.Logger.ErrorFormat(" {0}", reference.FullName);
+ context.Logger.LogError(" {0}", reference.FullName);
}
}
- context.Logger.Error("---END DEBUG INFO---");
+ context.Logger.LogError("---END DEBUG INFO---");
}
}
}
diff --git a/Confuser.Core/ConfuserParameters.cs b/Confuser.Core/ConfuserParameters.cs
index f3e05a2c1..5f9d00ba7 100644
--- a/Confuser.Core/ConfuserParameters.cs
+++ b/Confuser.Core/ConfuserParameters.cs
@@ -1,5 +1,6 @@
using System;
using Confuser.Core.Project;
+using Microsoft.Extensions.Logging;
namespace Confuser.Core {
///
@@ -16,7 +17,13 @@ public class ConfuserParameters {
/// Gets or sets the logger that used to log the protection process.
///
/// The logger, or null if logging is not needed.
- public ILogger Logger { get; set; }
+ public Microsoft.Extensions.Logging.ILogger Logger { get; set; }
+
+ ///
+ /// Gets or sets the progress reporter used to report protection progress.
+ ///
+ /// The progress reporter, or null if progress reporting is not needed.
+ public IProgressReporter ProgressReporter { get; set; }
internal bool PackerInitiated { get; set; }
@@ -36,8 +43,16 @@ public class ConfuserParameters {
/// Gets the actual non-null logger.
///
/// The logger.
- internal ILogger GetLogger() {
- return Logger ?? NullLogger.Instance;
+ internal Microsoft.Extensions.Logging.ILogger GetLogger() {
+ return Logger ?? Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance;
+ }
+
+ ///
+ /// Gets the actual non-null progress reporter.
+ ///
+ /// The progress reporter.
+ internal IProgressReporter GetProgressReporter() {
+ return ProgressReporter ?? NullProgressReporter.Instance;
}
///
diff --git a/Confuser.Core/Diagnostics/DiagnosticCollector.cs b/Confuser.Core/Diagnostics/DiagnosticCollector.cs
new file mode 100644
index 000000000..3616a5e76
--- /dev/null
+++ b/Confuser.Core/Diagnostics/DiagnosticCollector.cs
@@ -0,0 +1,163 @@
+using System;
+using System.Collections.Generic;
+using Confuser.Core.Project;
+using Microsoft.Extensions.Logging;
+
+namespace Confuser.Core.Diagnostics {
+ ///
+ /// A single captured log entry, rendered to text at capture time.
+ ///
+ public readonly struct DiagnosticLogEntry {
+ public DiagnosticLogEntry(LogLevel level, string message, string exception) {
+ Level = level;
+ Message = message;
+ Exception = exception;
+ }
+
+ /// The severity of the entry.
+ public LogLevel Level { get; }
+
+ /// The rendered message text.
+ public string Message { get; }
+
+ /// The rendered exception (including stack trace), or null if none.
+ public string Exception { get; }
+ }
+
+ ///
+ /// Wraps the real and used during an
+ /// obfuscation run, passing every call through while capturing a full-verbosity transcript,
+ /// timing and outcome. On completion — success or failure — it can produce a self-contained
+ /// markdown diagnostic report suitable for a bug report.
+ ///
+ ///
+ ///
+ /// Capture is intentionally independent of the inner logger's level: the collector reports
+ /// as true and keeps every entry, so a report from a default
+ /// (Information) run still contains the Debug detail needed to diagnose a failure. Display
+ /// filtering is preserved because entries are only forwarded to the inner logger when it is
+ /// enabled for that level.
+ ///
+ ///
+ /// The entry buffer is bounded (see ); once full, the oldest
+ /// entries are dropped and counted in so the report can note the
+ /// loss rather than silently mislead.
+ ///
+ ///
+ /// is last-wins: a packer runs a nested engine pass with the same
+ /// collector, so the top-level run — which finishes last — determines the reported outcome
+ /// and elapsed time.
+ ///
+ ///
+ public sealed class DiagnosticCollector : ILogger, IProgressReporter {
+ /// The default maximum number of log entries retained.
+ public const int DefaultCapacity = 2000;
+
+ readonly ILogger inner;
+ readonly IProgressReporter innerReporter;
+ readonly int capacity;
+ readonly object sync = new object();
+ readonly Queue entries;
+ readonly DateTime begin = DateTime.UtcNow;
+ int dropped;
+ bool? successful;
+ TimeSpan elapsed;
+
+ ///
+ /// Initializes a new collector.
+ ///
+ /// The logger to forward display output to. Required.
+ /// The progress reporter to forward to, or null.
+ /// The maximum number of log entries to retain.
+ public DiagnosticCollector(ILogger inner, IProgressReporter innerReporter = null, int capacity = DefaultCapacity) {
+ this.inner = inner ?? throw new ArgumentNullException(nameof(inner));
+ this.innerReporter = innerReporter;
+ this.capacity = capacity < 1 ? 1 : capacity;
+ entries = new Queue(Math.Min(this.capacity, 64));
+ }
+
+ ///
+ /// The project being processed, used to populate the report's configuration section.
+ ///
+ public ConfuserProject Project { get; set; }
+
+ ///
+ /// The run outcome: true on success, false on failure, null if the run
+ /// never reported completion.
+ ///
+ public bool? Successful {
+ get { lock (sync) return successful; }
+ }
+
+ /// The elapsed time recorded at the last call.
+ public TimeSpan Elapsed {
+ get { lock (sync) return elapsed; }
+ }
+
+ /// The number of log entries dropped because the buffer was full.
+ public int DroppedCount {
+ get { lock (sync) return dropped; }
+ }
+
+ ///
+ /// Returns an immutable copy of the currently retained log entries, oldest first.
+ ///
+ public IReadOnlyList Snapshot() {
+ lock (sync) return new List(entries);
+ }
+
+ ///
+ /// Produces the markdown diagnostic report. Never throws.
+ ///
+ public string GenerateReport() => DiagnosticReport.Generate(this);
+
+ #region ILogger
+
+ public IDisposable BeginScope(TState state) => inner.BeginScope(state);
+
+ public bool IsEnabled(LogLevel logLevel) => true;
+
+ public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception,
+ Func formatter) {
+ string message;
+ try {
+ message = formatter != null ? formatter(state, exception) : state?.ToString() ?? string.Empty;
+ }
+ catch {
+ message = state?.ToString() ?? string.Empty;
+ }
+
+ var entry = new DiagnosticLogEntry(logLevel, message, exception?.ToString());
+ lock (sync) {
+ entries.Enqueue(entry);
+ while (entries.Count > capacity) {
+ entries.Dequeue();
+ dropped++;
+ }
+ }
+
+ // Forward to the inner logger for display; it applies its own level filter.
+ if (inner.IsEnabled(logLevel))
+ inner.Log(logLevel, eventId, state, exception, formatter);
+ }
+
+ #endregion
+
+ #region IProgressReporter
+
+ public void Progress(int progress, int overall) => innerReporter?.Progress(progress, overall);
+
+ public void EndProgress() => innerReporter?.EndProgress();
+
+ public void Finish(bool successful) {
+ lock (sync) {
+ this.successful = successful;
+ elapsed = DateTime.UtcNow - begin;
+ }
+
+ innerReporter?.Finish(successful);
+ }
+
+ #endregion
+ }
+}
diff --git a/Confuser.Core/Diagnostics/DiagnosticRedactor.cs b/Confuser.Core/Diagnostics/DiagnosticRedactor.cs
new file mode 100644
index 000000000..9d1059099
--- /dev/null
+++ b/Confuser.Core/Diagnostics/DiagnosticRedactor.cs
@@ -0,0 +1,52 @@
+using System;
+using System.Text;
+
+namespace Confuser.Core.Diagnostics {
+ ///
+ /// Scrubs sensitive information from text destined for a diagnostic report.
+ ///
+ ///
+ /// Diagnostic reports are meant to be pasted into public issue trackers, so any text
+ /// that flows into one must have the reporter's identity removed. The most common leak
+ /// is the user-profile path (e.g. C:\Users\alice\...) which appears in absolute
+ /// paths throughout log output and project configuration.
+ ///
+ public static class DiagnosticRedactor {
+ ///
+ /// The placeholder substituted for the user-profile directory.
+ ///
+ public const string UserPlaceholder = "%USER%";
+
+ ///
+ /// Replaces every occurrence of the user-profile directory in
+ /// with . The match is case-insensitive because Windows
+ /// paths are.
+ ///
+ /// The text to scrub. Returned unchanged if null or empty.
+ /// The user-profile directory to redact, or null to skip.
+ /// The scrubbed text.
+ public static string Redact(string text, string userProfile) {
+ if (string.IsNullOrEmpty(text) || string.IsNullOrEmpty(userProfile))
+ return text;
+ return ReplaceCaseInsensitive(text, userProfile, UserPlaceholder);
+ }
+
+ static string ReplaceCaseInsensitive(string input, string search, string replacement) {
+ var sb = new StringBuilder(input.Length);
+ int index = 0;
+ while (true) {
+ int found = input.IndexOf(search, index, StringComparison.OrdinalIgnoreCase);
+ if (found < 0) {
+ sb.Append(input, index, input.Length - index);
+ break;
+ }
+
+ sb.Append(input, index, found - index);
+ sb.Append(replacement);
+ index = found + search.Length;
+ }
+
+ return sb.ToString();
+ }
+ }
+}
diff --git a/Confuser.Core/Diagnostics/DiagnosticReport.cs b/Confuser.Core/Diagnostics/DiagnosticReport.cs
new file mode 100644
index 000000000..afae508bd
--- /dev/null
+++ b/Confuser.Core/Diagnostics/DiagnosticReport.cs
@@ -0,0 +1,261 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Runtime.InteropServices;
+using System.Text;
+using Confuser.Core.Project;
+using dnlib.DotNet;
+using Microsoft.Extensions.Logging;
+
+namespace Confuser.Core.Diagnostics {
+ ///
+ /// Formats the data captured by a into a self-contained
+ /// markdown report suitable for pasting into a bug report.
+ ///
+ public static class DiagnosticReport {
+ ///
+ /// Generates the report for the given collector. Never throws — on any failure it returns
+ /// a minimal report noting the failure, because this runs precisely when things are already
+ /// going wrong.
+ ///
+ public static string Generate(DiagnosticCollector collector) {
+ if (collector == null)
+ return string.Empty;
+
+ try {
+ return Build(collector);
+ }
+ catch (Exception ex) {
+ return "# ConfuserEx Diagnostic Report" + Environment.NewLine + Environment.NewLine +
+ "Report generation failed: " + ex.Message + Environment.NewLine;
+ }
+ }
+
+ static string Build(DiagnosticCollector collector) {
+ string userProfile = SafeUserProfile();
+ var sb = new StringBuilder();
+ sb.AppendLine("# ConfuserEx Diagnostic Report");
+ sb.AppendLine();
+ AppendSystem(sb);
+ AppendProject(sb, collector.Project, userProfile);
+ AppendResult(sb, collector.Successful);
+ AppendLog(sb, collector, userProfile);
+ AppendElapsed(sb, collector.Elapsed);
+ return sb.ToString();
+ }
+
+ static void AppendSystem(StringBuilder sb) {
+ sb.AppendLine("## System");
+ sb.AppendLine("- OS: " + SafeOsDescription());
+ sb.AppendLine("- Runtime: " + SafeRuntimeDescription());
+ sb.AppendLine("- Architecture: " + RuntimeInformation.OSArchitecture +
+ " (process " + RuntimeInformation.ProcessArchitecture + ")");
+ sb.AppendLine("- ConfuserExx: " + ConfuserEngine.Version);
+ sb.AppendLine();
+ }
+
+ static void AppendProject(StringBuilder sb, ConfuserProject project, string userProfile) {
+ sb.AppendLine("## Project Configuration");
+ if (project == null) {
+ sb.AppendLine("- (no project information available)");
+ sb.AppendLine();
+ return;
+ }
+
+ sb.AppendLine("- Base Directory: " + Show(DiagnosticRedactor.Redact(project.BaseDirectory, userProfile)));
+ sb.AppendLine("- Output Directory: " + Show(DiagnosticRedactor.Redact(project.OutputDirectory, userProfile)));
+
+ var modules = project.Where(m => !m.IsExternal).Select(m => m.Path)
+ .Where(p => !string.IsNullOrEmpty(p)).ToList();
+ sb.AppendLine("- Modules: " + (modules.Count > 0 ? string.Join(", ", modules) : "(none)"));
+
+ var targetFrameworks = modules
+ .Select(m => TryReadTargetFramework(ResolveModulePath(project, m)))
+ .Where(tfm => !string.IsNullOrEmpty(tfm))
+ .Distinct()
+ .ToList();
+ if (targetFrameworks.Count > 0)
+ sb.AppendLine("- Target Framework: " + string.Join(", ", targetFrameworks));
+
+ var externals = project.Where(m => m.IsExternal).Select(m => m.Path)
+ .Where(p => !string.IsNullOrEmpty(p)).ToList();
+ if (externals.Count > 0)
+ sb.AppendLine("- External Modules: " + string.Join(", ", externals));
+
+ var protections = project.Rules
+ .SelectMany(r => r)
+ .Select(s => s.Id)
+ .Where(id => !string.IsNullOrEmpty(id))
+ .Distinct(StringComparer.OrdinalIgnoreCase)
+ .ToList();
+ sb.AppendLine("- Protections: " + (protections.Count > 0 ? string.Join(", ", protections) : "(preset only / none)"));
+
+ var presets = project.Rules.Where(r => r.Preset != ProtectionPreset.None)
+ .Select(r => r.Preset.ToString().ToLowerInvariant())
+ .Distinct().ToList();
+ if (presets.Count > 0)
+ sb.AppendLine("- Presets: " + string.Join(", ", presets));
+
+ sb.AppendLine("- Packer: " + (project.Packer != null && !string.IsNullOrEmpty(project.Packer.Id)
+ ? project.Packer.Id : "(none)"));
+
+ var probePaths = (project.ProbePaths ?? Enumerable.Empty())
+ .Select(p => DiagnosticRedactor.Redact(p, userProfile)).ToList();
+ sb.AppendLine("- Probe Paths: " + (probePaths.Count > 0 ? string.Join(", ", probePaths) : "(none)"));
+
+ var pluginPaths = (project.PluginPaths ?? Enumerable.Empty())
+ .Select(p => DiagnosticRedactor.Redact(p, userProfile)).ToList();
+ if (pluginPaths.Count > 0)
+ sb.AppendLine("- Plugins: " + string.Join(", ", pluginPaths));
+
+ sb.AppendLine();
+ }
+
+ static void AppendResult(StringBuilder sb, bool? successful) {
+ string status = successful == true ? "SUCCESS" : successful == false ? "FAILED" : "(did not complete)";
+ sb.AppendLine("## Result: " + status);
+ sb.AppendLine();
+ }
+
+ static void AppendLog(StringBuilder sb, DiagnosticCollector collector, string userProfile) {
+ sb.AppendLine("## Log Output");
+
+ var lines = new List();
+ if (collector.DroppedCount > 0)
+ lines.Add("... " + collector.DroppedCount + " earlier log entries truncated ...");
+
+ foreach (var entry in collector.Snapshot()) {
+ lines.Add(Prefix(entry.Level) + DiagnosticRedactor.Redact(entry.Message, userProfile));
+ if (!string.IsNullOrEmpty(entry.Exception))
+ foreach (var exLine in entry.Exception.Split('\n'))
+ lines.Add(DiagnosticRedactor.Redact(exLine.TrimEnd('\r'), userProfile));
+ }
+
+ string body = string.Join(Environment.NewLine, lines);
+ string fence = MakeFence(body);
+ sb.AppendLine(fence);
+ sb.AppendLine(body);
+ sb.AppendLine(fence);
+ sb.AppendLine();
+ }
+
+ static void AppendElapsed(StringBuilder sb, TimeSpan elapsed) {
+ sb.AppendLine("## Elapsed: " +
+ elapsed.TotalSeconds.ToString("F1", CultureInfo.InvariantCulture) + " s");
+ }
+
+ ///
+ /// Chooses a code-fence longer than the longest run of backticks in ,
+ /// so log content containing its own ``` fences cannot break out of the block.
+ ///
+ static string MakeFence(string body) {
+ int max = 0, run = 0;
+ foreach (char c in body) {
+ if (c == '`') {
+ run++;
+ if (run > max) max = run;
+ }
+ else {
+ run = 0;
+ }
+ }
+
+ return new string('`', Math.Max(3, max + 1));
+ }
+
+ static string Prefix(LogLevel level) {
+ switch (level) {
+ case LogLevel.Trace:
+ case LogLevel.Debug:
+ return "[DEBUG] ";
+ case LogLevel.Information:
+ return "[INFO] ";
+ case LogLevel.Warning:
+ return "[WARN] ";
+ case LogLevel.Error:
+ case LogLevel.Critical:
+ return "[ERROR] ";
+ default:
+ return "";
+ }
+ }
+
+ static string Show(string value) => string.IsNullOrEmpty(value) ? "(not set)" : value;
+
+ static string ResolveModulePath(ConfuserProject project, string modulePath) {
+ try {
+ if (!string.IsNullOrEmpty(project.BaseDirectory))
+ return Path.Combine(project.BaseDirectory, modulePath);
+ }
+ catch {
+ // Fall through to the bare module path.
+ }
+
+ return modulePath;
+ }
+
+ ///
+ /// Best-effort read of an assembly's target-framework moniker (e.g.
+ /// .NETCoreApp,Version=v8.0) from its TargetFrameworkAttribute. Returns
+ /// null if the file is missing, is not a valid assembly, or carries no such
+ /// attribute. The file is read into memory so it is never locked.
+ ///
+ public static string TryReadTargetFramework(string assemblyPath) {
+ try {
+ if (string.IsNullOrEmpty(assemblyPath) || !File.Exists(assemblyPath))
+ return null;
+
+ using (var module = ModuleDefMD.Load(File.ReadAllBytes(assemblyPath))) {
+ var assembly = module.Assembly;
+ if (assembly == null)
+ return null;
+
+ foreach (var attr in assembly.CustomAttributes) {
+ if (attr.TypeFullName != "System.Runtime.Versioning.TargetFrameworkAttribute")
+ continue;
+ if (attr.ConstructorArguments.Count == 0)
+ continue;
+
+ var moniker = attr.ConstructorArguments[0].Value?.ToString();
+ if (!string.IsNullOrEmpty(moniker))
+ return moniker;
+ }
+ }
+ }
+ catch {
+ // Diagnostic best-effort: any failure to read the framework is non-fatal.
+ }
+
+ return null;
+ }
+
+ static string SafeOsDescription() {
+ try {
+ return RuntimeInformation.OSDescription.Trim();
+ }
+ catch {
+ return Environment.OSVersion.ToString();
+ }
+ }
+
+ static string SafeRuntimeDescription() {
+ try {
+ return RuntimeInformation.FrameworkDescription;
+ }
+ catch {
+ return ".NET " + Environment.Version;
+ }
+ }
+
+ static string SafeUserProfile() {
+ try {
+ return Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
+ }
+ catch {
+ return null;
+ }
+ }
+ }
+}
diff --git a/Confuser.Core/DnlibUtils.cs b/Confuser.Core/DnlibUtils.cs
index 440b1af97..dd5e12bd6 100644
--- a/Confuser.Core/DnlibUtils.cs
+++ b/Confuser.Core/DnlibUtils.cs
@@ -166,7 +166,9 @@ public static bool IsVisibleOutside(this TypeDef typeDef, bool exeNonPublic = tr
/// The full name of the type of custom attribute.
/// true if the specified object has custom attribute; otherwise, false.
public static bool HasAttribute(this IHasCustomAttribute obj, string fullName) {
- return obj.CustomAttributes.Any(attr => attr.TypeFullName == fullName);
+ // dnlib's CustomAttributeCollection.IsDefined is the by-full-name lookup that dnlib
+ // 4.2 optimized — prefer it over a manual LINQ scan.
+ return obj.CustomAttributes.IsDefined(fullName);
}
///
diff --git a/Confuser.Core/DotNetCorePathResolver.cs b/Confuser.Core/DotNetCorePathResolver.cs
index e056613d6..b35503ce4 100644
--- a/Confuser.Core/DotNetCorePathResolver.cs
+++ b/Confuser.Core/DotNetCorePathResolver.cs
@@ -3,6 +3,7 @@
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
+using Microsoft.Extensions.Logging;
namespace Confuser.Core {
///
@@ -13,7 +14,7 @@ internal static class DotNetCorePathResolver {
/// Resolves runtime assembly paths for a given module by parsing its runtimeconfig.json
/// or probing standard dotnet installation directories.
///
- public static IEnumerable ResolveRuntimePaths(string modulePath, ILogger logger) {
+ public static IEnumerable ResolveRuntimePaths(string modulePath, Microsoft.Extensions.Logging.ILogger logger) {
// 1. Try runtimeconfig.json next to the module (target framework gets priority)
var runtimeConfigPaths = Enumerable.Empty();
var runtimeConfig = FindRuntimeConfig(modulePath);
@@ -39,36 +40,36 @@ static string FindRuntimeConfig(string modulePath) {
return File.Exists(configPath) ? configPath : null;
}
- static IEnumerable GetPathsFromRuntimeConfig(string configPath, ILogger logger) {
+ static IEnumerable GetPathsFromRuntimeConfig(string configPath, Microsoft.Extensions.Logging.ILogger logger) {
string content;
try {
content = File.ReadAllText(configPath);
}
catch (IOException ex) {
- logger.WarnFormat("Failed to read runtime config '{0}': {1}", configPath, ex.Message);
+ logger.LogWarning("Failed to read runtime config '{0}': {1}", configPath, ex.Message);
yield break;
}
catch (UnauthorizedAccessException ex) {
- logger.WarnFormat("Access denied reading runtime config '{0}': {1}", configPath, ex.Message);
+ logger.LogWarning("Access denied reading runtime config '{0}': {1}", configPath, ex.Message);
yield break;
}
var frameworks = ParseFrameworks(content);
if (frameworks.Count == 0) {
- logger.DebugFormat("No framework references found in '{0}'.", configPath);
+ logger.LogDebug("No framework references found in '{0}'.", configPath);
yield break;
}
var dotnetRoot = GetDotNetRoot();
if (dotnetRoot == null) {
- logger.Warn("Could not locate .NET installation directory. Set DOTNET_ROOT environment variable if installed in a non-standard location.");
+ logger.LogWarning("Could not locate .NET installation directory. Set DOTNET_ROOT environment variable if installed in a non-standard location.");
yield break;
}
foreach (var fw in frameworks) {
var sharedDir = Path.Combine(dotnetRoot, "shared", fw.Name);
if (!Directory.Exists(sharedDir)) {
- logger.DebugFormat("Framework directory not found: {0}", sharedDir);
+ logger.LogDebug("Framework directory not found: {0}", sharedDir);
continue;
}
@@ -88,7 +89,7 @@ static IEnumerable GetPathsFromRuntimeConfig(string configPath, ILogger
if (best != null)
yield return best;
else
- logger.WarnFormat("No installed runtime found matching {0} {1} in {2}", fw.Name, fw.Version, sharedDir);
+ logger.LogWarning("No installed runtime found matching {0} {1} in {2}", fw.Name, fw.Version, sharedDir);
}
}
@@ -137,16 +138,16 @@ static string GetMajorMinor(string version) {
return parts.Length >= 2 ? parts[0] + "." + parts[1] : version;
}
- static IEnumerable ProbeAllInstalledRuntimes(ILogger logger) {
+ static IEnumerable ProbeAllInstalledRuntimes(Microsoft.Extensions.Logging.ILogger logger) {
var dotnetRoot = GetDotNetRoot();
if (dotnetRoot == null) {
- logger.Warn("Could not locate .NET installation directory for runtime probing.");
+ logger.LogWarning("Could not locate .NET installation directory for runtime probing.");
yield break;
}
var sharedDir = Path.Combine(dotnetRoot, "shared");
if (!Directory.Exists(sharedDir)) {
- logger.WarnFormat("Shared framework directory not found: {0}", sharedDir);
+ logger.LogWarning("Shared framework directory not found: {0}", sharedDir);
yield break;
}
diff --git a/Confuser.Core/ILogger.cs b/Confuser.Core/ILogger.cs
deleted file mode 100644
index ec55db448..000000000
--- a/Confuser.Core/ILogger.cs
+++ /dev/null
@@ -1,104 +0,0 @@
-using System;
-
-namespace Confuser.Core {
- ///
- /// Defines a logger used to log Confuser events
- ///
- public interface ILogger {
- ///
- /// Logs a message at DEBUG level.
- ///
- /// The message.
- void Debug(string msg);
-
- ///
- /// Logs a message at DEBUG level with specified parameters.
- ///
- /// The format.
- /// The arguments.
- void DebugFormat(string format, params object[] args);
-
- ///
- /// Logs a message at INFO level.
- ///
- /// The message.
- void Info(string msg);
-
- ///
- /// Logs a message at INFO level with specified parameters.
- ///
- /// The format.
- /// The arguments.
- void InfoFormat(string format, params object[] args);
-
- ///
- /// Logs a message at WARN level.
- ///
- /// The message.
- void Warn(string msg);
-
- ///
- /// Logs a message at WARN level with specified parameters.
- ///
- /// The format.
- /// The arguments.
- void WarnFormat(string format, params object[] args);
-
- ///
- /// Logs a message at WARN level with specified exception.
- ///
- /// The message.
- /// The exception.
- void WarnException(string msg, Exception ex);
-
- ///
- /// Logs a message at ERROR level.
- ///
- /// The message.
- void Error(string msg);
-
- ///
- /// Logs a message at ERROR level with specified parameters.
- ///
- /// The format.
- /// The arguments.
- void ErrorFormat(string format, params object[] args);
-
- ///
- /// Logs a message at ERROR level with specified exception.
- ///
- /// The message.
- /// The exception.
- void ErrorException(string msg, Exception ex);
-
- ///
- /// Logs the progress of protection.
- ///
- ///
- /// This method is intended to be used with .
- ///
- ///
- ///
- /// for (int i = 0; i < defs.Length; i++) {
- /// logger.Progress(i + 1, defs.Length);
- /// }
- /// logger.EndProgress();
- ///
- ///
- /// The total work amount .
- /// The amount of work done.
- void Progress(int progress, int overall);
-
- ///
- /// End the progress of protection.
- ///
- ///
- void EndProgress();
-
- ///
- /// Logs the finish of protection.
- ///
- /// Indicated whether the protection process is successful.
- void Finish(bool successful);
- }
-}
diff --git a/Confuser.Core/IProgressReporter.cs b/Confuser.Core/IProgressReporter.cs
new file mode 100644
index 000000000..eb1812e67
--- /dev/null
+++ b/Confuser.Core/IProgressReporter.cs
@@ -0,0 +1,24 @@
+namespace Confuser.Core {
+ ///
+ /// Reports progress and completion of a protection process.
+ ///
+ public interface IProgressReporter {
+ ///
+ /// Reports the progress of protection.
+ ///
+ /// The amount of work done.
+ /// The total work amount.
+ void Progress(int progress, int overall);
+
+ ///
+ /// Signals the end of a progress sequence.
+ ///
+ void EndProgress();
+
+ ///
+ /// Signals the finish of the protection process.
+ ///
+ /// Whether the protection process succeeded.
+ void Finish(bool successful);
+ }
+}
diff --git a/Confuser.Core/Marker.cs b/Confuser.Core/Marker.cs
index 889ae036d..95709a058 100644
--- a/Confuser.Core/Marker.cs
+++ b/Confuser.Core/Marker.cs
@@ -7,6 +7,7 @@
using Confuser.Core.Project;
using Confuser.Core.Project.Patterns;
using dnlib.DotNet;
+using Microsoft.Extensions.Logging;
namespace Confuser.Core {
using Rules = Dictionary;
@@ -83,7 +84,7 @@ public static StrongNamePublicKey LoadSNPubKey(ConfuserContext context, string p
return new StrongNamePublicKey(path);
}
catch (Exception ex) {
- context.Logger.ErrorException("Cannot load the Strong Name Public Key located at: " + path, ex);
+ context.Logger.LogError(ex, "Cannot load the Strong Name Public Key located at: " + path);
throw new ConfuserException(ex);
}
}
@@ -117,7 +118,7 @@ public static StrongNameKey LoadSNKey(ConfuserContext context, string path, stri
return new StrongNameKey(path);
}
catch (Exception ex) {
- context.Logger.ErrorException("Cannot load the Strong Name Key located at: " + path, ex);
+ context.Logger.LogError(ex, "Cannot load the Strong Name Key located at: " + path);
throw new ConfuserException(ex);
}
}
@@ -134,11 +135,11 @@ protected internal virtual MarkerResult MarkProject(ConfuserProject proj, Confus
if (proj.Packer != null) {
if (!packers.ContainsKey(proj.Packer.Id)) {
- context.Logger.ErrorFormat("Cannot find packer with ID '{0}'.", proj.Packer.Id);
+ context.Logger.LogError("Cannot find packer with ID '{0}'.", proj.Packer.Id);
throw new ConfuserException(null);
}
if (proj.Debug)
- context.Logger.Warn("Generated Debug symbols might not be usable with packers!");
+ context.Logger.LogWarning("Generated Debug symbols might not be usable with packers!");
packer = packers[proj.Packer.Id];
packerParams = new Dictionary(proj.Packer, StringComparer.OrdinalIgnoreCase);
@@ -167,7 +168,7 @@ protected internal virtual MarkerResult MarkProject(ConfuserProject proj, Confus
}
foreach (var module in modules) {
- context.Logger.InfoFormat("Loading '{0}'...", module.Item1.Path);
+ context.Logger.LogInformation("Loading '{0}'...", module.Item1.Path);
Rules rules = ParseRules(proj, module.Item1, context);
context.Annotations.Set(module.Item2, SNKey, LoadSNKey(context, module.Item1.SNKeyPath == null ? null : Path.Combine(proj.BaseDirectory, module.Item1.SNKeyPath), module.Item1.SNKeyPassword));
@@ -219,12 +220,12 @@ protected Rules ParseRules(ConfuserProject proj, ProjectModule module, ConfuserC
ret.Add(rule, parser.Parse(rule.Pattern));
}
catch (InvalidPatternException ex) {
- context.Logger.ErrorFormat("Invalid rule pattern: " + rule.Pattern + ": {0}", ex.Message);
+ context.Logger.LogError("Invalid rule pattern: " + rule.Pattern + ": {0}", ex.Message);
throw new ConfuserException(ex);
}
foreach (var setting in rule) {
if (!protections.ContainsKey(setting.Id)) {
- context.Logger.ErrorFormat("Cannot find protection with ID '{0}'.", setting.Id);
+ context.Logger.LogError("Cannot find protection with ID '{0}'.", setting.Id);
throw new ConfuserException(null);
}
}
diff --git a/Confuser.Core/NullLogger.cs b/Confuser.Core/NullLogger.cs
deleted file mode 100644
index c40382274..000000000
--- a/Confuser.Core/NullLogger.cs
+++ /dev/null
@@ -1,64 +0,0 @@
-using System;
-using dnlib.DotNet;
-
-namespace Confuser.Core {
- ///
- /// An implementation that doesn't actually do any logging.
- ///
- internal class NullLogger : ILogger {
- ///
- /// The singleton instance of .
- ///
- public static readonly NullLogger Instance = new NullLogger();
-
- ///
- /// Prevents a default instance of the class from being created.
- ///
- NullLogger() { }
-
- ///
- public void Debug(string msg) { }
-
- ///
- public void DebugFormat(string format, params object[] args) { }
-
- ///
- public void Info(string msg) { }
-
- ///
- public void InfoFormat(string format, params object[] args) { }
-
- ///
- public void Warn(string msg) { }
-
- ///
- public void WarnFormat(string format, params object[] args) { }
-
- ///
- public void WarnException(string msg, Exception ex) { }
-
- ///
- public void Error(string msg) { }
-
- ///
- public void ErrorFormat(string format, params object[] args) { }
-
- ///
- public void ErrorException(string msg, Exception ex) { }
-
- ///
- public void Progress(int overall, int progress) { }
-
- ///
- public void EndProgress() { }
-
- ///
- public void Finish(bool successful) { }
-
- ///
- public void BeginModule(ModuleDef module) { }
-
- ///
- public void EndModule(ModuleDef module) { }
- }
-}
diff --git a/Confuser.Core/NullProgressReporter.cs b/Confuser.Core/NullProgressReporter.cs
new file mode 100644
index 000000000..53901f472
--- /dev/null
+++ b/Confuser.Core/NullProgressReporter.cs
@@ -0,0 +1,14 @@
+namespace Confuser.Core {
+ ///
+ /// An implementation that discards all progress reports.
+ ///
+ internal sealed class NullProgressReporter : IProgressReporter {
+ public static readonly NullProgressReporter Instance = new NullProgressReporter();
+
+ NullProgressReporter() { }
+
+ public void Progress(int progress, int overall) { }
+ public void EndProgress() { }
+ public void Finish(bool successful) { }
+ }
+}
diff --git a/Confuser.Core/ObfAttrMarker.cs b/Confuser.Core/ObfAttrMarker.cs
index 1c8cb068c..8dbe5ecca 100644
--- a/Confuser.Core/ObfAttrMarker.cs
+++ b/Confuser.Core/ObfAttrMarker.cs
@@ -8,6 +8,7 @@
using Confuser.Core.Project;
using Confuser.Core.Project.Patterns;
using dnlib.DotNet;
+using Microsoft.Extensions.Logging;
namespace Confuser.Core {
using Rules = Dictionary;
@@ -221,7 +222,7 @@ bool ToInfo(ObfuscationAttributeInfo attr, out ProtectionSettingsInfo info) {
}
if (!ok) {
- context.Logger.WarnFormat("Ignoring rule '{0}' in {1}.", info.Settings, attr.Owner);
+ context.Logger.LogWarning("Ignoring rule '{0}' in {1}.", info.Settings, attr.Owner);
return false;
}
@@ -299,7 +300,7 @@ protected internal override MarkerResult MarkProject(ConfuserProject proj, Confu
if (proj.Packer != null) {
if (!packers.ContainsKey(proj.Packer.Id)) {
- context.Logger.ErrorFormat("Cannot find packer with ID '{0}'.", proj.Packer.Id);
+ context.Logger.LogError("Cannot find packer with ID '{0}'.", proj.Packer.Id);
throw new ConfuserException(null);
}
@@ -325,20 +326,20 @@ protected internal override MarkerResult MarkProject(ConfuserProject proj, Confu
modules.Add(Tuple.Create(module, modDef));
}
catch (BadImageFormatException ex) {
- context.Logger.ErrorFormat("Failed to load \"{0}\" - Assembly does not appear to be a .NET assembly: \"{1}\".", module.Path, ex.Message);
+ context.Logger.LogError("Failed to load \"{0}\" - Assembly does not appear to be a .NET assembly: \"{1}\".", module.Path, ex.Message);
if (module.Path.EndsWith(".exe", StringComparison.OrdinalIgnoreCase)) {
var dllPath = Path.ChangeExtension(module.Path, ".dll");
var fullDllPath = Path.Combine(proj.BaseDirectory, dllPath);
if (File.Exists(fullDllPath))
- context.Logger.ErrorFormat("Hint: .NET 6+ apps use a native host .exe — try obfuscating \"{0}\" instead.", dllPath);
+ context.Logger.LogError("Hint: .NET 6+ apps use a native host .exe — try obfuscating \"{0}\" instead.", dllPath);
else
- context.Logger.Error("Hint: For .NET 6+ apps, obfuscate the .dll file, not the .exe (which is a native host stub).");
+ context.Logger.LogError("Hint: For .NET 6+ apps, obfuscate the .dll file, not the .exe (which is a native host stub).");
}
throw new ConfuserException(ex);
}
}
foreach (var module in modules) {
- context.Logger.InfoFormat("Loading '{0}'...", module.Item1.Path);
+ context.Logger.LogInformation("Loading '{0}'...", module.Item1.Path);
Rules rules = ParseRules(proj, module.Item1, context);
MarkModule(module.Item1, module.Item2, rules, module == modules[0]);
@@ -351,7 +352,7 @@ protected internal override MarkerResult MarkProject(ConfuserProject proj, Confu
}
if (proj.Debug && proj.Packer != null)
- context.Logger.Warn("Generated Debug symbols might not be usable with packers!");
+ context.Logger.LogWarning("Generated Debug symbols might not be usable with packers!");
return new MarkerResult(modules.Select(module => module.Item2).ToList(), packer, extModules);
}
@@ -384,7 +385,7 @@ ProtectionSettingsInfo AddRule(ObfuscationAttributeInfo attr, List
@@ -73,7 +74,8 @@ protected void ProtectStub(ConfuserContext context, string fileName, byte[] modu
ConfuserEngine
.Run(
new ConfuserParameters {
- Logger = new PackerLogger(context.Logger),
+ Logger = context.Logger,
+ ProgressReporter = new PackerProgressReporter(context.ProgressReporter, context.Logger),
PluginDiscovery = discovery,
Marker = new PackerMarker(snKey, snPubKey, snDelaySig, snSigKey, snPubSigKey),
Project = proj,
@@ -81,7 +83,7 @@ protected void ProtectStub(ConfuserContext context, string fileName, byte[] modu
}, context.token).Wait();
}
catch (AggregateException ex) {
- context.Logger.Error("Failed to protect packer stub.");
+ context.Logger.LogError("Failed to protect packer stub.");
throw new ConfuserException(ex);
}
@@ -95,71 +97,33 @@ protected void ProtectStub(ConfuserContext context, string fileName, byte[] modu
}
}
catch (IOException ex) {
- context.Logger.WarnException("Failed to remove temporary files of packer.", ex);
+ context.Logger.LogWarning(ex, "Failed to remove temporary files of packer.");
}
}
}
}
- internal class PackerLogger : ILogger {
- readonly ILogger baseLogger;
+ internal class PackerProgressReporter : IProgressReporter {
+ readonly IProgressReporter baseReporter;
+ readonly Microsoft.Extensions.Logging.ILogger baseLogger;
- public PackerLogger(ILogger baseLogger) {
+ public PackerProgressReporter(IProgressReporter baseReporter, Microsoft.Extensions.Logging.ILogger baseLogger) {
+ this.baseReporter = baseReporter;
this.baseLogger = baseLogger;
}
- public void Debug(string msg) {
- baseLogger.Debug(msg);
- }
-
- public void DebugFormat(string format, params object[] args) {
- baseLogger.DebugFormat(format, args);
- }
-
- public void Info(string msg) {
- baseLogger.Info(msg);
- }
-
- public void InfoFormat(string format, params object[] args) {
- baseLogger.InfoFormat(format, args);
- }
-
- public void Warn(string msg) {
- baseLogger.Warn(msg);
- }
-
- public void WarnFormat(string format, params object[] args) {
- baseLogger.WarnFormat(format, args);
- }
-
- public void WarnException(string msg, Exception ex) {
- baseLogger.WarnException(msg, ex);
- }
-
- public void Error(string msg) {
- baseLogger.Error(msg);
- }
-
- public void ErrorFormat(string format, params object[] args) {
- baseLogger.ErrorFormat(format, args);
- }
-
- public void ErrorException(string msg, Exception ex) {
- baseLogger.ErrorException(msg, ex);
- }
-
public void Progress(int progress, int overall) {
- baseLogger.Progress(progress, overall);
+ baseReporter.Progress(progress, overall);
}
public void EndProgress() {
- baseLogger.EndProgress();
+ baseReporter.EndProgress();
}
public void Finish(bool successful) {
if (!successful)
throw new ConfuserException(null);
- baseLogger.Info("Finish protecting packer stub.");
+ baseLogger.LogInformation("Finish protecting packer stub.");
}
}
diff --git a/Confuser.Core/PluginDiscovery.cs b/Confuser.Core/PluginDiscovery.cs
index 642dafcf8..c42de92d6 100644
--- a/Confuser.Core/PluginDiscovery.cs
+++ b/Confuser.Core/PluginDiscovery.cs
@@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.IO;
using System.Reflection;
+using Microsoft.Extensions.Logging;
namespace Confuser.Core {
///
@@ -64,7 +65,7 @@ protected static void AddPlugins(
protections.Add((Protection)Activator.CreateInstance(i));
}
catch (Exception ex) {
- context.Logger.ErrorException("Failed to instantiate protection '" + i.Name + "'.", ex);
+ context.Logger.LogError(ex, "Failed to instantiate protection '" + i.Name + "'.");
}
}
else if (typeof(Packer).IsAssignableFrom(i)) {
@@ -72,7 +73,7 @@ protected static void AddPlugins(
packers.Add((Packer)Activator.CreateInstance(i));
}
catch (Exception ex) {
- context.Logger.ErrorException("Failed to instantiate packer '" + i.Name + "'.", ex);
+ context.Logger.LogError(ex, "Failed to instantiate packer '" + i.Name + "'.");
}
}
else if (typeof(ConfuserComponent).IsAssignableFrom(i)) {
@@ -80,7 +81,7 @@ protected static void AddPlugins(
components.Add((ConfuserComponent)Activator.CreateInstance(i));
}
catch (Exception ex) {
- context.Logger.ErrorException("Failed to instantiate component '" + i.Name + "'.", ex);
+ context.Logger.LogError(ex, "Failed to instantiate component '" + i.Name + "'.");
}
}
}
@@ -103,7 +104,7 @@ protected virtual void GetPluginsInternal(
AddPlugins(context, protections, packers, components, protAsm);
}
catch (Exception ex) {
- context.Logger.WarnException("Failed to load built-in protections.", ex);
+ context.Logger.LogWarning(ex, "Failed to load built-in protections.");
}
try {
@@ -111,7 +112,7 @@ protected virtual void GetPluginsInternal(
AddPlugins(context, protections, packers, components, renameAsm);
}
catch (Exception ex) {
- context.Logger.WarnException("Failed to load renamer.", ex);
+ context.Logger.LogWarning(ex, "Failed to load renamer.");
}
try {
@@ -119,7 +120,7 @@ protected virtual void GetPluginsInternal(
AddPlugins(context, protections, packers, components, renameAsm);
}
catch (Exception ex) {
- context.Logger.WarnException("Failed to load dynamic cipher library.", ex);
+ context.Logger.LogWarning(ex, "Failed to load dynamic cipher library.");
}
foreach (string pluginPath in context.Project.PluginPaths) {
@@ -129,7 +130,7 @@ protected virtual void GetPluginsInternal(
AddPlugins(context, protections, packers, components, plugin);
}
catch (Exception ex) {
- context.Logger.WarnException("Failed to load plugin '" + pluginPath + "'.", ex);
+ context.Logger.LogWarning(ex, "Failed to load plugin '" + pluginPath + "'.");
}
}
}
diff --git a/Confuser.Core/ProtectionPipeline.cs b/Confuser.Core/ProtectionPipeline.cs
index 79da655dc..ba798f559 100644
--- a/Confuser.Core/ProtectionPipeline.cs
+++ b/Confuser.Core/ProtectionPipeline.cs
@@ -3,6 +3,7 @@
using System.Diagnostics;
using System.Linq;
using dnlib.DotNet;
+using Microsoft.Extensions.Logging;
namespace Confuser.Core {
///
@@ -127,14 +128,14 @@ public T FindPhase() where T : ProtectionPhase {
internal void ExecuteStage(PipelineStage stage, Action func, Func> targets, ConfuserContext context) {
foreach (ProtectionPhase pre in preStage[stage]) {
context.CheckCancellation();
- context.Logger.DebugFormat("Executing '{0}' phase...", pre.Name);
+ context.Logger.LogDebug("Executing '{0}' phase...", pre.Name);
pre.Execute(context, new ProtectionParameters(pre.Parent, Filter(context, targets(), pre)));
}
context.CheckCancellation();
func(context);
context.CheckCancellation();
foreach (ProtectionPhase post in postStage[stage]) {
- context.Logger.DebugFormat("Executing '{0}' phase...", post.Name);
+ context.Logger.LogDebug("Executing '{0}' phase...", post.Name);
post.Execute(context, new ProtectionParameters(post.Parent, Filter(context, targets(), post)));
context.CheckCancellation();
}
@@ -170,7 +171,7 @@ static IList Filter(ConfuserContext context, IList targets
ProtectionSettings parameters = ProtectionParameters.GetParameters(context, def);
Debug.Assert(parameters != null);
if (parameters == null) {
- context.Logger.ErrorFormat("'{0}' not marked for obfuscation, possibly a bug.", def);
+ context.Logger.LogError("'{0}' not marked for obfuscation, possibly a bug.", def);
throw new ConfuserException(null);
}
return parameters.ContainsKey(phase.Parent);
diff --git a/Confuser.Core/Utils.cs b/Confuser.Core/Utils.cs
index e941cc1e6..9df5c0d01 100644
--- a/Confuser.Core/Utils.cs
+++ b/Confuser.Core/Utils.cs
@@ -204,33 +204,33 @@ public static void RemoveWhere(this IList self, Predicate match) {
}
///
- /// Returns a that log the progress of iterating the specified list.
+ /// Returns a that reports the progress of iterating the specified list.
///
/// The type of list element
/// The list.
- /// The logger.
+ /// The progress reporter.
/// A wrapper of the list.
- public static IEnumerable WithProgress(this IEnumerable enumerable, ILogger logger) {
+ public static IEnumerable WithProgress(this IEnumerable enumerable, IProgressReporter reporter) {
switch (enumerable) {
case IReadOnlyCollection readOnlyCollection:
- return WithProgress(enumerable, readOnlyCollection.Count, logger);
+ return WithProgress(enumerable, readOnlyCollection.Count, reporter);
case ICollection collection:
- return WithProgress(enumerable, collection.Count, logger);
+ return WithProgress(enumerable, collection.Count, reporter);
default:
var buffered = enumerable.ToList();
- return WithProgress(buffered, buffered.Count, logger);
+ return WithProgress(buffered, buffered.Count, reporter);
}
}
- public static IEnumerable WithProgress(this IEnumerable enumerable, int totalCount, ILogger logger) {
+ public static IEnumerable WithProgress(this IEnumerable enumerable, int totalCount, IProgressReporter reporter) {
var counter = 0;
foreach (var obj in enumerable) {
- logger.Progress(counter, totalCount);
+ reporter.Progress(counter, totalCount);
yield return obj;
counter++;
}
- logger.Progress(totalCount, totalCount);
- logger.EndProgress();
+ reporter.Progress(totalCount, totalCount);
+ reporter.EndProgress();
}
}
}
diff --git a/Confuser.Core/WatermarkingProtection.cs b/Confuser.Core/WatermarkingProtection.cs
index 1b1cdb967..7fff32205 100644
--- a/Confuser.Core/WatermarkingProtection.cs
+++ b/Confuser.Core/WatermarkingProtection.cs
@@ -2,6 +2,7 @@
using Confuser.Core.Services;
using dnlib.DotNet;
using dnlib.DotNet.Emit;
+using Microsoft.Extensions.Logging;
namespace Confuser.Core {
public sealed class WatermarkingProtection : Protection {
@@ -45,7 +46,7 @@ public WatermarkingPhase(ConfuserComponent parent) : base(parent) { }
protected internal override void Execute(ConfuserContext context, ProtectionParameters parameters) {
var marker = context.Registry.GetService();
- context.Logger.Debug("Watermarking...");
+ context.Logger.LogDebug("Watermarking...");
foreach (var module in parameters.Targets.OfType()) {
var attrRef = module.CorLibTypes.GetTypeRef("System", "Attribute");
var attrType = module.FindNormal("ConfusedByAttribute");
diff --git a/Confuser.MSBuild.Tasks/ConfuseTask.cs b/Confuser.MSBuild.Tasks/ConfuseTask.cs
index f4526c910..02986a3f0 100644
--- a/Confuser.MSBuild.Tasks/ConfuseTask.cs
+++ b/Confuser.MSBuild.Tasks/ConfuseTask.cs
@@ -24,17 +24,18 @@ public override bool Execute() {
project.Load(xmlDoc);
project.OutputDirectory = Path.GetDirectoryName(Path.GetFullPath(OutputAssembly.ItemSpec));
- var logger = new MSBuildLogger(Log);
+ var progressReporter = new MSBuildProgressReporter();
var parameters = new ConfuserParameters {
Project = project,
- Logger = logger
+ Logger = new MSBuildMelLogger(Log),
+ ProgressReporter = progressReporter
};
ConfuserEngine.Run(parameters).Wait();
ConfusedFiles = project.Select(m => new TaskItem(Path.Combine(project.OutputDirectory, m.Path))).Cast().ToArray();
- return !logger.HasError;
+ return !progressReporter.HasError;
}
}
}
diff --git a/Confuser.MSBuild.Tasks/MSBuildLogger.cs b/Confuser.MSBuild.Tasks/MSBuildLogger.cs
index fdcb2319b..8f1f7c81f 100644
--- a/Confuser.MSBuild.Tasks/MSBuildLogger.cs
+++ b/Confuser.MSBuild.Tasks/MSBuildLogger.cs
@@ -1,61 +1,54 @@
-using System;
+using System;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
-using ILogger = Confuser.Core.ILogger;
+using Microsoft.Extensions.Logging;
namespace Confuser.MSBuild.Tasks {
- internal sealed class MSBuildLogger : ILogger {
+ internal sealed class MSBuildMelLogger : Microsoft.Extensions.Logging.ILogger {
private readonly TaskLoggingHelper loggingHelper;
- internal bool HasError { get; private set; }
-
- internal MSBuildLogger(TaskLoggingHelper loggingHelper) =>
+ internal MSBuildMelLogger(TaskLoggingHelper loggingHelper) =>
this.loggingHelper = loggingHelper ?? throw new ArgumentNullException(nameof(loggingHelper));
- void ILogger.Debug(string msg) => loggingHelper.LogMessage(MessageImportance.Low, "[DEBUG] " + msg);
-
- void ILogger.DebugFormat(string format, params object[] args) {
- loggingHelper.LogMessage(MessageImportance.Low, "[DEBUG] " + format, args);
+ public IDisposable BeginScope(TState state) => null;
+
+ public bool IsEnabled(LogLevel logLevel) => true;
+
+ public void Log(LogLevel logLevel, EventId eventId, TState state,
+ Exception exception, Func formatter) {
+ var message = formatter(state, exception);
+ switch (logLevel) {
+ case LogLevel.Trace:
+ case LogLevel.Debug:
+ loggingHelper.LogMessage(MessageImportance.Low, message);
+ break;
+ case LogLevel.Information:
+ loggingHelper.LogMessage(MessageImportance.Normal, message);
+ break;
+ case LogLevel.Warning:
+ loggingHelper.LogWarning(message);
+ if (exception != null) loggingHelper.LogWarningFromException(exception);
+ break;
+ case LogLevel.Error:
+ case LogLevel.Critical:
+ loggingHelper.LogError(message);
+ if (exception != null) loggingHelper.LogErrorFromException(exception);
+ break;
+ }
}
+ }
- void ILogger.EndProgress() { }
+ internal sealed class MSBuildProgressReporter : Confuser.Core.IProgressReporter {
+ internal bool HasError { get; private set; }
- void ILogger.Error(string msg) {
- loggingHelper.LogError(msg);
- HasError = true;
- }
+ void Confuser.Core.IProgressReporter.Progress(int progress, int overall) { }
- void ILogger.ErrorException(string msg, Exception ex) {
- loggingHelper.LogError(msg);
- loggingHelper.LogErrorFromException(ex);
- HasError = true;
- }
+ void Confuser.Core.IProgressReporter.EndProgress() { }
- void ILogger.ErrorFormat(string format, params object[] args) {
- loggingHelper.LogError(format, args);
- HasError = true;
- }
-
- void ILogger.Finish(bool successful) {
+ void Confuser.Core.IProgressReporter.Finish(bool successful) {
if (!successful) {
- HasError = false;
+ HasError = true;
}
}
-
- void ILogger.Info(string msg) => loggingHelper.LogMessage(MessageImportance.Normal, msg);
-
- void ILogger.InfoFormat(string format, params object[] args) =>
- loggingHelper.LogMessage(MessageImportance.Normal, format, args);
-
- void ILogger.Progress(int progress, int overall) { }
-
- void ILogger.Warn(string msg) => loggingHelper.LogWarning(msg);
-
- void ILogger.WarnException(string msg, Exception ex) {
- loggingHelper.LogWarning(msg);
- loggingHelper.LogWarningFromException(ex);
- }
-
- void ILogger.WarnFormat(string format, params object[] args) => loggingHelper.LogWarning(format, args);
}
}
diff --git a/Confuser.Protections/AntiTamper/JITBody.cs b/Confuser.Protections/AntiTamper/JITBody.cs
index f6c799eee..7149899f8 100644
--- a/Confuser.Protections/AntiTamper/JITBody.cs
+++ b/Confuser.Protections/AntiTamper/JITBody.cs
@@ -47,6 +47,12 @@ public uint GetVirtualSize() {
return GetFileLength();
}
+ // dnlib 4.x added IChunk.CalculateAlignment. Return 0 for default/no alignment,
+ // matching the implicit behaviour before the method existed in 3.x.
+ public uint CalculateAlignment() {
+ return 0;
+ }
+
public void WriteTo(DataWriter writer) {
writer.WriteUInt32((uint)(Body.Length >> 2));
writer.WriteBytes(Body);
@@ -224,6 +230,12 @@ public uint GetVirtualSize() {
return GetFileLength();
}
+ // dnlib 4.x added IChunk.CalculateAlignment. Return 0 for default/no alignment,
+ // matching the implicit behaviour before the method existed in 3.x.
+ public uint CalculateAlignment() {
+ return 0;
+ }
+
public void WriteTo(DataWriter writer) {
uint length = GetFileLength() - 4; // minus length field
writer.WriteUInt32((uint)bodies.Count);
diff --git a/Confuser.Protections/AntiTamper/JITMode.cs b/Confuser.Protections/AntiTamper/JITMode.cs
index 23f41e90f..074c54b40 100644
--- a/Confuser.Protections/AntiTamper/JITMode.cs
+++ b/Confuser.Protections/AntiTamper/JITMode.cs
@@ -12,6 +12,7 @@
using dnlib.DotNet.Emit;
using dnlib.DotNet.MD;
using dnlib.DotNet.Writer;
+using Microsoft.Extensions.Logging;
namespace Confuser.Protections.AntiTamper {
internal class JITMode : IModeHandler {
@@ -154,11 +155,11 @@ public void HandleMD(AntiTamperProtection parent, ConfuserContext context, Prote
void OnWriterEvent(object sender, ModuleWriterEventArgs e) {
var writer = (ModuleWriterBase)sender;
if (e.Event == ModuleWriterEvent.MDBeginWriteMethodBodies) {
- context.Logger.Debug("Extracting method bodies...");
+ context.Logger.LogDebug("Extracting method bodies...");
CreateSection(writer);
}
else if (e.Event == ModuleWriterEvent.BeginStrongNameSign) {
- context.Logger.Debug("Encrypting method section...");
+ context.Logger.LogDebug("Encrypting method section...");
EncryptSection(writer);
}
}
@@ -210,7 +211,7 @@ void CreateSection(ModuleWriterBase writer) {
newSection.Add(bodyIndex, 0x10);
// save methods
- foreach (MethodDef method in methods.WithProgress(context.Logger)) {
+ foreach (MethodDef method in methods.WithProgress(context.ProgressReporter)) {
if (!method.HasBody)
continue;
diff --git a/Confuser.Protections/Compress/Compressor.cs b/Confuser.Protections/Compress/Compressor.cs
index 28e8ec5cd..ca81339db 100644
--- a/Confuser.Protections/Compress/Compressor.cs
+++ b/Confuser.Protections/Compress/Compressor.cs
@@ -15,6 +15,7 @@
using dnlib.DotNet.MD;
using dnlib.DotNet.Writer;
using dnlib.PE;
+using Microsoft.Extensions.Logging;
using FileAttributes = dnlib.DotNet.FileAttributes;
using SR = System.Reflection;
@@ -50,7 +51,7 @@ protected override void PopulatePipeline(ProtectionPipeline pipeline) {
protected override void Pack(ConfuserContext context, ProtectionParameters parameters) {
var ctx = context.Annotations.Get(context, ContextKey);
if (ctx == null) {
- context.Logger.Error("No executable module!");
+ context.Logger.LogError("No executable module!");
throw new ConfuserException(null);
}
@@ -164,7 +165,7 @@ void PackModules(ConfuserContext context, CompressorContext compCtx, ModuleDef s
state = state * 0x5e3f1f + chr;
byte[] encrypted = compCtx.Encrypt(comp, entry.Value, state, progress => {
progress = (progress + moduleIndex) / modules.Count;
- context.Logger.Progress((int)(progress * 10000), 10000);
+ context.ProgressReporter.Progress((int)(progress * 10000), 10000);
});
context.CheckCancellation();
@@ -172,7 +173,7 @@ void PackModules(ConfuserContext context, CompressorContext compCtx, ModuleDef s
stubModule.Resources.Add(resource);
moduleIndex++;
}
- context.Logger.EndProgress();
+ context.ProgressReporter.EndProgress();
}
void InjectData(ConfuserContext context, ModuleDef stubModule, MethodDef method, byte[] data) {
@@ -222,7 +223,7 @@ void InjectStub(ConfuserContext context, CompressorContext compCtx, ProtectionPa
}
compCtx.Deriver.Init(context, random);
- context.Logger.Debug("Encrypting modules...");
+ context.Logger.LogDebug("Encrypting modules...");
// Main
MethodDef entryPoint = defs.OfType().Single(method => method.Name == "Main");
@@ -245,8 +246,8 @@ void InjectStub(ConfuserContext context, CompressorContext compCtx, ProtectionPa
compCtx.OriginModule = context.OutputModules[compCtx.ModuleIndex];
byte[] encryptedModule = compCtx.Encrypt(comp, compCtx.OriginModule, seed,
- progress => context.Logger.Progress((int)(progress * 10000), 10000));
- context.Logger.EndProgress();
+ progress => context.ProgressReporter.Progress((int)(progress * 10000), 10000));
+ context.ProgressReporter.EndProgress();
context.CheckCancellation();
compCtx.EncryptedModule = encryptedModule;
diff --git a/Confuser.Protections/Compress/ExtractPhase.cs b/Confuser.Protections/Compress/ExtractPhase.cs
index 24fd835b0..6fc5e39aa 100644
--- a/Confuser.Protections/Compress/ExtractPhase.cs
+++ b/Confuser.Protections/Compress/ExtractPhase.cs
@@ -7,6 +7,7 @@
using dnlib.DotNet;
using dnlib.DotNet.MD;
using dnlib.DotNet.Writer;
+using Microsoft.Extensions.Logging;
namespace Confuser.Protections.Compress {
internal class ExtractPhase : ProtectionPhase {
@@ -29,7 +30,7 @@ protected override void Execute(ConfuserContext context, ProtectionParameters pa
if (context.Annotations.Get(context, Compressor.ContextKey) != null) {
if (isExe) {
- context.Logger.Error("Too many executable modules!");
+ context.Logger.LogError("Too many executable modules!");
throw new ConfuserException(null);
}
return;
diff --git a/Confuser.Protections/Constants/EncodePhase.cs b/Confuser.Protections/Constants/EncodePhase.cs
index c108d14d4..b3db5bd68 100644
--- a/Confuser.Protections/Constants/EncodePhase.cs
+++ b/Confuser.Protections/Constants/EncodePhase.cs
@@ -38,12 +38,12 @@ protected override void Execute(ConfuserContext context, ProtectionParameters pa
// Encode constants
moduleCtx.ReferenceRepl = new Dictionary>>();
moduleCtx.EncodedBuffer = new List();
- foreach (var entry in ldInit.WithProgress(context.Logger)) // Ensure the array length haven't been encoded yet
+ foreach (var entry in ldInit.WithProgress(context.ProgressReporter)) // Ensure the array length haven't been encoded yet
{
EncodeInitializer(moduleCtx, entry.Key, entry.Value);
context.CheckCancellation();
}
- foreach (var entry in ldc.WithProgress(context.Logger)) {
+ foreach (var entry in ldc.WithProgress(context.ProgressReporter)) {
if (entry.Key is string) {
EncodeString(moduleCtx, (string)entry.Key, entry.Value);
}
@@ -234,7 +234,7 @@ void ExtractConstants(
Dictionary>> ldInit) {
var dataFields = new HashSet();
var fieldRefs = new HashSet();
- foreach (MethodDef method in parameters.Targets.OfType().WithProgress(context.Logger)) {
+ foreach (MethodDef method in parameters.Targets.OfType().WithProgress(context.ProgressReporter)) {
if (!method.HasBody)
continue;
diff --git a/Confuser.Protections/ControlFlow/ControlFlowPhase.cs b/Confuser.Protections/ControlFlow/ControlFlowPhase.cs
index 3f1b15e28..addeb6257 100644
--- a/Confuser.Protections/ControlFlow/ControlFlowPhase.cs
+++ b/Confuser.Protections/ControlFlow/ControlFlowPhase.cs
@@ -9,6 +9,7 @@
using dnlib.DotNet.MD;
using dnlib.DotNet.Pdb;
using dnlib.DotNet.Writer;
+using Microsoft.Extensions.Logging;
namespace Confuser.Protections.ControlFlow {
internal class ControlFlowPhase : ProtectionPhase {
@@ -74,7 +75,7 @@ protected override void Execute(ConfuserContext context, ProtectionParameters pa
bool disabledOpti = DisabledOptimization(context.CurrentModule);
RandomGenerator random = context.Registry.GetService().GetRandomGenerator(ControlFlowProtection._FullId);
- foreach (MethodDef method in parameters.Targets.OfType().WithProgress(context.Logger))
+ foreach (MethodDef method in parameters.Targets.OfType().WithProgress(context.ProgressReporter))
if (method.HasBody && method.Body.Instructions.Count > 0) {
ProcessMethod(method.Body, ParseParameters(method, context, parameters, random, disabledOpti));
context.CheckCancellation();
@@ -90,7 +91,7 @@ static ManglerBase GetMangler(CFType type) {
void ProcessMethod(CilBody body, CFContext ctx) {
uint maxStack;
if (!MaxStackCalculator.GetMaxStack(body.Instructions, body.ExceptionHandlers, out maxStack)) {
- ctx.Context.Logger.Error("Failed to calcuate maxstack.");
+ ctx.Context.Logger.LogError("Failed to calcuate maxstack.");
throw new ConfuserException(null);
}
body.MaxStack = (ushort)maxStack;
diff --git a/Confuser.Protections/HardeningPhase.cs b/Confuser.Protections/HardeningPhase.cs
index 17dabdfe4..0e5defa8c 100644
--- a/Confuser.Protections/HardeningPhase.cs
+++ b/Confuser.Protections/HardeningPhase.cs
@@ -4,6 +4,7 @@
using Confuser.Core.Services;
using dnlib.DotNet;
using dnlib.DotNet.Emit;
+using Microsoft.Extensions.Logging;
namespace Confuser.Protections {
internal sealed class HardeningPhase : ProtectionPhase {
@@ -31,7 +32,7 @@ protected override void Execute(ConfuserContext context, ProtectionParameters pa
private static void HardenMethod(ConfuserContext context, ModuleDef module) {
var cctor = module.GlobalType.FindStaticConstructor();
if (cctor == null) {
- context.Logger.Debug("No .cctor containing protection code found. Nothing to do.");
+ context.Logger.LogDebug("No .cctor containing protection code found. Nothing to do.");
return;
}
diff --git a/Confuser.Protections/ReferenceProxy/ReferenceProxyPhase.cs b/Confuser.Protections/ReferenceProxy/ReferenceProxyPhase.cs
index 3e299f2fa..e08baf3d4 100644
--- a/Confuser.Protections/ReferenceProxy/ReferenceProxyPhase.cs
+++ b/Confuser.Protections/ReferenceProxy/ReferenceProxyPhase.cs
@@ -103,7 +103,7 @@ protected override void Execute(ConfuserContext context, ProtectionParameters pa
var store = new RPStore { random = random };
- foreach (MethodDef method in parameters.Targets.OfType().WithProgress(context.Logger))
+ foreach (MethodDef method in parameters.Targets.OfType().WithProgress(context.ProgressReporter))
if (method.HasBody && method.Body.Instructions.Count > 0) {
ProcessMethod(ParseParameters(method, context, parameters, store));
context.CheckCancellation();
diff --git a/Confuser.Protections/Resources/InjectPhase.cs b/Confuser.Protections/Resources/InjectPhase.cs
index 3d086568b..de803fd58 100644
--- a/Confuser.Protections/Resources/InjectPhase.cs
+++ b/Confuser.Protections/Resources/InjectPhase.cs
@@ -10,6 +10,7 @@
using Confuser.Renamer;
using dnlib.DotNet;
using dnlib.DotNet.Emit;
+using Microsoft.Extensions.Logging;
namespace Confuser.Protections.Resources {
internal class InjectPhase : ProtectionPhase {
@@ -27,7 +28,7 @@ public override string Name {
protected override void Execute(ConfuserContext context, ProtectionParameters parameters) {
if (parameters.Targets.Any()) {
if (!UTF8String.IsNullOrEmpty(context.CurrentModule.Assembly.Culture)) {
- context.Logger.DebugFormat("Skipping resource encryption for satellite assembly '{0}'.",
+ context.Logger.LogDebug("Skipping resource encryption for satellite assembly '{0}'.",
context.CurrentModule.Assembly.FullName);
return;
}
diff --git a/Confuser.Protections/Resources/MDPhase.cs b/Confuser.Protections/Resources/MDPhase.cs
index 0f676cd26..741ebbf32 100644
--- a/Confuser.Protections/Resources/MDPhase.cs
+++ b/Confuser.Protections/Resources/MDPhase.cs
@@ -10,6 +10,7 @@
using dnlib.DotNet;
using dnlib.DotNet.MD;
using dnlib.DotNet.Writer;
+using Microsoft.Extensions.Logging;
namespace Confuser.Protections.Resources {
internal class MDPhase {
@@ -28,7 +29,7 @@ void OnWriterEvent(object sender, ModuleWriterEventArgs e) {
var writer = (ModuleWriterBase)sender;
if (e.Event == ModuleWriterEvent.MDBeginAddResources) {
ctx.Context.CheckCancellation();
- ctx.Context.Logger.Debug("Encrypting resources...");
+ ctx.Context.Logger.LogDebug("Encrypting resources...");
bool hasPacker = ctx.Context.Packer != null;
List resources = ctx.Module.Resources.OfType().ToList();
@@ -68,8 +69,8 @@ void OnWriterEvent(object sender, ModuleWriterEventArgs e) {
// compress
moduleBuff = ctx.Context.Registry.GetService().Compress(
moduleBuff,
- progress => ctx.Context.Logger.Progress((int)(progress * 10000), 10000));
- ctx.Context.Logger.EndProgress();
+ progress => ctx.Context.ProgressReporter.Progress((int)(progress * 10000), 10000));
+ ctx.Context.ProgressReporter.EndProgress();
ctx.Context.CheckCancellation();
uint compressedLen = (uint)(moduleBuff.Length + 3) / 4;
diff --git a/Confuser.Protections/TypeScrambler/AnalyzePhase.cs b/Confuser.Protections/TypeScrambler/AnalyzePhase.cs
index fbec05899..5c8304208 100644
--- a/Confuser.Protections/TypeScrambler/AnalyzePhase.cs
+++ b/Confuser.Protections/TypeScrambler/AnalyzePhase.cs
@@ -19,7 +19,7 @@ protected override void Execute(ConfuserContext context, ProtectionParameters pa
var typeService = context.Registry.GetService();
Debug.Assert(typeService != null, $"{nameof(typeService)} != null");
- foreach (var target in parameters.Targets.WithProgress(context.Logger)) {
+ foreach (var target in parameters.Targets.WithProgress(context.ProgressReporter)) {
switch (target) {
case TypeDef typeDef:
typeService.AddScannedItem(new ScannedType(typeDef));
diff --git a/Confuser.Protections/TypeScrambler/ScramblePhase.cs b/Confuser.Protections/TypeScrambler/ScramblePhase.cs
index 6fc86bfd0..1af58491f 100644
--- a/Confuser.Protections/TypeScrambler/ScramblePhase.cs
+++ b/Confuser.Protections/TypeScrambler/ScramblePhase.cs
@@ -27,7 +27,7 @@ protected override void Execute(ConfuserContext context, ProtectionParameters pa
// In this stage the references to the scrambled types need to be fixed. This needs to be done for all
// methods in the assembly, because all methods may contain references to the scrambled types and methods.
- foreach (var def in context.CurrentModule.FindDefinitions().WithProgress(context.Logger)) {
+ foreach (var def in context.CurrentModule.FindDefinitions().WithProgress(context.ProgressReporter)) {
switch (def) {
case MethodDef md:
if (md.HasReturnType)
diff --git a/Confuser.Renamer/AnalyzePhase.cs b/Confuser.Renamer/AnalyzePhase.cs
index 4d95e27d1..94e9f7753 100644
--- a/Confuser.Renamer/AnalyzePhase.cs
+++ b/Confuser.Renamer/AnalyzePhase.cs
@@ -6,6 +6,7 @@
using Confuser.Core.Services;
using Confuser.Renamer.Analyzers;
using dnlib.DotNet;
+using Microsoft.Extensions.Logging;
namespace Confuser.Renamer {
internal class AnalyzePhase : ProtectionPhase {
@@ -34,12 +35,12 @@ void ParseParameters(IDnlibDef def, ConfuserContext context, NameService service
protected override void Execute(ConfuserContext context, ProtectionParameters parameters) {
var service = (NameService)context.Registry.GetService();
- context.Logger.Debug("Building VTables & identifier list...");
+ context.Logger.LogDebug("Building VTables & identifier list...");
foreach (ModuleDef moduleDef in parameters.Targets.OfType())
moduleDef.EnableTypeDefFindCache = true;
- foreach (IDnlibDef def in parameters.Targets.WithProgress(context.Logger)) {
+ foreach (IDnlibDef def in parameters.Targets.WithProgress(context.ProgressReporter)) {
ParseParameters(def, context, service, parameters);
if (def is ModuleDef module) {
@@ -56,10 +57,10 @@ protected override void Execute(ConfuserContext context, ProtectionParameters pa
context.CheckCancellation();
}
- context.Logger.Debug("Analyzing...");
+ context.Logger.LogDebug("Analyzing...");
RegisterRenamers(context, service);
IList renamers = service.Renamers;
- foreach (IDnlibDef def in parameters.Targets.WithProgress(context.Logger)) {
+ foreach (IDnlibDef def in parameters.Targets.WithProgress(context.ProgressReporter)) {
Analyze(service, context, parameters, def, true);
context.CheckCancellation();
}
@@ -106,35 +107,35 @@ void RegisterRenamers(ConfuserContext context, NameService service) {
if (wpf) {
var wpfAnalyzer = new WPFAnalyzer();
- context.Logger.Debug("WPF found, enabling compatibility.");
+ context.Logger.LogDebug("WPF found, enabling compatibility.");
service.Renamers.Add(wpfAnalyzer);
if (caliburn) {
- context.Logger.Debug("Caliburn.Micro found, enabling compatibility.");
+ context.Logger.LogDebug("Caliburn.Micro found, enabling compatibility.");
service.Renamers.Add(new CaliburnAnalyzer(wpfAnalyzer));
}
}
if (winforms) {
var winformsAnalyzer = new WinFormsAnalyzer();
- context.Logger.Debug("WinForms found, enabling compatibility.");
+ context.Logger.LogDebug("WinForms found, enabling compatibility.");
service.Renamers.Add(winformsAnalyzer);
}
if (json) {
var jsonAnalyzer = new JsonAnalyzer();
- context.Logger.Debug("Newtonsoft.Json found, enabling compatibility.");
+ context.Logger.LogDebug("Newtonsoft.Json found, enabling compatibility.");
service.Renamers.Add(jsonAnalyzer);
}
if (visualBasic) {
var vbAnalyzer = new VisualBasicRuntimeAnalyzer();
- context.Logger.Debug("Visual Basic Embedded Runtime found, enabling compatibility.");
+ context.Logger.LogDebug("Visual Basic Embedded Runtime found, enabling compatibility.");
service.Renamers.Add(vbAnalyzer);
}
if (vsComposition) {
var analyzer = new VsCompositionAnalyzer();
- context.Logger.Debug("Visual Studio Composition found, enabling compatibility.");
+ context.Logger.LogDebug("Visual Studio Composition found, enabling compatibility.");
service.Renamers.Add(analyzer);
}
}
diff --git a/Confuser.Renamer/Analyzers/CallSiteAnalyzer.cs b/Confuser.Renamer/Analyzers/CallSiteAnalyzer.cs
index b4cf6f007..0a7cca3fa 100644
--- a/Confuser.Renamer/Analyzers/CallSiteAnalyzer.cs
+++ b/Confuser.Renamer/Analyzers/CallSiteAnalyzer.cs
@@ -4,6 +4,7 @@
using Confuser.Renamer.References;
using dnlib.DotNet;
using dnlib.DotNet.Emit;
+using Microsoft.Extensions.Logging;
namespace Confuser.Renamer.Analyzers {
internal sealed class CallSiteAnalyzer : IRenamer {
@@ -61,7 +62,7 @@ private static void HandleBinderInvokeMember(ConfuserContext context, MethodDef
BuildMemberReferences(context, typeDefOrRef, boundMemberName, nameInstruction);
}
else {
- context.Logger.WarnFormat(
+ context.Logger.LogWarning(
"Failed to resolve type for dynamic invoke member in {0} - blocking all members with name {1} from renaming.",
method, boundMemberName);
diff --git a/Confuser.Renamer/Analyzers/ManifestResourceAnalyzer.cs b/Confuser.Renamer/Analyzers/ManifestResourceAnalyzer.cs
index 6a911d0b4..3d23808b7 100644
--- a/Confuser.Renamer/Analyzers/ManifestResourceAnalyzer.cs
+++ b/Confuser.Renamer/Analyzers/ManifestResourceAnalyzer.cs
@@ -28,8 +28,11 @@ public static void PreRename(ModuleDef currentModule, ITraceService trace, Metho
!UTF8String.Equals(targetMethodDefOrRef.Name, "GetManifestResourceStream") ||
!UTF8String.Equals(targetMethodDefOrRef.DeclaringType.FullName, "System.Reflection.Assembly")) continue;
- var targetMethodDef = targetMethodDefOrRef.ResolveMethodDefThrow();
- if (targetMethodDef.Parameters.Count != 3) continue;
+ // The two-argument overload GetManifestResourceStream(Type, String) has two
+ // signature parameters (the instance 'this' is separate). Check the signature
+ // directly so we don't depend on resolving the BCL method.
+ var targetSig = targetMethodDefOrRef.MethodSig;
+ if (targetSig == null || targetSig.Params.Count != 2) continue;
var argumentIdx = methodTrace.Value.TraceArguments(instruction);
if (argumentIdx.Length != 3) continue;
@@ -53,11 +56,23 @@ public static void PreRename(ModuleDef currentModule, ITraceService trace, Metho
var resourceName = refTypeDefOrRef.Namespace + '.' + resName;
- var getManifestMethodDef = targetMethodDefOrRef.ResolveMethodDefThrow();
- var assemblyTypeDef = getManifestMethodDef.DeclaringType;
- var expectedSig = MethodSig.CreateInstance(getManifestMethodDef.MethodSig.RetType, getManifestMethodDef.MethodSig.Params.Last());
- var newMethodDef = assemblyTypeDef.FindMethod("GetManifestResourceStream", expectedSig);
- var newMethodRef = currentModule.Import(newMethodDef);
+ // Build the reference to the single-argument overload:
+ // Stream System.Reflection.Assembly::GetManifestResourceStream(String).
+ // Prefer resolving the real overload (matches production exactly); if the BCL
+ // declaring type can't be resolved — e.g. in a minimal analysis context where the
+ // runtime assemblies aren't on the resolver's search path — construct the member
+ // reference directly from the original call's signature.
+ var expectedSig = MethodSig.CreateInstance(targetSig.RetType, targetSig.Params[1]);
+ var assemblyTypeDef = targetMethodDefOrRef.ResolveMethodDef()?.DeclaringType;
+ IMethod newMethodRef;
+ if (assemblyTypeDef != null) {
+ var newMethodDef = assemblyTypeDef.FindMethod("GetManifestResourceStream", expectedSig);
+ newMethodRef = currentModule.Import(newMethodDef);
+ }
+ else {
+ newMethodRef = currentModule.Import(
+ new MemberRefUser(currentModule, "GetManifestResourceStream", expectedSig, targetMethodDefOrRef.DeclaringType));
+ }
resNameInstruction.Operand = resourceName;
instruction.Operand = newMethodRef;
diff --git a/Confuser.Renamer/Analyzers/ReflectionAnalyzer.cs b/Confuser.Renamer/Analyzers/ReflectionAnalyzer.cs
index 5deeeaa4a..f6bbc267c 100644
--- a/Confuser.Renamer/Analyzers/ReflectionAnalyzer.cs
+++ b/Confuser.Renamer/Analyzers/ReflectionAnalyzer.cs
@@ -7,7 +7,7 @@
using Confuser.Renamer.References;
using dnlib.DotNet;
using dnlib.DotNet.Emit;
-using ILogger = Confuser.Core.ILogger;
+using Microsoft.Extensions.Logging;
namespace Confuser.Renamer.Analyzers {
///
@@ -20,7 +20,7 @@ void IRenamer.Analyze(ConfuserContext context, INameService service, ProtectionP
Analyze(service, context.Registry.GetService(), context.Modules.Cast().ToArray(), context.Logger, method);
}
- public void Analyze(INameService nameService, ITraceService traceService, IReadOnlyList moduleDefs, ILogger logger, MethodDef method) {
+ public void Analyze(INameService nameService, ITraceService traceService, IReadOnlyList moduleDefs, Microsoft.Extensions.Logging.ILogger logger, MethodDef method) {
if (!method.HasBody) return;
MethodTrace methodTrace = null;
@@ -49,7 +49,7 @@ MethodTrace GetMethodTrace() {
var trace = GetMethodTrace();
var arguments = trace.TraceArguments(instr);
if (arguments == null) {
- logger.WarnFormat(Resources.ReflectionAnalyzer_Analyze_TracingArgumentsFailed, calledMethod.FullName, method.FullName);
+ logger.LogWarning(Resources.ReflectionAnalyzer_Analyze_TracingArgumentsFailed, calledMethod.FullName, method.FullName);
}
else if (arguments.Length >= 2) {
var types = GetReferencedTypes(method.Body.Instructions[arguments[0]], method, trace);
diff --git a/Confuser.Renamer/Analyzers/ResourceAnalyzer.cs b/Confuser.Renamer/Analyzers/ResourceAnalyzer.cs
index 9f58d66ce..fccfc9d40 100644
--- a/Confuser.Renamer/Analyzers/ResourceAnalyzer.cs
+++ b/Confuser.Renamer/Analyzers/ResourceAnalyzer.cs
@@ -6,6 +6,7 @@
using Confuser.Renamer.References;
using dnlib.DotNet;
using dnlib.DotNet.Emit;
+using Microsoft.Extensions.Logging;
namespace Confuser.Renamer.Analyzers {
internal class ResourceAnalyzer : IRenamer {
@@ -23,7 +24,7 @@ public void Analyze(ConfuserContext context, INameService service, ProtectionPar
string nameAsmName = asmName.Substring(0, asmName.Length - ".resources".Length);
ModuleDef mainModule = context.Modules.SingleOrDefault(mod => mod.Assembly.Name == nameAsmName);
if (mainModule == null) {
- context.Logger.ErrorFormat("Could not find main assembly of satellite assembly '{0}'.", module.Assembly.FullName);
+ context.Logger.LogError("Could not find main assembly of satellite assembly '{0}'.", module.Assembly.FullName);
throw new ConfuserException(null);
}
@@ -35,7 +36,7 @@ public void Analyze(ConfuserContext context, INameService service, ProtectionPar
TypeDef type = mainModule.FindReflection(typeName);
if (type == null) {
- context.Logger.WarnFormat(Resources.ResourceAnalyzer_Analyze_CouldNotFindResourceType, typeName);
+ context.Logger.LogWarning(Resources.ResourceAnalyzer_Analyze_CouldNotFindResourceType, typeName);
continue;
}
string format = $"{{0}}.{culture}.resources";
@@ -68,7 +69,7 @@ public void Analyze(ConfuserContext context, INameService service, ProtectionPar
}
if (type == null) {
- context.Logger.WarnFormat(Resources.ResourceAnalyzer_Analyze_CouldNotFindResourceType, typeName);
+ context.Logger.LogWarning(Resources.ResourceAnalyzer_Analyze_CouldNotFindResourceType, typeName);
continue;
}
service.ReduceRenameMode(type, RenameMode.Reflection);
diff --git a/Confuser.Renamer/Analyzers/TypeBlobAnalyzer.cs b/Confuser.Renamer/Analyzers/TypeBlobAnalyzer.cs
index 15a3e94f1..1d2f69496 100644
--- a/Confuser.Renamer/Analyzers/TypeBlobAnalyzer.cs
+++ b/Confuser.Renamer/Analyzers/TypeBlobAnalyzer.cs
@@ -7,6 +7,7 @@
using dnlib.DotNet;
using dnlib.DotNet.Emit;
using dnlib.DotNet.MD;
+using Microsoft.Extensions.Logging;
namespace Confuser.Renamer.Analyzers {
public sealed class TypeBlobAnalyzer : IRenamer {
@@ -16,7 +17,7 @@ void IRenamer.Analyze(ConfuserContext context, INameService service, ProtectionP
Analyze(service, context.Modules, context.Logger, moduleDef);
}
- public static void Analyze(INameService service, ICollection modules, Core.ILogger logger, ModuleDefMD module) {
+ public static void Analyze(INameService service, ICollection modules, Microsoft.Extensions.Logging.ILogger logger, ModuleDefMD module) {
// MemberRef
var table = module.TablesStream.Get(Table.Method);
var len = table.Rows;
@@ -68,14 +69,18 @@ public static void Analyze(INameService service, ICollection module
foreach (CANamedArgument arg in attr.Properties)
AnalyzeCAArgument(modules, service, arg.Argument);
- TypeDef attrType = attr.AttributeType.ResolveTypeDefThrow();
- if (!modules.Contains((ModuleDefMD)attrType.Module))
+ // External attribute types (e.g. compiler-emitted BCL attributes like
+ // RefSafetyRulesAttribute) may not be resolvable, and are never renamed anyway
+ // since they aren't defined in the modules being obfuscated. Skip rather than
+ // throw so obfuscation doesn't fail on an unresolvable external attribute type.
+ TypeDef attrType = attr.AttributeType.ResolveTypeDef();
+ if (attrType == null || !modules.Contains((ModuleDefMD)attrType.Module))
continue;
foreach (var arg in attr.NamedArguments) {
var memberDef = FindArgumentMemberDef(arg, attrType);
if (memberDef == null)
- logger.WarnFormat(
+ logger.LogWarning(
arg.IsField ? "Failed to resolve CA field '{0}::{1} : {2}'." : "Failed to resolve CA property '{0}::{1} : {2}'.",
attrType, arg.Name, arg.Type);
else
@@ -130,8 +135,9 @@ private static void AnalyzeCAArgument(ICollection modules, INameSer
if (arg.Type.DefinitionAssembly.IsCorLib() && arg.Type.FullName == "System.Type") {
var typeSig = (TypeSig)arg.Value;
foreach (ITypeDefOrRef typeRef in typeSig.FindTypeRefs()) {
- TypeDef typeDef = typeRef.ResolveTypeDefThrow();
- if (modules.Contains((ModuleDefMD)typeDef.Module)) {
+ // Skip external/unresolvable types — only types in the obfuscated modules are renamed.
+ TypeDef typeDef = typeRef.ResolveTypeDef();
+ if (typeDef != null && modules.Contains((ModuleDefMD)typeDef.Module)) {
if (typeRef is TypeRef)
service.AddReference(typeDef, new TypeRefReference((TypeRef)typeRef, typeDef));
service.ReduceRenameMode(typeDef, RenameMode.Reflection);
@@ -159,8 +165,9 @@ private static void AnalyzeMemberRef(ICollection modules, INameServ
if (sig is GenericInstSig) {
var inst = (GenericInstSig)sig;
Debug.Assert(!(inst.GenericType.TypeDefOrRef is TypeSpec));
- TypeDef openType = inst.GenericType.TypeDefOrRef.ResolveTypeDefThrow();
- if (!modules.Contains((ModuleDefMD)openType.Module) ||
+ // Skip external/unresolvable generic types — only in-module members are tracked.
+ TypeDef openType = inst.GenericType.TypeDefOrRef.ResolveTypeDef();
+ if (openType == null || !modules.Contains((ModuleDefMD)openType.Module) ||
memberRef.IsArrayAccessors())
return;
diff --git a/Confuser.Renamer/Analyzers/WPFAnalyzer.cs b/Confuser.Renamer/Analyzers/WPFAnalyzer.cs
index ebf49dceb..51840bfd9 100644
--- a/Confuser.Renamer/Analyzers/WPFAnalyzer.cs
+++ b/Confuser.Renamer/Analyzers/WPFAnalyzer.cs
@@ -14,6 +14,7 @@
using dnlib.DotNet;
using dnlib.DotNet.Emit;
using dnlib.IO;
+using Microsoft.Extensions.Logging;
namespace Confuser.Renamer.Analyzers {
internal class WPFAnalyzer : IRenamer {
@@ -47,7 +48,7 @@ public void PreRename(ConfuserContext context, INameService service, ProtectionP
var renameMode = parameters.GetParameter(context, def, "renXamlMode", RenameMode.Letters);
if (renameMode < RenameMode.Letters) {
var illegalValues = Enum.GetValues(typeof(RenameMode)).Cast().Where(m => m < RenameMode.Letters);
- context.Logger.Warn("The renaming modes " + String.Join(", ", illegalValues) + " are not allowed for XAML resources. Letters mode will be used.");
+ context.Logger.LogWarning("The renaming modes " + String.Join(", ", illegalValues) + " are not allowed for XAML resources. Letters mode will be used.");
renameMode = RenameMode.Letters;
}
@@ -82,7 +83,7 @@ public void PreRename(ConfuserContext context, INameService service, ProtectionP
string decodedNewName = decodedDirectory + fileName;
string encodedNewName = encodedDirectory + fileName;
- context.Logger.Debug(String.Format("Preserving virtual paths. Replaced {0} with {1}", decodedName, decodedNewName));
+ context.Logger.LogDebug(String.Format("Preserving virtual paths. Replaced {0} with {1}", decodedName, decodedNewName));
bool renameOk = references.All(r => r.CanRename(module, decodedName, decodedNewName) || r.CanRename(module, encodedName, encodedNewName));
@@ -199,7 +200,7 @@ void AnalyzeMethod(ConfuserContext context, INameService service, MethodDef meth
operand = match.Groups[2].Value;
}
else if (operand.Contains("/"))
- context.Logger.WarnFormat("Fail to extract XAML name from '{0}'.", instr.Operand);
+ context.Logger.LogWarning("Fail to extract XAML name from '{0}'.", instr.Operand);
var reference = new BAMLStringReference(refModule, instr);
operand = WebUtility.UrlDecode(operand.TrimStart('/'));
@@ -222,14 +223,14 @@ void AnalyzeMethod(ConfuserContext context, INameService service, MethodDef meth
int[] args = trace.TraceArguments(instrInfo.Item2);
if (args == null) {
if (!erred)
- context.Logger.WarnFormat("Failed to extract dependency property name in '{0}'.", method.FullName);
+ context.Logger.LogWarning("Failed to extract dependency property name in '{0}'.", method.FullName);
erred = true;
continue;
}
Instruction ldstr = method.Body.Instructions[args[0]];
if (ldstr.OpCode.Code != Code.Ldstr) {
if (!erred)
- context.Logger.WarnFormat("Failed to extract dependency property name in '{0}'.", method.FullName);
+ context.Logger.LogWarning("Failed to extract dependency property name in '{0}'.", method.FullName);
erred = true;
continue;
}
@@ -270,10 +271,10 @@ void AnalyzeMethod(ConfuserContext context, INameService service, MethodDef meth
}
if (!found) {
if (instrInfo.Item1)
- context.Logger.WarnFormat("Failed to find the accessors of attached dependency property '{0}' in type '{1}'.",
+ context.Logger.LogWarning("Failed to find the accessors of attached dependency property '{0}' in type '{1}'.",
name, declType.FullName);
else
- context.Logger.WarnFormat("Failed to find the CLR property of normal dependency property '{0}' in type '{1}'.",
+ context.Logger.LogWarning("Failed to find the CLR property of normal dependency property '{0}' in type '{1}'.",
name, declType.FullName);
}
}
@@ -283,14 +284,14 @@ void AnalyzeMethod(ConfuserContext context, INameService service, MethodDef meth
int[] args = trace.TraceArguments(instr);
if (args == null) {
if (!erred)
- context.Logger.WarnFormat("Failed to extract routed event name in '{0}'.", method.FullName);
+ context.Logger.LogWarning("Failed to extract routed event name in '{0}'.", method.FullName);
erred = true;
continue;
}
Instruction ldstr = method.Body.Instructions[args[0]];
if (ldstr.OpCode.Code != Code.Ldstr) {
if (!erred)
- context.Logger.WarnFormat("Failed to extract routed event name in '{0}'.", method.FullName);
+ context.Logger.LogWarning("Failed to extract routed event name in '{0}'.", method.FullName);
erred = true;
continue;
}
@@ -300,7 +301,7 @@ void AnalyzeMethod(ConfuserContext context, INameService service, MethodDef meth
EventDef eventDef = null;
if ((eventDef = declType.FindEvent(name)) == null) {
- context.Logger.WarnFormat("Failed to find the CLR event of routed event '{0}' in type '{1}'.",
+ context.Logger.LogWarning("Failed to find the CLR event of routed event '{0}' in type '{1}'.",
name, declType.FullName);
continue;
}
diff --git a/Confuser.Renamer/Analyzers/WinFormsAnalyzer.cs b/Confuser.Renamer/Analyzers/WinFormsAnalyzer.cs
index c8f78e92c..9a33714c7 100644
--- a/Confuser.Renamer/Analyzers/WinFormsAnalyzer.cs
+++ b/Confuser.Renamer/Analyzers/WinFormsAnalyzer.cs
@@ -6,6 +6,7 @@
using Confuser.Core.Services;
using dnlib.DotNet;
using dnlib.DotNet.Emit;
+using Microsoft.Extensions.Logging;
namespace Confuser.Renamer.Analyzers {
public class WinFormsAnalyzer : IRenamer {
@@ -66,7 +67,7 @@ void AnalyzeMethod(ConfuserContext context, INameService service, MethodDef meth
int[] args = trace.TraceArguments(instrInfo.Item2);
if (args == null) {
if (!erred)
- context.Logger.WarnFormat("Failed to extract binding property name in '{0}'.", method.FullName);
+ context.Logger.LogWarning("Failed to extract binding property name in '{0}'.", method.FullName);
erred = true;
continue;
}
@@ -75,14 +76,14 @@ void AnalyzeMethod(ConfuserContext context, INameService service, MethodDef meth
var propertyName = ResolveNameInstruction(method, args, ref argumentIndex);
if (propertyName.OpCode.Code != Code.Ldstr) {
if (!erred)
- context.Logger.WarnFormat("Failed to extract binding property name in '{0}'.", method.FullName);
+ context.Logger.LogWarning("Failed to extract binding property name in '{0}'.", method.FullName);
erred = true;
}
else {
List props;
if (!properties.TryGetValue((string)propertyName.Operand, out props)) {
if (!erred)
- context.Logger.WarnFormat("Failed to extract target property in '{0}'.", method.FullName);
+ context.Logger.LogWarning("Failed to extract target property in '{0}'.", method.FullName);
erred = true;
}
else {
@@ -95,14 +96,14 @@ void AnalyzeMethod(ConfuserContext context, INameService service, MethodDef meth
var dataMember = ResolveNameInstruction(method, args, ref argumentIndex);
if (dataMember.OpCode.Code != Code.Ldstr) {
if (!erred)
- context.Logger.WarnFormat("Failed to extract binding property name in '{0}'.", method.FullName);
+ context.Logger.LogWarning("Failed to extract binding property name in '{0}'.", method.FullName);
erred = true;
}
else {
List props;
if (!properties.TryGetValue((string)dataMember.Operand, out props)) {
if (!erred)
- context.Logger.WarnFormat("Failed to extract target property in '{0}'.", method.FullName);
+ context.Logger.LogWarning("Failed to extract target property in '{0}'.", method.FullName);
erred = true;
}
else {
@@ -116,7 +117,7 @@ void AnalyzeMethod(ConfuserContext context, INameService service, MethodDef meth
int[] args = trace.TraceArguments(instrInfo);
if (args == null) {
if (!erred)
- context.Logger.WarnFormat("Failed to extract binding property name in '{0}'.", method.FullName);
+ context.Logger.LogWarning("Failed to extract binding property name in '{0}'.", method.FullName);
erred = true;
continue;
}
@@ -125,13 +126,13 @@ void AnalyzeMethod(ConfuserContext context, INameService service, MethodDef meth
var propertyName = ResolveNameInstruction(method, args, ref argumentIndex);
if (propertyName.OpCode.Code != Code.Ldstr) {
if (!erred)
- context.Logger.WarnFormat("Failed to extract binding property name in '{0}'.", method.FullName);
+ context.Logger.LogWarning("Failed to extract binding property name in '{0}'.", method.FullName);
erred = true;
}
else {
if (!properties.TryGetValue((string)propertyName.Operand, out var props)) {
if (!erred)
- context.Logger.WarnFormat("Failed to extract target property in '{0}'.", method.FullName);
+ context.Logger.LogWarning("Failed to extract target property in '{0}'.", method.FullName);
erred = true;
}
else {
diff --git a/Confuser.Renamer/BAML/BAMLAnalyzer.cs b/Confuser.Renamer/BAML/BAMLAnalyzer.cs
index dec64a2e7..f4569a595 100644
--- a/Confuser.Renamer/BAML/BAMLAnalyzer.cs
+++ b/Confuser.Renamer/BAML/BAMLAnalyzer.cs
@@ -9,6 +9,7 @@
using Confuser.Renamer.Analyzers;
using Confuser.Renamer.References;
using dnlib.DotNet;
+using Microsoft.Extensions.Logging;
namespace Confuser.Renamer.BAML {
internal class BAMLAnalyzer {
@@ -338,7 +339,7 @@ void ProcessElementBody(BamlElement root, BamlElement elem) {
if (attrInfo.Item1 is EventDef) {
MethodDef method = root.Type.FindMethod(propRec.Value);
if (method == null)
- context.Logger.WarnFormat("Cannot resolve method '{0}' in '{1}'.", root.Type.FullName, propRec.Value);
+ context.Logger.LogWarning("Cannot resolve method '{0}' in '{1}'.", root.Type.FullName, propRec.Value);
else {
var reference = new BAMLAttributeReference(method, propRec);
service.AddReference(method, reference);
@@ -472,7 +473,7 @@ void ProcessConverter(PropertyWithConverterRecord rec, TypeDef type) {
AddDefReference(field, reference);
}
if (property == null && field == null)
- context.Logger.WarnFormat("Could not resolve command '{0}' in '{1}'.", cmd, CurrentBAMLName);
+ context.Logger.LogWarning("Could not resolve command '{0}' in '{1}'.", cmd, CurrentBAMLName);
}
}
}
@@ -540,7 +541,7 @@ void ProcessConverter(PropertyWithConverterRecord rec, TypeDef type) {
src = match.Groups[2].Value;
}
else if (rec.Value.Contains("/"))
- context.Logger.WarnFormat("Fail to extract XAML name from '{0}'.", rec.Value);
+ context.Logger.LogWarning("Fail to extract XAML name from '{0}'.", rec.Value);
if (!src.StartsWith(packScheme, StringComparison.OrdinalIgnoreCase)) {
var rel = new Uri(new Uri(packScheme + "application:,,,/" + CurrentBAMLName), src);
diff --git a/Confuser.Renamer/RenamePhase.cs b/Confuser.Renamer/RenamePhase.cs
index a73630deb..2302ddd74 100644
--- a/Confuser.Renamer/RenamePhase.cs
+++ b/Confuser.Renamer/RenamePhase.cs
@@ -5,6 +5,7 @@
using Confuser.Core;
using dnlib.DotNet;
using dnlib.DotNet.Pdb;
+using Microsoft.Extensions.Logging;
namespace Confuser.Renamer {
class RenamePhase : ProtectionPhase {
@@ -22,7 +23,7 @@ protected override void Execute(ConfuserContext context, ProtectionParameters pa
var service = (NameService)context.Registry.GetService();
bool overloadConfusion = parameters.GetParameter(context, context.CurrentModule, "overload", false);
- context.Logger.Debug("Renaming...");
+ context.Logger.LogDebug("Renaming...");
foreach (var renamer in service.Renamers) {
foreach (var def in parameters.Targets)
renamer.PreRename(context, service, parameters, def);
@@ -32,7 +33,7 @@ protected override void Execute(ConfuserContext context, ProtectionParameters pa
var targets = parameters.Targets.ToList();
service.GetRandom().Shuffle(targets);
var pdbDocs = new HashSet();
- foreach (var def in GetTargetsWithDelay(targets, context, service).WithProgress(targets.Count, context.Logger)) {
+ foreach (var def in GetTargetsWithDelay(targets, context, service).WithProgress(targets.Count, context.ProgressReporter)) {
if (def is ModuleDef moduleDef && parameters.GetParameter(context, moduleDef, "rickroll", false))
RickRoller.CommenceRickroll(context, moduleDef);
@@ -115,7 +116,7 @@ protected override void Execute(ConfuserContext context, ProtectionParameters pa
foreach (var reference in updatedReferenceList) {
errorBuilder.Append(" - ").AppendLine(reference.ToString(service));
}
- context.Logger.Error(errorBuilder.ToString().Trim());
+ context.Logger.LogError(errorBuilder.ToString().Trim());
throw new ConfuserException();
}
context.CheckCancellation();
@@ -189,7 +190,7 @@ static IEnumerable GetTargetsWithDelay(IList definitions,
foreach (var def in delayedItems) {
errorBuilder.Append("• ").AppendDescription(def, service).AppendLine();
}
- context.Logger.Warn(errorBuilder.ToString().Trim());
+ context.Logger.LogWarning(errorBuilder.ToString().Trim());
yield break;
}
lastCount = delayedItems.Count;
diff --git a/Confuser.Renamer/VTable.cs b/Confuser.Renamer/VTable.cs
index cca4effa4..7a62802c7 100644
--- a/Confuser.Renamer/VTable.cs
+++ b/Confuser.Renamer/VTable.cs
@@ -4,7 +4,7 @@
using System.Linq;
using Confuser.Core;
using dnlib.DotNet;
-using ILogger = Confuser.Core.ILogger;
+using Microsoft.Extensions.Logging;
namespace Confuser.Renamer {
public class VTableSignature {
@@ -314,31 +314,31 @@ static void Inherits(VTableConstruction vTbl, VTable baseVTbl) {
[Conditional("DEBUG")]
static void CheckKeyExist(VTableStorage storage, IDictionary dictionary, TKey key, string name) {
if (!dictionary.ContainsKey(key)) {
- storage.GetLogger().ErrorFormat("{0} not found: {1}", name, key);
+ storage.GetLogger().LogError("{0} not found: {1}", name, key);
foreach (var k in dictionary.Values)
- storage.GetLogger().ErrorFormat(" {0}", k);
+ storage.GetLogger().LogError(" {0}", k);
}
}
[Conditional("DEBUG")]
static void CheckKeyExist(VTableStorage storage, ILookup lookup, TKey key, string name) {
if (!lookup.Contains(key)) {
- storage.GetLogger().ErrorFormat("{0} not found: {1}", name, key);
+ storage.GetLogger().LogError("{0} not found: {1}", name, key);
foreach (var k in lookup.Select(g => g.Key))
- storage.GetLogger().ErrorFormat(" {0}", k);
+ storage.GetLogger().LogError(" {0}", k);
}
}
}
public class VTableStorage {
Dictionary storage = new Dictionary();
- ILogger logger;
+ Microsoft.Extensions.Logging.ILogger logger;
- public VTableStorage(ILogger logger) {
+ public VTableStorage(Microsoft.Extensions.Logging.ILogger logger) {
this.logger = logger;
}
- public ILogger GetLogger() {
+ public Microsoft.Extensions.Logging.ILogger GetLogger() {
return logger;
}
diff --git a/ConfuserEx/ConfuserEx.csproj b/ConfuserEx/ConfuserEx.csproj
index 5f11173c4..09231a04e 100644
--- a/ConfuserEx/ConfuserEx.csproj
+++ b/ConfuserEx/ConfuserEx.csproj
@@ -19,6 +19,8 @@
+
+
diff --git a/ConfuserEx/FlowDocumentSink.cs b/ConfuserEx/FlowDocumentSink.cs
new file mode 100644
index 000000000..46a5dc70d
--- /dev/null
+++ b/ConfuserEx/FlowDocumentSink.cs
@@ -0,0 +1,70 @@
+using System;
+using System.Windows;
+using System.Windows.Documents;
+using System.Windows.Media;
+using Serilog.Core;
+using Serilog.Events;
+
+namespace ConfuserEx {
+ ///
+ /// A Serilog sink that writes log events to a WPF
+ /// with color-coded output matching the original ConfuserEx console style.
+ ///
+ internal sealed class FlowDocumentSink : ILogEventSink {
+ readonly Paragraph paragraph;
+
+ public FlowDocumentSink(Paragraph paragraph) {
+ this.paragraph = paragraph ?? throw new ArgumentNullException(nameof(paragraph));
+ }
+
+ public void Emit(LogEvent logEvent) {
+ var brush = GetBrush(logEvent.Level);
+ var prefix = GetPrefix(logEvent.Level);
+ var message = logEvent.RenderMessage();
+
+ Application.Current.Dispatcher.BeginInvoke(new Action(() => {
+ paragraph.Inlines.Add(new Run(prefix + message) { Foreground = brush });
+ paragraph.Inlines.Add(new LineBreak());
+
+ if (logEvent.Exception != null) {
+ paragraph.Inlines.Add(new Run("Exception: " + logEvent.Exception) { Foreground = brush });
+ paragraph.Inlines.Add(new LineBreak());
+ }
+ }));
+ }
+
+ static Brush GetBrush(LogEventLevel level) {
+ switch (level) {
+ case LogEventLevel.Verbose:
+ case LogEventLevel.Debug:
+ return Brushes.Gray;
+ case LogEventLevel.Information:
+ return Brushes.White;
+ case LogEventLevel.Warning:
+ return Brushes.Yellow;
+ case LogEventLevel.Error:
+ case LogEventLevel.Fatal:
+ return Brushes.Red;
+ default:
+ return Brushes.White;
+ }
+ }
+
+ static string GetPrefix(LogEventLevel level) {
+ switch (level) {
+ case LogEventLevel.Verbose:
+ case LogEventLevel.Debug:
+ return "[DEBUG] ";
+ case LogEventLevel.Information:
+ return " [INFO] ";
+ case LogEventLevel.Warning:
+ return " [WARN] ";
+ case LogEventLevel.Error:
+ case LogEventLevel.Fatal:
+ return "[ERROR] ";
+ default:
+ return "";
+ }
+ }
+ }
+}
diff --git a/ConfuserEx/ViewModel/UI/ProtectTabVM.cs b/ConfuserEx/ViewModel/UI/ProtectTabVM.cs
index a9f7088cc..b36356148 100644
--- a/ConfuserEx/ViewModel/UI/ProtectTabVM.cs
+++ b/ConfuserEx/ViewModel/UI/ProtectTabVM.cs
@@ -7,12 +7,16 @@
using System.Windows.Media;
using CommunityToolkit.Mvvm.Input;
using Confuser.Core;
+using Confuser.Core.Diagnostics;
using Confuser.Core.Project;
+using Microsoft.Extensions.Logging;
+using Serilog;
namespace ConfuserEx.ViewModel {
- internal class ProtectTabVM : TabViewModel, ILogger {
+ internal class ProtectTabVM : TabViewModel, IProgressReporter {
readonly Paragraph documentContent;
CancellationTokenSource cancelSrc;
+ DiagnosticCollector collector;
double? progress = 0;
bool? result;
@@ -31,6 +35,10 @@ public ICommand CancelCmd {
get { return new RelayCommand(DoCancel, () => App.NavigationDisabled); }
}
+ public ICommand CopyReportCmd {
+ get { return new RelayCommand(DoCopyReport, () => Result != null && collector != null); }
+ }
+
public double? Progress {
get { return progress; }
set { SetProperty(ref progress, value, "Progress"); }
@@ -48,9 +56,28 @@ void DoProtect() {
parameters.Project = ((IViewModel)App.Project).Model;
if (File.Exists(App.FileName))
Environment.CurrentDirectory = Path.GetDirectoryName(App.FileName);
- parameters.Logger = this;
documentContent.Inlines.Clear();
+
+ var serilogLogger = new LoggerConfiguration()
+ .MinimumLevel.Debug()
+ .WriteTo.Sink(new FlowDocumentSink(documentContent))
+ .CreateLogger();
+
+ // The logger factory must outlive the async protection run — ConfuserEngine.Run
+ // executes on a background thread, so we dispose it in the continuation below
+ // rather than with a method-scoped 'using' (which would dispose it too early).
+ var loggerFactory = LoggerFactory.Create(builder =>
+ builder.AddSerilog(serilogLogger, dispose: true));
+ var melLogger = loggerFactory.CreateLogger("ConfuserEx");
+
+ // The collector wraps the logger and this progress reporter so a diagnostic report —
+ // covering both successful and failed runs — can be copied afterwards. It captures the
+ // full transcript regardless of the display level and forwards everything through.
+ collector = new DiagnosticCollector(melLogger, this) { Project = parameters.Project };
+ parameters.Logger = collector;
+ parameters.ProgressReporter = collector;
+
cancelSrc = new CancellationTokenSource();
Result = null;
Progress = null;
@@ -58,90 +85,63 @@ void DoProtect() {
App.NavigationDisabled = true;
ConfuserEngine.Run(parameters, cancelSrc.Token)
- .ContinueWith(_ =>
- Application.Current.Dispatcher.BeginInvoke(new Action(() => {
- Progress = 0;
- App.NavigationDisabled = false;
- CommandManager.InvalidateRequerySuggested();
- })));
+ .ContinueWith(_ => {
+ loggerFactory.Dispose();
+ Application.Current.Dispatcher.BeginInvoke(new Action(() => {
+ Progress = 0;
+ App.NavigationDisabled = false;
+ CommandManager.InvalidateRequerySuggested();
+ }));
+ });
}
void DoCancel() {
cancelSrc.Cancel();
}
- void AppendLine(string format, Brush foreground, params object[] args) {
- Application.Current.Dispatcher.BeginInvoke(new Action(() => {
- documentContent.Inlines.Add(new Run(string.Format(format, args)) { Foreground = foreground });
- documentContent.Inlines.Add(new LineBreak());
- }));
- }
-
- #region Logger Impl
+ void DoCopyReport() {
+ if (collector == null)
+ return;
- DateTime begin;
-
- void ILogger.Debug(string msg) {
- AppendLine("[DEBUG] {0}", Brushes.Gray, msg);
- }
-
- void ILogger.DebugFormat(string format, params object[] args) {
- AppendLine("[DEBUG] {0}", Brushes.Gray, string.Format(format, args));
+ try {
+ Clipboard.SetText(collector.GenerateReport());
+ }
+ catch {
+ // The clipboard can be transiently locked by another process; a failed copy
+ // should never crash the app. The user can simply retry.
+ }
}
- void ILogger.Info(string msg) {
- AppendLine(" [INFO] {0}", Brushes.White, msg);
- }
-
- void ILogger.InfoFormat(string format, params object[] args) {
- AppendLine(" [INFO] {0}", Brushes.White, string.Format(format, args));
- }
+ #region IProgressReporter
- void ILogger.Warn(string msg) {
- AppendLine(" [WARN] {0}", Brushes.Yellow, msg);
- }
-
- void ILogger.WarnFormat(string format, params object[] args) {
- AppendLine(" [WARN] {0}", Brushes.Yellow, string.Format(format, args));
- }
-
- void ILogger.WarnException(string msg, Exception ex) {
- AppendLine(" [WARN] {0}", Brushes.Yellow, msg);
- AppendLine("Exception: {0}", Brushes.Yellow, ex);
- }
-
- void ILogger.Error(string msg) {
- AppendLine("[ERROR] {0}", Brushes.Red, msg);
- }
-
- void ILogger.ErrorFormat(string format, params object[] args) {
- AppendLine("[ERROR] {0}", Brushes.Red, string.Format(format, args));
- }
-
- void ILogger.ErrorException(string msg, Exception ex) {
- AppendLine("[ERROR] {0}", Brushes.Red, msg);
- AppendLine("Exception: {0}", Brushes.Red, ex);
- }
+ DateTime begin;
- void ILogger.Progress(int progress, int overall) {
+ void IProgressReporter.Progress(int progress, int overall) {
Progress = (double)progress / overall;
}
- void ILogger.EndProgress() {
+ void IProgressReporter.EndProgress() {
Progress = null;
}
- void ILogger.Finish(bool successful) {
+ void IProgressReporter.Finish(bool successful) {
DateTime now = DateTime.Now;
string timeString = string.Format(
"at {0}, {1}:{2:d2} elapsed.",
now.ToShortTimeString(),
(int)now.Subtract(begin).TotalMinutes,
now.Subtract(begin).Seconds);
- if (successful)
- AppendLine("Finished {0}", Brushes.Lime, timeString);
- else
- AppendLine("Failed {0}", Brushes.Red, timeString);
+
+ Application.Current.Dispatcher.BeginInvoke(new Action(() => {
+ if (successful) {
+ documentContent.Inlines.Add(new Run("Finished " + timeString) { Foreground = Brushes.Lime });
+ }
+ else {
+ documentContent.Inlines.Add(new Run("Failed " + timeString) { Foreground = Brushes.Red });
+ }
+ documentContent.Inlines.Add(new LineBreak());
+ }));
+
Result = successful;
}
diff --git a/ConfuserEx/Views/ProtectTabView.xaml b/ConfuserEx/Views/ProtectTabView.xaml
index 34fd123be..fb45034a3 100644
--- a/ConfuserEx/Views/ProtectTabView.xaml
+++ b/ConfuserEx/Views/ProtectTabView.xaml
@@ -10,15 +10,19 @@
+
-
-
-
+
+
+
diff --git a/Tests/Confuser.CLI.Test/CliEndToEndTest.cs b/Tests/Confuser.CLI.Test/CliEndToEndTest.cs
index 5853bf88c..2d56bc97c 100644
--- a/Tests/Confuser.CLI.Test/CliEndToEndTest.cs
+++ b/Tests/Confuser.CLI.Test/CliEndToEndTest.cs
@@ -70,6 +70,55 @@ public void Obfuscate_SampleApp_ProducesRunnableOutput() {
}
}
+ [Fact]
+ public void Cli_DumpFlag_WritesDiagnosticReport() {
+ var sampleAppExe = Path.Combine(AppContext.BaseDirectory, "Fixtures", "SampleApp", "bin", "SampleApp.exe");
+ Assert.True(File.Exists(sampleAppExe), $"Pre-built SampleApp.exe not found at {sampleAppExe}");
+
+ var cliDll = Path.Combine(AppContext.BaseDirectory, "Confuser.CLI.dll");
+ Assert.True(File.Exists(cliDll), $"Confuser.CLI.dll not found at {cliDll}");
+
+ var testDir = Path.Combine(Path.GetTempPath(), "confuserex-cli-dump-" + Guid.NewGuid().ToString("N")[..8]);
+ Directory.CreateDirectory(testDir);
+
+ try {
+ File.Copy(sampleAppExe, Path.Combine(testDir, "SampleApp.exe"));
+
+ var crproj = Path.Combine(testDir, "SampleApp.crproj");
+ File.WriteAllText(crproj,
+@"
+
+
+
+
+");
+
+ var reportPath = Path.Combine(testDir, "report.md");
+
+ // Act — run Confuser.CLI with the --dump flag pointing at an explicit path
+ var cliResult = RunProcess("dotnet", $"\"{cliDll}\" -n --dump=\"{reportPath}\" \"{crproj}\"");
+ output.WriteLine("=== Confuser.CLI Output ===");
+ output.WriteLine(cliResult.stdout);
+ if (!string.IsNullOrEmpty(cliResult.stderr))
+ output.WriteLine(cliResult.stderr);
+ Assert.Equal(0, cliResult.exitCode);
+
+ // Assert — the report was written and announced
+ Assert.True(File.Exists(reportPath), $"Diagnostic report should exist at {reportPath}");
+ Assert.Contains("Diagnostic report written to:", cliResult.stdout);
+
+ var report = File.ReadAllText(reportPath);
+ Assert.Contains("## System", report);
+ Assert.Contains("## Project Configuration", report);
+ Assert.Contains("## Result: SUCCESS", report);
+ Assert.Contains("## Log Output", report);
+ Assert.Contains("SampleApp.exe", report);
+ }
+ finally {
+ try { Directory.Delete(testDir, true); } catch { }
+ }
+ }
+
[Fact]
public void Cli_NoArgs_ReturnsNonZeroAndShowsUsage() {
var cliDll = Path.Combine(AppContext.BaseDirectory, "Confuser.CLI.dll");
diff --git a/Tests/Confuser.CLI.Test/Confuser.CLI.Test.csproj b/Tests/Confuser.CLI.Test/Confuser.CLI.Test.csproj
index 1c3448807..b7f4c1a97 100644
--- a/Tests/Confuser.CLI.Test/Confuser.CLI.Test.csproj
+++ b/Tests/Confuser.CLI.Test/Confuser.CLI.Test.csproj
@@ -5,12 +5,6 @@
false
-
-
-
-
-
-
@@ -27,12 +21,4 @@
Properties="Configuration=Release;OutputPath=$([System.IO.Path]::GetFullPath('$(OutputPath)Fixtures\SampleApp\bin\'))" />
-
-
-
-
-
diff --git a/Tests/Confuser.Core.Test/Diagnostics/DiagnosticCollectorTest.cs b/Tests/Confuser.Core.Test/Diagnostics/DiagnosticCollectorTest.cs
new file mode 100644
index 000000000..8dba96d29
--- /dev/null
+++ b/Tests/Confuser.Core.Test/Diagnostics/DiagnosticCollectorTest.cs
@@ -0,0 +1,152 @@
+using System;
+using System.Collections.Generic;
+using Confuser.Core;
+using Confuser.Core.Diagnostics;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+using Xunit;
+
+namespace Confuser.Core.Test.Diagnostics {
+ public class DiagnosticCollectorTest {
+ [Fact]
+ public void Log_CapturesEntry_EvenWhenInnerLevelWouldFilterIt() {
+ // GIVEN a collector wrapping an inner logger
+ var collector = new DiagnosticCollector(NullLogger.Instance);
+
+ // WHEN a debug message is logged (would be filtered from display)
+ collector.LogDebug("hidden detail {0}", 42);
+
+ // THEN the collector still captures it in full
+ var snapshot = collector.Snapshot();
+ Assert.Single(snapshot);
+ Assert.Equal(LogLevel.Debug, snapshot[0].Level);
+ Assert.Equal("hidden detail 42", snapshot[0].Message);
+ }
+
+ [Fact]
+ public void Log_ForwardsToInnerLogger() {
+ var inner = new ListLogger();
+ var collector = new DiagnosticCollector(inner);
+
+ collector.LogInformation("forward me");
+
+ Assert.Contains("forward me", inner.Messages);
+ }
+
+ [Fact]
+ public void Log_CapturesExceptionText() {
+ var collector = new DiagnosticCollector(NullLogger.Instance);
+ var ex = new InvalidOperationException("boom");
+
+ collector.LogError(ex, "failed");
+
+ var entry = Assert.Single(collector.Snapshot());
+ Assert.Equal("failed", entry.Message);
+ Assert.Contains("boom", entry.Exception);
+ }
+
+ [Fact]
+ public void IsEnabled_ReturnsTrue_ToCaptureAllLevels() {
+ var collector = new DiagnosticCollector(NullLogger.Instance);
+ Assert.True(collector.IsEnabled(LogLevel.Trace));
+ }
+
+ [Fact]
+ public void Finish_RecordsSuccess() {
+ var collector = new DiagnosticCollector(NullLogger.Instance);
+ ((IProgressReporter)collector).Finish(true);
+ Assert.True(collector.Successful);
+ }
+
+ [Fact]
+ public void Finish_LastWins_SoNestedPackerRunDoesNotClobberTopLevelResult() {
+ var collector = new DiagnosticCollector(NullLogger.Instance);
+ var reporter = (IProgressReporter)collector;
+
+ // nested packer stub finishes first (success), then the top-level run fails
+ reporter.Finish(true);
+ reporter.Finish(false);
+
+ Assert.False(collector.Successful);
+ }
+
+ [Fact]
+ public void Finish_ForwardsToInnerReporter() {
+ var spy = new SpyReporter();
+ var collector = new DiagnosticCollector(NullLogger.Instance, spy);
+
+ ((IProgressReporter)collector).Finish(true);
+
+ Assert.Equal(1, spy.FinishCount);
+ Assert.True(spy.LastSuccessful);
+ }
+
+ [Fact]
+ public void Progress_ForwardsToInnerReporter() {
+ var spy = new SpyReporter();
+ var collector = new DiagnosticCollector(NullLogger.Instance, spy);
+ var reporter = (IProgressReporter)collector;
+
+ reporter.Progress(3, 10);
+ reporter.EndProgress();
+
+ Assert.Equal(1, spy.ProgressCount);
+ Assert.Equal(1, spy.EndProgressCount);
+ }
+
+ [Fact]
+ public void Snapshot_KeepsOnlyMostRecentEntries_AndCountsDropped() {
+ var collector = new DiagnosticCollector(NullLogger.Instance, capacity: 3);
+
+ for (int i = 0; i < 5; i++)
+ collector.LogInformation("entry {0}", i);
+
+ var snapshot = collector.Snapshot();
+ Assert.Equal(3, snapshot.Count);
+ Assert.Equal(2, collector.DroppedCount);
+ Assert.Equal("entry 2", snapshot[0].Message);
+ Assert.Equal("entry 4", snapshot[2].Message);
+ }
+
+ [Fact]
+ public void GenerateReport_NeverThrows_AndContainsCoreSections() {
+ var collector = new DiagnosticCollector(NullLogger.Instance);
+ collector.LogInformation("did a thing");
+ ((IProgressReporter)collector).Finish(false);
+
+ var report = collector.GenerateReport();
+
+ Assert.Contains("## System", report);
+ Assert.Contains("## Result", report);
+ Assert.Contains("did a thing", report);
+ }
+
+ sealed class ListLogger : ILogger {
+ public List Messages { get; } = new List();
+ public IDisposable BeginScope(TState state) => NullScope.Instance;
+ public bool IsEnabled(LogLevel logLevel) => true;
+
+ public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception,
+ Func formatter) => Messages.Add(formatter(state, exception));
+
+ sealed class NullScope : IDisposable {
+ public static readonly NullScope Instance = new NullScope();
+ public void Dispose() { }
+ }
+ }
+
+ sealed class SpyReporter : IProgressReporter {
+ public int ProgressCount;
+ public int EndProgressCount;
+ public int FinishCount;
+ public bool LastSuccessful;
+
+ public void Progress(int progress, int overall) => ProgressCount++;
+ public void EndProgress() => EndProgressCount++;
+ public void Finish(bool successful) {
+ FinishCount++;
+ LastSuccessful = successful;
+ }
+ }
+ }
+}
diff --git a/Tests/Confuser.Core.Test/Diagnostics/DiagnosticReportTest.cs b/Tests/Confuser.Core.Test/Diagnostics/DiagnosticReportTest.cs
new file mode 100644
index 000000000..cbdc959c5
--- /dev/null
+++ b/Tests/Confuser.Core.Test/Diagnostics/DiagnosticReportTest.cs
@@ -0,0 +1,132 @@
+using System.IO;
+using Confuser.Core;
+using Confuser.Core.Diagnostics;
+using Confuser.Core.Project;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+using Xunit;
+
+namespace Confuser.Core.Test.Diagnostics {
+ public class DiagnosticReportTest {
+ static DiagnosticCollector CollectorFor(ConfuserProject project) {
+ var collector = new DiagnosticCollector(NullLogger.Instance) { Project = project };
+ return collector;
+ }
+
+ [Fact]
+ public void Generate_ResultFailed_WhenRunUnsuccessful() {
+ var collector = CollectorFor(new ConfuserProject());
+ ((IProgressReporter)collector).Finish(false);
+
+ Assert.Contains("## Result: FAILED", collector.GenerateReport());
+ }
+
+ [Fact]
+ public void Generate_ResultSuccess_WhenRunSuccessful() {
+ var collector = CollectorFor(new ConfuserProject());
+ ((IProgressReporter)collector).Finish(true);
+
+ Assert.Contains("## Result: SUCCESS", collector.GenerateReport());
+ }
+
+ [Fact]
+ public void Generate_ListsProtections_FromProjectRules() {
+ var project = new ConfuserProject();
+ var rule = new Rule();
+ rule.Add(new SettingItem("rename"));
+ rule.Add(new SettingItem("constants"));
+ project.Rules.Add(rule);
+
+ var report = CollectorFor(project).GenerateReport();
+
+ Assert.Contains("rename", report);
+ Assert.Contains("constants", report);
+ }
+
+ [Fact]
+ public void Generate_NeverLeaksStrongNamePassword() {
+ var project = new ConfuserProject { BaseDirectory = @"C:\proj", OutputDirectory = @"C:\proj\out" };
+ project.Add(new ProjectModule {
+ Path = "App.dll",
+ SNKeyPath = @"C:\proj\key.snk",
+ SNKeyPassword = "SuperSecret123",
+ SNSigKeyPassword = "AlsoSecret456"
+ });
+
+ var report = CollectorFor(project).GenerateReport();
+
+ Assert.DoesNotContain("SuperSecret123", report);
+ Assert.DoesNotContain("AlsoSecret456", report);
+ // but the module itself is still listed for context
+ Assert.Contains("App.dll", report);
+ }
+
+ [Fact]
+ public void Generate_ListsPacker_OrNoneWhenAbsent() {
+ var withPacker = new ConfuserProject { Packer = new SettingItem("compressor") };
+ Assert.Contains("compressor", CollectorFor(withPacker).GenerateReport());
+
+ var noPacker = new ConfuserProject();
+ Assert.Contains("Packer: (none)", CollectorFor(noPacker).GenerateReport());
+ }
+
+ [Fact]
+ public void Generate_UsesDynamicFence_WhenLogContainsBacktickFence() {
+ var collector = CollectorFor(new ConfuserProject());
+ collector.LogInformation("here is a ``` fence inside a message");
+
+ var report = collector.GenerateReport();
+
+ // a plain ``` fence would be broken by the message; the report must use a longer fence
+ Assert.Contains("````", report);
+ }
+
+ [Theory]
+ [InlineData(@"C:\Users\alice\proj\App.dll", @"C:\Users\alice", "%USER%")]
+ [InlineData(@"c:\users\ALICE\proj\App.dll", @"C:\Users\alice", "%USER%")]
+ public void Redact_ReplacesUserProfilePrefix(string text, string userProfile, string expectedMarker) {
+ var result = DiagnosticRedactor.Redact(text, userProfile);
+ Assert.DoesNotContain("alice", result, System.StringComparison.OrdinalIgnoreCase);
+ Assert.Contains(expectedMarker, result);
+ }
+
+ [Fact]
+ public void Redact_LeavesTextWithoutProfileUntouched() {
+ Assert.Equal("no paths here", DiagnosticRedactor.Redact("no paths here", @"C:\Users\alice"));
+ }
+
+ [Fact]
+ public void TryReadTargetFramework_ReadsMoniker_FromRealAssembly() {
+ var path = typeof(DiagnosticCollector).Assembly.Location;
+ var tfm = DiagnosticReport.TryReadTargetFramework(path);
+ Assert.NotNull(tfm);
+ Assert.Contains("Version=v", tfm);
+ }
+
+ [Fact]
+ public void TryReadTargetFramework_ReturnsNull_ForMissingFile() {
+ Assert.Null(DiagnosticReport.TryReadTargetFramework(@"C:\does\not\exist.dll"));
+ }
+
+ [Fact]
+ public void TryReadTargetFramework_ReturnsNull_ForNonAssemblyFile() {
+ var tmp = Path.GetTempFileName();
+ File.WriteAllText(tmp, "definitely not a PE file");
+ try {
+ Assert.Null(DiagnosticReport.TryReadTargetFramework(tmp));
+ }
+ finally {
+ File.Delete(tmp);
+ }
+ }
+
+ [Fact]
+ public void Generate_IncludesTargetFramework_WhenModuleResolves() {
+ var coreDll = typeof(DiagnosticCollector).Assembly.Location;
+ var project = new ConfuserProject { BaseDirectory = Path.GetDirectoryName(coreDll) };
+ project.Add(new ProjectModule { Path = Path.GetFileName(coreDll) });
+
+ Assert.Contains("Target Framework:", CollectorFor(project).GenerateReport());
+ }
+ }
+}
diff --git a/Tests/Confuser.GUI.Test/Confuser.GUI.Test.csproj b/Tests/Confuser.GUI.Test/Confuser.GUI.Test.csproj
index 2bbe390a7..19fabdf65 100644
--- a/Tests/Confuser.GUI.Test/Confuser.GUI.Test.csproj
+++ b/Tests/Confuser.GUI.Test/Confuser.GUI.Test.csproj
@@ -8,9 +8,6 @@
-
-
-
diff --git a/Tests/Confuser.GUI.Test/GuiSmokeTest.cs b/Tests/Confuser.GUI.Test/GuiSmokeTest.cs
index ac6f04a64..50e3e85e1 100644
--- a/Tests/Confuser.GUI.Test/GuiSmokeTest.cs
+++ b/Tests/Confuser.GUI.Test/GuiSmokeTest.cs
@@ -150,15 +150,28 @@ public void Gui_ProtectSampleApp_ShowsSuccess() {
LaunchGui($"\"{crprojPath}\"");
var mainWindow = WaitForMainWindow(app);
- // Navigate to the Protect! tab
+ // Navigate to the Protect! tab. Match by TabItem control type + name —
+ // NOT ByText, which ambiguously matches both the tab header and the
+ // Protect! button (they share the caption "Protect!").
var protectTab = Retry.WhileNull(
- () => mainWindow.FindFirstDescendant(cf => cf.ByText("Protect!")),
- TimeSpan.FromSeconds(5),
- TimeSpan.FromMilliseconds(500)).Result;
+ () => mainWindow.FindFirstDescendant(cf =>
+ cf.ByControlType(FlaUI.Core.Definitions.ControlType.TabItem)
+ .And(cf.ByName("Protect!"))),
+ TimeSpan.FromSeconds(10),
+ TimeSpan.FromMilliseconds(300)).Result;
Assert.NotNull(protectTab);
- protectTab.Click();
- // Find and click the Protect! button
+ // Select the tab and wait until it is actually selected. WPF virtualizes
+ // inactive tab content, so the Protect! button does not enter the UIA tree
+ // until the tab is selected and its content has rendered.
+ var tabItem = protectTab.AsTabItem();
+ tabItem.Select();
+ Retry.WhileFalse(
+ () => tabItem.IsSelected,
+ TimeSpan.FromSeconds(5),
+ TimeSpan.FromMilliseconds(200));
+
+ // Find and click the Protect! button (only present once the tab is rendered).
var protectButton = Retry.WhileNull(
() => {
var buttons = mainWindow.FindAllDescendants(cf => cf.ByControlType(FlaUI.Core.Definitions.ControlType.Button));
@@ -167,8 +180,8 @@ public void Gui_ProtectSampleApp_ShowsSuccess() {
}
return null;
},
- TimeSpan.FromSeconds(5),
- TimeSpan.FromMilliseconds(500)).Result;
+ TimeSpan.FromSeconds(15),
+ TimeSpan.FromMilliseconds(300)).Result;
Assert.NotNull(protectButton);
output.WriteLine("Clicking Protect! button...");
diff --git a/Tests/Confuser.Renamer.Test/Analyzers/ReflectionAnalyzerTest.cs b/Tests/Confuser.Renamer.Test/Analyzers/ReflectionAnalyzerTest.cs
index fe2ff1be0..c1ff62411 100644
--- a/Tests/Confuser.Renamer.Test/Analyzers/ReflectionAnalyzerTest.cs
+++ b/Tests/Confuser.Renamer.Test/Analyzers/ReflectionAnalyzerTest.cs
@@ -6,10 +6,10 @@
using Confuser.Renamer.Analyzers;
using Confuser.UnitTest;
using dnlib.DotNet;
+using Microsoft.Extensions.Logging;
using Moq;
using Xunit;
using Xunit.Abstractions;
-using ILogger = Confuser.Core.ILogger;
namespace Confuser.Renamer.Test.Analyzers {
public sealed class ReflectionAnalyzerTest {
@@ -21,7 +21,7 @@ public sealed class ReflectionAnalyzerTest {
public ReflectionAnalyzerTest(ITestOutputHelper outputHelper) =>
_outputHelper = outputHelper ?? throw new ArgumentNullException(nameof(outputHelper));
- private ILogger CreateLogger() => new XunitLogger(_outputHelper);
+ private Microsoft.Extensions.Logging.ILogger CreateLogger() => new XunitLogger(_outputHelper);
private string ReferenceProperty { get; }
diff --git a/Tests/Confuser.UnitTest/Confuser.UnitTest.csproj b/Tests/Confuser.UnitTest/Confuser.UnitTest.csproj
index 557a11859..c16da1b77 100644
--- a/Tests/Confuser.UnitTest/Confuser.UnitTest.csproj
+++ b/Tests/Confuser.UnitTest/Confuser.UnitTest.csproj
@@ -6,10 +6,11 @@
net462;net10.0
+
-
-
diff --git a/Tests/Confuser.UnitTest/TestBase.cs b/Tests/Confuser.UnitTest/TestBase.cs
index c23372ebc..006b8ae64 100644
--- a/Tests/Confuser.UnitTest/TestBase.cs
+++ b/Tests/Confuser.UnitTest/TestBase.cs
@@ -80,9 +80,11 @@ protected async Task Run(string[] inputFileNames, string[] expectedOutput, IEnum
if (rule.Count > 0)
proj.Rules.Add(rule);
+ var xunitLogger = new XunitLogger(outputHelper, outputAction);
var parameters = new ConfuserParameters {
Project = proj,
- Logger = new XunitLogger(outputHelper, outputAction)
+ Logger = xunitLogger,
+ ProgressReporter = xunitLogger
};
await ConfuserEngine.Run(parameters);
diff --git a/Tests/Confuser.UnitTest/XUnitLogger.cs b/Tests/Confuser.UnitTest/XUnitLogger.cs
index aa627b036..a7d8cddd3 100644
--- a/Tests/Confuser.UnitTest/XUnitLogger.cs
+++ b/Tests/Confuser.UnitTest/XUnitLogger.cs
@@ -1,9 +1,10 @@
-using System;
+using System;
using Confuser.Core;
+using Microsoft.Extensions.Logging;
using Xunit.Abstractions;
namespace Confuser.UnitTest {
- public sealed class XunitLogger : ILogger {
+ public sealed class XunitLogger : Microsoft.Extensions.Logging.ILogger, IProgressReporter {
private readonly ITestOutputHelper _outputHelper;
private readonly Action _outputAction;
@@ -14,45 +15,25 @@ public XunitLogger(ITestOutputHelper outputHelper, Action outputAction)
_outputAction = outputAction;
}
- void ILogger.Debug(string msg) =>
- ProcessOutput("[DEBUG] " + msg);
+ public IDisposable BeginScope(TState state) => null;
- void ILogger.DebugFormat(string format, params object[] args) =>
- ProcessOutput("[DEBUG] " + format, args);
+ public bool IsEnabled(LogLevel logLevel) => true;
- void ILogger.EndProgress() { }
+ public void Log(LogLevel logLevel, EventId eventId, TState state,
+ Exception exception, Func formatter) {
+ var message = formatter(state, exception);
- void ILogger.Error(string msg) =>
- throw new Exception(msg);
+ if (logLevel >= LogLevel.Error)
+ throw new Exception(message, exception);
- void ILogger.ErrorException(string msg, Exception ex) =>
- throw new Exception(msg, ex);
-
- void ILogger.ErrorFormat(string format, params object[] args) =>
- throw new Exception(string.Format(format, args));
-
- void ILogger.Finish(bool successful) =>
- ProcessOutput("[DONE]");
-
- void ILogger.Info(string msg) =>
- ProcessOutput("[INFO] " + msg);
-
- void ILogger.InfoFormat(string format, params object[] args) =>
- ProcessOutput("[INFO] " + format, args);
-
- void ILogger.Progress(int progress, int overall) { }
-
- void ILogger.Warn(string msg) =>
- ProcessOutput("[WARN] " + msg);
+ ProcessOutput(message);
+ }
- void ILogger.WarnException(string msg, Exception ex) =>
- ProcessOutput("[WARN] " + msg + Environment.NewLine + ex.ToString());
+ void IProgressReporter.Progress(int progress, int overall) { }
- void ILogger.WarnFormat(string format, params object[] args) =>
- ProcessOutput("[WARN] " + format, args);
+ void IProgressReporter.EndProgress() { }
- private void ProcessOutput(string format, params object[] args) =>
- ProcessOutput(string.Format(format, args));
+ void IProgressReporter.Finish(bool successful) => ProcessOutput("[DONE]");
private void ProcessOutput(string message) {
_outputAction?.Invoke(message);
diff --git a/Tests/CrossFramework.Library.Net10/SampleService.cs b/Tests/CrossFramework.Library.Net10/SampleService.cs
index 110d9fa41..04ada0451 100644
--- a/Tests/CrossFramework.Library.Net10/SampleService.cs
+++ b/Tests/CrossFramework.Library.Net10/SampleService.cs
@@ -40,4 +40,12 @@ public string Process(string data) {
return new string(chars);
}
}
+
+ // Carries a C# 13+ 'allows ref struct' generic constraint, which the compiler emits as
+ // the GenericParamAttributes.AllowByRefLike flag. Used to verify obfuscation preserves it.
+ public static class RefStructConsumer {
+ public static void Consume(T value) where T : allows ref struct {
+ // Intentionally empty — the 'allows ref struct' constraint is what this exercises.
+ }
+ }
}
diff --git a/Tests/CrossFramework.Test/CrossFramework.Test.csproj b/Tests/CrossFramework.Test/CrossFramework.Test.csproj
index 84e67f836..8ce559c21 100644
--- a/Tests/CrossFramework.Test/CrossFramework.Test.csproj
+++ b/Tests/CrossFramework.Test/CrossFramework.Test.csproj
@@ -5,12 +5,6 @@
false
-
-
-
-
-
-
@@ -43,12 +37,4 @@
-
-
-
-
-
diff --git a/Tests/CrossFramework.Test/LibraryFrameworkTest.cs b/Tests/CrossFramework.Test/LibraryFrameworkTest.cs
index 17a977088..2d47de6e7 100644
--- a/Tests/CrossFramework.Test/LibraryFrameworkTest.cs
+++ b/Tests/CrossFramework.Test/LibraryFrameworkTest.cs
@@ -1,8 +1,10 @@
using System.IO;
+using System.Linq;
using System.Threading.Tasks;
using Confuser.Core;
using Confuser.Core.Project;
using Confuser.UnitTest;
+using dnlib.DotNet;
using Xunit;
using Xunit.Abstractions;
@@ -64,5 +66,31 @@ public Task Library_Net10_RenameProtection() =>
new SettingItem("rename"),
outputDirSuffix: "-lib-net10",
checkOutput: false);
+
+ // Regression guard: the subject has a 'allows ref struct' generic constraint
+ // (GenericParamAttributes.AllowByRefLike). Renaming must not drop it — verify the flag
+ // survives obfuscation on the output assembly. (dnlib 4.x names this flag; ConfuserEx
+ // only renames generic parameters, so the attribute bits must be preserved.)
+ [Fact]
+ [Trait("Category", "CrossFramework")]
+ [Trait("AppType", "Library")]
+ [Trait("TFM", "net10.0")]
+ public Task Library_Net10_PreservesAllowByRefLike() =>
+ Run("CrossFramework.Library.Net10.dll",
+ null,
+ new SettingItem("rename"),
+ outputDirSuffix: "-lib-net10-refstruct",
+ checkOutput: false,
+ postProcessAction: outputPath => {
+ var modulePath = Path.Combine(outputPath, "CrossFramework.Library.Net10.dll");
+ using var module = ModuleDefMD.Load(modulePath);
+ var genericParams = module.GetTypes()
+ .SelectMany(type => type.Methods)
+ .SelectMany(method => method.GenericParameters)
+ .Concat(module.GetTypes().SelectMany(type => type.GenericParameters));
+ Assert.Contains(genericParams,
+ gp => (gp.Flags & GenericParamAttributes.AllowByRefLike) != 0);
+ return Task.CompletedTask;
+ });
}
}
diff --git a/Tests/Directory.Build.props b/Tests/Directory.Build.props
index a309b3686..57b3cea27 100644
--- a/Tests/Directory.Build.props
+++ b/Tests/Directory.Build.props
@@ -3,18 +3,38 @@
-
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
- $(MSBuildThisFileDirectory)coverlet.runsettings
-
+
+
+
+
+
+
diff --git a/Tests/Directory.Build.targets b/Tests/Directory.Build.targets
new file mode 100644
index 000000000..2e73f35ba
--- /dev/null
+++ b/Tests/Directory.Build.targets
@@ -0,0 +1,22 @@
+
+
+
+
+ $(MSBuildThisFileDirectory)coverlet.runsettings
+
+
+
diff --git a/Tests/MessageDeobfuscation.Test/MessageDeobfuscationTest.cs b/Tests/MessageDeobfuscation.Test/MessageDeobfuscationTest.cs
index 0b1617842..7e0dbfe5e 100644
--- a/Tests/MessageDeobfuscation.Test/MessageDeobfuscationTest.cs
+++ b/Tests/MessageDeobfuscation.Test/MessageDeobfuscationTest.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.IO;
+using System.Linq;
using System.Threading.Tasks;
using Confuser.Core;
using Confuser.Core.Project;
@@ -22,80 +23,58 @@ public class MessageDeobfuscationTest : TestBase {
public MessageDeobfuscationTest(ITestOutputHelper outputHelper) : base(outputHelper) { }
[Theory]
- [MemberData(nameof(RenameModeAndExpectedObfuscatedOutput))]
+ [MemberData(nameof(RenameModes))]
[Trait("Category", "Protection")]
[Trait("Protection", "rename")]
- public async Task MessageDeobfuscationWithSymbolsMap(string renameMode, string[] expectedObfuscatedOutput) =>
+ public async Task MessageDeobfuscationWithSymbolsMap(string renameMode) =>
await Run(
"MessageDeobfuscation.exe",
- expectedObfuscatedOutput,
+ Array.Empty(),
new SettingItem("rename") { ["mode"] = renameMode },
$"SymbolsMap_{renameMode}",
seed: "1234",
+ // The obfuscated program output contains the renamed identifiers, which are
+ // volatile — don't assert on their exact text. The symbols map round-trip below
+ // verifies the renaming and deobfuscation instead.
+ checkOutput: false,
postProcessAction: outputPath => {
- var deobfuscator = MessageDeobfuscator.Load(Path.Combine(outputPath, "symbols.map"));
- var deobfuscatedMessage =
- deobfuscator.DeobfuscateMessage(string.Join(Environment.NewLine, expectedObfuscatedOutput));
-
- string classId, nestedClassId, methodId, fieldId, propertyId, eventId;
- if (renameMode == nameof(RenameMode.Decodable)) {
- classId = "_OokpKOmal5JNZMPvSAFgHLHjBke";
- nestedClassId = "_tc5CFDIJ2J9Fx3ehd3sgjTMAxCaA";
- methodId = "_zbgDV4jbK6Oi9WBq66uG2ct7IoRA";
- fieldId = "_QHxqC1xaBFmUQawCZSOQpattICo";
- propertyId = "_FJthtfOBOiQFgVDIymbi3wwJoeN";
- eventId = "_cbPBZqkDuaNXOkmJtacrG2uYfZs";
- }
- else {
- classId = "_F";
- nestedClassId = "_D";
- methodId = "_c";
- fieldId = "_C";
- propertyId = "_e";
- eventId = "_A";
- }
-
- void CheckName(string expectedFullName, string expectedShortName, string obfuscatedName) {
- var fullName = deobfuscator.DeobfuscateSymbol(obfuscatedName, false);
- Assert.Equal(expectedFullName, fullName);
- Assert.Equal(expectedShortName, MessageDeobfuscator.ExtractShortName(fullName));
+ var symbolsPath = Path.Combine(outputPath, "symbols.map");
+ var map = File.ReadAllLines(symbolsPath)
+ .Select(line => line.Split('\t'))
+ .Where(parts => parts.Length == 2)
+ .ToDictionary(parts => parts[0], parts => parts[1]);
+
+ // The exact obfuscated identifiers depend on the rename algorithm, dnlib
+ // version and framework, so assert on the original names — the symbols map
+ // must contain each renamed member's original full name.
+ var expectedOriginals = new[] {
+ "MessageDeobfuscation.Class",
+ "MessageDeobfuscation.Class/NestedClass",
+ "MessageDeobfuscation.Class::Method(System.String,System.Int32)",
+ "MessageDeobfuscation.Class::Field",
+ "MessageDeobfuscation.Class::Property",
+ "MessageDeobfuscation.Class::Event"
+ };
+ foreach (var original in expectedOriginals)
+ Assert.Contains(original, map.Values);
+
+ // The deobfuscator must reverse the map: each obfuscated symbol resolves back
+ // to its original full name (verified using the map's own keys, so this is
+ // robust to identifier drift).
+ var deobfuscator = MessageDeobfuscator.Load(symbolsPath);
+ foreach (var original in expectedOriginals) {
+ var obfuscated = map.First(entry => entry.Value == original).Key;
+ Assert.Equal(original, deobfuscator.DeobfuscateSymbol(obfuscated, false));
}
- CheckName("MessageDeobfuscation.Class", "MessageDeobfuscation.Class",
- classId);
- CheckName("MessageDeobfuscation.Class/NestedClass", "NestedClass",
- nestedClassId);
- CheckName("MessageDeobfuscation.Class::Method(System.String,System.Int32)", "Method",
- methodId);
- CheckName("MessageDeobfuscation.Class::Field", "Field",
- fieldId);
- CheckName("MessageDeobfuscation.Class::Property", "Property",
- propertyId);
- CheckName("MessageDeobfuscation.Class::Event", "Event",
- eventId);
-
- Assert.Equal(_expectedDeobfuscatedOutput, deobfuscatedMessage);
return Task.Delay(0);
}
);
- public static IEnumerable