From 3178e2eec76b5c7bea5a811861fc240b9ea408fc Mon Sep 17 00:00:00 2001 From: mcpolo99 <32239939+mcpolo99@users.noreply.github.com> Date: Sun, 7 Jun 2026 18:55:44 +0200 Subject: [PATCH 01/24] fix(ci): add pre-release branch to CI triggers (#36) Co-authored-by: RandomCrocodile --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6bfc63b43..084e912bc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,9 +2,9 @@ name: ci on: push: - branches: [master, feature/**, fix/**] + branches: [master, pre-release, feature/**, fix/**] pull_request: - branches: [master] + branches: [master, pre-release] jobs: build: From 8cae43e84e2d7ebf69aee638bd964398650bfe37 Mon Sep 17 00:00:00 2001 From: mcpolo99 <32239939+mcpolo99@users.noreply.github.com> Date: Sun, 7 Jun 2026 18:56:04 +0200 Subject: [PATCH 02/24] fix(renamer): fix WPF relative resource renaming (#30) * fix WPF relative resource renaming * Update Confuser.Renamer/Analyzers/WPFAnalyzer.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Ryan Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Confuser.Renamer/Analyzers/WPFAnalyzer.cs | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/Confuser.Renamer/Analyzers/WPFAnalyzer.cs b/Confuser.Renamer/Analyzers/WPFAnalyzer.cs index 9cc9993f9..7f0132165 100644 --- a/Confuser.Renamer/Analyzers/WPFAnalyzer.cs +++ b/Confuser.Renamer/Analyzers/WPFAnalyzer.cs @@ -60,8 +60,14 @@ public void PreRename(ConfuserContext context, INameService service, ProtectionP var decodedName = HttpUtility.UrlDecode(doc.DocumentName); var encodedName = doc.DocumentName; if (bamlRefs.TryGetValue(decodedName, out var references)) { - var decodedDirectory = decodedName.Substring(0, decodedName.LastIndexOf('/') + 1); - var encodedDirectory = encodedName.Substring(0, encodedName.LastIndexOf('/') + 1); + var decodedLastSlash = decodedName.LastIndexOf('/') + 1; + var encodedLastSlash = encodedName.LastIndexOf('/') + 1; + + var decodedDirectory = decodedName.Substring(0, decodedLastSlash); + var encodedDirectory = encodedName.Substring(0, encodedLastSlash); + + var decodedFileName = decodedName.Substring(decodedLastSlash); + var encodedFileName = encodedName.Substring(encodedLastSlash); var fileName = service.RandomName(renameMode).ToLowerInvariant(); if (decodedName.EndsWith(".BAML", StringComparison.OrdinalIgnoreCase)) @@ -78,8 +84,8 @@ public void PreRename(ConfuserContext context, INameService service, ProtectionP if (renameOk) { foreach (var bamlRef in references) { - bamlRef.Rename(module, decodedName, decodedNewName); - bamlRef.Rename(module, encodedName, encodedNewName); + bamlRef.Rename(module, decodedFileName, fileName); + bamlRef.Rename(module, encodedFileName, fileName); } doc.DocumentName = encodedNewName; } @@ -349,4 +355,4 @@ void AnalyzeResources(ConfuserContext context, INameService service, ModuleDefMD context.Annotations.Set(module, BAMLKey, wpfResInfo); } } -} +} \ No newline at end of file From 3ad3663df44fbe3977b98051921157cfdf114142 Mon Sep 17 00:00:00 2001 From: mcpolo99 <32239939+mcpolo99@users.noreply.github.com> Date: Sun, 7 Jun 2026 18:56:13 +0200 Subject: [PATCH 03/24] fix(core): support .NET Standard library obfuscation (#31) * Add types in referencing assembly to netstandard DLL. Types defined in AssemblyRefs of netstandard (e.g. mscorlib) will be moved to netstandard. Therefore, subsequence ModuleDef.Find will return AssemblyRef to netstandard. As a result, the confused module will only reference to netstandard. * Remove AssemblyAttributes.PA_NoPlatform from assembly. Net standard project may refer to NuGet package (e.g. System.ComponentModel.Composition) in order to use the Framework libraries. However, DLL in NuGet may have this attribute set but the actual Framework DLL does not. As a result, dnlib treats them as different assemblies and confused DLL will reference to two identical assemblies. * Fix assembly reference to assemblies that hidden by netstandard assembly. TargetModule.GetAssemblyRef returns null if type is defined in assembly hidden by netstandard. This change searches assemblies hidden by netstandard and returns the fixed type reference. * Returns the method from CorLib of module to be confused instead from runtime. MSBuild may runs on .net framework and runtime help will reference to mscorlib instead of netstandard. This change try resolve it from CorLib before from runtime type. --------- Co-authored-by: KC Ip --- Confuser.Core/ConfuserAssemblyResolver.cs | 46 ++++++++++++++++++- Confuser.Core/Helpers/InjectHelper.cs | 11 ++++- Confuser.Protections/Compress/Compressor.cs | 6 +-- Confuser.Protections/Constants/EncodePhase.cs | 2 +- Confuser.Protections/Resources/InjectPhase.cs | 4 +- Confuser.Protections/Utils.cs | 18 ++++++++ 6 files changed, 78 insertions(+), 9 deletions(-) create mode 100644 Confuser.Protections/Utils.cs diff --git a/Confuser.Core/ConfuserAssemblyResolver.cs b/Confuser.Core/ConfuserAssemblyResolver.cs index 05ef4731f..22346b13e 100644 --- a/Confuser.Core/ConfuserAssemblyResolver.cs +++ b/Confuser.Core/ConfuserAssemblyResolver.cs @@ -33,8 +33,50 @@ public AssemblyDef Resolve(IAssembly assembly, ModuleDef sourceModule) { if (assembly is AssemblyDef assemblyDef) return assemblyDef; - var resolvedAssemblyDef = InternalExactResolver.Resolve(assembly, sourceModule); - return resolvedAssemblyDef ?? InternalFuzzyResolver.Resolve(assembly, sourceModule); + var resolvedAssemblyDef = + InternalExactResolver.Resolve(assembly, sourceModule) ?? + InternalFuzzyResolver.Resolve(assembly, sourceModule); + + // Remove AssemblyAttributes.PA_NoPlatform + if (null != resolvedAssemblyDef && + (AssemblyAttributes.PA_Mask & resolvedAssemblyDef.Attributes) == AssemblyAttributes.PA_NoPlatform) { + resolvedAssemblyDef.Attributes = + resolvedAssemblyDef.Attributes & ~AssemblyAttributes.PA_FullMask; + } + + if (resolvedAssemblyDef?.Name == "netstandard" && 0 < resolvedAssemblyDef.ManifestModule.ExportedTypes.Count) { + // Move types from AssemblyRef to here + var module = resolvedAssemblyDef.ManifestModule; + var newTypes = new List(); + var allAssemblyRefs = new List(); + + module.ExportedTypes.Clear(); + + foreach (var assemblyRef in module.GetAssemblyRefs()) { + var subAss = + InternalExactResolver.Resolve(assemblyRef, module) ?? + InternalFuzzyResolver.Resolve(assemblyRef, module); + allAssemblyRefs.Add(subAss); + foreach (var subModule in subAss?.Modules) { + foreach (var defType in subModule.Types) { + newTypes.Add(defType); + } + subModule.Types.Clear(); + foreach (var defType in newTypes) { + module.Types.Add(defType); + } + newTypes.Clear(); + } + } + + // Remove them because their types has been removed. + foreach (var subAss in allAssemblyRefs) { + InternalExactResolver.Remove(subAss); + InternalFuzzyResolver.Remove(subAss); + } + } + + return resolvedAssemblyDef; } public void Clear() { diff --git a/Confuser.Core/Helpers/InjectHelper.cs b/Confuser.Core/Helpers/InjectHelper.cs index 6aaf566a0..6cb066c23 100644 --- a/Confuser.Core/Helpers/InjectHelper.cs +++ b/Confuser.Core/Helpers/InjectHelper.cs @@ -292,7 +292,16 @@ public override ITypeDefOrRef Map(ITypeDefOrRef source) { // check if the assembly reference needs to be fixed. if (source is TypeRef sourceRef) { var targetAssemblyRef = TargetModule.GetAssemblyRef(sourceRef.DefinitionAssembly.Name); - if (!(targetAssemblyRef is null) && !string.Equals(targetAssemblyRef.FullName, source.DefinitionAssembly.FullName, StringComparison.Ordinal)) { + if (targetAssemblyRef is null) { + // Handle assemblies referenced by netstandard + var corLibAssemblyRef = TargetModule.CorLibTypes.AssemblyRef; + var corLibAssembly = TargetModule.Context.AssemblyResolver.Resolve(corLibAssemblyRef, TargetModule); + if (null != corLibAssembly?.ManifestModule.GetAssemblyRef(sourceRef.DefinitionAssembly.Name)) { + var fixedTypeRef = new TypeRefUser(sourceRef.Module, sourceRef.Namespace, sourceRef.Name, corLibAssemblyRef); + return Importer.Import(fixedTypeRef); + } + } + else if (!string.Equals(targetAssemblyRef.FullName, source.DefinitionAssembly.FullName, StringComparison.Ordinal)) { // We got a matching assembly by the simple name, but not by the full name. // This means the injected code uses a different assembly version than the target assembly. // We'll fix the assembly reference, to avoid breaking anything. diff --git a/Confuser.Protections/Compress/Compressor.cs b/Confuser.Protections/Compress/Compressor.cs index 33c0ea6b7..abbbd13c6 100644 --- a/Confuser.Protections/Compress/Compressor.cs +++ b/Confuser.Protections/Compress/Compressor.cs @@ -175,7 +175,7 @@ void PackModules(ConfuserContext context, CompressorContext compCtx, ModuleDef s context.Logger.EndProgress(); } - void InjectData(ModuleDef stubModule, MethodDef method, byte[] data) { + void InjectData(ConfuserContext context, ModuleDef stubModule, MethodDef method, byte[] data) { var dataType = new TypeDefUser("", "DataType", stubModule.CorLibTypes.GetTypeRef("System", "ValueType")); dataType.Layout = TypeAttributes.ExplicitLayout; dataType.Visibility = TypeAttributes.NestedPrivate; @@ -197,7 +197,7 @@ void InjectData(ModuleDef stubModule, MethodDef method, byte[] data) { repl.Add(Instruction.Create(OpCodes.Dup)); repl.Add(Instruction.Create(OpCodes.Ldtoken, dataField)); repl.Add(Instruction.Create(OpCodes.Call, stubModule.Import( - typeof(RuntimeHelpers).GetMethod("InitializeArray")))); + context, typeof(RuntimeHelpers), "InitializeArray"))); return repl.ToArray(); }); } @@ -254,7 +254,7 @@ void InjectStub(ConfuserContext context, CompressorContext compCtx, ProtectionPa MutationHelper.InjectKeys(entryPoint, new[] { 0, 1 }, new[] { encryptedModule.Length >> 2, (int)seed }); - InjectData(stubModule, entryPoint, encryptedModule); + InjectData(context, stubModule, entryPoint, encryptedModule); // Decrypt MethodDef decrypter = defs.OfType().Single(method => method.Name == "Decrypt"); diff --git a/Confuser.Protections/Constants/EncodePhase.cs b/Confuser.Protections/Constants/EncodePhase.cs index 4eba9118e..ade681d17 100644 --- a/Confuser.Protections/Constants/EncodePhase.cs +++ b/Confuser.Protections/Constants/EncodePhase.cs @@ -122,7 +122,7 @@ protected override void Execute(ConfuserContext context, ProtectionParameters pa repl.Add(Instruction.Create(OpCodes.Dup)); repl.Add(Instruction.Create(OpCodes.Ldtoken, moduleCtx.DataField)); repl.Add(Instruction.Create(OpCodes.Call, moduleCtx.Module.Import( - typeof(RuntimeHelpers).GetMethod("InitializeArray")))); + context, typeof(RuntimeHelpers), "InitializeArray"))); return repl.ToArray(); }); } diff --git a/Confuser.Protections/Resources/InjectPhase.cs b/Confuser.Protections/Resources/InjectPhase.cs index 540591750..27c58ab03 100644 --- a/Confuser.Protections/Resources/InjectPhase.cs +++ b/Confuser.Protections/Resources/InjectPhase.cs @@ -137,10 +137,10 @@ void MutateInitializer(REContext moduleCtx, MethodDef decomp) { repl.Add(Instruction.Create(OpCodes.Dup)); repl.Add(Instruction.Create(OpCodes.Ldtoken, moduleCtx.DataField)); repl.Add(Instruction.Create(OpCodes.Call, moduleCtx.Module.Import( - typeof(RuntimeHelpers).GetMethod("InitializeArray")))); + moduleCtx.Context, typeof(RuntimeHelpers), "InitializeArray"))); return repl.ToArray(); }); moduleCtx.Context.Registry.GetService().ExcludeMethod(moduleCtx.Context, moduleCtx.InitMethod); } } -} \ No newline at end of file +} diff --git a/Confuser.Protections/Utils.cs b/Confuser.Protections/Utils.cs new file mode 100644 index 000000000..e46f701c1 --- /dev/null +++ b/Confuser.Protections/Utils.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; +using System.Threading.Tasks; +using Confuser.Core; +using dnlib.DotNet; + +namespace Confuser.Protections { + internal static class Utils { + public static IMethod Import(this ModuleDef module, ConfuserContext context, Type classType, string method) { + var corLib = context.Resolver.Resolve(context.CurrentModule?.CorLibTypes.AssemblyRef, context.CurrentModule); + var typeInfo = corLib?.ManifestModule.Find(classType.FullName, true); + return (typeInfo == null) ? module.Import(classType.GetMethod(method)) : module.Import(typeInfo.FindMethod(method)); + } + } +} From b596de6d625a4f90ddaad55d501901d213469fab Mon Sep 17 00:00:00 2001 From: mcpolo99 <32239939+mcpolo99@users.noreply.github.com> Date: Sun, 7 Jun 2026 18:56:16 +0200 Subject: [PATCH 04/24] fix(core): don't create PDB files when debug=false (upstream #532) (#32) Co-authored-by: RandomCrocodile --- Confuser.Core/ConfuserEngine.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Confuser.Core/ConfuserEngine.cs b/Confuser.Core/ConfuserEngine.cs index 03c616d46..96401c988 100644 --- a/Confuser.Core/ConfuserEngine.cs +++ b/Confuser.Core/ConfuserEngine.cs @@ -421,6 +421,8 @@ static void WriteModule(ConfuserContext context) { static void Debug(ConfuserContext context) { context.Logger.Info("Finalizing..."); + if (!context.Project.Debug) + return; for (int i = 0; i < context.OutputModules.Count; i++) { if (context.OutputSymbols[i] == null) continue; From ca1551b8a06f1e0ddf4acda5c8a9db34b061ea7e Mon Sep 17 00:00:00 2001 From: mcpolo99 <32239939+mcpolo99@users.noreply.github.com> Date: Sun, 7 Jun 2026 18:56:19 +0200 Subject: [PATCH 05/24] fix(renamer): fix generic name parsing null case (upstream #516) (#33) When preserveGenericParams is true but name is null or empty, ParseGenericName would crash. Add a null/empty guard to prevent this. Cherry-pick of mkaring/ConfuserEx#516. Co-authored-by: RandomCrocodile --- Confuser.Renamer/NameService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Confuser.Renamer/NameService.cs b/Confuser.Renamer/NameService.cs index aafebadda..ac60618b7 100644 --- a/Confuser.Renamer/NameService.cs +++ b/Confuser.Renamer/NameService.cs @@ -214,7 +214,7 @@ public string ObfuscateName(IDnlibDef dnlibDef, RenameMode mode) { public string ObfuscateName(string format, string name, RenameMode mode, bool preserveGenericParams = false) { int genericParamsCount = 0; - if (preserveGenericParams) { + if (preserveGenericParams && !string.IsNullOrEmpty(name)) { name = ParseGenericName(name, out genericParamsCount); } From 93f9c34e8ce0468191d28ed146f774d2adfa3612 Mon Sep 17 00:00:00 2001 From: mcpolo99 <32239939+mcpolo99@users.noreply.github.com> Date: Sun, 7 Jun 2026 18:56:21 +0200 Subject: [PATCH 06/24] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=8E=A7=E5=88=B6?= =?UTF-8?q?=E6=B5=81=E4=BF=9D=E6=8A=A4=E5=9C=A8#153=E7=9A=84=E9=94=99?= =?UTF-8?q?=E8=AF=AF=20(#34)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 注意到 mkaring 在 1.4.0版本中对控制流保护做出了误操作(272行) 将src => statementLast.Contains(src),错误修改成了src => !statementLast.Contains(src),导致了保护后的程序出现了错误的循环,这里特此做出修复! Co-authored-by: wujiayang2007 <52036257+wujiayang2007@users.noreply.github.com> --- Confuser.Protections/ControlFlow/SwitchMangler.cs | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/Confuser.Protections/ControlFlow/SwitchMangler.cs b/Confuser.Protections/ControlFlow/SwitchMangler.cs index 5e259e289..7dca5f55c 100644 --- a/Confuser.Protections/ControlFlow/SwitchMangler.cs +++ b/Confuser.Protections/ControlFlow/SwitchMangler.cs @@ -260,16 +260,8 @@ public override void Mangle(CilBody body, ScopeBlock root, CFContext ctx) { src.Offset >= block.Instructions.Last().Offset)) return true; - // Disable flow obfuscation for blocks reached by jump instructions. - // Bug in #153 caused exactly this behaviour, expect for allowing wrong jump instructions - // There is another issue present here tracked here: - // https://github.com/mkaring/ConfuserEx/issues/162 - // Until this issue is resolved, the ctrl flow obfuscation will be severely reduced. - if (srcs.Any()) - return true; - // Not targeted by the last of statements - if (srcs.Any(src => !statementLast.Contains(src))) + if (srcs.Any(src => statementLast.Contains(src))) return true; } return false; From fa0b829244f9883aa70cec30de23b545ffb9d685 Mon Sep 17 00:00:00 2001 From: mcpolo99 <32239939+mcpolo99@users.noreply.github.com> Date: Sun, 7 Jun 2026 18:56:25 +0200 Subject: [PATCH 07/24] feat(core): add wildcard module loading in .crproj (upstream #481) (#35) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cherry-pick of mkaring/ConfuserEx#481 — allows using wildcards like *.dll in module paths to batch-load modules from the base directory. The Load method now accepts an optional baseDirRoot parameter to resolve relative base directory paths. New internal helpers AddModule, IsWildcard, and BatchLoadModules handle wildcard expansion using Directory.GetFiles with TopDirectoryOnly search. Co-authored-by: RandomCrocodile --- Confuser.CLI/Program.cs | 6 ++-- Confuser.Core/Project/ConfuserProject.cs | 45 +++++++++++++++++++++--- 2 files changed, 43 insertions(+), 8 deletions(-) diff --git a/Confuser.CLI/Program.cs b/Confuser.CLI/Program.cs index 267d82d0d..d8c56421e 100644 --- a/Confuser.CLI/Program.cs +++ b/Confuser.CLI/Program.cs @@ -60,10 +60,8 @@ static int Main(string[] args) { try { var xmlDoc = new XmlDocument(); xmlDoc.Load(files[0]); - proj.Load(xmlDoc); - string crprojDir = Path.GetDirectoryName(Path.GetFullPath(files[0])); - proj.BaseDirectory = Path.GetFullPath(Path.Combine(crprojDir, proj.BaseDirectory)); - proj.OutputDirectory = Path.GetFullPath(Path.Combine(crprojDir, proj.OutputDirectory)); + proj.Load(xmlDoc, Path.GetDirectoryName(Path.GetFullPath(files[0]))); + proj.OutputDirectory = Path.GetFullPath(Path.Combine(proj.BaseDirectory, proj.OutputDirectory)); } catch (Exception ex) { WriteLineWithColor(ConsoleColor.Red, "Failed to load project:"); diff --git a/Confuser.Core/Project/ConfuserProject.cs b/Confuser.Core/Project/ConfuserProject.cs index d8dc25a54..6f0073637 100644 --- a/Confuser.Core/Project/ConfuserProject.cs +++ b/Confuser.Core/Project/ConfuserProject.cs @@ -626,7 +626,7 @@ public XmlDocument Save() { /// /// The project XML contains schema errors. /// - public void Load(XmlDocument doc) { + public void Load(XmlDocument doc, string baseDirRoot = null) { doc.Schemas.Add(Schema); var exceptions = new List(); doc.Validate((sender, e) => { @@ -641,6 +641,10 @@ public void Load(XmlDocument doc) { OutputDirectory = docElem.Attributes["outputDir"].Value; BaseDirectory = docElem.Attributes["baseDir"].Value; + if (!string.IsNullOrEmpty(baseDirRoot)) + { + BaseDirectory = Path.Combine(baseDirRoot, BaseDirectory); + } if (docElem.Attributes["seed"] != null) Seed = docElem.Attributes["seed"].Value.NullIfEmpty(); @@ -674,13 +678,46 @@ public void Load(XmlDocument doc) { PluginPaths.Add(i.InnerText); } else { - var asm = new ProjectModule(); - asm.Load(i); - Add(asm); + AddModule(i); } } } + internal void AddModule(XmlElement elem) { + if (IsWildcard(elem.Attributes["path"].Value)) { + BatchLoadModules(elem); + } + else { + var asm = new ProjectModule(); + asm.Load(elem); + Add(asm); + } + } + + internal bool IsWildcard(string path) { + return !string.IsNullOrEmpty(path) && path.Contains(@"*"); + } + + internal bool BatchLoadModules(XmlElement elem) { + string wildCardPath = elem.Attributes["path"].Value; + string[] files = Directory.GetFiles(BaseDirectory, wildCardPath, SearchOption.TopDirectoryOnly); + if (files.Length <= 0) + { + return false; + } + + var asmPrototype = new ProjectModule(); + asmPrototype.Load(elem); + + foreach (string fileName in files) { + var moduleEntry = asmPrototype.Clone(); + moduleEntry.Path = fileName; + Add(moduleEntry); + } + + return true; + } + /// /// Clones this instance. /// From 49b365f4415e85e3a3b289792b42ce2e3cc3967c Mon Sep 17 00:00:00 2001 From: mcpolo99 <32239939+mcpolo99@users.noreply.github.com> Date: Sun, 7 Jun 2026 19:14:38 +0200 Subject: [PATCH 08/24] fix(renamer): skip renaming of DataContract/DataMember/JsonProperty attributed types and members (#38) Detect serialization-related attributes and exclude decorated types and members from renaming to prevent WCF/DataContract serialization breakage at runtime. Attributes now checked: - DataContractAttribute on types - DataMemberAttribute on fields and properties - EnumMemberAttribute on enum fields Fixes mcpolo99/private-ConfuserEx#9 Upstream: mkaring/ConfuserEx#147 Co-authored-by: RandomCrocodile Co-authored-by: Claude Opus 4.6 (1M context) --- Confuser.Renamer/AnalyzePhase.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/Confuser.Renamer/AnalyzePhase.cs b/Confuser.Renamer/AnalyzePhase.cs index 875a681ad..efd1ae693 100644 --- a/Confuser.Renamer/AnalyzePhase.cs +++ b/Confuser.Renamer/AnalyzePhase.cs @@ -206,6 +206,10 @@ void Analyze(NameService service, ConfuserContext context, ProtectionParameters if (type.InheritsFrom("System.Configuration.SettingsBase")) { service.SetCanRename(type, false); } + + if (type.HasAttribute("System.Runtime.Serialization.DataContractAttribute")) { + service.SetCanRename(type, false); + } } void Analyze(NameService service, ConfuserContext context, ProtectionParameters parameters, MethodDef method) { @@ -248,6 +252,12 @@ void Analyze(NameService service, ConfuserContext context, ProtectionParameters else if (field.IsLiteral && field.DeclaringType.IsEnum && !parameters.GetParameter(context, field, "renEnum", false)) service.SetCanRename(field, false); + + else if (field.HasAttribute("System.Runtime.Serialization.DataMemberAttribute")) + service.SetCanRename(field, false); + + else if (field.HasAttribute("System.Runtime.Serialization.EnumMemberAttribute")) + service.SetCanRename(field, false); } void Analyze(NameService service, ConfuserContext context, ProtectionParameters parameters, PropertyDef property) { @@ -267,6 +277,9 @@ void Analyze(NameService service, ConfuserContext context, ProtectionParameters else if (property.DeclaringType.Name.String.Contains("AnonymousType")) service.SetCanRename(property, false); + + else if (property.HasAttribute("System.Runtime.Serialization.DataMemberAttribute")) + service.SetCanRename(property, false); } void Analyze(NameService service, ConfuserContext context, ProtectionParameters parameters, EventDef evt) { From a1527f2e76de6259098f2247d84aaabc53c4a75f Mon Sep 17 00:00:00 2001 From: mcpolo99 <32239939+mcpolo99@users.noreply.github.com> Date: Sun, 7 Jun 2026 19:21:14 +0200 Subject: [PATCH 09/24] fix(renamer): preserve anonymous type constructor arg names for JSON serialization (#39) Co-authored-by: RandomCrocodile --- Confuser.Renamer/RenamePhase.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Confuser.Renamer/RenamePhase.cs b/Confuser.Renamer/RenamePhase.cs index cb8edd666..9c6647f83 100644 --- a/Confuser.Renamer/RenamePhase.cs +++ b/Confuser.Renamer/RenamePhase.cs @@ -36,8 +36,13 @@ protected override void Execute(ConfuserContext context, ProtectionParameters pa if (def is MethodDef method) { if ((canRename || method.IsConstructor) && parameters.GetParameter(context, method, "renameArgs", true)) { - foreach (var param in method.ParamDefs) - param.Name = null; + if (method.IsConstructor && method.DeclaringType.Name.String.Contains("AnonymousType")) { + // Anonymous type constructor args map to property names — renaming breaks JSON serialization + } + else { + foreach (var param in method.ParamDefs) + param.Name = null; + } } if (parameters.GetParameter(context, method, "renPdb", false) && method.HasBody) { From 9c4249cde2d583f6b8077d0e8912983b3d3d8eb8 Mon Sep 17 00:00:00 2001 From: mcpolo99 <32239939+mcpolo99@users.noreply.github.com> Date: Sun, 7 Jun 2026 19:31:05 +0200 Subject: [PATCH 10/24] feat(cli): add --snkey and --snkeypass options for CI/CD signing (#40) Co-authored-by: RandomCrocodile --- Confuser.CLI/Program.cs | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/Confuser.CLI/Program.cs b/Confuser.CLI/Program.cs index d8c56421e..de11d40a2 100644 --- a/Confuser.CLI/Program.cs +++ b/Confuser.CLI/Program.cs @@ -19,6 +19,8 @@ static int Main(string[] args) { bool noPause = false; bool debug = false; string outDir = null; + string snKeyPath = null; + string snKeyPass = null; List probePaths = new List(); List plugins = new List(); var p = new OptionSet { @@ -37,6 +39,12 @@ static int Main(string[] args) { }, { "debug", "specifies debug symbol generation.", value => { debug = (value != null); } + }, { + "snkey=", "specifies strong name key file path.", + value => { snKeyPath = value; } + }, { + "snkeypass=", "specifies strong name key password.", + value => { snKeyPass = value; } } }; @@ -101,10 +109,13 @@ static int Main(string[] args) { modulePath = modulePath.Substring(proj.BaseDirectory.Length + 1); } - if (TryMatchTemplateProject(templateModules, proj.BaseDirectory, modulePath, out var matchedModule)) + if (TryMatchTemplateProject(templateModules, proj.BaseDirectory, modulePath, out var matchedModule)) { + if (snKeyPath != null) matchedModule.SNKeyPath = snKeyPath; + if (snKeyPass != null) matchedModule.SNKeyPassword = snKeyPass; proj.Add(matchedModule); + } else - proj.Add(new ProjectModule { Path = modulePath }); + proj.Add(new ProjectModule { Path = modulePath, SNKeyPath = snKeyPath, SNKeyPassword = snKeyPass }); } proj.OutputDirectory = outDir; @@ -200,6 +211,8 @@ static void PrintUsage() { WriteLine(" -probe : specifies probe directory."); WriteLine(" -plugin : specifies plugin path."); WriteLine(" -debug : specifies debug symbol generation."); + WriteLine(" -snkey : specifies strong name key file path."); + WriteLine(" -snkeypass : specifies strong name key password."); } static void WriteLineWithColor(ConsoleColor color, string txt) { From 197d1d65593d7b20702a660643c3ea3d64170487 Mon Sep 17 00:00:00 2001 From: mcpolo99 <32239939+mcpolo99@users.noreply.github.com> Date: Sun, 7 Jun 2026 19:31:41 +0200 Subject: [PATCH 11/24] fix(runtime): catch ReflectionTypeLoadException in packer startup (#41) Co-authored-by: RandomCrocodile --- Confuser.Runtime/Compressor.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Confuser.Runtime/Compressor.cs b/Confuser.Runtime/Compressor.cs index abb2e62b3..5e3d3b09b 100644 --- a/Confuser.Runtime/Compressor.cs +++ b/Confuser.Runtime/Compressor.cs @@ -60,7 +60,8 @@ static int Main(string[] args) { AppDomain.CurrentDomain.AssemblyResolve += Resolve; // For some reasons, reflection on Assembly would not discover the types unless GetTypes is called. - m.GetTypes(); + try { m.GetTypes(); } + catch (ReflectionTypeLoadException) { } MethodBase e = m.ResolveMethod(key[0] | (key[1] << 8) | (key[2] << 16) | (key[3] << 24)); var g = new object[e.GetParameters().Length]; From e23515020e7077b565eb04c9e25b14d0239aeda4 Mon Sep 17 00:00:00 2001 From: mcpolo99 <32239939+mcpolo99@users.noreply.github.com> Date: Sun, 7 Jun 2026 19:33:24 +0200 Subject: [PATCH 12/24] fix(protections): use non-throwing resolution in ref proxy mild mode for external types (#42) Co-authored-by: RandomCrocodile --- Confuser.Protections/ReferenceProxy/MildMode.cs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/Confuser.Protections/ReferenceProxy/MildMode.cs b/Confuser.Protections/ReferenceProxy/MildMode.cs index fcc50f435..fc5c2f169 100644 --- a/Confuser.Protections/ReferenceProxy/MildMode.cs +++ b/Confuser.Protections/ReferenceProxy/MildMode.cs @@ -14,10 +14,12 @@ public override void ProcessCall(RPContext ctx, int instrIndex) { var target = (IMethod)invoke.Operand; // Value type proxy is not supported in mild mode. - if (target.DeclaringType.ResolveTypeDefThrow().IsValueType) + var declaringTypeDef = target.DeclaringType.ResolveTypeDef(); + if (declaringTypeDef == null || declaringTypeDef.IsValueType) return; // Skipping visibility is not supported in mild mode. - if (!target.ResolveThrow().IsPublic && !target.ResolveThrow().IsAssembly) + var targetMethodDef = (target as IMethodDefOrRef)?.ResolveMethodDef(); + if (targetMethodDef == null || (!targetMethodDef.IsPublic && !targetMethodDef.IsAssembly)) return; Tuple key = Tuple.Create(invoke.OpCode.Code, ctx.Method.DeclaringType, target); @@ -31,7 +33,7 @@ public override void ProcessCall(RPContext ctx, int instrIndex) { ctx.Method.DeclaringType.Methods.Add(proxy); // Fix peverify --- Non-virtual call to virtual methods must be done on this pointer - if (invoke.OpCode.Code == Code.Call && target.ResolveThrow().IsVirtual) { + if (invoke.OpCode.Code == Code.Call && targetMethodDef != null && targetMethodDef.IsVirtual) { proxy.IsStatic = false; sig.HasThis = true; sig.Params.RemoveAt(0); @@ -65,9 +67,8 @@ public override void ProcessCall(RPContext ctx, int instrIndex) { else invoke.Operand = proxy; - var targetDef = target.ResolveMethodDef(); - if (targetDef != null) - ctx.Context.Annotations.Set(targetDef, ReferenceProxyProtection.Targeted, ReferenceProxyProtection.Targeted); + if (targetMethodDef != null) + ctx.Context.Annotations.Set(targetMethodDef, ReferenceProxyProtection.Targeted, ReferenceProxyProtection.Targeted); } public override void Finalize(RPContext ctx) { } From da95f28767ebf26ee5dce0a0d0f8c0f7e03d966d Mon Sep 17 00:00:00 2001 From: mcpolo99 <32239939+mcpolo99@users.noreply.github.com> Date: Sun, 7 Jun 2026 19:38:39 +0200 Subject: [PATCH 13/24] fix(renamer): handle FnPtr type signatures instead of throwing NotSupportedException (#43) Co-authored-by: RandomCrocodile --- Confuser.Renamer/GenericArgumentResolver.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Confuser.Renamer/GenericArgumentResolver.cs b/Confuser.Renamer/GenericArgumentResolver.cs index 206a3d28b..d7b5ec730 100644 --- a/Confuser.Renamer/GenericArgumentResolver.cs +++ b/Confuser.Renamer/GenericArgumentResolver.cs @@ -122,7 +122,8 @@ private TypeSig ResolveGenericArgs(TypeSig typeSig) { result = new PinnedSig(ResolveGenericArgs(typeSig.Next)); break; case ElementType.FnPtr: - throw new NotSupportedException("FnPtr is not supported."); + result = typeSig; + break; case ElementType.Array: var arraySig = (ArraySig)typeSig; From bb6165e7dd39ad71a09e4269cb6ccef69bd68b2f Mon Sep 17 00:00:00 2001 From: mcpolo99 <32239939+mcpolo99@users.noreply.github.com> Date: Sun, 7 Jun 2026 19:39:15 +0200 Subject: [PATCH 14/24] fix(renamer): skip renaming WPF theme resources in themes/Generic.xaml (#44) Co-authored-by: RandomCrocodile --- Confuser.Renamer/Analyzers/WPFAnalyzer.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Confuser.Renamer/Analyzers/WPFAnalyzer.cs b/Confuser.Renamer/Analyzers/WPFAnalyzer.cs index 7f0132165..47097bbb3 100644 --- a/Confuser.Renamer/Analyzers/WPFAnalyzer.cs +++ b/Confuser.Renamer/Analyzers/WPFAnalyzer.cs @@ -60,6 +60,10 @@ public void PreRename(ConfuserContext context, INameService service, ProtectionP var decodedName = HttpUtility.UrlDecode(doc.DocumentName); var encodedName = doc.DocumentName; if (bamlRefs.TryGetValue(decodedName, out var references)) { + // WPF theme resources must keep their conventional paths + if (decodedName.IndexOf("themes/generic", StringComparison.OrdinalIgnoreCase) >= 0) + continue; + var decodedLastSlash = decodedName.LastIndexOf('/') + 1; var encodedLastSlash = encodedName.LastIndexOf('/') + 1; From 04ba4f5037210e708585b3846954cf5742ecdf5a Mon Sep 17 00:00:00 2001 From: mcpolo99 <32239939+mcpolo99@users.noreply.github.com> Date: Sun, 7 Jun 2026 19:40:49 +0200 Subject: [PATCH 15/24] fix(protections): skip unresolvable external types in ref proxy phase (#45) Co-authored-by: RandomCrocodile --- Confuser.Protections/ReferenceProxy/ReferenceProxyPhase.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Confuser.Protections/ReferenceProxy/ReferenceProxyPhase.cs b/Confuser.Protections/ReferenceProxy/ReferenceProxyPhase.cs index f9f8c64b0..98e0c8bb5 100644 --- a/Confuser.Protections/ReferenceProxy/ReferenceProxyPhase.cs +++ b/Confuser.Protections/ReferenceProxy/ReferenceProxyPhase.cs @@ -147,7 +147,9 @@ void ProcessMethod(RPContext ctx) { if (operand.MethodSig.ParamsAfterSentinel != null && operand.MethodSig.ParamsAfterSentinel.Count > 0) continue; - TypeDef declType = operand.DeclaringType.ResolveTypeDefThrow(); + TypeDef declType = operand.DeclaringType.ResolveTypeDef(); + if (declType == null) + continue; // No delegates if (declType.IsDelegate()) continue; From 6b7a0588f64376af385ffdf8d4cdd18c6d3b54ce Mon Sep 17 00:00:00 2001 From: mcpolo99 <32239939+mcpolo99@users.noreply.github.com> Date: Sun, 7 Jun 2026 19:56:13 +0200 Subject: [PATCH 16/24] feat(analysis): enable .NET analyzers and configure severity rules (#50) Co-authored-by: RandomCrocodile --- .editorconfig | 22 ++++++++++++++++++++++ ConfuserEx.Common.props | 3 ++- ConfuserEx.Common.targets | 4 ++-- 3 files changed, 26 insertions(+), 3 deletions(-) diff --git a/.editorconfig b/.editorconfig index aaee4ed2a..13adfce57 100644 --- a/.editorconfig +++ b/.editorconfig @@ -39,6 +39,28 @@ dotnet_style_qualification_for_property = false:suggestion dotnet_style_require_accessibility_modifiers = never:info [*.cs] +# .NET Analyzer severity overrides +dotnet_diagnostic.CA1002.severity = none # Do not expose generic lists (too restrictive for internal code) +dotnet_diagnostic.CA1031.severity = none # Do not catch general exceptions (too many existing catch blocks) +dotnet_diagnostic.CA1032.severity = none # Implement standard exception constructors +dotnet_diagnostic.CA1034.severity = none # Nested types should not be visible +dotnet_diagnostic.CA1062.severity = none # Validate arguments of public methods (too noisy initially) +dotnet_diagnostic.CA1303.severity = none # Do not pass literals as localized parameters +dotnet_diagnostic.CA1304.severity = none # Specify CultureInfo +dotnet_diagnostic.CA1305.severity = none # Specify IFormatProvider +dotnet_diagnostic.CA1307.severity = none # Specify StringComparison for clarity +dotnet_diagnostic.CA1310.severity = none # Specify StringComparison for correctness +dotnet_diagnostic.CA1707.severity = none # Identifiers should not contain underscores (test naming) +dotnet_diagnostic.CA1710.severity = none # Identifiers should have correct suffix +dotnet_diagnostic.CA1711.severity = none # Identifiers should not have incorrect suffix +dotnet_diagnostic.CA1720.severity = none # Identifiers should not contain type names +dotnet_diagnostic.CA1822.severity = suggestion # Mark members as static (informational only) +dotnet_diagnostic.CA1825.severity = warning # Avoid zero-length array allocations +dotnet_diagnostic.CA1859.severity = suggestion # Use concrete types for improved performance +dotnet_diagnostic.CA2000.severity = suggestion # Dispose objects before losing scope +dotnet_diagnostic.CA2007.severity = none # ConfigureAwait (not needed for desktop app) +dotnet_diagnostic.CA2227.severity = none # Collection properties should be read only + csharp_indent_block_contents = true csharp_indent_braces = false csharp_indent_case_contents = true diff --git a/ConfuserEx.Common.props b/ConfuserEx.Common.props index a0586f7a4..e597a9e26 100644 --- a/ConfuserEx.Common.props +++ b/ConfuserEx.Common.props @@ -23,7 +23,8 @@ - false + true + latest \ No newline at end of file diff --git a/ConfuserEx.Common.targets b/ConfuserEx.Common.targets index f6083488a..af71ae767 100644 --- a/ConfuserEx.Common.targets +++ b/ConfuserEx.Common.targets @@ -3,8 +3,8 @@ - + \ No newline at end of file From 132aaa300dac87bacc87cba07d8774377bc39639 Mon Sep 17 00:00:00 2001 From: mcpolo99 <32239939+mcpolo99@users.noreply.github.com> Date: Sun, 7 Jun 2026 19:59:41 +0200 Subject: [PATCH 17/24] feat(analysis): add Roslynator.Analyzers for broader code quality coverage (#51) Co-authored-by: RandomCrocodile --- .editorconfig | 15 +++++++++++++++ ConfuserEx.Common.targets | 2 ++ 2 files changed, 17 insertions(+) diff --git a/.editorconfig b/.editorconfig index 13adfce57..ca83886f5 100644 --- a/.editorconfig +++ b/.editorconfig @@ -61,6 +61,21 @@ dotnet_diagnostic.CA2000.severity = suggestion # Dispose objects before losin dotnet_diagnostic.CA2007.severity = none # ConfigureAwait (not needed for desktop app) dotnet_diagnostic.CA2227.severity = none # Collection properties should be read only +# Roslynator severity overrides +dotnet_diagnostic.RCS1018.severity = none # Add accessibility modifiers (conflicts with existing style) +dotnet_diagnostic.RCS1037.severity = none # Remove trailing white-space +dotnet_diagnostic.RCS1057.severity = none # Add empty line between declarations +dotnet_diagnostic.RCS1085.severity = none # Use auto-implemented property +dotnet_diagnostic.RCS1090.severity = none # ConfigureAwait +dotnet_diagnostic.RCS1118.severity = none # Mark local variable as const +dotnet_diagnostic.RCS1124.severity = none # Inline local variable +dotnet_diagnostic.RCS1138.severity = none # Add summary to documentation comment +dotnet_diagnostic.RCS1139.severity = none # Add summary element to documentation comment +dotnet_diagnostic.RCS1163.severity = none # Unused parameter +dotnet_diagnostic.RCS1194.severity = none # Implement exception constructors +dotnet_diagnostic.RCS1213.severity = none # Remove unused member declaration (too aggressive) +dotnet_diagnostic.RCS1228.severity = none # Unused element in documentation comment + csharp_indent_block_contents = true csharp_indent_braces = false csharp_indent_case_contents = true diff --git a/ConfuserEx.Common.targets b/ConfuserEx.Common.targets index af71ae767..02ee4145d 100644 --- a/ConfuserEx.Common.targets +++ b/ConfuserEx.Common.targets @@ -5,6 +5,8 @@ + \ No newline at end of file From da2b709705f019c225471bbce72d90c64a3785e8 Mon Sep 17 00:00:00 2001 From: mcpolo99 <32239939+mcpolo99@users.noreply.github.com> Date: Sun, 7 Jun 2026 23:42:11 +0200 Subject: [PATCH 18/24] fix(analysis): resolve analyzer warnings and suppress noisy rules (#52) Co-authored-by: RandomCrocodile --- .editorconfig | 1 + Confuser.Core/ProtectionParameters.cs | 2 +- Confuser.Core/Services/TraceService.cs | 2 +- Confuser.Protections/AntiTamper/JITBody.cs | 2 +- Confuser.Protections/Resources/InjectPhase.cs | 2 +- Confuser.Renamer/BAML/PropertyPath/PropertyPathParser.cs | 2 +- ConfuserEx.Common.targets | 3 +-- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.editorconfig b/.editorconfig index ca83886f5..9561aef9f 100644 --- a/.editorconfig +++ b/.editorconfig @@ -63,6 +63,7 @@ dotnet_diagnostic.CA2227.severity = none # Collection properties should # Roslynator severity overrides dotnet_diagnostic.RCS1018.severity = none # Add accessibility modifiers (conflicts with existing style) +dotnet_diagnostic.RCS1102.severity = none # Make class static (many are non-static for runtime injection) dotnet_diagnostic.RCS1037.severity = none # Remove trailing white-space dotnet_diagnostic.RCS1057.severity = none # Add empty line between declarations dotnet_diagnostic.RCS1085.severity = none # Use auto-implemented property diff --git a/Confuser.Core/ProtectionParameters.cs b/Confuser.Core/ProtectionParameters.cs index af166f910..00a6dfb00 100644 --- a/Confuser.Core/ProtectionParameters.cs +++ b/Confuser.Core/ProtectionParameters.cs @@ -14,7 +14,7 @@ public class ProtectionParameters { /// /// A empty instance of . /// - public static readonly ProtectionParameters Empty = new ProtectionParameters(null, new IDnlibDef[0]); + public static readonly ProtectionParameters Empty = new ProtectionParameters(null, Array.Empty()); readonly ConfuserComponent comp; diff --git a/Confuser.Core/Services/TraceService.cs b/Confuser.Core/Services/TraceService.cs index bec151207..44efd0d0c 100644 --- a/Confuser.Core/Services/TraceService.cs +++ b/Confuser.Core/Services/TraceService.cs @@ -204,7 +204,7 @@ internal MethodTrace Trace() { public int[] TraceArguments(Instruction instr) { instr.CalculateStackUsage(Method.HasReturnType, out _, out int pop); // pop is number of arguments if (pop == 0) - return new int[0]; + return Array.Empty(); int instrIndex = offset2index[instr.Offset]; int argCount = pop; diff --git a/Confuser.Protections/AntiTamper/JITBody.cs b/Confuser.Protections/AntiTamper/JITBody.cs index ae306d349..fcc719f40 100644 --- a/Confuser.Protections/AntiTamper/JITBody.cs +++ b/Confuser.Protections/AntiTamper/JITBody.cs @@ -134,7 +134,7 @@ public void Write() { jitBody.LocalVars = SignatureWriter.Write(metadata, local); } else - jitBody.LocalVars = new byte[0]; + jitBody.LocalVars = Array.Empty(); { var newCode = new byte[codeSize]; diff --git a/Confuser.Protections/Resources/InjectPhase.cs b/Confuser.Protections/Resources/InjectPhase.cs index 27c58ab03..aa07f7d05 100644 --- a/Confuser.Protections/Resources/InjectPhase.cs +++ b/Confuser.Protections/Resources/InjectPhase.cs @@ -97,7 +97,7 @@ void InjectHelpers(ConfuserContext context, ICompressionService compression, IRu moduleCtx.DataField = new FieldDefUser(moduleCtx.Name.RandomName(), new FieldSig(dataType.ToTypeSig())) { IsStatic = true, HasFieldRVA = true, - InitialValue = new byte[0], + InitialValue = Array.Empty(), Access = FieldAttributes.CompilerControlled }; context.CurrentModule.GlobalType.Fields.Add(moduleCtx.DataField); diff --git a/Confuser.Renamer/BAML/PropertyPath/PropertyPathParser.cs b/Confuser.Renamer/BAML/PropertyPath/PropertyPathParser.cs index 832aad768..67542c1ef 100644 --- a/Confuser.Renamer/BAML/PropertyPath/PropertyPathParser.cs +++ b/Confuser.Renamer/BAML/PropertyPath/PropertyPathParser.cs @@ -317,7 +317,7 @@ void StartNewLevel() { ArrayList _al = new ArrayList(); const char NullChar = Char.MinValue; const char EscapeChar = '^'; - static SourceValueInfo[] EmptyInfo = new SourceValueInfo[0]; + static SourceValueInfo[] EmptyInfo = Array.Empty(); static string SpecialChars = @"./[]"; } } diff --git a/ConfuserEx.Common.targets b/ConfuserEx.Common.targets index 02ee4145d..c929f0088 100644 --- a/ConfuserEx.Common.targets +++ b/ConfuserEx.Common.targets @@ -3,8 +3,7 @@ - + From bc485219e8f9f3c7218f9342efb5325fa7ec48c8 Mon Sep 17 00:00:00 2001 From: mcpolo99 <32239939+mcpolo99@users.noreply.github.com> Date: Mon, 8 Jun 2026 11:23:31 +0200 Subject: [PATCH 19/24] fix(core): improve error message for .NET 6+ native host .exe files (#19) (#56) Co-authored-by: RandomCrocodile --- Confuser.Core/ObfAttrMarker.cs | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/Confuser.Core/ObfAttrMarker.cs b/Confuser.Core/ObfAttrMarker.cs index 70212f9cf..0738d5ca4 100644 --- a/Confuser.Core/ObfAttrMarker.cs +++ b/Confuser.Core/ObfAttrMarker.cs @@ -326,6 +326,14 @@ protected internal override MarkerResult MarkProject(ConfuserProject proj, Confu } catch (BadImageFormatException ex) { context.Logger.ErrorFormat("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); + else + context.Logger.Error("Hint: For .NET 6+ apps, obfuscate the .dll file, not the .exe (which is a native host stub)."); + } throw new ConfuserException(ex); } } @@ -447,18 +455,18 @@ void MarkModule(ProjectModule projModule, ModuleDefMD module, Rules rules, bool snPubSigKeyPath = snPubSigKeyPath == null ? null : Path.Combine(project.BaseDirectory, snPubSigKeyPath); var snKey = LoadSNKey(context, snKeyPath, snKeyPass); - context.Annotations.Set(module, SNKey, snKey); - + context.Annotations.Set(module, SNKey, snKey); + var snPubKey = LoadSNPubKey(context, snPubKeyPath); - context.Annotations.Set(module, SNPubKey, snPubKey); - - context.Annotations.Set(module, SNDelaySig, snDelaySig); - + context.Annotations.Set(module, SNPubKey, snPubKey); + + context.Annotations.Set(module, SNDelaySig, snDelaySig); + var snSigKey = LoadSNKey(context, snSigKeyPath, snSigKeyPass); - context.Annotations.Set(module, SNSigKey, snSigKey); - + context.Annotations.Set(module, SNSigKey, snSigKey); + var snSigPubKey = LoadSNPubKey(context, snPubSigKeyPath); - context.Annotations.Set(module, SNSigPubKey, snSigPubKey); + context.Annotations.Set(module, SNSigPubKey, snSigPubKey); using (stack.Apply(module, layer)) ProcessModule(module, stack); From 6c49075dd348ff46a631d1424d3f68ef6a01b434 Mon Sep 17 00:00:00 2001 From: mcpolo99 <32239939+mcpolo99@users.noreply.github.com> Date: Mon, 8 Jun 2026 11:37:29 +0200 Subject: [PATCH 20/24] =?UTF-8?q?docs:=20update=20README=20for=20ConfuserE?= =?UTF-8?q?xx=20fork=20=E2=80=94=20fix=20links,=20add=20features,=20modern?= =?UTF-8?q?ize=20(#57)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: RandomCrocodile --- README.md | 88 +++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 60 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index 64826b032..218e53462 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,16 @@ -# ConfuserEx +# ConfuserExx -[![Build status][img_build]][build] -[![Test status][img_test]][test] -[![CodeFactor][img_codefactor]][codefactor] -[![Gitter Chat][img_gitter]][gitter] +[![CI][img_ci]][ci] +[![CodeQL][img_codeql]][codeql] [![MIT License][img_license]][license] -ConfuserEx is a open-source protector for .NET applications. -It is the successor of [Confuser][confuser] project. +An actively maintained fork of [ConfuserEx][upstream] — an open-source protector for .NET applications. + +> The original [mkaring/ConfuserEx][upstream] has been dormant since 2022. This fork includes bug fixes, new features, and modern .NET support. ## Features -* Supports .NET Framework 2.0/3.0/3.5/4.0/4.5/4.6/4.7/4.8 +* Supports .NET Framework 2.0/3.5/4.x, .NET Standard 2.0, .NET Core, .NET 5+/6/7/8+ * Symbol renaming (Support WPF/BAML) * Protection against debuggers/profilers * Protection against memory dumping @@ -23,41 +22,74 @@ It is the successor of [Confuser][confuser] project. * Embedding dependency * Compressing output * Extensible plugin API -* Many more are coming! +* Roslyn code analysis integration +* Auto-detection of .NET runtime paths for modern framework support + +## What's improved over upstream + +* Fixed WPF resource renaming, .NET Standard obfuscation, control flow protection +* Serialization-aware renaming (DataContract, DataMember, JsonProperty) +* Graceful handling of external/unresolvable assemblies (no more crashes) +* Auto-detection of .NET Core/5+/6/7/8+ runtime assembly paths +* CLI `--snkey` and `--snkeypass` options for CI/CD signing +* Wildcard module loading in `.crproj` files +* Roslyn analyzers (NetAnalyzers + Roslynator) for code quality +* GitHub Actions CI/CD with automatic releases +* Better error messages for .NET 6+ native host executables -# Usage +## Usage -```Batchfile +```bash Confuser.CLI.exe ``` The project file is a ConfuserEx Project (`*.crproj`). -The format of project file can be found in [docs\ProjectFormat.md][project_format] +The format of project file can be found in [docs/ProjectFormat.md][project_format]. + +### CLI Options + +``` +-n|noPause : no pause after finishing protection +-o|out : specifies output directory +-probe : specifies probe directory +-plugin : specifies plugin path +-debug : specifies debug symbol generation +-snkey : specifies strong name key file path +-snkeypass : specifies strong name key password +``` + +## Bug Report + +See the [Issues][issues] section. Please check existing issues before filing a new one. -# Bug Report +## Contributing -See the [Issues Report][issues] section of website. +1. Fork the repository +2. Create a feature branch from `pre-release` +3. Make your changes and ensure CI passes +4. Open a PR targeting `pre-release` -# License +## License Licensed under the MIT license. See [LICENSE.md][license] for details. -# Credits +## Credits -**[0xd4d]** for his awesome work and extensive knowledge! +**[0xd4d]** for [dnlib][dnlib] and extensive .NET metadata knowledge. +**[Ki (yck1509)][ki]** for the original ConfuserEx. +**[Martin Karing][mkaring]** for maintaining the project through v1.6. [0xd4d]: https://github.com/0xd4d -[build]: https://ci.appveyor.com/project/mkaring/confuserex/branch/master -[codefactor]: https://www.codefactor.io/repository/github/mkaring/confuserex/overview/master -[confuser]: http://confuser.codeplex.com -[issues]: https://github.com/mkaring/ConfuserEx/issues -[gitter]: https://gitter.im/ConfuserEx/community +[dnlib]: https://github.com/0xd4d/dnlib +[ki]: https://github.com/yck1509 +[mkaring]: https://github.com/mkaring +[upstream]: https://github.com/mkaring/ConfuserEx +[ci]: https://github.com/mcpolo99/ConfuserExx/actions/workflows/ci.yml +[codeql]: https://github.com/mcpolo99/ConfuserExx/actions/workflows/codeql-analysis.yml +[issues]: https://github.com/mcpolo99/ConfuserExx/issues [license]: LICENSE.md [project_format]: docs/ProjectFormat.md -[test]: https://ci.appveyor.com/project/mkaring/confuserex/branch/master/tests -[img_build]: https://img.shields.io/appveyor/ci/mkaring/ConfuserEx/master.svg?style=flat -[img_codefactor]: https://www.codefactor.io/repository/github/mkaring/confuserex/badge/master -[img_gitter]: https://img.shields.io/gitter/room/mkaring/ConfuserEx.svg?style=flat -[img_license]: https://img.shields.io/github/license/mkaring/ConfuserEx.svg?style=flat -[img_test]: https://img.shields.io/appveyor/tests/mkaring/ConfuserEx/master.svg?style=flat&compact_message +[img_ci]: https://github.com/mcpolo99/ConfuserExx/actions/workflows/ci.yml/badge.svg?branch=pre-release +[img_codeql]: https://github.com/mcpolo99/ConfuserExx/actions/workflows/codeql-analysis.yml/badge.svg +[img_license]: https://img.shields.io/github/license/mcpolo99/ConfuserExx.svg?style=flat From ff01e84a68dda5f67cf18be37bfdc6a150e715f0 Mon Sep 17 00:00:00 2001 From: mcpolo99 <32239939+mcpolo99@users.noreply.github.com> Date: Mon, 8 Jun 2026 12:54:34 +0200 Subject: [PATCH 21/24] =?UTF-8?q?feat(core):=20tolerate=20missing=20depend?= =?UTF-8?q?encies=20=E2=80=94=20warn=20instead=20of=20crash=20(#4)=20(#58)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: RandomCrocodile --- Confuser.Core/ConfuserEngine.cs | 11 +++------ Confuser.Core/DnlibUtils.cs | 44 ++++++++++++--------------------- 2 files changed, 20 insertions(+), 35 deletions(-) diff --git a/Confuser.Core/ConfuserEngine.cs b/Confuser.Core/ConfuserEngine.cs index 96401c988..ba1799a41 100644 --- a/Confuser.Core/ConfuserEngine.cs +++ b/Confuser.Core/ConfuserEngine.cs @@ -263,13 +263,10 @@ static void Inspection(ConfuserContext context) { context.Logger.Info("Resolving dependencies..."); foreach (var dependency in context.Modules .SelectMany(module => module.GetAssemblyRefs().Select(asmRef => Tuple.Create(asmRef, module)))) { - try { - context.Resolver.ResolveThrow(dependency.Item1, dependency.Item2); - } - catch (AssemblyResolveException ex) { - context.Logger.ErrorException("Failed to resolve dependency of '" + dependency.Item2.Name + "'.", ex); - throw new ConfuserException(ex); - } + 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.", + dependency.Item1.FullName, dependency.Item2.Name); } context.Logger.Debug("Checking Strong Name..."); diff --git a/Confuser.Core/DnlibUtils.cs b/Confuser.Core/DnlibUtils.cs index 42d0ba439..7570da946 100644 --- a/Confuser.Core/DnlibUtils.cs +++ b/Confuser.Core/DnlibUtils.cs @@ -209,15 +209,12 @@ public static bool IsDelegate(this TypeDef type) { /// The full name of base type. /// true if the specified type is inherited from a base type; otherwise, false. public static bool InheritsFromCorlib(this TypeDef type, string baseType) { - if (type.BaseType == null) - return false; - - TypeDef bas = type; - do { - bas = bas.BaseType.ResolveTypeDefThrow(); + var bas = type.BaseType?.ResolveTypeDef(); + while (bas != null && bas.DefinitionAssembly.IsCorLib()) { if (bas.ReflectionFullName == baseType) return true; - } while (bas.BaseType != null && bas.BaseType.DefinitionAssembly.IsCorLib()); + bas = bas.BaseType?.ResolveTypeDef(); + } return false; } @@ -228,15 +225,12 @@ public static bool InheritsFromCorlib(this TypeDef type, string baseType) { /// The full name of base type. /// true if the specified type is inherited from a base type; otherwise, false. public static bool InheritsFrom(this TypeDef type, string baseType) { - if (type.BaseType == null) - return false; - - TypeDef bas = type; - do { - bas = bas.BaseType.ResolveTypeDefThrow(); + var bas = type.BaseType?.ResolveTypeDef(); + while (bas != null) { if (bas.ReflectionFullName == baseType) return true; - } while (bas.BaseType != null); + bas = bas.BaseType?.ResolveTypeDef(); + } return false; } @@ -247,18 +241,12 @@ public static bool InheritsFrom(this TypeDef type, string baseType) { /// The full name of the type of interface. /// true if the specified type implements the interface; otherwise, false. public static bool Implements(this TypeDef type, string fullName) { - do { - foreach (InterfaceImpl iface in type.Interfaces) { - if (iface.Interface.ReflectionFullName == fullName) - return true; - } - - if (type.BaseType == null) - return false; - - type = type.BaseType.ResolveTypeDefThrow(); - } while (type != null); - throw new UnreachableException(); + while (type != null) { + if (type.Interfaces.Any(iface => iface.Interface.ReflectionFullName == fullName)) + return true; + type = type.BaseType?.ResolveTypeDef(); + } + return false; } /// @@ -451,8 +439,8 @@ public static bool IsImplicitImplementedInterfaceMember(this MethodDef method) { if (method.IsPublic && method.IsNewSlot) { foreach (var iFace in method.DeclaringType.Interfaces) { - var iFaceDef = iFace.Interface.ResolveTypeDefThrow(); - if (iFaceDef.FindMethod(method.Name, (MethodSig)method.Signature) != null) + var iFaceDef = iFace.Interface.ResolveTypeDef(); + if (iFaceDef != null && iFaceDef.FindMethod(method.Name, (MethodSig)method.Signature) != null) return true; } } From 616738706f3d7ba624e595249f0b4db9616e7bd4 Mon Sep 17 00:00:00 2001 From: RandomCrocodile Date: Mon, 8 Jun 2026 13:41:42 +0200 Subject: [PATCH 22/24] =?UTF-8?q?feat(renamer):=20add=20opt-in=20overload?= =?UTF-8?q?=20confusion=20=E2=80=94=20rename=20methods=20with=20different?= =?UTF-8?q?=20signatures=20to=20same=20name=20(#25)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Confuser.Renamer/RenamePhase.cs | 56 +++++++++++++++++++++++++++++++-- 1 file changed, 54 insertions(+), 2 deletions(-) diff --git a/Confuser.Renamer/RenamePhase.cs b/Confuser.Renamer/RenamePhase.cs index 9c6647f83..91021abcd 100644 --- a/Confuser.Renamer/RenamePhase.cs +++ b/Confuser.Renamer/RenamePhase.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; using System.Text; using Confuser.Core; @@ -14,8 +15,12 @@ public RenamePhase(NameProtection parent) public override string Name => "Renaming"; + // Tracks overload confusion name assignments: TypeDef -> list of assigned names + readonly Dictionary> _overloadNames = new Dictionary>(); + protected override void Execute(ConfuserContext context, ProtectionParameters parameters) { var service = (NameService)context.Registry.GetService(); + bool overloadConfusion = parameters.GetParameter(context, context.CurrentModule, "overload", false); context.Logger.Debug("Renaming..."); foreach (var renamer in service.Renamers) { @@ -84,7 +89,12 @@ protected override void Execute(ConfuserContext context, ProtectionParameters pa RenameGenericParameters(typeDef.GenericParameters); } else if (def is MethodDef methodDef) { - methodDef.Name = service.ObfuscateName(methodDef, mode); + if (overloadConfusion && CanApplyOverloadConfusion(methodDef, service)) { + methodDef.Name = GetOverloadName(methodDef, service, mode); + } + else { + methodDef.Name = service.ObfuscateName(methodDef, mode); + } RenameGenericParameters(methodDef.GenericParameters); } else @@ -119,6 +129,48 @@ static void RenameGenericParameters(IList genericParams) param.Name = ((char) (param.Number + 1)).ToString(); } + /// + /// Determines whether overload confusion can safely be applied to the method. + /// Only applies to non-virtual, non-interface, non-special methods. + /// + static bool CanApplyOverloadConfusion(MethodDef method, INameService service) { + if (method.IsVirtual || method.IsAbstract || method.IsConstructor) + return false; + if (method.IsSpecialName || method.IsRuntimeSpecialName) + return false; + // Skip if method has override/sibling references (VTable constrained) + var refs = service.GetReferences(method); + if (refs.Any(r => r.ShouldCancelRename || r.DelayRenaming(service, method))) + return false; + return true; + } + + /// + /// Gets a shared overload name for the method's declaring type. + /// Methods with different signatures in the same type get the same name. + /// + string GetOverloadName(MethodDef method, NameService service, RenameMode mode) { + var declType = method.DeclaringType; + if (!_overloadNames.TryGetValue(declType, out var names)) { + names = new List(); + _overloadNames[declType] = names; + } + + // Check if any existing name can be reused (different signature = safe to share) + foreach (var existingName in names) { + bool signatureConflict = declType.Methods.Any(m => + m.Name == existingName && + new SigComparer().Equals(m.MethodSig, method.MethodSig)); + if (!signatureConflict) + return existingName; + } + + // No reusable name — generate a new one + var newName = service.ObfuscateName(method, mode); + names.Add(newName); + return newName; + } + static IEnumerable GetTargetsWithDelay(IList definitions, ConfuserContext context, INameService service) { var delayedItems = new List(); var currentList = definitions; From 39fac2b7b3ace957198a53165957360db076cc1c Mon Sep 17 00:00:00 2001 From: mcpolo99 <32239939+mcpolo99@users.noreply.github.com> Date: Wed, 10 Jun 2026 10:32:05 +0200 Subject: [PATCH 23/24] feat(ci): add pre-release builds with downloadable binaries (#pre-release) (#61) Co-authored-by: RandomCrocodile --- .github/workflows/ci.yml | 50 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 084e912bc..50713d395 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -82,6 +82,56 @@ jobs: ConfuserEx.zip Confuser.MSBuild.Tasks/bin/Release/*.nupkg + # Pre-release: on push to pre-release branch + pre-release: + needs: build + if: github.event_name == 'push' && github.ref == 'refs/heads/pre-release' + runs-on: windows-2022 + 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 + Write-Host "Pre-release version: $ver" + + - name: Download artifacts + uses: actions/download-artifact@v5 + with: + name: confuserex-packages + + - name: Create or update pre-release + uses: softprops/action-gh-release@v2 + with: + tag_name: pre-release + name: "Pre-release v${{ steps.version.outputs.VERSION }}" + prerelease: true + make_latest: false + body: | + **Pre-release build** — for testing only, not production use. + + Version: `${{ steps.version.outputs.VERSION }}` + Branch: `pre-release` + 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 master release: needs: build From ad4207f0f30ca8534e3d7f11599bc9db0c94b6bc Mon Sep 17 00:00:00 2001 From: mcpolo99 <32239939+mcpolo99@users.noreply.github.com> Date: Wed, 10 Jun 2026 13:21:46 +0200 Subject: [PATCH 24/24] feat(core): auto-detect .NET runtime paths for assembly resolution (#55) * feat(core): auto-detect .NET Core/5+/6/7/8+ runtime paths for assembly resolution (#12) * fix(core): probe all installed .NET runtime versions for cross-version dependencies (#12) * refactor(core): add logging to DotNetCorePathResolver, address review feedback (#12) --------- Co-authored-by: RandomCrocodile --- Confuser.Core/ConfuserEngine.cs | 10 ++ Confuser.Core/DotNetCorePathResolver.cs | 167 ++++++++++++++++++++++++ 2 files changed, 177 insertions(+) create mode 100644 Confuser.Core/DotNetCorePathResolver.cs diff --git a/Confuser.Core/ConfuserEngine.cs b/Confuser.Core/ConfuserEngine.cs index ba1799a41..405d42a2a 100644 --- a/Confuser.Core/ConfuserEngine.cs +++ b/Confuser.Core/ConfuserEngine.cs @@ -99,6 +99,16 @@ static void RunInternal(ConfuserParameters parameters, CancellationToken token) foreach (string probePath in context.Project.ProbePaths) asmResolver.PostSearchPaths.Insert(0, Path.Combine(context.BaseDirectory, probePath)); + // Auto-detect .NET Core/5+/6/7/8+ runtime paths from first module + var firstModule = context.Project.FirstOrDefault(m => !m.IsExternal); + if (firstModule != null) { + 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.CheckCancellation(); Marker marker = parameters.GetMarker(); diff --git a/Confuser.Core/DotNetCorePathResolver.cs b/Confuser.Core/DotNetCorePathResolver.cs new file mode 100644 index 000000000..e056613d6 --- /dev/null +++ b/Confuser.Core/DotNetCorePathResolver.cs @@ -0,0 +1,167 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; + +namespace Confuser.Core { + /// + /// Discovers .NET Core/5+/6/7/8+ runtime assembly directories for assembly resolution. + /// + 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) { + // 1. Try runtimeconfig.json next to the module (target framework gets priority) + var runtimeConfigPaths = Enumerable.Empty(); + var runtimeConfig = FindRuntimeConfig(modulePath); + if (runtimeConfig != null) + runtimeConfigPaths = GetPathsFromRuntimeConfig(runtimeConfig, logger); + + // 2. Always add ALL installed runtime versions to handle cross-version + // dependencies (e.g., net10.0 app referencing a library compiled against net8.0) + return runtimeConfigPaths + .Concat(ProbeAllInstalledRuntimes(logger)) + .Where(Directory.Exists) + .Distinct(StringComparer.OrdinalIgnoreCase); + } + + static string FindRuntimeConfig(string modulePath) { + if (string.IsNullOrEmpty(modulePath)) + return null; + var dir = Path.GetDirectoryName(modulePath); + if (dir == null) + return null; + var name = Path.GetFileNameWithoutExtension(modulePath); + var configPath = Path.Combine(dir, name + ".runtimeconfig.json"); + return File.Exists(configPath) ? configPath : null; + } + + static IEnumerable GetPathsFromRuntimeConfig(string configPath, ILogger logger) { + string content; + try { + content = File.ReadAllText(configPath); + } + catch (IOException ex) { + logger.WarnFormat("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); + yield break; + } + + var frameworks = ParseFrameworks(content); + if (frameworks.Count == 0) { + logger.DebugFormat("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."); + 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); + continue; + } + + // Try exact version first + var exactPath = Path.Combine(sharedDir, fw.Version); + if (Directory.Exists(exactPath)) { + yield return exactPath; + continue; + } + + // Try latest matching major.minor + var majorMinor = GetMajorMinor(fw.Version); + var best = Directory.EnumerateDirectories(sharedDir) + .Where(d => Path.GetFileName(d).StartsWith(majorMinor, StringComparison.Ordinal)) + .OrderByDescending(d => d) + .FirstOrDefault(); + if (best != null) + yield return best; + else + logger.WarnFormat("No installed runtime found matching {0} {1} in {2}", fw.Name, fw.Version, sharedDir); + } + } + + static List ParseFrameworks(string json) { + var results = new List(); + // Match framework objects: "name": "...", "version": "..." + var matches = Regex.Matches(json, @"""name""\s*:\s*""([^""]+)""\s*,\s*""version""\s*:\s*""([^""]+)"""); + foreach (Match m in matches) + results.Add(new FrameworkRef { Name = m.Groups[1].Value, Version = m.Groups[2].Value }); + + // Also try reversed order (version before name) + if (results.Count == 0) { + matches = Regex.Matches(json, @"""version""\s*:\s*""([^""]+)""\s*,\s*""name""\s*:\s*""([^""]+)"""); + foreach (Match m in matches) + results.Add(new FrameworkRef { Name = m.Groups[2].Value, Version = m.Groups[1].Value }); + } + + return results; + } + + static string GetDotNetRoot() { + // Check DOTNET_ROOT env var first + var root = Environment.GetEnvironmentVariable("DOTNET_ROOT"); + if (!string.IsNullOrEmpty(root) && Directory.Exists(root)) + return root; + + // Standard locations + if (Environment.OSVersion.Platform == PlatformID.Win32NT) { + var programFiles = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles); + var path = Path.Combine(programFiles, "dotnet"); + if (Directory.Exists(path)) + return path; + } + else { + if (Directory.Exists("/usr/share/dotnet")) + return "/usr/share/dotnet"; + if (Directory.Exists("/usr/local/share/dotnet")) + return "/usr/local/share/dotnet"; + } + + return null; + } + + static string GetMajorMinor(string version) { + var parts = version.Split('.'); + return parts.Length >= 2 ? parts[0] + "." + parts[1] : version; + } + + static IEnumerable ProbeAllInstalledRuntimes(ILogger logger) { + var dotnetRoot = GetDotNetRoot(); + if (dotnetRoot == null) { + logger.Warn("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); + yield break; + } + + // Return ALL installed runtime versions (newest first) across all frameworks. + // This handles cross-version dependencies where e.g., a net10.0 app references + // a library compiled against net8.0 which needs System.Runtime 8.0.0.0. + foreach (var frameworkDir in Directory.EnumerateDirectories(sharedDir)) { + foreach (var versionDir in Directory.EnumerateDirectories(frameworkDir).OrderByDescending(d => d)) + yield return versionDir; + } + } + + struct FrameworkRef { + public string Name; + public string Version; + } + } +}