Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions !RED+.sln
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,44 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
.editorconfig = .editorconfig
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RED.Tests", "RED.Tests\RED.Tests.csproj", "{91DC5578-B3A6-4C5C-AAC8-F77C3170D943}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "RED", "RED", "{B4F0820F-41E8-57EB-F9D8-0C51BF3D66C8}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{91B25075-18EE-4ADE-8704-A9C0935E15F7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{91B25075-18EE-4ADE-8704-A9C0935E15F7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{91B25075-18EE-4ADE-8704-A9C0935E15F7}.Debug|x64.ActiveCfg = Debug|x64
{91B25075-18EE-4ADE-8704-A9C0935E15F7}.Debug|x64.Build.0 = Debug|x64
{91B25075-18EE-4ADE-8704-A9C0935E15F7}.Debug|x86.ActiveCfg = Debug|Any CPU
{91B25075-18EE-4ADE-8704-A9C0935E15F7}.Debug|x86.Build.0 = Debug|Any CPU
{91B25075-18EE-4ADE-8704-A9C0935E15F7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{91B25075-18EE-4ADE-8704-A9C0935E15F7}.Release|Any CPU.Build.0 = Release|Any CPU
{91B25075-18EE-4ADE-8704-A9C0935E15F7}.Release|x64.ActiveCfg = Release|x64
{91B25075-18EE-4ADE-8704-A9C0935E15F7}.Release|x64.Build.0 = Release|x64
{91B25075-18EE-4ADE-8704-A9C0935E15F7}.Release|x86.ActiveCfg = Release|Any CPU
{91B25075-18EE-4ADE-8704-A9C0935E15F7}.Release|x86.Build.0 = Release|Any CPU
{91DC5578-B3A6-4C5C-AAC8-F77C3170D943}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{91DC5578-B3A6-4C5C-AAC8-F77C3170D943}.Debug|Any CPU.Build.0 = Debug|Any CPU
{91DC5578-B3A6-4C5C-AAC8-F77C3170D943}.Debug|x64.ActiveCfg = Debug|Any CPU
{91DC5578-B3A6-4C5C-AAC8-F77C3170D943}.Debug|x64.Build.0 = Debug|Any CPU
{91DC5578-B3A6-4C5C-AAC8-F77C3170D943}.Debug|x86.ActiveCfg = Debug|Any CPU
{91DC5578-B3A6-4C5C-AAC8-F77C3170D943}.Debug|x86.Build.0 = Debug|Any CPU
{91DC5578-B3A6-4C5C-AAC8-F77C3170D943}.Release|Any CPU.ActiveCfg = Release|Any CPU
{91DC5578-B3A6-4C5C-AAC8-F77C3170D943}.Release|Any CPU.Build.0 = Release|Any CPU
{91DC5578-B3A6-4C5C-AAC8-F77C3170D943}.Release|x64.ActiveCfg = Release|Any CPU
{91DC5578-B3A6-4C5C-AAC8-F77C3170D943}.Release|x64.Build.0 = Release|Any CPU
{91DC5578-B3A6-4C5C-AAC8-F77C3170D943}.Release|x86.ActiveCfg = Release|Any CPU
{91DC5578-B3A6-4C5C-AAC8-F77C3170D943}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
7 changes: 2 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,13 @@ jobs:
steps:
- uses: actions/checkout@v4

- name: Setup MSBuild
uses: microsoft/setup-msbuild@v2

- name: Setup .NET SDK
uses: actions/setup-dotnet@v4
with:
dotnet-version: '9.0.x'

- name: Build Release
run: msbuild "!RED+.sln" /p:Configuration=Release /p:Platform="Any CPU"
run: dotnet build "RED/RED+.csproj" -c Release

- name: Engine unit tests
run: dotnet test "RED.Tests/RED.Tests.csproj" -c Release --logger "console;verbosity=normal"
Expand All @@ -48,7 +45,7 @@ jobs:
New-Item -ItemType Directory -Path $emptyDir, $nonEmpty, $junctionTarget, $deny | Out-Null
Set-Content -Path (Join-Path $nonEmpty 'keep.txt') -Value 'content' -NoNewline
New-Item -ItemType File -Path (Join-Path $root 'empty-file.txt') | Out-Null
cmd /c mklink /J "$junction" "$junctionTarget"
New-Item -ItemType Junction -Path $junction -Target $junctionTarget | Out-Null

$currentUser = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name
$denyAcl = $currentUser + ':(OI)(CI)(R)'
Expand Down
67 changes: 58 additions & 9 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,22 +21,71 @@ jobs:
steps:
- uses: actions/checkout@v4

- name: Setup MSBuild
uses: microsoft/setup-msbuild@v2
- name: Setup .NET SDK
uses: actions/setup-dotnet@v4
with:
dotnet-version: '9.0.x'

# Self-contained single-file: no .NET runtime install required on the target
# machine, preserving RED++'s "unzip and run" portability. Larger artifact
# (~65 MB) than a framework-dependent build, but no external dependency. WPF +
# reflection-based resource loading are not trim-safe, so trimming is left off.
- name: Publish self-contained single-file
run: dotnet publish "RED/RED+.csproj" -c Release -r win-x64 --self-contained true -p:PublishSingleFile=true -o publish

# Gate the release on the actual shipping artifact passing the safety
# invariants (empty-dir/file deletion; reparse-point, deny-ACL, and
# AutoProtectRoot protection). Never ship a file-deleting tool that fails this.
- name: Safety smoke on published artifact
shell: pwsh
run: |
$ErrorActionPreference = 'Stop'
$exe = Join-Path $PWD 'publish\RED+.exe'
if (!(Test-Path $exe)) { throw "Missing published RED+.exe at $exe" }

$root = Join-Path $env:RUNNER_TEMP ("redpp-rel-" + [guid]::NewGuid().ToString("N"))
$junctionTarget = $root + '-junction-target'
New-Item -ItemType Directory -Path $root | Out-Null
try {
$emptyDir = Join-Path $root 'empty-child'
$nonEmpty = Join-Path $root 'non-empty'
$junction = Join-Path $root 'junction'
$deny = Join-Path $root 'deny-access'
New-Item -ItemType Directory -Path $emptyDir, $nonEmpty, $junctionTarget, $deny | Out-Null
Set-Content -Path (Join-Path $nonEmpty 'keep.txt') -Value 'content' -NoNewline
New-Item -ItemType File -Path (Join-Path $root 'empty-file.txt') | Out-Null
New-Item -ItemType Junction -Path $junction -Target $junctionTarget | Out-Null

- name: Setup NuGet
uses: NuGet/setup-nuget@v2
$currentUser = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name
icacls "$deny" /deny ($currentUser + ':(OI)(CI)(R)') | Out-Null
try {
& $exe -silent -path $root -emptyfiles -mode direct | Tee-Object -Variable output
if ($LASTEXITCODE -ne 0) { throw "RED++ exited with $LASTEXITCODE`n$output" }
} finally {
icacls "$deny" /remove:d "$currentUser" | Out-Null
}

- name: Restore NuGet packages
run: nuget restore "!RED+.sln"
if (($output -join "`n") -notmatch 'Found 1 empty directories') { throw "Unexpected found-directory count" }
if (!(Test-Path $root)) { throw 'Root deleted despite AutoProtectRoot' }
if (Test-Path $emptyDir) { throw 'Empty child not deleted' }
if (!(Test-Path (Join-Path $nonEmpty 'keep.txt'))) { throw 'Non-empty payload deleted' }
if (!(Test-Path $junction)) { throw 'Junction deleted' }
if (!(Test-Path $junctionTarget)) { throw 'Junction target deleted' }
if (!(Test-Path $deny)) { throw 'Deny-ACL dir deleted' }
if (Test-Path (Join-Path $root 'empty-file.txt')) { throw 'Empty file not deleted' }

- name: Build Release
run: msbuild "!RED+.sln" /p:Configuration=Release /p:Platform="Any CPU"
& $exe -version | Tee-Object -Variable ver
if (($ver -join "`n") -notmatch 'RED\+\+\s+\d+\.\d+\.\d+') { throw "Unexpected -version output: $ver" }
}
finally {
Remove-Item -LiteralPath $root -Recurse -Force -ErrorAction SilentlyContinue
Remove-Item -LiteralPath $junctionTarget -Recurse -Force -ErrorAction SilentlyContinue
}

- name: Create release archive
run: |
mkdir release
copy bin\Release\RED+.exe release\
copy publish\RED+.exe release\
copy LICENSE release\
xcopy help release\help\ /E /I
xcopy language release\language\ /E /I
Expand Down
5 changes: 2 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,5 @@ RESEARCH.md
# Local build/scratch (refasm, screenshots, smoke fixtures)
tmp/

# Local-only build shim (lets the legacy net481 project build under the bare
# .NET SDK without Visual Studio). Not shipped; proper fix is the SDK migration.
Directory.Build.targets
# dotnet publish output (self-contained single-file artifact)
publish/
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@

## Unreleased

### Build / Runtime
- Migrate from .NET Framework 4.8.1 (old-style csproj + `packages.config`) to an SDK-style project targeting `net9.0-windows` (WPF + WinForms). The verified scan/delete engine and every P/Invoke struct are unchanged; the 50-test xUnit suite and the headless safety smoke (empty-dir/file deletion, reparse-point/junction protection, deny-ACL fail-closed, AutoProtectRoot, recycle→undo round-trip) pass identically on the new runtime.
- Replace the removed `Directory.GetAccessControl(path)` static with the `DirectoryInfo.GetAccessControl()` extension (same default ACL sections) in the deletion lock check.
- Make version reporting single-file safe: read the file version from `Environment.ProcessPath` / the embedded `AssemblyFileVersion` attribute instead of `Assembly.Location` (which is empty in a published single-file bundle and previously crashed `-version`/`-json`).
- Modern .NET fixes the .resx resource friction that forced a build-time workaround on 4.8.1: the WPF shell and WinForms fallback now build and render cleanly (icons and image resources load natively).
- Release builds are now a **self-contained single-file** `win-x64` artifact (`dotnet publish -p:PublishSingleFile=true --self-contained`) — no .NET runtime install required on the target machine, preserving the "unzip and run" portability. CI builds and tests with `dotnet` (Visual Studio / MSBuild no longer required).

### Documentation
- Drop the advertised "Enter to scan, Del to delete selected" keyboard shortcuts from the feature list: the default modern WPF shell is click-only by design, matching the project's no-keyboard-shortcuts convention.

Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,9 @@ RED+.exe -silent -path "D:\Shares" -log cleanup.log

## System Requirements

- Windows 10 22H2, Windows 11, or Windows Server 2022/2025
- Microsoft .NET Framework 4.8.1
- No installer required. Unzip and run.
- Windows 10 22H2, Windows 11, or Windows Server 2022/2025 (64-bit / x64)
- No runtime to install — the release is a self-contained single-file build that bundles .NET 9. Just unzip and run.
- No installer required.

## Verify Your Download

Expand Down
23 changes: 11 additions & 12 deletions RED.Tests/RED.Tests.csproj
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net481</TargetFramework>
<LangVersion>9.0</LangVersion>
<TargetFramework>net9.0-windows</TargetFramework>
<!-- The engine assembly references WPF + WinForms; the test host must load the
Windows Desktop runtime to resolve those types when exercising internals. -->
<UseWPF>true</UseWPF>
<UseWindowsForms>true</UseWindowsForms>
<LangVersion>latest</LangVersion>
<Nullable>disable</Nullable>
<ImplicitUsings>disable</ImplicitUsings>
<IsPackable>false</IsPackable>
<AssemblyName>RED.Tests</AssemblyName>
<RootNamespace>RED.Tests</RootNamespace>
<!-- The engine assembly is the WinExe; no entry point needed here. -->
<GenerateProgramFile>true</GenerateProgramFile>
</PropertyGroup>

<ItemGroup>
Expand All @@ -19,15 +22,11 @@
</PackageReference>
</ItemGroup>

<!-- Reference the compiled engine assembly directly. CI builds RED+.exe with
MSBuild first, then runs `dotnet test`; this keeps the verified engine
build authoritative and avoids rebuilding the WinForms/WPF resources
under the dotnet SDK. Internals are exposed via InternalsVisibleTo. -->
<!-- Reference the engine project directly now that both are SDK-style and on the
same TFM family. Internals are exposed via InternalsVisibleTo("RED.Tests")
in RED\Properties\AssemblyInfo.cs. -->
<ItemGroup>
<Reference Include="RED+">
<HintPath>..\bin\Release\RED+.exe</HintPath>
<Private>true</Private>
</Reference>
<ProjectReference Include="..\RED\RED+.csproj" />
</ItemGroup>

</Project>
19 changes: 17 additions & 2 deletions RED/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -851,8 +851,23 @@ private static void PrintVersion()

private static string GetFileVersion()
{
var vi = System.Diagnostics.FileVersionInfo.GetVersionInfo(System.Reflection.Assembly.GetExecutingAssembly().Location);
return vi.FileVersion;
// Single-file safe: Assembly.Location is empty in a published single-file
// bundle, which would throw here. Environment.ProcessPath is the apphost
// exe (it carries the version resource); fall back to the embedded
// AssemblyFileVersion attribute if the path is unavailable.
try
{
string path = Environment.ProcessPath;
if (!string.IsNullOrEmpty(path))
{
string fv = System.Diagnostics.FileVersionInfo.GetVersionInfo(path).FileVersion;
if (!string.IsNullOrEmpty(fv)) return fv;
}
}
catch { }
var attr = (System.Reflection.AssemblyFileVersionAttribute)Attribute.GetCustomAttribute(
System.Reflection.Assembly.GetExecutingAssembly(), typeof(System.Reflection.AssemblyFileVersionAttribute));
return attr != null ? attr.Version : "0.0.0";
}

private static void PrintUsage()
Expand Down
Loading
Loading