diff --git a/README.md b/README.md
index 60b1232..90ef69a 100644
--- a/README.md
+++ b/README.md
@@ -6,6 +6,7 @@ This repository houses a collection of test sample projects designed to demonstr
The repository contains these folders:
- **`DotnetSDK`** — Integration test samples for all supported .NET versions, used as part of the smoke testing suite
- **`feature-tests`** — Isolated samples for testing specific functionalities
+- **`tools`** - Internal tools used for testing and debugging obfuscated applications
## 🧪 Feature Tests
This section lists the available feature-test solutions, each designed to test specific functionalities in isolation:
@@ -15,6 +16,15 @@ This section lists the available feature-test solutions, each designed to test s
- **MauiAppforChecks** — MAUI App for testing RASP Root check or other checks
- **SimpleChecks** — .NET 10 and .NET Framework console samples for testing Debug Check and other RASP checks
+## 🧰 Tools
+Contains internal tools used in Dotfuscator development process, including testing and debugging.
+- **EnvFileViewer** - Reads the environment file (`environment0f617e02.bin`) and displays data in JSON format
+ - Usage: run as Administrator `EnvFileViewer.exe --register` to register `Preview` action for `.bin` files
+ - Unregister Preview action: run as Administrator `EnvFileViewer.exe --unregister`
+ - Preview a file: drag-and-drop a file on `EnvFileViewer.exe` or run `EnvFileViewer.exe ...full-path\environment0f617e02.bin`
+ - By default, waits for a key press before exit
+ - Allows using `--no-wait` command line argument to avoid waiting for a key press.
+
## 👥 Audience
This repository is primarily intended for the technical team responsible for developing, testing, and maintaining Dotfuscator. The samples provide practical, code-based scenarios to aid in product validation, feature development, and understanding Dotfuscator's behavior across various application types.
diff --git a/tools/EnvFileViewer/EnvFileViewer/FileAssociationManager.cs b/tools/EnvFileViewer/EnvFileViewer/FileAssociationManager.cs
new file mode 100644
index 0000000..fa17dd4
--- /dev/null
+++ b/tools/EnvFileViewer/EnvFileViewer/FileAssociationManager.cs
@@ -0,0 +1,73 @@
+using Microsoft.Win32;
+
+namespace EnvFileViewer;
+
+public static class FileAssociationManager
+{
+ private const string Extension = ".bin";
+ private const string ProgId = "binfile";
+ private const string Verb = "Preview";
+
+ ///
+ /// Registers the "Preview" context menu action for *.bin files.
+ ///
+ /// Full path to your application executable.
+ public static void RegisterPreviewAction(string applicationPath)
+ {
+ try
+ {
+ // 1. Ensure .bin points to our ProgID (binfile)
+ using (RegistryKey extKey = Registry.ClassesRoot.CreateSubKey(Extension))
+ {
+ extKey.SetValue("", ProgId);
+ }
+
+ // 2. Create the shell verb structure: binfile\shell\Preview\command
+ string verbKeyPath = $@"{ProgId}\shell\{Verb}";
+ using (RegistryKey verbKey = Registry.ClassesRoot.CreateSubKey(verbKeyPath))
+ {
+ verbKey.SetValue("", Verb); // The text shown in the context menu
+ }
+
+ using (RegistryKey commandKey = Registry.ClassesRoot.CreateSubKey($@"{verbKeyPath}\command"))
+ {
+ // Format: "C:\Path\To\App.exe" "%1"
+ commandKey.SetValue("", $"\"{applicationPath}\" \"%1\"");
+ }
+
+ Console.WriteLine("Successfully registered 'Preview' action.");
+ }
+ catch (UnauthorizedAccessException)
+ {
+ Console.WriteLine("Error: You must run this application as an Administrator.");
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"An error occurred during registration: {ex.Message}");
+ }
+ }
+
+ ///
+ /// De-registers the "Preview" context menu action.
+ ///
+ public static void UnregisterPreviewAction()
+ {
+ try
+ {
+ string verbKeyPath = $@"{ProgId}\shell\{Verb}";
+
+ // Delete the Preview key and all its subkeys (like 'command')
+ Registry.ClassesRoot.DeleteSubKeyTree(verbKeyPath, throwOnMissingSubKey: false);
+
+ Console.WriteLine("Successfully de-registered 'Preview' action.");
+ }
+ catch (UnauthorizedAccessException)
+ {
+ Console.WriteLine("Error: You must run this application as an Administrator.");
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"An error occurred during de-registration: {ex.Message}");
+ }
+ }
+}
\ No newline at end of file
diff --git a/tools/EnvFileViewer/EnvFileViewer/Program.cs b/tools/EnvFileViewer/EnvFileViewer/Program.cs
index 8447a7b..904e860 100644
--- a/tools/EnvFileViewer/EnvFileViewer/Program.cs
+++ b/tools/EnvFileViewer/EnvFileViewer/Program.cs
@@ -1,4 +1,25 @@
-using EnvLib;
+using EnvFileViewer;
+using EnvLib;
+
+if (args.Contains("--register"))
+{
+ var applicationPath = System.Reflection.Assembly.GetExecutingAssembly().Location;
+ if (applicationPath.EndsWith(".dll", StringComparison.OrdinalIgnoreCase))
+ {
+ var exePath = Path.Combine(Path.GetDirectoryName(applicationPath) ?? string.Empty, Path.GetFileNameWithoutExtension(applicationPath) + ".exe");
+ if (File.Exists(exePath))
+ {
+ applicationPath = exePath;
+ }
+ }
+ FileAssociationManager.RegisterPreviewAction(applicationPath);
+ return;
+}
+if (args.Contains("--unregister"))
+{
+ FileAssociationManager.UnregisterPreviewAction();
+ return;
+}
var noWait = args.Contains("--no-wait");
var filePath = args.FirstOrDefault(a => a != "--no-wait");
@@ -6,6 +27,8 @@
if (string.IsNullOrWhiteSpace(filePath))
{
Console.WriteLine("Usage: EnvFileViewer [--no-wait]");
+ Console.WriteLine(" EnvFileViewer --register Register the application as a preview handler for .bin files");
+ Console.WriteLine(" EnvFileViewer --unregister Unregister the application as a preview handler for .bin files");
return;
}
diff --git a/tools/EnvFileViewer/EnvLib/EnvFileModel.cs b/tools/EnvFileViewer/EnvLib/EnvFileModel.cs
index 43f0aed..701bd97 100644
--- a/tools/EnvFileViewer/EnvLib/EnvFileModel.cs
+++ b/tools/EnvFileViewer/EnvLib/EnvFileModel.cs
@@ -43,6 +43,7 @@ public class ModuleInfo
public bool FileExists { get; set; }
public bool ContentMatches { get; set; }
+ public string? FileName { get; set; }
public string FileNameHashHex => BitConverter.ToString(FileNameHash).Replace("-", "").ToLowerInvariant();
diff --git a/tools/EnvFileViewer/EnvLib/EnvFileParser.cs b/tools/EnvFileViewer/EnvLib/EnvFileParser.cs
index d31a689..2d4583e 100644
--- a/tools/EnvFileViewer/EnvLib/EnvFileParser.cs
+++ b/tools/EnvFileViewer/EnvLib/EnvFileParser.cs
@@ -220,26 +220,28 @@ private static List GetModules(string folder, uint seed, byte[] cont
private static void FillModuleStatus(string folder, uint seed, IEnumerable modules)
{
var files = Helper.GetModuleFiles(folder);
- List<(byte[] fileNameHash, byte[] contentHash)> moduleHashes = new List<(byte[], byte[])>();
+ List<(byte[] FileNameHash, byte[] ContentHash, string FileName)> moduleHashes = new List<(byte[] FileNameHash, byte[] ContentHash, string FileName)>();
foreach (var module in files)
{
- var fileName = Path.GetFileName(module)?.ToLowerInvariant();
- if (fileName == null)
+ var rawRelativePath = Path.GetRelativePath(folder, module).Replace("\\", "/");
+ var relativeFilePath = GetModuleRelativePath(module, folder);
+ if (relativeFilePath == null)
{
continue;
}
- var fileNameHash = Helper.GetStringSHA256Hash(fileName, seed);
+ var relativePathHash = Helper.GetStringSHA256Hash(relativeFilePath, seed);
var contentHash = Helper.GetSHA256Hash(module, seed);
- moduleHashes.Add((fileNameHash, contentHash));
+ moduleHashes.Add((relativePathHash, contentHash, rawRelativePath));
}
foreach(var module in modules)
{
- var matchingModule = moduleHashes.FirstOrDefault(m => m.fileNameHash.SequenceEqual(module.FileNameHash));
- if (matchingModule.fileNameHash != null)
+ var matchingModule = moduleHashes.FirstOrDefault(m => m.FileNameHash.SequenceEqual(module.FileNameHash));
+ if (matchingModule.FileNameHash != null)
{
module.FileExists = true;
- module.ContentMatches = matchingModule.contentHash.SequenceEqual(module.ContentHash);
+ module.FileName = matchingModule.FileName;
+ module.ContentMatches = matchingModule.ContentHash.SequenceEqual(module.ContentHash);
}
else
{
@@ -248,4 +250,33 @@ private static void FillModuleStatus(string folder, uint seed, IEnumerable basePathNormalized.Length &&
+ fullPathNormalized.Substring(0, basePathNormalized.Length).Equals(basePathNormalized, StringComparison.OrdinalIgnoreCase))
+ {
+ relativePath = fullPathNormalized.Substring(basePathNormalized.Length);
+ }
+ else
+ {
+ // Fallback: if fullPath is not under basePath, use filename
+ relativePath = Path.GetFileName(fullPathNormalized);
+ }
+
+ // Replace backslashes with forward slashes
+ relativePath = relativePath.Replace('\\', '/');
+
+ // Convert to lowercase
+ return relativePath.ToLower();
+ }
}
diff --git a/tools/EnvFileViewer/EnvLib/Helper.cs b/tools/EnvFileViewer/EnvLib/Helper.cs
index b26e565..49865d5 100644
--- a/tools/EnvFileViewer/EnvLib/Helper.cs
+++ b/tools/EnvFileViewer/EnvLib/Helper.cs
@@ -7,11 +7,32 @@ internal class Helper
{
public static string[] GetModuleFiles(string outputPath)
{
- var files = Directory.GetFiles(outputPath, "*", SearchOption.TopDirectoryOnly)
- .Where(f => f.EndsWith(".dll", StringComparison.OrdinalIgnoreCase)
- || f.EndsWith(".exe", StringComparison.OrdinalIgnoreCase)
- || IsAotExecutable(f)
- )
+ string runtimesPath = Path.Combine(outputPath, "runtimes") + Path.DirectorySeparatorChar;
+ var files = Directory.GetFiles(outputPath, "*", SearchOption.AllDirectories)
+ .Where(f =>
+ {
+ // Exclude files from runtimes subfolder
+ if (f.StartsWith(runtimesPath, StringComparison.OrdinalIgnoreCase))
+ return false;
+
+ var isDll = f.EndsWith(".dll", StringComparison.OrdinalIgnoreCase);
+ var isExe = f.EndsWith(".exe", StringComparison.OrdinalIgnoreCase);
+
+ // .exe files only in top directory (direct children of outputPath)
+ if (isExe)
+ return Path.GetDirectoryName(f) == outputPath;
+
+ // .dll files and AoT executables allowed anywhere (except runtimes)
+ if (isDll)
+ return true;
+
+#if NET6_0_OR_GREATER
+ if (IsAotExecutable(f))
+ return true;
+#endif
+
+ return false;
+ })
.ToArray();
return files;