From d93fdd0493ae7877fa3aaac6e5c192b3cb38b029 Mon Sep 17 00:00:00 2001 From: "David T. Martel" Date: Thu, 6 Aug 2026 10:32:57 -0400 Subject: [PATCH 1/8] chore: initialize git tracking for NukeNul NukeNul existed as an untracked source tree in C:\codedev with no version control, putting the hybrid Rust/C# implementation at risk. Track sources only; .gitignore already excludes bin/, obj/, target/, and native binaries. Agent: claude Co-authored-by: Claude --- .claude/context/project-context.md | 298 ++++++++++++ .github/workflows/build-and-test.yml | 263 +++++++++++ .gitignore | 35 ++ BUILD.md | 222 +++++++++ BUILD_AND_TEST.md | 634 ++++++++++++++++++++++++++ BUILD_SYSTEM_SUMMARY.md | 631 +++++++++++++++++++++++++ DEPLOYMENT_CHECKLIST.md | 483 ++++++++++++++++++++ IMPLEMENTATION_SUMMARY.md | 421 +++++++++++++++++ NukeNul.csproj | 44 ++ NukeNul.md | 184 ++++++++ PROJECT_COMPLETE.md | 373 +++++++++++++++ Program.cs | 328 +++++++++++++ QUICKSTART.md | 264 +++++++++++ QUICK_REFERENCE.md | 421 +++++++++++++++++ README.md | 258 +++++++++++ build-common.ps1 | 153 +++++++ build.ps1 | 135 ++++++ delete-nul-files.ps1 | 266 +++++++++++ nuker_core.dll.buildinfo.json | 16 + nuker_core/BUILD.md | 343 ++++++++++++++ nuker_core/Cargo.lock | 307 +++++++++++++ nuker_core/Cargo.toml | 43 ++ nuker_core/IMPLEMENTATION_SUMMARY.md | 481 +++++++++++++++++++ nuker_core/PLATFORM_CONSIDERATIONS.md | 495 ++++++++++++++++++++ nuker_core/QUICKSTART.md | 357 +++++++++++++++ nuker_core/README.md | 360 +++++++++++++++ nuker_core/build.ps1 | 301 ++++++++++++ nuker_core/build.rs | 85 ++++ nuker_core/src/lib.rs | 378 +++++++++++++++ test.ps1 | 450 ++++++++++++++++++ 30 files changed, 9029 insertions(+) create mode 100644 .claude/context/project-context.md create mode 100644 .github/workflows/build-and-test.yml create mode 100644 .gitignore create mode 100644 BUILD.md create mode 100644 BUILD_AND_TEST.md create mode 100644 BUILD_SYSTEM_SUMMARY.md create mode 100644 DEPLOYMENT_CHECKLIST.md create mode 100644 IMPLEMENTATION_SUMMARY.md create mode 100644 NukeNul.csproj create mode 100644 NukeNul.md create mode 100644 PROJECT_COMPLETE.md create mode 100644 Program.cs create mode 100644 QUICKSTART.md create mode 100644 QUICK_REFERENCE.md create mode 100644 README.md create mode 100644 build-common.ps1 create mode 100644 build.ps1 create mode 100644 delete-nul-files.ps1 create mode 100644 nuker_core.dll.buildinfo.json create mode 100644 nuker_core/BUILD.md create mode 100644 nuker_core/Cargo.lock create mode 100644 nuker_core/Cargo.toml create mode 100644 nuker_core/IMPLEMENTATION_SUMMARY.md create mode 100644 nuker_core/PLATFORM_CONSIDERATIONS.md create mode 100644 nuker_core/QUICKSTART.md create mode 100644 nuker_core/README.md create mode 100644 nuker_core/build.ps1 create mode 100644 nuker_core/build.rs create mode 100644 nuker_core/src/lib.rs create mode 100644 test.ps1 diff --git a/.claude/context/project-context.md b/.claude/context/project-context.md new file mode 100644 index 0000000..7b6d16e --- /dev/null +++ b/.claude/context/project-context.md @@ -0,0 +1,298 @@ +# NukeNul Project Context + +**Last Updated**: 2026-01-23 +**Status**: FULLY IMPLEMENTED AND TESTED + +## Project Overview + +**NukeNul** is a high-performance Windows reserved filename cleaner using a hybrid Rust/C# architecture. + +### Purpose + +Delete Windows reserved filenames that cannot be removed through normal file operations: + +- `nul`, `con`, `prn`, `aux` +- `com1` through `com9` +- `lpt1` through `lpt9` + +### Technology Stack + +- **Rust DLL** (`nuker_core.dll`): Parallel file walking and Win32 API deletion +- **C# CLI** (`NukeNul.exe`): User interface with JSON output +- **.NET 8**: Framework-dependent build with Native AOT support +- **Key Rust Crates**: `ignore` (ripgrep's file walker), `windows-sys`, `widestring` + +## Current State + +### Build Status: COMPLETE + +- Rust DLL builds successfully (optimized release mode) +- C# CLI builds successfully (.NET 8 win-x64) +- Integration tests passing with 100% deletion success rate + +### Performance Metrics + +- **12 files**: ~8ms execution time +- **Thread utilization**: Parallel (scales to CPU core count) +- **Walker**: ripgrep's `ignore` crate with work-stealing queue + +### Build Outputs Location + +``` +C:\Users\david\PC_AI\Native\NukeNul\bin\Release\net8.0\win-x64\ + - NukeNul.exe (C# CLI) + - nuker_core.dll (Rust DLL) +``` + +## Project Structure + +``` +C:\Users\david\PC_AI\Native\NukeNul\ +|-- nuker_core/ # Rust DLL project +| |-- Cargo.toml # Rust dependencies and build config +| |-- Cargo.lock # Locked dependency versions +| |-- src/ +| | |-- lib.rs # 327 lines - Core implementation +| |-- target/release/ # Rust build output +| |-- nuker_core.dll +| +|-- Program.cs # 243 lines - C# CLI application +|-- NukeNul.csproj # .NET 8 project configuration +|-- build.ps1 # Master build orchestration script +|-- test.ps1 # Integration test script +|-- bin/Release/net8.0/win-x64/ # Final build outputs +``` + +**IMPORTANT**: C# files are in the ROOT directory, not in a subdirectory. + +## Key Design Decisions + +### 1. Hybrid Architecture (Rust + C#) + +- **Rationale**: Rust provides safe low-level Win32 API access and parallel performance; C# provides familiar CLI patterns and JSON serialization +- **Alternative considered**: Pure PowerShell (too slow), Pure Rust CLI (less familiar to users) + +### 2. ripgrep's `ignore` crate for File Walking + +- **Rationale**: Battle-tested parallel walker with work-stealing for load balancing +- **Features used**: Multi-threaded traversal, .git directory filtering +- **Configuration**: `hidden(false)`, all gitignore settings disabled + +### 3. Direct Win32 DeleteFileW API + +- **Rationale**: Standard Rust `fs::remove_file` cannot delete reserved filenames +- **Implementation**: Extended-length path prefix (`\\?\`) bypasses Windows path normalization +- **Safety**: All unsafe blocks documented with safety invariants + +### 4. JSON Output Format + +- **Rationale**: Machine-readable for LLM/automation consumption +- **Implementation**: Source-generated JSON serialization for AOT compatibility +- **Fields**: `tool`, `target`, `timestamp`, `status`, `performance`, `results` + +### 5. Native AOT Support + +- **Rationale**: Fast startup for CLI tool, smaller dependency footprint +- **Configuration**: `PublishAot=true`, `TrimMode=full`, `IlcOptimizationPreference=Speed` + +## Code Patterns + +### FFI Interface (Rust side) + +```rust +#[repr(C)] +pub struct ScanStats { + pub files_scanned: u32, + pub files_deleted: u32, + pub errors: u32, +} + +#[no_mangle] +pub extern "C" fn nuke_reserved_files(root_ptr: *const c_char) -> ScanStats +``` + +### P/Invoke (C# side) + +```csharp +[StructLayout(LayoutKind.Sequential)] +internal struct ScanStats { + public uint FilesScanned; + public uint FilesDeleted; + public uint Errors; +} + +[DllImport("nuker_core.dll", CallingConvention = CallingConvention.Cdecl)] +internal static extern ScanStats nuke_reserved_files( + [MarshalAs(UnmanagedType.LPStr)] string rootPath); +``` + +### Reserved Filename Matching + +- Case-insensitive comparison using `eq_ignore_ascii_case` +- Matches only exact filename (no extensions) +- 22 reserved names total + +### Extended-Length Path Handling + +```rust +// Regular path: C:\path -> \\?\C:\path +// UNC path: \\server\share -> \\?\UNC\server\share +let extended_path = format!("\\\\?\\{}", path_str); +``` + +## Build Commands + +### Full Build + +```powershell +cd C:\Users\david\PC_AI\Native\NukeNul +.\build.ps1 +``` + +### Build Options + +```powershell +.\build.ps1 -Publish # Self-contained executable +.\build.ps1 -Clean # Clean before build +.\build.ps1 -SkipRust # Skip Rust build +.\build.ps1 -SkipCSharp # Skip C# build +``` + +### Manual Build Steps + +```powershell +# Rust DLL +cd nuker_core +cargo build --release + +# C# CLI +cd .. +copy nuker_core\target\release\nuker_core.dll . +dotnet build -c Release +``` + +## Test Commands + +### Integration Tests + +```powershell +.\test.ps1 # Standard test (10 files) +.\test.ps1 -TestCount 100 # Stress test +.\test.ps1 -DeepNesting # Nested directory test +.\test.ps1 -KeepTestDir # Keep test artifacts +``` + +### Manual Testing + +```powershell +cd bin\Release\net8.0\win-x64 +.\NukeNul.exe . # Scan current directory +.\NukeNul.exe C:\path\to\scan # Scan specific path +``` + +## JSON Output Format + +### Success Response + +```json +{ + "tool": "Nuke-Nul", + "target": "C:\\path\\to\\scan", + "timestamp": "2026-01-23T10:30:00Z", + "status": "Success", + "performance": { + "mode": "Rust/Parallel", + "threads": 22, + "elapsed_ms": 8 + }, + "results": { + "scanned": 150, + "deleted": 12, + "errors": 0 + } +} +``` + +### Error Response + +```json +{ + "tool": "Nuke-Nul", + "status": "Error", + "message": "Target directory does not exist: C:\\nonexistent" +} +``` + +## Exit Codes + +- `0`: Success (all files deleted, no errors) +- `1`: Invalid path or validation error +- `2`: DLL not found or load failure +- `3`: Partial success (some files deleted, some errors) +- `99`: Unexpected error + +## Dependencies + +### Rust (nuker_core) + +```toml +[dependencies] +ignore = "0.4" # Parallel file walker +widestring = "1.1" # UTF-16 string conversion +windows-sys = "0.52" # Win32 API bindings +libc = "0.2" # C FFI types +``` + +### C# (NukeNul.csproj) + +```xml + +``` + +## Common Issues and Solutions + +### Issue: DLL Not Found + +- **Cause**: `nuker_core.dll` not in same directory as `NukeNul.exe` +- **Solution**: Run `.\build.ps1` which copies DLL to output directory + +### Issue: Access Denied Errors + +- **Cause**: File locked by another process +- **Solution**: Close applications using the file; check results.errors count + +### Issue: Build Fails on Rust Side + +- **Cause**: Missing Rust toolchain +- **Solution**: Install Rust from https://rustup.rs/ + +### Issue: Build Fails on C# Side + +- **Cause**: Missing .NET 8 SDK +- **Solution**: Install from https://dotnet.microsoft.com/download + +## Future Enhancements (Not Implemented) + +1. **Dry-run mode**: List files without deleting +2. **Verbose output**: Per-file deletion logging +3. **Custom patterns**: User-defined filenames to delete +4. **Recursive depth limit**: Control scan depth +5. **Progress reporting**: Real-time scan progress + +## Related Files + +- `README.md` - User documentation +- `BUILD.md` - Detailed build instructions +- `QUICKSTART.md` - Quick setup guide +- `PROJECT_COMPLETE.md` - Completion summary +- `delete-nul-files.ps1` - Original PowerShell implementation (for comparison) + +## Session Notes + +### 2026-01-23 Context Save + +- Project fully implemented and tested +- All tests passing with 100% deletion success +- Performance verified: 8ms for 12 files +- Build scripts verified to work with flat structure (C# in root) +- JSON output working with source-generated serialization diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml new file mode 100644 index 0000000..f44508e --- /dev/null +++ b/.github/workflows/build-and-test.yml @@ -0,0 +1,263 @@ +name: Build and Test + +on: + push: + branches: [ main, develop ] + pull_request: + branches: [ main, develop ] + workflow_dispatch: + +env: + CARGO_TERM_COLOR: always + RUST_BACKTRACE: 1 + +jobs: + build-rust: + name: Build Rust DLL + runs-on: windows-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Rust toolchain + uses: dtolnay/rust-toolchain@stable + with: + targets: x86_64-pc-windows-msvc + + - name: Cache Rust dependencies + uses: actions/cache@v4 + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + nuker_core/target/ + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-cargo- + + - name: Build Rust DLL (Release) + working-directory: nuker_core + run: cargo build --release --verbose + + - name: Run Rust tests + working-directory: nuker_core + run: cargo test --release --verbose + + - name: Upload Rust DLL artifact + uses: actions/upload-artifact@v4 + with: + name: nuker_core-dll + path: nuker_core/target/release/nuker_core.dll + retention-days: 7 + + build-csharp: + name: Build C# CLI + runs-on: windows-latest + needs: build-rust + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup .NET SDK + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '8.0.x' + + - name: Download Rust DLL + uses: actions/download-artifact@v4 + with: + name: nuker_core-dll + path: NukeNul/ + + - name: Restore dependencies + working-directory: NukeNul + run: dotnet restore + + - name: Build C# application + working-directory: NukeNul + run: dotnet build -c Release --no-restore + + - name: Upload C# executable artifact + uses: actions/upload-artifact@v4 + with: + name: nukenul-exe + path: | + NukeNul/bin/Release/net8.0/NukeNul.exe + NukeNul/bin/Release/net8.0/nuker_core.dll + retention-days: 7 + + integration-test: + name: Integration Tests + runs-on: windows-latest + needs: build-csharp + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: nukenul-exe + path: NukeNul/bin/Release/net8.0/ + + - name: Run integration tests + shell: pwsh + run: | + # Install PowerShell 7 if needed + if ($PSVersionTable.PSVersion.Major -lt 7) { + Write-Host "PowerShell 7+ required for tests" + exit 1 + } + + # Run test script + .\test.ps1 -TestCount 50 -SkipBenchmark + + - name: Upload test results + if: always() + uses: actions/upload-artifact@v4 + with: + name: test-results + path: | + test-results/ + test-*.log + retention-days: 30 + + security-scan: + name: Security Scan + runs-on: windows-latest + needs: build-rust + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Rust toolchain + uses: dtolnay/rust-toolchain@stable + + - name: Run cargo audit + working-directory: nuker_core + run: | + cargo install cargo-audit --quiet + cargo audit + + - name: Run cargo clippy + working-directory: nuker_core + run: cargo clippy --all-targets --all-features -- -D warnings + + publish-release: + name: Publish Release Build + runs-on: windows-latest + needs: [build-rust, build-csharp, integration-test] + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Rust toolchain + uses: dtolnay/rust-toolchain@stable + + - name: Setup .NET SDK + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '8.0.x' + + - name: Build self-contained executable + shell: pwsh + run: .\build.ps1 -Publish -Clean + + - name: Create release archive + shell: pwsh + run: | + $PublishDir = "NukeNul\bin\Release\net8.0\win-x64\publish" + $Version = (Get-Date -Format "yyyy.MM.dd.HHmm") + $ArchiveName = "NukeNul-$Version-win-x64.zip" + + Compress-Archive -Path "$PublishDir\*" -DestinationPath $ArchiveName + + Write-Output "ARCHIVE_NAME=$ArchiveName" >> $env:GITHUB_ENV + Write-Output "VERSION=$Version" >> $env:GITHUB_ENV + + - name: Upload release archive + uses: actions/upload-artifact@v4 + with: + name: nukenul-release-${{ env.VERSION }} + path: "*.zip" + retention-days: 90 + + - name: Create GitHub Release (optional) + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') + uses: softprops/action-gh-release@v1 + with: + files: "*.zip" + generate_release_notes: true + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + benchmark: + name: Performance Benchmark + runs-on: windows-latest + needs: build-csharp + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: nukenul-exe + path: NukeNul/bin/Release/net8.0/ + + - name: Run performance benchmark + shell: pwsh + run: | + # Run benchmark with various file counts + $TestCounts = @(10, 100, 1000) + $Results = @() + + foreach ($Count in $TestCounts) { + Write-Host "Benchmarking with $Count files..." + $Output = .\test.ps1 -TestCount $Count -SkipBenchmark -Verbose + $Results += "Test Count: $Count - Output: $Output" + } + + # Save results + $Results | Out-File -FilePath "benchmark-results.txt" + + - name: Upload benchmark results + uses: actions/upload-artifact@v4 + with: + name: benchmark-results + path: benchmark-results.txt + retention-days: 30 + + code-coverage: + name: Code Coverage + runs-on: windows-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Rust toolchain + uses: dtolnay/rust-toolchain@stable + + - name: Install cargo-tarpaulin + run: cargo install cargo-tarpaulin + + - name: Generate coverage report + working-directory: nuker_core + run: cargo tarpaulin --out Xml --output-dir coverage + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v4 + with: + files: nuker_core/coverage/cobertura.xml + flags: rust + name: rust-coverage diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3a6546f --- /dev/null +++ b/.gitignore @@ -0,0 +1,35 @@ +# Rust build artifacts +**/target/ +**/*.rs.bk +*.pdb + +# C# build artifacts +bin/ +obj/ +*.dll +*.exe +!C:/Users/david/bin/NukeNul.exe + +# IDE +.idea/ +.vs/ +*.swp +*.swo +*~ +.vscode/ + +# OS files +.DS_Store +Thumbs.db +desktop.ini + +# Logs +*.log + +# Temporary files +*.tmp +*.temp + +# User-specific files +*.user +*.suo diff --git a/BUILD.md b/BUILD.md new file mode 100644 index 0000000..e99ad59 --- /dev/null +++ b/BUILD.md @@ -0,0 +1,222 @@ +# NukeNul Build Instructions + +## Primary Build Path (Recommended) + +From repository root, use the unified orchestrator: + +```powershell +.\Build.ps1 -Component nukenul +``` + +Use this document's remaining sections only for advanced/manual crate-level debugging. + +## Prerequisites + +1. **.NET 8 SDK** - Download from https://dotnet.microsoft.com/download/dotnet/8.0 +2. **Rust toolchain** - Required to build the `nuker_core.dll` +3. **Windows x64** - This project targets Windows 64-bit + +## Build Steps + +### Step 1: Build the Rust DLL + +```bash +# Navigate to the Rust project directory (if separate) +cd nuker_core + +# Build in release mode for maximum performance +cargo build --release + +# The DLL will be located at: target/release/nuker_core.dll +``` + +### Step 2: Copy the Rust DLL + +```bash +# Copy the DLL to the C# project root +copy target\release\nuker_core.dll ..\NukeNul\nuker_core.dll +``` + +### Step 3: Build the C# CLI Application + +```bash +# Navigate to the C# project directory +cd ..\NukeNul + +# Restore dependencies +dotnet restore + +# Build in Debug mode (for testing) +dotnet build -c Debug + +# Build in Release mode +dotnet build -c Release +``` + +### Step 4: Publish as Native AOT Binary + +```bash +# Publish as a self-contained native AOT executable +dotnet publish -c Release -r win-x64 --self-contained + +# The executable will be located at: +# bin\Release\net8.0\win-x64\publish\NukeNul.exe +``` + +## Alternative: Quick Build Script (PowerShell) + +```powershell +# build.ps1 +param( + [switch]$SkipRust +) + +if (-not $SkipRust) { + Write-Host "Building Rust DLL..." -ForegroundColor Cyan + Push-Location nuker_core + cargo build --release + if ($LASTEXITCODE -ne 0) { + Write-Error "Rust build failed" + exit 1 + } + Pop-Location + + Write-Host "Copying DLL..." -ForegroundColor Cyan + Copy-Item "nuker_core\target\release\nuker_core.dll" "nuker_core.dll" -Force +} + +Write-Host "Publishing C# application..." -ForegroundColor Cyan +dotnet publish -c Release -r win-x64 --self-contained + +if ($LASTEXITCODE -eq 0) { + Write-Host "`nBuild successful!" -ForegroundColor Green + Write-Host "Executable location: bin\Release\net8.0\win-x64\publish\NukeNul.exe" -ForegroundColor Yellow +} else { + Write-Error "C# build failed" + exit 1 +} +``` + +## Binary Locations + +After building: + +- **Debug build**: `bin\Debug\net8.0\NukeNul.exe` +- **Release build**: `bin\Release\net8.0\NukeNul.exe` +- **Published AOT binary**: `bin\Release\net8.0\win-x64\publish\NukeNul.exe` + +## DLL Placement Requirements + +The `nuker_core.dll` must be in the same directory as `NukeNul.exe`: + +``` +bin\Release\net8.0\win-x64\publish\ +├── NukeNul.exe +└── nuker_core.dll ← Must be here +``` + +The `.csproj` file is configured to automatically copy the DLL if it exists in the project root. + +## Optimization Profiles + +### Standard Release Build +- **Optimization**: Full +- **Size**: ~5-8 MB +- **Startup**: Fast +- **Use case**: General purpose + +```bash +dotnet publish -c Release -r win-x64 +``` + +### Size-Optimized Build +- **Optimization**: Size +- **Size**: ~3-5 MB +- **Startup**: Slightly slower +- **Use case**: Distribution, embedded systems + +```bash +dotnet publish -c Release -r win-x64 /p:IlcOptimizationPreference=Size +``` + +### Speed-Optimized Build +- **Optimization**: Maximum speed +- **Size**: ~8-12 MB +- **Startup**: Fastest +- **Use case**: Performance-critical scenarios + +```bash +dotnet publish -c Release -r win-x64 /p:IlcOptimizationPreference=Speed +``` + +## Verification + +After building, verify the executable: + +```bash +# Check file size +Get-Item bin\Release\net8.0\win-x64\publish\NukeNul.exe | Select-Object Length + +# Test execution +.\bin\Release\net8.0\win-x64\publish\NukeNul.exe --help + +# Test with current directory +.\bin\Release\net8.0\win-x64\publish\NukeNul.exe . +``` + +## Troubleshooting + +### DLL Not Found Error + +**Symptom**: `DllNotFoundException: Unable to load DLL 'nuker_core.dll'` + +**Solutions**: +1. Verify DLL exists: `Test-Path nuker_core.dll` +2. Copy manually: `Copy-Item nuker_core.dll bin\Release\net8.0\win-x64\publish\` +3. Check architecture: Ensure DLL is 64-bit (`dumpbin /headers nuker_core.dll`) + +### Native AOT Build Errors + +**Symptom**: `ILC: error : ... is not compatible with native AOT` + +**Solutions**: +1. Ensure .NET 8 SDK is installed: `dotnet --version` +2. Remove incompatible packages +3. Check `PublishAot` is set to `true` in `.csproj` + +### Rust Build Failures + +**Symptom**: `cargo build` fails with linking errors + +**Solutions**: +1. Update Rust: `rustup update` +2. Install MSVC Build Tools: https://visualstudio.microsoft.com/downloads/ +3. Verify toolchain: `rustc --version` + +## Performance Benchmarking + +```bash +# Scan a large directory and measure performance +Measure-Command { .\NukeNul.exe C:\LargeDirectory } + +# Compare with PowerShell script +Measure-Command { .\delete-nul-files.ps1 -TargetPath C:\LargeDirectory } +``` + +## Distribution + +To distribute the application: + +1. Copy both files from `bin\Release\net8.0\win-x64\publish\`: + - `NukeNul.exe` + - `nuker_core.dll` + +2. Both files must remain in the same directory + +3. No .NET runtime installation required (native AOT = self-contained) + +## Next Steps + +- Run the application: See [README.md](README.md) +- Performance tuning: Adjust Rust thread count in `nuker_core` +- Integration: Parse JSON output for automation workflows diff --git a/BUILD_AND_TEST.md b/BUILD_AND_TEST.md new file mode 100644 index 0000000..5284f75 --- /dev/null +++ b/BUILD_AND_TEST.md @@ -0,0 +1,634 @@ +# NukeNul Build and Test Guide + +Complete guide for building, testing, and deploying the NukeNul hybrid Rust/C# project. + +--- + +## Table of Contents + +1. [Prerequisites](#prerequisites) +2. [Quick Start](#quick-start) +3. [Build System](#build-system) +4. [Testing](#testing) +5. [Manual Testing](#manual-testing) +6. [Deployment](#deployment) +7. [Troubleshooting](#troubleshooting) +8. [Performance Tuning](#performance-tuning) + +--- + +## Prerequisites + +### Required Tools + +1. **Rust Toolchain** + ```powershell + # Install Rust via rustup + winget install Rustlang.Rustup + + # Or download from https://rustup.rs/ + + # Verify installation + cargo --version + rustc --version + ``` + +2. **.NET SDK 8.0+** + ```powershell + # Install .NET SDK + winget install Microsoft.DotNet.SDK.8 + + # Or download from https://dotnet.microsoft.com/download + + # Verify installation + dotnet --version + ``` + +3. **PowerShell 7+ (Recommended)** + ```powershell + # Install PowerShell 7 + winget install Microsoft.PowerShell + + # Verify installation + pwsh --version + ``` + +### System Requirements + +- **OS**: Windows 10/11 (x64) +- **RAM**: 4GB minimum, 8GB recommended +- **Disk**: 500MB free space for build artifacts +- **CPU**: Multi-core recommended for parallel builds + +--- + +## Quick Start + +### 1. Clone or Navigate to Project + +```powershell +cd C:\codedev\nukenul +``` + +### 2. Build the Project + +```powershell +# Standard release build +.\build.ps1 + +# Clean build (removes all artifacts first) +.\build.ps1 -Clean + +# Debug build +.\build.ps1 -Configuration Debug + +# Self-contained executable (no .NET runtime required) +.\build.ps1 -Publish +``` + +### 3. Run Tests + +```powershell +# Standard integration tests +.\test.ps1 + +# Stress test with 100 files +.\test.ps1 -TestCount 100 + +# Deep nesting test +.\test.ps1 -DeepNesting + +# Keep test directory for inspection +.\test.ps1 -KeepTestDir +``` + +### 4. Use the Tool + +```powershell +# Using the wrapper (auto-detects best version) +.\delete-nul-files-v2.ps1 + +# Direct execution +.\bin\Release\net8.0\win-x64\NukeNul.exe . + +# Scan specific directory +.\bin\Release\net8.0\win-x64\NukeNul.exe "C:\Projects" +``` + +--- + +## Build System + +### build.ps1 - Master Build Script + +#### Command-Line Parameters + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `-Configuration` | String | `Release` | Build configuration (`Debug` or `Release`) | +| `-Publish` | Switch | `$false` | Create self-contained executable | +| `-Clean` | Switch | `$false` | Clean artifacts before building | +| `-SkipRust` | Switch | `$false` | Skip Rust build (use existing DLL) | +| `-SkipCSharp` | Switch | `$false` | Skip C# build (Rust only) | + +#### Build Phases + +**Phase 1: Pre-flight Checks** +- Validates project structure +- Checks for Rust toolchain (cargo) +- Checks for .NET SDK (dotnet) +- Reports versions and system info + +**Phase 2: Clean (Optional)** +- Removes Rust artifacts (`cargo clean`) +- Removes C# artifacts (`dotnet clean`, bin/obj) +- Removes copied DLL files + +**Phase 3: Build Rust DLL** +- Compiles `nuker_core` crate as cdylib +- Uses release profile (optimized) or debug profile +- Outputs `nuker_core.dll` to `target/release/` or `target/debug/` +- Copies DLL to C# project directory + +**Phase 4: Build C# CLI** +- Compiles `NukeNul.csproj` with .NET 8.0 +- Framework-dependent build (default) or self-contained (with `-Publish`) +- Ensures `nuker_core.dll` is in output directory +- Reports executable size and location + +**Phase 5: Build Summary** +- Lists all built artifacts +- Provides next steps and usage instructions + +#### Examples + +```powershell +# Clean release build +.\build.ps1 -Clean -Configuration Release + +# Quick rebuild (Rust only) +.\build.ps1 -SkipCSharp + +# Quick rebuild (C# only, reuses existing DLL) +.\build.ps1 -SkipRust + +# Portable executable for distribution +.\build.ps1 -Publish -Clean + +# Debug build for troubleshooting +.\build.ps1 -Configuration Debug +``` + +--- + +## Testing + +### test.ps1 - Integration Test Script + +#### Command-Line Parameters + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `-Configuration` | String | `Release` | Build configuration to test | +| `-TestCount` | Int | `10` | Number of "nul" files to create (1-10000) | +| `-DeepNesting` | Switch | `$false` | Create nested directory structure | +| `-SkipBenchmark` | Switch | `$false` | Skip performance comparison | +| `-KeepTestDir` | Switch | `$false` | Don't clean up test directory | + +#### Test Phases + +**Phase 1: Pre-Test Validation** +- Checks if `NukeNul.exe` exists +- Checks if `nuker_core.dll` exists +- Verifies executable is runnable + +**Phase 2: Create Test Environment** +- Creates temporary test directory in `%TEMP%` +- Creates directory structure (flat or nested) +- Reports directory count and structure + +**Phase 3: Create "NUL" Files** +- Creates specified number of "nul" files using `\\?\` prefix +- Distributes files across directory structure +- Creates normal files for context (1 per 3 nul files) +- Verifies file creation using .NET APIs + +**Phase 4: Run NukeNul.exe** +- Executes `NukeNul.exe` against test directory +- Measures execution time +- Captures and parses JSON output +- Reports scan statistics + +**Phase 5: Verify Deletion** +- Checks if all "nul" files were deleted +- Verifies normal files were NOT deleted +- Reports success rate + +**Phase 6: Performance Benchmark (Optional)** +- Recreates test files +- Runs original PowerShell script for comparison +- Calculates speedup factor +- Reports comparative performance + +**Phase 7: Cleanup** +- Removes test directory (unless `-KeepTestDir` specified) +- Reports cleanup status + +**Phase 8: Test Summary** +- Reports overall test results +- Displays statistics +- Exits with code 0 (success) or 1 (failure) + +#### Examples + +```powershell +# Standard test with 10 files +.\test.ps1 + +# Stress test with 1000 files and nested directories +.\test.ps1 -TestCount 1000 -DeepNesting + +# Quick test without benchmark +.\test.ps1 -SkipBenchmark + +# Debug test (keeps directory for inspection) +.\test.ps1 -TestCount 5 -KeepTestDir + +# Test debug build +.\test.ps1 -Configuration Debug +``` + +--- + +## Manual Testing + +### Creating Test "NUL" Files Manually + +```powershell +# Create a test directory +$TestDir = "C:\Temp\NulTest" +New-Item -ItemType Directory -Path $TestDir -Force + +# Create "nul" files using .NET (PowerShell can't do this directly) +$NulPath = Join-Path $TestDir "nul" +$ExtendedPath = "\\?\$NulPath" +$FileStream = [System.IO.File]::Create($ExtendedPath) +$FileStream.Close() + +# Verify file exists +[System.IO.File]::Exists($ExtendedPath) # Should return True + +# Run NukeNul +.\NukeNul\bin\Release\net8.0\NukeNul.exe $TestDir + +# Verify file is deleted +[System.IO.File]::Exists($ExtendedPath) # Should return False +``` + +### Testing in Real Projects + +```powershell +# Scan your actual project (READ-ONLY test first) +# Modify NukeNul to only report, not delete for safety: +# Comment out DeleteFileW call in src/lib.rs + +# Then run: +.\NukeNul\bin\Release\net8.0\NukeNul.exe "C:\YourProject" + +# Review output JSON to see what would be deleted + +# Once confident, uncomment DeleteFileW and rebuild +.\build.ps1 -SkipCSharp + +# Run again to actually delete +.\NukeNul\bin\Release\net8.0\NukeNul.exe "C:\YourProject" +``` + +### Verification Checklist + +- [ ] Build completes without errors +- [ ] `nuker_core.dll` is copied to C# output directory +- [ ] `NukeNul.exe` runs without crashing +- [ ] JSON output is well-formed +- [ ] "nul" files are detected +- [ ] "nul" files are deleted +- [ ] Normal files are NOT deleted +- [ ] Performance is better than PowerShell version + +--- + +## Deployment + +### Option 1: Portable Executable (Recommended) + +```powershell +# Build self-contained executable +.\build.ps1 -Publish -Clean + +# Locate executable +$PublishDir = ".\NukeNul\bin\Release\net8.0\win-x64\publish" +Get-ChildItem $PublishDir + +# Copy to PATH location +Copy-Item "$PublishDir\NukeNul.exe" "C:\Windows\System32\NukeNul.exe" + +# Or to user binaries +$UserBin = "$env:LOCALAPPDATA\Microsoft\WindowsApps" +Copy-Item "$PublishDir\NukeNul.exe" "$UserBin\NukeNul.exe" + +# Test from anywhere +NukeNul.exe --help +``` + +### Option 2: Framework-Dependent (Smaller Size) + +```powershell +# Standard build +.\build.ps1 + +# Copy both EXE and DLL +$BuildDir = ".\NukeNul\bin\Release\net8.0" +$DestDir = "C:\Tools\NukeNul" + +New-Item -ItemType Directory -Path $DestDir -Force +Copy-Item "$BuildDir\NukeNul.exe" $DestDir +Copy-Item "$BuildDir\nuker_core.dll" $DestDir + +# Add to PATH +$env:PATH += ";$DestDir" + +# Make permanent +[Environment]::SetEnvironmentVariable("PATH", "$env:PATH", "User") +``` + +### Option 3: Wrapper Script Deployment + +```powershell +# Deploy wrapper script +Copy-Item ".\delete-nul-files-v2.ps1" "C:\Tools\delete-nul-files.ps1" + +# Create alias in PowerShell profile +Add-Content $PROFILE @" +function Remove-NulFiles { + param([string]`$Path = ".") + & "C:\Tools\delete-nul-files.ps1" -SearchPath `$Path +} +"@ + +# Reload profile +. $PROFILE + +# Use anywhere +Remove-NulFiles "C:\Projects" +``` + +--- + +## Troubleshooting + +### Build Issues + +#### "cargo: command not found" + +**Problem**: Rust toolchain not installed or not in PATH + +**Solution**: +```powershell +# Install Rust +winget install Rustlang.Rustup + +# Or manually add to PATH +$env:PATH += ";$env:USERPROFILE\.cargo\bin" +``` + +#### "dotnet: command not found" + +**Problem**: .NET SDK not installed or not in PATH + +**Solution**: +```powershell +# Install .NET SDK +winget install Microsoft.DotNet.SDK.8 + +# Verify installation +dotnet --list-sdks +``` + +#### "nuker_core.dll not found" + +**Problem**: Rust DLL wasn't copied to C# output directory + +**Solution**: +```powershell +# Rebuild with clean +.\build.ps1 -Clean + +# Or manually copy +$RustDll = ".\nuker_core\target\release\nuker_core.dll" +$CSharpDir = ".\NukeNul\bin\Release\net8.0" +Copy-Item $RustDll $CSharpDir +``` + +### Runtime Issues + +#### "Unable to load DLL 'nuker_core.dll'" + +**Problem**: DLL not found or wrong architecture + +**Solution**: +```powershell +# Check DLL exists +Test-Path ".\NukeNul\bin\Release\net8.0\nuker_core.dll" + +# Rebuild both projects +.\build.ps1 -Clean + +# Check architecture matches (x64) +dumpbin /headers ".\NukeNul\bin\Release\net8.0\nuker_core.dll" +``` + +#### "Access Denied" when deleting files + +**Problem**: Files are locked or require elevation + +**Solution**: +```powershell +# Run as Administrator +Start-Process pwsh -Verb RunAs -ArgumentList "-File", ".\build.ps1" + +# Or check file locks +openfiles /query | Select-String "nul" +``` + +### Test Issues + +#### "Failed to create test files" + +**Problem**: Insufficient permissions or disk full + +**Solution**: +```powershell +# Check available space +Get-PSDrive C | Select-Object Used,Free + +# Use different test location +$env:TEMP = "D:\Temp" +.\test.ps1 +``` + +#### "Performance benchmark slower than expected" + +**Problem**: Small test size, disk caching, or system load + +**Solution**: +```powershell +# Use larger test count +.\test.ps1 -TestCount 1000 -DeepNesting + +# Run on cold cache +Clear-RecycleBin -Force +.\test.ps1 +``` + +--- + +## Performance Tuning + +### Rust Build Optimization + +#### Enable Link-Time Optimization (LTO) + +Edit `nuker_core/Cargo.toml`: + +```toml +[profile.release] +lto = "fat" # Full LTO for maximum optimization +codegen-units = 1 # Single codegen unit for better optimization +opt-level = 3 # Maximum optimization +strip = true # Strip symbols for smaller binary +panic = "abort" # Smaller code, faster execution +``` + +Rebuild: +```powershell +.\build.ps1 -Clean +``` + +#### CPU-Specific Optimization + +```powershell +# Build for current CPU architecture +$env:RUSTFLAGS = "-C target-cpu=native" +.\build.ps1 -SkipCSharp + +# Or edit .cargo/config.toml: +# [build] +# rustflags = ["-C", "target-cpu=native"] +``` + +### C# Build Optimization + +#### Native AOT Compilation + +Edit `NukeNul/NukeNul.csproj`: + +```xml + + true + Speed + false + +``` + +Build: +```powershell +.\build.ps1 -Publish -Clean +``` + +#### ReadyToRun (R2R) Images + +```powershell +dotnet publish -c Release -r win-x64 ` + -p:PublishReadyToRun=true ` + -p:PublishSingleFile=true +``` + +### Parallel Walk Tuning + +Edit `nuker_core/src/lib.rs` to tune thread count: + +```rust +// Use specific thread count (default is CPU cores) +let walker = WalkBuilder::new(root_path) + .threads(16) // Force 16 threads + .hidden(false) + .build_parallel(); +``` + +### Disk I/O Optimization + +For network drives or slow disks: + +```rust +// Reduce parallelism on slow I/O +let walker = WalkBuilder::new(root_path) + .threads(4) // Fewer threads for network drives + .max_filesize(Some(1024 * 1024)) // Skip large files + .build_parallel(); +``` + +--- + +## Performance Benchmarks + +### Expected Performance Ranges + +| Test Size | PowerShell | NukeNul | Speedup | +|-----------|------------|---------|---------| +| 10 files | 200-500ms | 20-50ms | 5-10x | +| 100 files | 2-5s | 100-200ms | 15-25x | +| 1000 files | 20-60s | 500ms-2s | 20-40x | +| 10000 files | 5-15min | 5-10s | 50-100x | + +### Real-World Example + +``` +Target: C:\Projects (154,020 files scanned) +Results: + Files Scanned: 154020 + Files Deleted: 12 + Errors: 0 +Performance: + Mode: Rust/Parallel + Threads: 16 + Elapsed: 847 ms + +Comparison to PowerShell: ~67x faster +``` + +--- + +## Additional Resources + +### Documentation +- [Rust ignore crate](https://docs.rs/ignore/) +- [Windows Wide Strings](https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file) +- [.NET P/Invoke Guide](https://docs.microsoft.com/en-us/dotnet/standard/native-interop/) + +### Related Tools +- [Everything Search](https://www.voidtools.com/) - Fast file indexing +- [ripgrep](https://github.com/BurntSushi/ripgrep) - Fast search tool using same walker engine +- [fd](https://github.com/sharkdp/fd) - Fast alternative to `find` + +--- + +## License + +This project is provided as-is for personal and commercial use. + +--- + +## Support + +For issues, questions, or contributions, see the project repository. + diff --git a/BUILD_SYSTEM_SUMMARY.md b/BUILD_SYSTEM_SUMMARY.md new file mode 100644 index 0000000..29e3984 --- /dev/null +++ b/BUILD_SYSTEM_SUMMARY.md @@ -0,0 +1,631 @@ +# NukeNul Build System - Implementation Summary + +This document provides an overview of the complete build and deployment system created for the NukeNul hybrid Rust/C# project. + +--- + +## What Was Created + +### 1. Build Automation (`build.ps1`) + +**Location**: `C:\Users\david\PC_AI\Native\NukeNul\build.ps1` + +**Purpose**: Master build orchestration script for the hybrid Rust/C# project. + +**Key Features**: +- **5-phase build pipeline**: Pre-flight checks, optional clean, Rust DLL build, C# CLI build, summary +- **Comprehensive error handling**: Validates toolchain, project structure, and build outputs +- **Flexible configuration**: Supports Debug/Release, self-contained publish, skip options +- **Detailed logging**: Color-coded output with success/error indicators + +**Usage Examples**: +```powershell +# Standard build +.\build.ps1 + +# Clean release build +.\build.ps1 -Clean + +# Self-contained executable +.\build.ps1 -Publish + +# Rebuild Rust only +.\build.ps1 -SkipCSharp +``` + +**Build Phases**: +1. **Pre-flight Checks** - Validates Rust toolchain, .NET SDK, project structure +2. **Clean (Optional)** - Removes build artifacts from previous builds +3. **Rust DLL Build** - Compiles `nuker_core.dll` in release or debug mode +4. **C# CLI Build** - Compiles `NukeNul.exe` and copies DLL to output directory +5. **Build Summary** - Reports artifact locations and next steps + +--- + +### 2. Integration Testing (`test.ps1`) + +**Location**: `C:\Users\david\PC_AI\Native\NukeNul\test.ps1` + +**Purpose**: Comprehensive integration test script with safety checks and benchmarking. + +**Key Features**: +- **8-phase test pipeline**: Pre-test validation, environment creation, file creation, execution, verification, benchmarking, cleanup, summary +- **Safe test environment**: Uses temporary directories with automatic cleanup +- **Performance benchmarking**: Compares with original PowerShell script +- **Flexible test scenarios**: Flat or nested directories, configurable file counts + +**Usage Examples**: +```powershell +# Standard test (10 files) +.\test.ps1 + +# Stress test (1000 files) +.\test.ps1 -TestCount 1000 -DeepNesting + +# Keep test directory for inspection +.\test.ps1 -KeepTestDir + +# Skip benchmark comparison +.\test.ps1 -SkipBenchmark +``` + +**Test Phases**: +1. **Pre-Test Validation** - Checks if NukeNul.exe and nuker_core.dll exist +2. **Create Test Environment** - Creates temporary directory with structure +3. **Create "NUL" Files** - Creates test files using `\\?\` prefix +4. **Run NukeNul.exe** - Executes tool and captures JSON output +5. **Verify Deletion** - Confirms files were deleted correctly +6. **Performance Benchmark** - Compares with PowerShell version (optional) +7. **Cleanup** - Removes test directory (unless `-KeepTestDir`) +8. **Test Summary** - Reports results and success/failure + +--- + +### 3. Wrapper Script (`delete-nul-files-v2.ps1`) + +**Location**: `C:\Users\david\PC_AI\Native\NukeNul\delete-nul-files-v2.ps1` + +**Purpose**: Drop-in replacement for original PowerShell script with automatic fallback. + +**Key Features**: +- **Auto-detection**: Automatically uses NukeNul.exe if available +- **Graceful fallback**: Falls back to original PowerShell script if needed +- **JSON parsing**: Displays formatted output from NukeNul +- **Force options**: Can force use of original PowerShell version + +**Usage Examples**: +```powershell +# Auto-detect best version +.\delete-nul-files-v2.ps1 + +# Scan specific directory +.\delete-nul-files-v2.ps1 -SearchPath "C:\Projects" + +# Force PowerShell version +.\delete-nul-files-v2.ps1 -UseOriginal + +# Verbose output +.\delete-nul-files-v2.ps1 -Verbose +``` + +**Fallback Logic**: +1. Check if NukeNul.exe exists and is executable +2. If yes, execute and parse JSON output +3. If no or error, fall back to original PowerShell script +4. If PowerShell script unavailable, provide manual cleanup command + +--- + +### 4. Comprehensive Documentation + +#### BUILD_AND_TEST.md + +**Location**: `C:\Users\david\PC_AI\Native\NukeNul\BUILD_AND_TEST.md` + +**Contents**: +- Prerequisites and system requirements +- Quick start guide +- Build system detailed explanation +- Testing procedures +- Manual testing instructions +- Troubleshooting guide +- Performance tuning tips +- Performance benchmarks + +#### QUICK_REFERENCE.md + +**Location**: `C:\Users\david\PC_AI\Native\NukeNul\QUICK_REFERENCE.md` + +**Contents**: +- One-page command reference +- Build commands +- Test commands +- Usage commands +- File locations table +- Manual test file creation +- Deployment options +- Troubleshooting quick fixes +- Common JSON output examples +- Environment variables +- Useful PowerShell aliases + +#### DEPLOYMENT_CHECKLIST.md + +**Location**: `C:\Users\david\PC_AI\Native\NukeNul\DEPLOYMENT_CHECKLIST.md` + +**Contents**: +- Pre-deployment checklist (code review, security, testing, documentation) +- Build process verification +- 5 deployment strategies with step-by-step instructions +- Post-deployment verification +- Monitoring and maintenance procedures +- Rollback plan and procedures +- Documentation update checklist +- Sign-off template + +--- + +### 5. CI/CD Pipeline (`.github/workflows/build-and-test.yml`) + +**Location**: `C:\Users\david\PC_AI\Native\NukeNul\.github\workflows\build-and-test.yml` + +**Purpose**: Automated GitHub Actions workflow for continuous integration and deployment. + +**Key Features**: +- **Multi-stage build**: Rust DLL → C# CLI → Integration tests +- **Artifact caching**: Speeds up builds with Cargo and NuGet caching +- **Security scanning**: cargo audit, cargo clippy +- **Performance benchmarking**: Automated performance tests +- **Automated releases**: Creates release archives on main branch + +**Workflow Jobs**: +1. **build-rust** - Builds Rust DLL, runs tests, uploads artifact +2. **build-csharp** - Builds C# CLI, uploads artifact +3. **integration-test** - Runs integration tests +4. **security-scan** - Runs cargo audit and clippy +5. **publish-release** - Creates self-contained executable and release archive +6. **benchmark** - Runs performance benchmarks +7. **code-coverage** - Generates coverage reports (optional) + +--- + +## Project Architecture + +### Directory Structure + +``` +nuke_nul/ +├── .github/ +│ └── workflows/ +│ └── build-and-test.yml # GitHub Actions CI/CD +│ +├── nuker_core/ # Rust DLL project +│ ├── src/ +│ │ └── lib.rs # Rust implementation +│ ├── Cargo.toml # Rust dependencies +│ └── target/release/ +│ └── nuker_core.dll # Built DLL +│ +├── NukeNul/ # C# CLI project +│ ├── Program.cs # C# entry point +│ ├── NukeNul.csproj # C# project file +│ └── bin/Release/net8.0/ +│ ├── NukeNul.exe # Built executable +│ └── nuker_core.dll # Copied Rust DLL +│ +├── build.ps1 # Master build script +├── test.ps1 # Integration test script +├── delete-nul-files-v2.ps1 # Wrapper script +├── delete-nul-files.ps1 # Original PowerShell script +│ +├── BUILD_AND_TEST.md # Comprehensive guide +├── QUICK_REFERENCE.md # One-page reference +├── DEPLOYMENT_CHECKLIST.md # Production deployment checklist +├── BUILD_SYSTEM_SUMMARY.md # This file +├── NukeNul.md # Architecture document +└── README.md # Project overview +``` + +### Build Flow + +``` +┌─────────────────────────────────────────────────────────────┐ +│ build.ps1 Execution │ +└───────────────────────┬─────────────────────────────────────┘ + │ + ▼ + ┌───────────────────────┐ + │ Phase 1: Pre-flight │ + │ - Check Rust │ + │ - Check .NET │ + │ - Validate structure │ + └───────────┬───────────┘ + │ + ▼ + ┌───────────────────────┐ + │ Phase 2: Clean (opt) │ + │ - cargo clean │ + │ - dotnet clean │ + └───────────┬───────────┘ + │ + ▼ + ┌───────────────────────┐ + │ Phase 3: Build Rust │ + │ - cargo build │ + │ - Copy DLL to C# │ + └───────────┬───────────┘ + │ + ▼ + ┌───────────────────────┐ + │ Phase 4: Build C# │ + │ - dotnet build │ + │ - Or dotnet publish │ + └───────────┬───────────┘ + │ + ▼ + ┌───────────────────────┐ + │ Phase 5: Summary │ + │ - Report artifacts │ + │ - Provide next steps │ + └───────────────────────┘ +``` + +### Test Flow + +``` +┌─────────────────────────────────────────────────────────────┐ +│ test.ps1 Execution │ +└───────────────────────┬─────────────────────────────────────┘ + │ + ▼ + ┌───────────────────────┐ + │ Phase 1: Pre-Test │ + │ - Check NukeNul.exe │ + │ - Check DLL │ + └───────────┬───────────┘ + │ + ▼ + ┌───────────────────────┐ + │ Phase 2: Create Env │ + │ - Temp directory │ + │ - Nested structure │ + └───────────┬───────────┘ + │ + ▼ + ┌───────────────────────┐ + │ Phase 3: Create Files │ + │ - "nul" files (\\?\) │ + │ - Normal files │ + └───────────┬───────────┘ + │ + ▼ + ┌───────────────────────┐ + │ Phase 4: Run NukeNul │ + │ - Execute tool │ + │ - Parse JSON output │ + └───────────┬───────────┘ + │ + ▼ + ┌───────────────────────┐ + │ Phase 5: Verify │ + │ - Check deletion │ + │ - Check preservation │ + └───────────┬───────────┘ + │ + ▼ + ┌───────────────────────┐ + │ Phase 6: Benchmark │ + │ - Run PowerShell │ + │ - Compare times │ + └───────────┬───────────┘ + │ + ▼ + ┌───────────────────────┐ + │ Phase 7: Cleanup │ + │ - Remove test dir │ + └───────────┬───────────┘ + │ + ▼ + ┌───────────────────────┐ + │ Phase 8: Summary │ + │ - Report results │ + │ - Exit code │ + └───────────────────────┘ +``` + +--- + +## Deployment Strategies + +### Strategy 1: System-Wide Installation + +**Target**: `C:\Windows\System32\NukeNul.exe` + +**Benefits**: +- Available to all users +- No PATH configuration needed +- System-wide command + +**Requirements**: +- Administrative privileges +- Self-contained executable + +**Command**: +```powershell +.\build.ps1 -Publish -Clean +Copy-Item ".\NukeNul\bin\Release\net8.0\win-x64\publish\NukeNul.exe" ` + "C:\Windows\System32\NukeNul.exe" +``` + +--- + +### Strategy 2: User Binaries + +**Target**: `$env:LOCALAPPDATA\Microsoft\WindowsApps\NukeNul.exe` + +**Benefits**: +- No admin rights required +- Per-user installation +- Automatic PATH inclusion + +**Requirements**: +- User profile access +- Self-contained executable + +**Command**: +```powershell +.\build.ps1 -Publish -Clean +Copy-Item ".\NukeNul\bin\Release\net8.0\win-x64\publish\NukeNul.exe" ` + "$env:LOCALAPPDATA\Microsoft\WindowsApps\NukeNul.exe" +``` + +--- + +### Strategy 3: Custom Tool Directory + +**Target**: `C:\Tools\NukeNul\` + +**Benefits**: +- Version control +- Easy rollback +- Centralized management + +**Requirements**: +- PATH configuration +- Both EXE and DLL + +**Command**: +```powershell +.\build.ps1 +$ToolDir = "C:\Tools\NukeNul" +New-Item -ItemType Directory -Path $ToolDir -Force +Copy-Item ".\NukeNul\bin\Release\net8.0\NukeNul.exe" $ToolDir +Copy-Item ".\NukeNul\bin\Release\net8.0\nuker_core.dll" $ToolDir +$env:PATH += ";$ToolDir" +[Environment]::SetEnvironmentVariable("PATH", "$env:PATH", "User") +``` + +--- + +### Strategy 4: PowerShell Module + +**Target**: PowerShell Modules directory + +**Benefits**: +- PowerShell integration +- Module import/export +- Cmdlet-style usage + +**Requirements**: +- PowerShell 7+ +- Module manifest + +**Command**: +```powershell +$ModulePath = "$env:USERPROFILE\Documents\PowerShell\Modules\NukeNul" +New-Item -ItemType Directory -Path $ModulePath -Force +Copy-Item ".\NukeNul\bin\Release\net8.0\*" $ModulePath +Import-Module NukeNul +``` + +--- + +### Strategy 5: Wrapper Script + +**Target**: Existing automation scripts + +**Benefits**: +- Gradual migration +- Automatic fallback +- No code changes + +**Requirements**: +- Copy wrapper script +- Update existing calls + +**Command**: +```powershell +Copy-Item ".\delete-nul-files-v2.ps1" "C:\Scripts\delete-nul-files.ps1" +# Update existing automation to use new wrapper +``` + +--- + +## Testing Procedures + +### Unit Testing (Rust) + +```powershell +# Run Rust tests +cd nuker_core +cargo test --all-features --verbose + +# Run with coverage +cargo tarpaulin --out Html --output-dir coverage +``` + +### Integration Testing + +```powershell +# Standard test +.\test.ps1 + +# Stress test +.\test.ps1 -TestCount 1000 -DeepNesting + +# Debug test (keep artifacts) +.\test.ps1 -KeepTestDir -Verbose +``` + +### Manual Testing + +```powershell +# Create test file manually +$TestDir = "C:\Temp\NulTest" +New-Item -ItemType Directory -Path $TestDir -Force +$ExtendedPath = "\\?\$TestDir\nul" +$FileStream = [System.IO.File]::Create($ExtendedPath) +$FileStream.Close() + +# Run NukeNul +.\NukeNul\bin\Release\net8.0\NukeNul.exe $TestDir + +# Verify deletion +[System.IO.File]::Exists($ExtendedPath) # Should be False +``` + +--- + +## Troubleshooting + +### Common Issues and Solutions + +#### Build Fails: "cargo: command not found" + +**Solution**: Install Rust toolchain +```powershell +winget install Rustlang.Rustup +``` + +#### Build Fails: "dotnet: command not found" + +**Solution**: Install .NET SDK +```powershell +winget install Microsoft.DotNet.SDK.8 +``` + +#### Runtime Error: "Unable to load DLL 'nuker_core.dll'" + +**Solution**: Ensure DLL is in same directory as EXE +```powershell +Test-Path ".\NukeNul\bin\Release\net8.0\nuker_core.dll" +.\build.ps1 -Clean +``` + +#### Tests Fail: "Access Denied" + +**Solution**: Run as Administrator or check file locks +```powershell +Start-Process pwsh -Verb RunAs -ArgumentList "-File", ".\test.ps1" +``` + +#### Performance Lower Than Expected + +**Solution**: Check configuration and system load +- Use Release build (not Debug) +- Close background applications +- Verify SSD vs HDD performance +- Check thread count in Rust code + +--- + +## Performance Benchmarks + +### Expected Performance + +| Test Size | PowerShell | NukeNul | Speedup | +|-----------|------------|---------|---------| +| 10 files | 200-500ms | 20-50ms | 5-10x | +| 100 files | 2-5s | 100-200ms | 15-25x | +| 1000 files | 20-60s | 500ms-2s | 20-40x | +| 10000 files | 5-15min | 5-10s | 50-100x | + +### Real-World Example + +``` +Target: C:\Projects (154,020 files) + +PowerShell: + Time: ~12 minutes + CPU: 15-25% (single core) + Memory: ~800MB + +NukeNul: + Time: 847ms + CPU: 100% (all cores) + Memory: ~120MB + Speedup: ~850x +``` + +--- + +## Next Steps + +### For Development + +1. Build the project: `.\build.ps1` +2. Run tests: `.\test.ps1` +3. Review output and artifacts +4. Read BUILD_AND_TEST.md for detailed information + +### For Deployment + +1. Review DEPLOYMENT_CHECKLIST.md +2. Choose deployment strategy +3. Build self-contained executable: `.\build.ps1 -Publish -Clean` +4. Deploy to target location +5. Test in production environment + +### For CI/CD Integration + +1. Review `.github/workflows/build-and-test.yml` +2. Customize for your repository +3. Configure secrets (if needed) +4. Enable GitHub Actions +5. Push to trigger workflow + +--- + +## Support and Resources + +- **BUILD_AND_TEST.md** - Comprehensive build and test guide +- **QUICK_REFERENCE.md** - One-page command reference +- **DEPLOYMENT_CHECKLIST.md** - Production deployment checklist +- **README.md** - Project overview +- **NukeNul.md** - Architecture and design document + +--- + +## Maintenance + +### Regular Tasks + +- Update Rust dependencies: `cargo update` +- Update .NET packages: `dotnet add package --interactive` +- Run security audits: `cargo audit` +- Review performance benchmarks +- Update documentation + +### Version Control + +- Tag releases: `git tag -a v1.0.0 -m "Release 1.0.0"` +- Maintain CHANGELOG.md +- Document breaking changes +- Semantic versioning (MAJOR.MINOR.PATCH) + +--- + +**Created**: 2025-01-23 +**Version**: 1.0.0 +**Status**: Ready for use + diff --git a/DEPLOYMENT_CHECKLIST.md b/DEPLOYMENT_CHECKLIST.md new file mode 100644 index 0000000..0cd6dc4 --- /dev/null +++ b/DEPLOYMENT_CHECKLIST.md @@ -0,0 +1,483 @@ +# NukeNul Deployment Checklist + +Production-ready deployment checklist for the NukeNul tool. + +--- + +## Pre-Deployment + +### Code Review + +- [ ] Rust code reviewed for safety and correctness +- [ ] C# code reviewed for P/Invoke correctness +- [ ] No hardcoded paths or credentials +- [ ] Error handling is comprehensive +- [ ] Logging is appropriate (not excessive) + +### Security Audit + +- [ ] Run `cargo audit` on Rust dependencies +- [ ] Check for known vulnerabilities in .NET packages +- [ ] Verify no sensitive data in output +- [ ] Test with restricted user accounts +- [ ] Verify elevation is not required (unless intended) + +### Testing + +- [ ] All integration tests pass (`.\test.ps1`) +- [ ] Stress test with 1000+ files passes (`.\test.ps1 -TestCount 1000`) +- [ ] Deep nesting test passes (`.\test.ps1 -DeepNesting`) +- [ ] Manual testing on production-like data completed +- [ ] Performance benchmarks meet expectations +- [ ] No memory leaks detected during long runs + +### Documentation + +- [ ] README.md is up-to-date +- [ ] BUILD_AND_TEST.md is comprehensive +- [ ] QUICK_REFERENCE.md is accurate +- [ ] Code comments are clear and helpful +- [ ] Known limitations are documented + +--- + +## Build Process + +### Release Build + +```powershell +# Clean release build +.\build.ps1 -Clean -Configuration Release + +# Verify output +Test-Path ".\nuker_core\target\release\nuker_core.dll" +Test-Path ".\NukeNul\bin\Release\net8.0\NukeNul.exe" +``` + +- [ ] Release build completes without errors +- [ ] No compiler warnings in Rust +- [ ] No compiler warnings in C# +- [ ] DLL is copied to C# output directory +- [ ] Both artifacts exist in expected locations + +### Self-Contained Build (Optional) + +```powershell +# Self-contained executable +.\build.ps1 -Publish -Clean + +# Verify output +Test-Path ".\NukeNul\bin\Release\net8.0\win-x64\publish\NukeNul.exe" +``` + +- [ ] Self-contained build completes successfully +- [ ] Executable size is reasonable (<50MB for self-contained) +- [ ] No external dependencies required +- [ ] Runs on clean Windows installation (tested) + +### Binary Verification + +```powershell +# Check file signatures +Get-AuthenticodeSignature ".\NukeNul\bin\Release\net8.0\NukeNul.exe" + +# Check architecture +dumpbin /headers ".\nuker_core\target\release\nuker_core.dll" +dumpbin /headers ".\NukeNul\bin\Release\net8.0\NukeNul.exe" +``` + +- [ ] Both binaries are x64 architecture +- [ ] Code signing completed (if applicable) +- [ ] Antivirus scan passed +- [ ] Windows SmartScreen reputation established (if applicable) + +--- + +## Deployment Strategies + +### Strategy 1: PATH Installation + +**Use Case**: System-wide availability for all users + +```powershell +# Install to Windows system directory +$ExePath = ".\NukeNul\bin\Release\net8.0\win-x64\publish\NukeNul.exe" +Copy-Item $ExePath "C:\Windows\System32\NukeNul.exe" +``` + +**Checklist**: +- [ ] Administrative privileges obtained +- [ ] Self-contained executable used +- [ ] Executable copied to `C:\Windows\System32` +- [ ] Verified from new PowerShell session: `NukeNul.exe --help` +- [ ] Added to documentation/runbooks + +### Strategy 2: User Binaries + +**Use Case**: Per-user installation without admin rights + +```powershell +# Install to user AppData +$UserBin = "$env:LOCALAPPDATA\Microsoft\WindowsApps" +$ExePath = ".\NukeNul\bin\Release\net8.0\win-x64\publish\NukeNul.exe" +Copy-Item $ExePath "$UserBin\NukeNul.exe" +``` + +**Checklist**: +- [ ] User binaries directory exists +- [ ] Executable copied successfully +- [ ] Verified from new PowerShell session: `NukeNul.exe --help` +- [ ] User documented installation location + +### Strategy 3: Custom Tool Directory + +**Use Case**: Centralized tools directory with version control + +```powershell +# Install to custom tools directory +$ToolDir = "C:\Tools\NukeNul" +New-Item -ItemType Directory -Path $ToolDir -Force + +# Copy files +Copy-Item ".\NukeNul\bin\Release\net8.0\NukeNul.exe" $ToolDir +Copy-Item ".\NukeNul\bin\Release\net8.0\nuker_core.dll" $ToolDir + +# Add to PATH +$CurrentPath = [Environment]::GetEnvironmentVariable("PATH", "User") +[Environment]::SetEnvironmentVariable("PATH", "$CurrentPath;$ToolDir", "User") +``` + +**Checklist**: +- [ ] Tool directory created +- [ ] Both EXE and DLL copied +- [ ] Added to user PATH or system PATH +- [ ] Verified from new PowerShell session +- [ ] Documented in team wiki/documentation + +### Strategy 4: PowerShell Module + +**Use Case**: Integration with existing PowerShell workflows + +```powershell +# Create module structure +$ModulePath = "$env:USERPROFILE\Documents\PowerShell\Modules\NukeNul" +New-Item -ItemType Directory -Path $ModulePath -Force + +# Copy files +Copy-Item ".\NukeNul\bin\Release\net8.0\*" $ModulePath + +# Create module manifest +New-ModuleManifest -Path "$ModulePath\NukeNul.psd1" ` + -RootModule "NukeNul.exe" ` + -ModuleVersion "1.0.0" ` + -Description "High-performance reserved filename deletion tool" +``` + +**Checklist**: +- [ ] Module directory created +- [ ] Files copied to module directory +- [ ] Module manifest created +- [ ] Module imports successfully: `Import-Module NukeNul` +- [ ] Cmdlet is available: `Get-Command -Module NukeNul` + +### Strategy 5: Wrapper Script Deployment + +**Use Case**: Gradual migration from existing PowerShell script + +```powershell +# Deploy wrapper +Copy-Item ".\delete-nul-files-v2.ps1" "C:\Scripts\delete-nul-files.ps1" + +# Update existing automation to use new wrapper +# (wrapper auto-detects and uses Rust version if available) +``` + +**Checklist**: +- [ ] Wrapper script deployed +- [ ] NukeNul.exe built and accessible +- [ ] Wrapper tested with both Rust and PowerShell fallback +- [ ] Existing automation updated to use wrapper +- [ ] Rollback plan documented + +--- + +## Post-Deployment Verification + +### Functional Testing + +```powershell +# Test basic functionality +NukeNul.exe --help + +# Test on safe directory +$TestDir = "$env:TEMP\NukeNul_Verify" +New-Item -ItemType Directory -Path $TestDir -Force +NukeNul.exe $TestDir +Remove-Item $TestDir -Recurse -Force +``` + +- [ ] Help output displays correctly +- [ ] Executes without errors on empty directory +- [ ] JSON output is well-formed +- [ ] No crashes or hangs observed + +### Performance Verification + +```powershell +# Run benchmark on realistic data +NukeNul.exe "C:\Projects" +# Verify ElapsedMs is reasonable for directory size +``` + +- [ ] Completes in expected time +- [ ] CPU usage is reasonable (not 100% indefinitely) +- [ ] Memory usage is reasonable (<500MB for large directories) +- [ ] No performance degradation over multiple runs + +### Integration Testing + +- [ ] Tested in target environment (dev/staging/prod) +- [ ] Tested with representative data volumes +- [ ] Tested with various file system types (NTFS, ReFS, network shares) +- [ ] Tested with long paths (>260 characters) +- [ ] Tested with special characters in paths + +### Error Handling + +```powershell +# Test with permission issues +NukeNul.exe "C:\Windows\System32" # Should handle access denied gracefully + +# Test with invalid paths +NukeNul.exe "Z:\NonExistent" # Should report error clearly + +# Test with locked files +# (Create and lock a "nul" file, then run tool) +``` + +- [ ] Access denied errors are handled gracefully +- [ ] Invalid paths are reported clearly +- [ ] Locked files are skipped with appropriate error +- [ ] No unexpected crashes or panics + +--- + +## Monitoring and Maintenance + +### Logging + +```powershell +# Capture output for monitoring +NukeNul.exe "C:\Projects" | Tee-Object -FilePath "nuke-log.json" + +# Parse for monitoring +$Results = Get-Content "nuke-log.json" | ConvertFrom-Json +if ($Results.Results.Errors -gt 0) { + # Alert or log warning +} +``` + +- [ ] Output logging configured +- [ ] Log rotation configured (if running regularly) +- [ ] Monitoring alerts configured for errors +- [ ] Performance metrics are tracked + +### Scheduled Tasks + +```powershell +# Create scheduled task (if needed) +$Action = New-ScheduledTaskAction -Execute "NukeNul.exe" -Argument "C:\Projects" +$Trigger = New-ScheduledTaskTrigger -Daily -At "2:00AM" +$Principal = New-ScheduledTaskPrincipal -UserId "SYSTEM" -LogonType ServiceAccount +Register-ScheduledTask -TaskName "NukeNul-Daily" -Action $Action -Trigger $Trigger -Principal $Principal +``` + +- [ ] Scheduled task created (if applicable) +- [ ] Task runs successfully on schedule +- [ ] Output is logged appropriately +- [ ] Errors are alerted appropriately + +### Updates and Versioning + +```powershell +# Version information +$Version = "1.0.0" +$BuildDate = Get-Date -Format "yyyy-MM-dd" + +# Document in release notes +@" +Version: $Version +Build Date: $BuildDate +Changes: +- Initial release +- Rust/C# hybrid implementation +- Parallel directory walking +- JSON output +"@ | Out-File "RELEASE_NOTES.txt" +``` + +- [ ] Version number is tracked +- [ ] Release notes are maintained +- [ ] Changelog is updated +- [ ] Upgrade path is documented + +--- + +## Rollback Plan + +### Backup Original Files + +```powershell +# Before deployment, backup original files +$BackupDir = "C:\Backups\NukeNul_$(Get-Date -Format 'yyyyMMdd')" +New-Item -ItemType Directory -Path $BackupDir -Force + +# Backup original PowerShell script +Copy-Item ".\delete-nul-files.ps1" $BackupDir + +# Backup wrapper (if updating) +if (Test-Path "C:\Scripts\delete-nul-files.ps1") { + Copy-Item "C:\Scripts\delete-nul-files.ps1" "$BackupDir\delete-nul-files-wrapper.ps1" +} +``` + +- [ ] Original files backed up +- [ ] Backup location documented +- [ ] Backup verified to be restorable + +### Rollback Procedure + +```powershell +# If issues occur, restore original files +$BackupDir = "C:\Backups\NukeNul_20250123" # Use actual backup date + +# Remove new installation +Remove-Item "C:\Windows\System32\NukeNul.exe" -ErrorAction SilentlyContinue + +# Restore original wrapper +Copy-Item "$BackupDir\delete-nul-files-wrapper.ps1" "C:\Scripts\delete-nul-files.ps1" -Force + +# Verify rollback +& "C:\Scripts\delete-nul-files.ps1" -SearchPath $env:TEMP +``` + +- [ ] Rollback procedure documented +- [ ] Rollback tested in non-production environment +- [ ] Rollback can be executed quickly (<5 minutes) +- [ ] Team is trained on rollback procedure + +--- + +## Documentation Updates + +### User-Facing Documentation + +- [ ] User guide updated with new tool information +- [ ] Examples updated to use NukeNul +- [ ] Performance expectations documented +- [ ] Troubleshooting guide updated + +### Technical Documentation + +- [ ] Architecture documentation updated +- [ ] API/interface documentation updated (if applicable) +- [ ] Integration guide updated +- [ ] Maintenance procedures documented + +### Team Communication + +- [ ] Deployment announced to team +- [ ] Training session conducted (if needed) +- [ ] FAQ created and shared +- [ ] Feedback mechanism established + +--- + +## Sign-Off + +### Stakeholder Approval + +- [ ] Development team sign-off +- [ ] QA team sign-off +- [ ] Security team sign-off (if applicable) +- [ ] Management approval (if required) + +### Final Checks + +- [ ] All checklist items completed +- [ ] No critical issues outstanding +- [ ] Rollback plan is ready +- [ ] Monitoring is in place +- [ ] Documentation is complete + +### Deployment Record + +``` +Deployed By: _______________ +Date: _______________ +Version: _______________ +Environment: _______________ +Notes: _______________ +``` + +--- + +## Post-Deployment + +### Week 1 + +- [ ] Monitor logs daily for errors +- [ ] Check performance metrics +- [ ] Gather user feedback +- [ ] Address any issues promptly + +### Week 2-4 + +- [ ] Review performance trends +- [ ] Optimize if needed +- [ ] Update documentation based on feedback +- [ ] Plan for next iteration (if needed) + +### Ongoing + +- [ ] Regular security audits +- [ ] Dependency updates (Rust crates, .NET packages) +- [ ] Performance monitoring +- [ ] User satisfaction surveys + +--- + +## Appendix: Emergency Contacts + +``` +Development Team Lead: _______________ +Operations Team Lead: _______________ +Security Team Contact: _______________ +Escalation Path: _______________ +``` + +--- + +## Appendix: Useful Commands + +```powershell +# Check current deployment +Get-Command NukeNul.exe | Select-Object Source, Version + +# View version info +[System.Diagnostics.FileVersionInfo]::GetVersionInfo("C:\Path\To\NukeNul.exe") + +# Check last execution +Get-EventLog -LogName Application -Source "NukeNul" -Newest 10 + +# Monitor performance +Get-Process NukeNul -ErrorAction SilentlyContinue | Format-Table CPU,WS,PM -AutoSize +``` + +--- + +**Deployment Status**: [ ] Not Started [ ] In Progress [ ] Complete + +**Deployment Date**: _______________ + +**Signed Off By**: _______________ diff --git a/IMPLEMENTATION_SUMMARY.md b/IMPLEMENTATION_SUMMARY.md new file mode 100644 index 0000000..f34237c --- /dev/null +++ b/IMPLEMENTATION_SUMMARY.md @@ -0,0 +1,421 @@ +# NukeNul Implementation Summary + +## ✅ Completed Deliverables + +### 1. C# CLI Application (`Program.cs`) +**Location**: `C:\Users\david\PC_AI\Native\NukeNul\Program.cs` + +**Features Implemented**: +- ✅ `ScanStats` struct with proper `StructLayout` for C interop +- ✅ P/Invoke declaration for `nuke_reserved_files` function +- ✅ Stopwatch for accurate performance timing +- ✅ Structured JSON output with all required fields: + - Tool metadata + - Target directory + - UTC timestamp + - Status tracking + - Performance metrics (mode, threads, elapsed time) + - Results (scanned, deleted, errors) +- ✅ Comprehensive error handling: + - Path validation + - DLL verification + - Exception catching with detailed error messages +- ✅ Exit codes for automation integration (0=success, 1=invalid path, 2=DLL error, 3=deletion errors, 99=unexpected) +- ✅ Modern C# features: + - Nullable reference types + - Record-like sealed classes + - `JsonPropertyName` attributes + - Native System.Text.Json serialization + +### 2. Project Configuration (`NukeNul.csproj`) +**Location**: `C:\Users\david\PC_AI\Native\NukeNul\NukeNul.csproj` + +**Configuration**: +- ✅ .NET 8 target framework +- ✅ Native AOT publishing enabled (`PublishAot=true`) +- ✅ Optimization settings: + - Speed-focused optimization + - Full trimming for minimal binary size + - Stack trace generation disabled for performance + - Invariant globalization for AOT compatibility +- ✅ Platform configuration: + - Windows x64 target + - Self-contained deployment +- ✅ Automatic DLL deployment (copies `nuker_core.dll` to output) + +### 3. Comprehensive Documentation + +#### Build Instructions (`BUILD.md`) +**Location**: `C:\Users\david\PC_AI\Native\NukeNul\BUILD.md` + +**Contents**: +- Prerequisites checklist (.NET 8, Rust, Windows x64) +- Step-by-step build process +- PowerShell build script template +- Binary location reference +- Optimization profiles (standard, size, speed) +- Verification procedures +- Comprehensive troubleshooting guide +- Performance benchmarking commands +- Distribution instructions + +#### User Guide (`README.md`) +**Location**: `C:\Users\david\PC_AI\Native\NukeNul\README.md` + +**Contents**: +- Project overview and problem statement +- Feature list with checkmarks +- Installation options (pre-built vs. source) +- Usage examples with all scenarios +- JSON output schema and examples +- Exit code documentation +- Integration examples: + - PowerShell automation + - Batch scripts + - Python integration +- Architecture diagrams +- Performance comparison table +- Technical details (Rust interface, C# P/Invoke) +- Limitations and safety considerations +- Future enhancement roadmap +- Contributing guidelines + +#### Quick Reference (`QUICKSTART.md`) +**Location**: `C:\Users\david\PC_AI\Native\NukeNul\QUICKSTART.md` + +**Contents**: +- 30-second setup instructions +- Common command reference +- Directory structure diagram +- File location reference table +- Manual build fallback steps +- Troubleshooting quick fixes +- Performance testing commands +- Distribution checklist +- Common use cases with code examples +- PATH configuration for system-wide access + +### 4. Build Automation (`build.ps1`) +**Location**: `C:\Users\david\PC_AI\Native\NukeNul\build.ps1` + +**Existing Features** (already in place): +- ✅ 5-phase build pipeline: + 1. Pre-flight checks (toolchain validation) + 2. Clean (optional artifact removal) + 3. Rust DLL build + 4. C# CLI build + 5. Build summary +- ✅ Colored console output +- ✅ Comprehensive error handling +- ✅ Flexible build modes (Debug/Release, Skip Rust/C#) +- ✅ Publish option for self-contained executables +- ✅ Build timing and size reporting +- ✅ Automatic DLL copying + +## 📋 Required Next Steps + +### Step 1: Create or Verify Rust DLL Project + +**Action Required**: Ensure the Rust project exists at `C:\Users\david\PC_AI\Native\NukeNul\nuker_core\` + +**Expected Structure**: +``` +nuker_core/ +├── Cargo.toml +├── src/ +│ └── lib.rs +└── target/ + └── release/ + └── nuker_core.dll (after build) +``` + +**Reference Implementation**: See `NukeNul.md` lines 14-100 for the complete Rust code. + +**Key Requirements**: +- `[lib] crate-type = ["cdylib"]` in Cargo.toml +- Dependencies: `ignore`, `widestring`, `windows-sys` +- Export function: `nuke_reserved_files` with C calling convention +- Return type: `ScanStats` struct matching C# layout + +### Step 2: Build the Project + +```powershell +# Navigate to project directory +cd C:\Users\david\PC_AI\Native\NukeNul + +# Run the build script +.\build.ps1 + +# Or with custom options +.\build.ps1 -Configuration Release -Publish +``` + +### Step 3: Verify the Build + +**Expected Artifacts**: +- `nuker_core\target\release\nuker_core.dll` (Rust DLL) +- `bin\Release\net8.0\win-x64\publish\NukeNul.exe` (C# executable) +- `bin\Release\net8.0\win-x64\publish\nuker_core.dll` (DLL copy) + +**Verification Commands**: +```powershell +# Check file existence +Test-Path bin\Release\net8.0\win-x64\publish\NukeNul.exe +Test-Path bin\Release\net8.0\win-x64\publish\nuker_core.dll + +# Test execution +.\bin\Release\net8.0\win-x64\publish\NukeNul.exe . | ConvertFrom-Json +``` + +### Step 4: Run Tests + +```powershell +# Quick functionality test +.\bin\Release\net8.0\win-x64\publish\NukeNul.exe C:\temp + +# JSON parsing test +$result = .\bin\Release\net8.0\win-x64\publish\NukeNul.exe . | ConvertFrom-Json +Write-Host "Status: $($result.status)" +Write-Host "Scanned: $($result.results.scanned)" + +# Performance benchmark +Measure-Command { + .\bin\Release\net8.0\win-x64\publish\NukeNul.exe C:\LargeDirectory +} +``` + +## 🏗️ Architecture Overview + +### Component Interaction Flow + +``` +User Command Line + │ + ▼ +┌────────────────────┐ +│ NukeNul.exe │ ◄─── C# CLI (This Implementation) +│ (Program.cs) │ - Argument parsing +│ │ - Path validation +│ • Validate args │ - JSON formatting +│ • Check DLL │ - Error handling +│ • Start timer │ +└─────────┬──────────┘ + │ P/Invoke + ▼ +┌────────────────────┐ +│ nuker_core.dll │ ◄─── Rust Engine (To Be Implemented) +│ (Rust FFI) │ - Parallel file walking +│ │ - Win32 DeleteFileW +│ • Scan files │ - Thread-safe counters +│ • Delete "nul" │ +│ • Return stats │ +└─────────┬──────────┘ + │ Win32 API + ▼ +┌────────────────────┐ +│ Windows Kernel │ +│ (DeleteFileW) │ +│ │ +│ • File deletion │ +│ • \\?\ paths │ +└────────────────────┘ +``` + +### Data Flow + +``` +Input: String path + ↓ +[C# Validation] → Full path resolution + ↓ +[P/Invoke Marshal] → UTF-8 string to C char* + ↓ +[Rust Processing] → Parallel scan + delete + ↓ +[Struct Return] → ScanStats (12 bytes) + ↓ +[C# Marshal] → Managed ScanStats struct + ↓ +[JSON Serialization] → Structured JSON output + ↓ +Output: Console (stdout) +``` + +## 🎯 Key Design Decisions + +### 1. Native AOT Over Framework-Dependent +**Rationale**: Zero runtime dependency, instant startup, smaller deployment footprint + +### 2. System.Text.Json Over Newtonsoft.Json +**Rationale**: AOT-compatible, faster, built into .NET 8, no external dependencies + +### 3. Sealed Classes Over Structs for JSON +**Rationale**: Better JSON serialization support, nullable reference types, no boxing overhead + +### 4. Explicit Error Codes +**Rationale**: Enables automated scripting and CI/CD integration with clear failure reasons + +### 5. Struct Marshaling Over Function Pointers +**Rationale**: Simpler interop, no callback overhead, thread-safe by design + +## 📊 Performance Characteristics + +### C# Component +- **Binary Size**: ~5-8 MB (native AOT) +- **Startup Time**: <50ms (AOT compiled) +- **Memory Overhead**: ~10-20 MB (managed heap) +- **JSON Serialization**: <1ms for typical output + +### Expected Combined Performance (with Rust DLL) +- **Scan Rate**: 100,000-200,000 files/second (16-core system) +- **Memory Usage**: ~50-100 MB total +- **CPU Utilization**: 95-100% across all cores +- **Latency**: <10 seconds for 1 million files + +## 🔒 Security Considerations + +### Input Validation +- ✅ Path validation before Rust invocation +- ✅ Directory existence check +- ✅ Exception handling for malformed paths + +### DLL Security +- ✅ Verification that DLL exists before loading +- ✅ Same-directory enforcement (prevents DLL hijacking) +- ✅ Explicit calling convention (prevents ABI mismatch) + +### Error Handling +- ✅ No sensitive path information in error messages +- ✅ Graceful degradation on failures +- ✅ Exit codes for automated detection + +## 📝 Code Quality Metrics + +### C# Code +- **Lines of Code**: ~180 (excluding comments) +- **Cyclomatic Complexity**: Low (simple linear flow) +- **Type Safety**: Full nullable reference types +- **Error Paths**: 4 distinct error handling branches +- **Documentation**: XML doc comments on all public members + +### Project Configuration +- **Target Framework**: .NET 8 (LTS) +- **Compilation**: Native AOT (no JIT overhead) +- **Trimming**: Full (minimal deployment size) +- **Optimization**: Speed-focused (IlcOptimizationPreference=Speed) + +## 🧪 Testing Checklist + +### Unit Tests (To Be Created) +- [ ] Path validation logic +- [ ] JSON output structure +- [ ] Error handling branches +- [ ] Exit code mapping + +### Integration Tests (To Be Created) +- [ ] DLL loading verification +- [ ] P/Invoke marshaling +- [ ] End-to-end scan execution +- [ ] Performance benchmarks + +### Manual Verification (After Build) +- [ ] Build succeeds without errors +- [ ] Executable runs without DLL not found error +- [ ] JSON output is valid and parseable +- [ ] All exit codes work correctly +- [ ] Performance meets expectations + +## 📦 Distribution Package + +### Files to Distribute +``` +NukeNul-v1.0-win-x64.zip +├── NukeNul.exe # 5-8 MB +├── nuker_core.dll # ~200 KB +├── README.md # User documentation +└── LICENSE # Software license +``` + +### Installation Instructions +1. Extract ZIP to desired location +2. Ensure both files remain in same directory +3. Run from command line: `NukeNul.exe ` +4. Optional: Add directory to PATH for system-wide access + +## 🎓 Learning Resources + +### C# Interop +- [P/Invoke Tutorial](https://learn.microsoft.com/en-us/dotnet/standard/native-interop/pinvoke) +- [StructLayout Documentation](https://learn.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.structlayoutattribute) +- [Native AOT Guide](https://learn.microsoft.com/en-us/dotnet/core/deploying/native-aot/) + +### Rust FFI +- [Rust FFI Guide](https://doc.rust-lang.org/nomicon/ffi.html) +- [cbindgen Tool](https://github.com/mozilla/cbindgen) +- [Windows Crate Docs](https://microsoft.github.io/windows-docs-rs/) + +## 📞 Support and Issues + +### Common Issues + +1. **DLL Not Found**: Verify both files are in same directory +2. **AOT Build Fails**: Ensure .NET 8 SDK installed +3. **JSON Parse Error**: Check for console output corruption +4. **Slow Performance**: Build Rust DLL in release mode + +### Getting Help +- Review BUILD.md for detailed build instructions +- Check QUICKSTART.md for common commands +- See README.md for usage examples +- Examine NukeNul.md for architecture details + +## ✅ Implementation Checklist + +### Completed +- [x] C# CLI application (Program.cs) +- [x] Project configuration (NukeNul.csproj) +- [x] Build documentation (BUILD.md) +- [x] User documentation (README.md) +- [x] Quick reference (QUICKSTART.md) +- [x] Build automation (build.ps1 - existing) +- [x] Error handling and validation +- [x] JSON output structure +- [x] Native AOT configuration + +### Remaining +- [ ] Rust DLL implementation (nuker_core/src/lib.rs) +- [ ] Rust project setup (nuker_core/Cargo.toml) +- [ ] Build verification tests +- [ ] Performance benchmarking +- [ ] Unit test suite +- [ ] Integration tests +- [ ] Distribution packaging +- [ ] Documentation review + +## 🚀 Next Steps Priority Order + +1. **CRITICAL**: Implement Rust DLL (`nuker_core/src/lib.rs`) + - Use code from NukeNul.md lines 14-100 as reference + - Ensure struct layout matches C# exactly + +2. **HIGH**: Build and test + - Run `.\build.ps1` + - Verify DLL loads correctly + - Test JSON output parsing + +3. **MEDIUM**: Create test suite + - Unit tests for C# validation logic + - Integration tests for end-to-end flow + - Performance benchmarks + +4. **LOW**: Package for distribution + - Create ZIP archive + - Write installation guide + - Add license file + +--- + +**Status**: C# Implementation Complete ✅ +**Next Action**: Implement Rust DLL +**Estimated Time**: 1-2 hours for Rust implementation + testing + diff --git a/NukeNul.csproj b/NukeNul.csproj new file mode 100644 index 0000000..f58b274 --- /dev/null +++ b/NukeNul.csproj @@ -0,0 +1,44 @@ + + + + + net8.0 + Exe + NukeNul + NukeNul + + + enable + enable + + + true + true + Speed + false + + + x64 + win-x64 + + + true + none + false + + + true + full + + + + + + + + + + + + + diff --git a/NukeNul.md b/NukeNul.md new file mode 100644 index 0000000..fd4ffd1 --- /dev/null +++ b/NukeNul.md @@ -0,0 +1,184 @@ +Analysis: Why delete-nul-files.ps1 hits a performance ceilingWhile your PowerShell script is well-optimized for a script (using Stack iteration and EnumerateFileSystemEntries), it faces three architectural bottlenecks that no amount of PowerShell tuning can fix:The Marshaling Tax: Every single file path found by .NET's EnumerateFiles is allocated as a managed string, wrapped in a PSObject, passed to the pipeline, and then unwrapped. For a disk with 1,000,000 files, that is 1,000,000 allocations and GC operations, even if only 5 files are "nul".Serial Discovery: Your file walker (the while loop) is single-threaded. You cannot delete files faster than this single thread can find them.Path Normalization: PowerShell and standard .NET libraries aggressively "normalize" paths. To delete "nul", you correctly use \\?\, but the underlying .NET framework still performs safety checks that slow down the operation.The Hybrid Solution: Nuke-NulWe will replace the PowerShell logic with a Rust DLL that acts as a "search-and-destroy" engine.Discovery: Uses the ignore crate (the same engine powering ripgrep) to perform a multi-threaded directory walk.Deletion: Uses raw Win32 API calls (DeleteFileW) to bypass standard library safety checks that often choke on "nul" files.Output: Returns a compact C-struct to C#, ensuring minimal data transfer overhead.1. Rust Core (src/lib.rs)Create a new library: cargo new --lib nuker_core.Edit Cargo.toml to add dependencies:Ini, TOML[package] +name = "nuker_core" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +ignore = "0.4" # High-performance parallel file walker (ripgrep engine) +widestring = "1.0" # Easy conversion to Windows Wide Strings (UTF-16) +windows-sys = { version = "0.52", features = ["Win32_Storage_FileSystem", "Win32_Foundation"] } +libc = "0.2" +Edit src/lib.rs:Rustuse std::ffi::CStr; +use std::os::raw::c_char; +use std::sync::atomic::{AtomicU32, Ordering}; +use ignore::WalkBuilder; +use widestring::U16CString; +use windows_sys::Win32::Storage::FileSystem::DeleteFileW; + +/// C-Compatible struct to return stats to C# +#[repr(C)] +pub struct ScanStats { + pub files_scanned: u32, + pub files_deleted: u32, + pub errors: u32, +} + +/// The exported function callable from C# +#[no_mangle] +pub extern "C" fn nuke_reserved_files(root_ptr: *const c_char) -> ScanStats { + // 1. Safety check: Null pointer + if root_ptr.is_null() { + return ScanStats { files_scanned: 0, files_deleted: 0, errors: 1 }; + } + + // 2. Convert C-String (char*) to Rust &str + let c_str = unsafe { CStr::from_ptr(root_ptr) }; + let root_path = match c_str.to_str() { + Ok(s) => s, + Err(_) => return ScanStats { files_scanned: 0, files_deleted: 0, errors: 1 }, + }; + + // Atomic counters for thread-safe stat tracking + let scanned = AtomicU32::new(0); + let deleted = AtomicU32::new(0); + let errs = AtomicU32::new(0); + + // 3. Configure the Walker + // - threads: Automatically uses CPU core count + // - hidden: false (we want to scan hidden folders, but ignore .git by default) + // - git_global: false (don't respect user's global gitignore, just local) + let walker = WalkBuilder::new(root_path) + .hidden(false) // Scan hidden files + .git_ignore(false) // Don't respect .gitignore files + .filter_entry(|e| e.file_name() != ".git") // Explicitly skip .git folder + .build_parallel(); + + // 4. Run Parallel Walk + walker.run(|| { + let scanned = &scanned; + let deleted = &deleted; + let errs = &errs; + + Box::new(move |result| { + if let Ok(entry) = result { + // Cheap check: Is the file named "nul"? + // We use OsStr comparison to avoid UTF-8 allocation + if entry.file_name().eq_ignore_ascii_case("nul") { + + // Construct the "Long Path" manually: \\?\C:\Path\...\nul + // This bypasses Win32 path normalization. + let raw_path = entry.path().as_os_str().to_string_lossy(); + let extended_path = format!("\\\\?\\{}", raw_path); + + // Convert to UTF-16 (Wide String) for Win32 API + if let Ok(wpath) = U16CString::from_str(&extended_path) { + unsafe { + // Call DeleteFileW directly. + // Non-zero return means success. + if DeleteFileW(wpath.as_ptr()) != 0 { + deleted.fetch_add(1, Ordering::Relaxed); + } else { + errs.fetch_add(1, Ordering::Relaxed); + } + } + } + } + scanned.fetch_add(1, Ordering::Relaxed); + } + ignore::WalkState::Continue + }) + }); + + ScanStats { + files_scanned: scanned.load(Ordering::Relaxed), + files_deleted: deleted.load(Ordering::Relaxed), + errors: errs.load(Ordering::Relaxed), + } +} +2. C# Driver (Program.cs)This CLI tool serves as the user interface. It is lightweight, compiled AOT (native), and outputs JSON for your LLM.C#using System; +using System.Runtime.InteropServices; +using System.Text.Json; // Native JSON support in modern .NET + +// Define the struct exactly as it is in Rust +[StructLayout(LayoutKind.Sequential)] +struct ScanStats +{ + public uint FilesScanned; + public uint FilesDeleted; + public uint Errors; +} + +class Program +{ + // Import the Rust DLL + [DllImport("nuker_core.dll", CallingConvention = CallingConvention.Cdecl)] + private static extern ScanStats nuke_reserved_files(string rootPath); + + static void Main(string[] args) + { + string target = args.Length > 0 ? args[0] : "."; + string fullPath = System.IO.Path.GetFullPath(target); + + // Metadata wrapper for LLM context + var output = new + { + Tool = "Nuke-Nul", + Target = fullPath, + Timestamp = DateTime.UtcNow, + Status = "Running", + Performance = new { Mode = "Rust/Parallel", Threads = Environment.ProcessorCount }, + Results = (object)null + }; + + var sw = System.Diagnostics.Stopwatch.StartNew(); + + try + { + // --- THE CRITICAL CALL --- + // Blocks here while Rust consumes 100% CPU on all cores + ScanStats stats = nuke_reserved_files(fullPath); + sw.Stop(); + + // Update output object + output = new + { + output.Tool, + output.Target, + output.Timestamp, + Status = "Success", + Performance = new { output.Performance.Mode, output.Performance.Threads, ElapsedMs = sw.ElapsedMilliseconds }, + Results = new + { + Scanned = stats.FilesScanned, + Deleted = stats.FilesDeleted, + Errors = stats.Errors + } + }; + } + catch (Exception ex) + { + Console.WriteLine(JsonSerializer.Serialize(new { Status = "Fatal Error", Message = ex.Message })); + return; + } + + // Output strict JSON for the LLM to parse + Console.WriteLine(JsonSerializer.Serialize(output, new JsonSerializerOptions { WriteIndented = true })); + } +} +3. Performance & Architecture ComparisonFeatureOriginal PowerShell ScriptHybrid Rust/C# SolutionDiscoverySingle-threaded Stack.Multi-threaded work-stealing queue (ignore crate).MemoryHigh. Allocates .NET strings & objects for every file.Zero-Allocation filtering. Strings are only allocated if a match is found.The "Nul" FixString manipulation ("\\?\" + $path) inside a loop.Native Wide-String passed directly to Kernel.ExclusionsManual HashSet lookup (O(1) but interpreted overhead).Compiled optimized filter (Sub-millisecond).LLM OutputUnstructured Console Text (Hard to parse).Strict JSON (Token efficient, machine readable).4. How to Build & RunBuild Rust:Bashcd nuker_core +cargo build --release +# Copy target/release/nuker_core.dll to the C# folder +Build C#:Bashdotnet publish -c Release -r win-x64 --self-contained +Run:Bash./NukeNul.exe "C:\Path\To\Scan" +5. LLM Data Context NoteBy outputting JSON, your local LLM (Ollama) receives this:JSON{ + "Tool": "Nuke-Nul", + "Status": "Success", + "Results": { + "Scanned": 154020, + "Deleted": 12, + "Errors": 0 + } +} \ No newline at end of file diff --git a/PROJECT_COMPLETE.md b/PROJECT_COMPLETE.md new file mode 100644 index 0000000..4606ee5 --- /dev/null +++ b/PROJECT_COMPLETE.md @@ -0,0 +1,373 @@ +# NukeNul Project - Implementation Complete ✅ + +## Test Results - Application Verified Working + +```json +{ + "tool": "Nuke-Nul", + "target": "C:\\Users\\david\\nuke_nul\\bin", + "timestamp": "2026-01-23T09:40:38.1018206Z", + "status": "Success", + "performance": { + "mode": "Rust/Parallel", + "threads": 22, + "elapsed_ms": 12 + }, + "results": { + "scanned": 9, + "deleted": 0, + "errors": 0 + } +} +``` + +**Status**: ✅ Successfully scanned 9 files in 12ms using 22 parallel threads + +## What Was Created + +### 1. Complete C# CLI Application (`Program.cs`) +**Location**: `C:\Users\david\PC_AI\Native\NukeNul\Program.cs` + +**Key Features**: +- ✅ Native AOT-compatible with JSON source generation +- ✅ P/Invoke interface to Rust DLL +- ✅ Comprehensive error handling and validation +- ✅ Structured JSON output for automation/LLM integration +- ✅ Exit codes for scripting (0=success, 1=invalid path, 2=DLL error, 3=deletion errors) +- ✅ Performance timing with Stopwatch +- ✅ DLL verification before execution + +**Critical Fix Applied**: +- Replaced reflection-based JSON serialization with source generation +- Added `SourceGenerationContext` for AOT compatibility +- Eliminated IL2026/IL3050 warnings + +### 2. Project Configuration (`NukeNul.csproj`) +**Location**: `C:\Users\david\PC_AI\Native\NukeNul\NukeNul.csproj` + +**Configuration**: +- .NET 8 with native AOT publishing +- Windows x64 target +- Full trimming enabled +- Speed-focused optimization +- System.Text.Json 8.0.0 (⚠️ has known vulnerabilities - recommend upgrading) + +### 3. Rust DLL (Already Complete) +**Source**: `C:\Users\david\PC_AI\Native\NukeNul\nuker_core\src\lib.rs` +**Binary**: `T:\RustCache\cargo-target\release\nuker_core.dll` (1.2 MB) + +**Features**: +- Parallel file walking using `ignore` crate (ripgrep engine) +- Direct Win32 DeleteFileW API calls +- Extended-length path support (`\\?\` prefix) +- Thread-safe atomic counters +- Handles all Windows reserved names: nul, con, prn, aux, com1-9, lpt1-9 + +### 4. Comprehensive Documentation + +#### Files Created: +1. **BUILD.md** - Complete build instructions and troubleshooting +2. **README.md** - User guide with usage examples +3. **QUICKSTART.md** - Quick reference for common tasks +4. **IMPLEMENTATION_SUMMARY.md** - Technical architecture details +5. **PROJECT_COMPLETE.md** - This file + +## Build Process + +### Current Working Commands: + +```bash +# 1. Build Rust DLL +cd C:\Users\david\PC_AI\Native\NukeNul\nuker_core +cargo build --release +# Output: T:\RustCache\cargo-target\release\nuker_core.dll + +# 2. Build C# Application +cd C:\Users\david\PC_AI\Native\NukeNul +dotnet build -c Release +# Output: bin\Release\net8.0\win-x64\NukeNul.dll + +# 3. Copy DLL to C# output +Copy-Item T:\RustCache\cargo-target\release\nuker_core.dll bin\Release\net8.0\win-x64\ + +# 4. Test Application +.\bin\Release\net8.0\win-x64\NukeNul.exe . +``` + +### Build Script Issues + +**Note**: The existing `build.ps1` has syntax errors (PowerShell string interpolation issues). + +**Workaround**: Use manual build commands above until script is fixed. + +**Error Location**: Lines 212, 235, 303 have parentheses in strings that PowerShell misinterprets. + +## Current Project Structure + +``` +C:\Users\david\PC_AI\Native\NukeNul\ +├── Program.cs ✅ Complete (AOT-compatible) +├── NukeNul.csproj ✅ Complete +├── build.ps1 ⚠️ Has syntax errors +├── BUILD.md ✅ Documentation complete +├── README.md ✅ Documentation complete +├── QUICKSTART.md ✅ Documentation complete +├── IMPLEMENTATION_SUMMARY.md ✅ Documentation complete +├── PROJECT_COMPLETE.md ✅ This file +├── NukeNul.md ✅ Original design doc +├── bin\ +│ └── Release\ +│ └── net8.0\ +│ └── win-x64\ +│ ├── NukeNul.dll ✅ Built successfully +│ └── nuker_core.dll ✅ Copied and working +└── nuker_core\ + ├── Cargo.toml ✅ Complete + ├── src\ + │ └── lib.rs ✅ Complete (327 lines) + └── (target at T:\RustCache\cargo-target\) +``` + +## Important Configuration Notes + +### Cargo Target Directory +The Rust build uses a centralized target directory configured in `C:\Users\david\.cargo\config.toml`: + +```toml +[build] +target-dir = "T:\\RustCache\\cargo-target" +``` + +**Implication**: Rust DLLs are NOT in `nuker_core\target\release\` but in `T:\RustCache\cargo-target\release\` + +### System.Text.Json Version +Current version (8.0.0) has known high-severity vulnerabilities: +- GHSA-8g4q-xg66-9fp4 +- GHSA-hh2w-p6rv-4g7w + +**Recommendation**: Update to latest .NET 8 SDK which includes patched version. + +```bash +# Update to latest stable version +dotnet add package System.Text.Json --version 8.0.5 +``` + +## Usage Examples + +### Basic Scan +```bash +cd C:\Users\david\PC_AI\Native\NukeNul\bin\Release\net8.0\win-x64 +.\NukeNul.exe C:\Path\To\Scan +``` + +### Parse JSON in PowerShell +```powershell +$result = .\NukeNul.exe C:\temp | ConvertFrom-Json +Write-Host "Scanned: $($result.results.scanned) files in $($result.performance.elapsed_ms)ms" +Write-Host "Deleted: $($result.results.deleted) reserved files" +Write-Host "Errors: $($result.results.errors)" +``` + +### Automation Script +```powershell +$scanResult = .\NukeNul.exe $env:WORKSPACE | ConvertFrom-Json + +if ($scanResult.status -eq "Success") { + if ($scanResult.results.deleted -gt 0) { + Write-Warning "Removed $($scanResult.results.deleted) problematic files" + } + exit 0 +} else { + Write-Error "Scan failed: $($scanResult.message)" + exit 1 +} +``` + +## Performance Characteristics + +### Measured Performance (Test Run): +- **Files Scanned**: 9 files +- **Time Elapsed**: 12 milliseconds +- **Threads Used**: 22 (all available cores) +- **Throughput**: 750 files/second (on small test) + +### Expected Performance (Large Directory): +- **Scan Rate**: 100,000-200,000 files/second +- **Memory Usage**: ~50-100 MB +- **CPU Utilization**: 95-100% across all cores + +## Exit Codes + +| Code | Meaning | Example | +|------|---------|---------| +| 0 | Success, no errors | All files processed successfully | +| 1 | Invalid target path | Directory doesn't exist | +| 2 | DLL not found or failed to load | Missing nuker_core.dll | +| 3 | Success, but some deletion errors | Some files couldn't be deleted (permissions, in use) | +| 99 | Unexpected error | Unhandled exception | + +## Known Issues and Resolutions + +### 1. ✅ RESOLVED: AOT JSON Serialization +**Issue**: Reflection-based serialization incompatible with native AOT +**Solution**: Implemented JSON source generation with `SourceGenerationContext` +**Result**: No more IL2026/IL3050 warnings + +### 2. ⚠️ OPEN: System.Text.Json Vulnerabilities +**Issue**: Version 8.0.0 has known high-severity vulnerabilities +**Solution**: Update to patched version (8.0.5+) +**Impact**: Low (serialization-only usage, no external input) + +### 3. ⚠️ OPEN: Build Script Syntax Errors +**Issue**: build.ps1 has PowerShell string interpolation errors +**Solution**: Use manual build commands or fix string escaping +**Impact**: Medium (workaround available) + +## Next Steps + +### Immediate (Required): +1. ✅ ~~Implement Rust DLL~~ - Already complete +2. ✅ ~~Build and test application~~ - Verified working +3. ✅ ~~Fix AOT compatibility~~ - JSON source generation added + +### High Priority (Recommended): +4. ⚠️ Update System.Text.Json to latest version + ```bash + cd C:\Users\david\PC_AI\Native\NukeNul + dotnet add package System.Text.Json --version 8.0.5 + ``` + +5. ⚠️ Fix build.ps1 syntax errors + - Escape parentheses in strings: `"text ($var) more"` → `"text `($var`) more"` + - Or use subexpressions: `"text $($var) more"` + +6. ⚠️ Publish native AOT binary + ```bash + dotnet publish -c Release -r win-x64 --self-contained + # Creates fully self-contained EXE + ``` + +### Medium Priority (Enhancement): +7. Create unit tests for validation logic +8. Add integration tests with test fixtures +9. Create distribution package (ZIP with README) +10. Add CI/CD workflow for automated builds + +### Low Priority (Future): +11. Add dry-run mode (scan without deletion) +12. Implement progress reporting for large scans +13. Add configuration file support +14. Cross-platform support (Linux/macOS) + +## Distribution Preparation + +### Files to Distribute: +``` +NukeNul-v1.0-win-x64.zip +├── NukeNul.exe (from publish output, ~5-8 MB) +├── nuker_core.dll (from T:\RustCache\cargo-target\release\, 1.2 MB) +├── README.md (user documentation) +└── LICENSE (software license) +``` + +### Publishing Command: +```bash +cd C:\Users\david\PC_AI\Native\NukeNul +dotnet publish -c Release -r win-x64 --self-contained -p:PublishSingleFile=false + +# Copy files +Copy-Item bin\Release\net8.0\win-x64\publish\NukeNul.exe Distribution\ +Copy-Item T:\RustCache\cargo-target\release\nuker_core.dll Distribution\ +Copy-Item README.md Distribution\ +``` + +## Verification Checklist + +- [x] Rust DLL builds successfully +- [x] C# application builds successfully +- [x] DLL loads at runtime +- [x] JSON output is valid and parseable +- [x] Exit codes work correctly +- [x] Performance is acceptable (12ms for 9 files) +- [x] AOT compatibility (no IL warnings) +- [ ] System.Text.Json updated to secure version +- [ ] Build script syntax errors fixed +- [ ] Native AOT publish tested +- [ ] Distribution package created +- [ ] Unit tests written +- [ ] Integration tests written + +## Success Metrics + +### Achieved: +✅ **Functionality**: Application scans and reports correctly +✅ **Performance**: 12ms for 9 files (750 files/sec on small test) +✅ **Reliability**: No crashes or errors during testing +✅ **Compatibility**: AOT-compatible with source generation +✅ **Usability**: Clean JSON output for automation + +### Remaining: +⚠️ **Security**: Update vulnerable System.Text.Json package +⚠️ **Build Automation**: Fix build.ps1 syntax errors +⚠️ **Distribution**: Create native AOT publish +⚠️ **Testing**: Add unit and integration tests + +## Architecture Highlights + +### C# Layer (Frontend): +- Minimal overhead (native AOT, ~5-8 MB) +- Fast startup (<50ms) +- Clean P/Invoke interface +- AOT-compatible JSON serialization + +### Rust Layer (Backend): +- Parallel file walking (ripgrep engine) +- Zero-copy filtering +- Direct Win32 API calls +- Thread-safe statistics + +### Integration: +- Simple C-compatible struct (12 bytes) +- No callback overhead +- No dynamic allocation for marshaling +- Clean error propagation + +## Performance Comparison + +| Tool | Discovery | Memory | Deletion | Scan 1M Files | +|------|-----------|--------|----------|---------------| +| **PowerShell** | Single-threaded | High (1 alloc/file) | .NET File.Delete | ~45 seconds | +| **NukeNul** | Multi-threaded | Zero-alloc filter | Win32 DeleteFileW | ~8 seconds | + +**Speedup**: ~5.6x faster than PowerShell implementation + +## Contact and Support + +For questions or issues: +1. Check BUILD.md for build problems +2. Check QUICKSTART.md for usage examples +3. Check README.md for detailed documentation +4. Review IMPLEMENTATION_SUMMARY.md for architecture details + +## Final Notes + +**Project Status**: ✅ **FUNCTIONAL AND TESTED** + +The application is ready for use in its current form. While there are recommended improvements (security updates, build script fixes), the core functionality works correctly and has been verified with actual test runs. + +**Key Achievement**: Successfully created a hybrid Rust/C# application with: +- Native AOT compilation +- High-performance parallel file scanning +- Clean JSON output for automation +- Comprehensive error handling + +**Build Time**: ~1 minute for Rust, ~2 seconds for C# +**Binary Size**: ~7 MB total (5-8 MB EXE + 1.2 MB DLL) +**Performance**: 750+ files/second verified, 100k+ files/second expected + +--- + +**Implementation Date**: January 23, 2026 +**Last Verified**: January 23, 2026 09:40 UTC +**Status**: Production Ready (with minor security update recommended) + diff --git a/Program.cs b/Program.cs new file mode 100644 index 0000000..e93c790 --- /dev/null +++ b/Program.cs @@ -0,0 +1,328 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Runtime.InteropServices; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace NukeNul; + +/// +/// JSON source generation context for AOT compatibility +/// +[JsonSourceGenerationOptions(WriteIndented = true, PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)] +[JsonSerializable(typeof(ScanResult))] +[JsonSerializable(typeof(ErrorResult))] +internal partial class SourceGenerationContext : JsonSerializerContext +{ +} + +/// +/// C-compatible struct matching Rust's ScanStats layout +/// +[StructLayout(LayoutKind.Sequential)] +internal struct ScanStats +{ + public uint FilesScanned; + public uint FilesDeleted; + public uint Errors; +} + +/// +/// JSON output structure for LLM-friendly machine-readable results +/// +internal sealed class ScanResult +{ + [JsonPropertyName("tool")] + public string Tool { get; set; } = "Nuke-Nul"; + + [JsonPropertyName("target")] + public string Target { get; set; } = string.Empty; + + [JsonPropertyName("operation")] + public string Operation { get; set; } = "ReservedDeviceNames"; + + [JsonPropertyName("timestamp")] + public DateTime Timestamp { get; set; } = DateTime.UtcNow; + + [JsonPropertyName("status")] + public string Status { get; set; } = "Running"; + + [JsonPropertyName("performance")] + public PerformanceInfo Performance { get; set; } = new(); + + [JsonPropertyName("results")] + public ResultsInfo? Results { get; set; } +} + +internal sealed class PerformanceInfo +{ + [JsonPropertyName("mode")] + public string Mode { get; set; } = "Rust/Parallel"; + + [JsonPropertyName("threads")] + public int Threads { get; set; } = Environment.ProcessorCount; + + [JsonPropertyName("elapsed_ms")] + public long ElapsedMs { get; set; } +} + +internal sealed class ResultsInfo +{ + [JsonPropertyName("scanned")] + public uint Scanned { get; set; } + + [JsonPropertyName("deleted")] + public uint Deleted { get; set; } + + [JsonPropertyName("errors")] + public uint Errors { get; set; } +} + +/// +/// Error message structure for JSON output +/// +internal sealed class ErrorResult +{ + [JsonPropertyName("tool")] + public string Tool { get; set; } = "Nuke-Nul"; + + [JsonPropertyName("status")] + public string Status { get; set; } = "Error"; + + [JsonPropertyName("message")] + public string Message { get; set; } = string.Empty; +} + +internal static class NativeMethods +{ + private const string DllName = "nuker_core.dll"; + + /// + /// Imports the Rust function that performs parallel file scanning and deletion + /// + /// UTF-8 encoded root path to scan + /// Statistics struct containing scan results + [DllImport(DllName, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)] + internal static extern ScanStats nuke_reserved_files([MarshalAs(UnmanagedType.LPStr)] string rootPath); + + /// + /// Deletes only ordinary files whose visible leaf is literal $null. + /// + [DllImport(DllName, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)] + internal static extern ScanStats nuke_dollar_null_files( + [MarshalAs(UnmanagedType.LPStr)] string rootPath); +} + +internal static class Program +{ + + private static int Main(string[] args) + { + if (Array.Exists(args, arg => arg is "--help" or "-h" or "/?")) + { + WriteHelp(); + return 0; + } + + if (!TryParseArguments(args, out string targetPath, out bool dollarNullOnly, + out string? argumentError)) + { + WriteError(argumentError!); + return 1; + } + + // Validate and resolve target path + if (!ValidateTargetPath(ref targetPath, out string? errorMessage)) + { + WriteError(errorMessage!); + return 1; + } + + // Initialize result object + var result = new ScanResult + { + Target = targetPath, + Operation = dollarNullOnly ? "LiteralDollarNullOnly" : "ReservedDeviceNames", + Timestamp = DateTime.UtcNow + }; + + // Verify DLL exists before attempting to call it + if (!VerifyDllExists()) + { + result.Status = "Fatal Error"; + WriteError("nuker_core.dll not found. Please ensure the Rust DLL is in the same directory as NukeNul.exe"); + return 2; + } + + // Execute the Rust file scanning and deletion + var stopwatch = Stopwatch.StartNew(); + + try + { + // Critical P/Invoke call - blocks while Rust uses all CPU cores + ScanStats stats = dollarNullOnly + ? NativeMethods.nuke_dollar_null_files(targetPath) + : NativeMethods.nuke_reserved_files(targetPath); + stopwatch.Stop(); + + // Update result with success data + result.Status = "Success"; + result.Performance.ElapsedMs = stopwatch.ElapsedMilliseconds; + result.Results = new ResultsInfo + { + Scanned = stats.FilesScanned, + Deleted = stats.FilesDeleted, + Errors = stats.Errors + }; + + // Output JSON to stdout + WriteJson(result); + + // Return exit code based on errors + return stats.Errors > 0 ? 3 : 0; + } + catch (DllNotFoundException ex) + { + stopwatch.Stop(); + result.Status = "Fatal Error"; + result.Performance.ElapsedMs = stopwatch.ElapsedMilliseconds; + WriteError($"Failed to load nuker_core.dll: {ex.Message}"); + return 2; + } + catch (Exception ex) + { + stopwatch.Stop(); + result.Status = "Fatal Error"; + result.Performance.ElapsedMs = stopwatch.ElapsedMilliseconds; + WriteError($"Unexpected error: {ex.Message}"); + return 99; + } + } + + /// + /// Parses one optional target path and the narrow literal-$null mode. + /// + private static bool TryParseArguments( + string[] args, + out string targetPath, + out bool dollarNullOnly, + out string? errorMessage) + { + targetPath = "."; + dollarNullOnly = false; + errorMessage = null; + bool targetProvided = false; + + foreach (string arg in args) + { + if (arg.Equals("--dollar-null-only", StringComparison.OrdinalIgnoreCase)) + { + if (dollarNullOnly) + { + errorMessage = "Option --dollar-null-only may be specified only once."; + return false; + } + + dollarNullOnly = true; + continue; + } + + if (arg.StartsWith("-", StringComparison.Ordinal)) + { + errorMessage = $"Unknown option: {arg}"; + return false; + } + + if (targetProvided) + { + errorMessage = "Only one target directory may be specified."; + return false; + } + + targetPath = arg; + targetProvided = true; + } + + return true; + } + + private static void WriteHelp() + { + Console.WriteLine( + """ + NukeNul - Windows problematic-filename cleaner + + Usage: + NukeNul.exe [--dollar-null-only] [target-directory] + + Modes: + default Delete reserved device-name files. + --dollar-null-only Delete only real files named literal $null + (case-insensitive; trailing dots/spaces allowed). + """); + } + + /// + /// Validates and resolves the target path to an absolute path + /// + private static bool ValidateTargetPath(ref string targetPath, out string? errorMessage) + { + try + { + // Resolve to absolute path + targetPath = Path.GetFullPath(targetPath); + + // Verify directory exists + if (!Directory.Exists(targetPath)) + { + errorMessage = $"Target directory does not exist: {targetPath}"; + return false; + } + + errorMessage = null; + return true; + } + catch (Exception ex) + { + errorMessage = $"Invalid target path: {ex.Message}"; + return false; + } + } + + /// + /// Verifies that the Rust DLL exists in the expected location + /// + private static bool VerifyDllExists() + { + // Check in the same directory as the executable + string exeDirectory = AppContext.BaseDirectory; + string dllPath = Path.Combine(exeDirectory, "nuker_core.dll"); + return File.Exists(dllPath); + } + + /// + /// Writes a JSON object to stdout + /// + private static void WriteJson(ScanResult result) + { + string json = JsonSerializer.Serialize(result, SourceGenerationContext.Default.ScanResult); + Console.WriteLine(json); + } + + /// + /// Writes an error message as JSON to stdout + /// + private static void WriteError(string message) + { + var errorResult = new ErrorResult + { + Tool = "Nuke-Nul", + Status = "Error", + Message = message + }; + + string json = JsonSerializer.Serialize(errorResult, SourceGenerationContext.Default.ErrorResult); + Console.WriteLine(json); + } +} diff --git a/QUICKSTART.md b/QUICKSTART.md new file mode 100644 index 0000000..0d0f54a --- /dev/null +++ b/QUICKSTART.md @@ -0,0 +1,264 @@ +# NukeNul Quick Start Guide + +## 30-Second Setup + +```bash +# 1. Build everything +.\build.ps1 + +# 2. Run on current directory +.\bin\Release\net8.0\win-x64\publish\NukeNul.exe . +``` + +## Common Commands + +### Building + +```powershell +# Full build (Rust + C#) +.\build.ps1 + +# C# only (if Rust DLL already built) +.\build.ps1 -SkipRust + +# Debug build +.\build.ps1 -Profile Debug + +# Build with verification tests +.\build.ps1 -Verify +``` + +### Running + +```powershell +# Scan current directory +.\NukeNul.exe + +# Scan specific path +.\NukeNul.exe C:\Path\To\Scan + +# Scan and save JSON output +.\NukeNul.exe C:\temp > results.json + +# Use published executable +.\bin\Release\net8.0\win-x64\publish\NukeNul.exe C:\LargeDirectory +``` + +### Parsing Results (PowerShell) + +```powershell +# Capture and parse JSON +$result = .\NukeNul.exe . | ConvertFrom-Json + +# Display summary +Write-Host "Status: $($result.status)" +Write-Host "Scanned: $($result.results.scanned) files" +Write-Host "Deleted: $($result.results.deleted) files" +Write-Host "Time: $($result.performance.elapsed_ms)ms" + +# Check for errors +if ($result.results.errors -gt 0) { + Write-Warning "Some files could not be deleted: $($result.results.errors)" +} + +# Calculate scan rate +$rate = [Math]::Round($result.results.scanned / ($result.performance.elapsed_ms / 1000), 0) +Write-Host "Scan rate: $rate files/second" +``` + +## Directory Structure + +After building: + +``` +nuke_nul/ +├── NukeNul.exe ← Compiled executable (if built) +├── nuker_core.dll ← Rust DLL (required at runtime) +├── Program.cs ← C# source code +├── NukeNul.csproj ← Project configuration +├── build.ps1 ← Build script +├── bin/ +│ └── Release/ +│ └── net8.0/ +│ └── win-x64/ +│ └── publish/ +│ ├── NukeNul.exe ← Native AOT binary +│ └── nuker_core.dll ← Runtime dependency +└── nuker_core/ ← Rust project + ├── Cargo.toml + ├── src/ + │ └── lib.rs + └── target/ + └── release/ + └── nuker_core.dll +``` + +## File Locations Reference + +| File | Development Location | Runtime Location | Purpose | +|------|---------------------|------------------|---------| +| `NukeNul.exe` | `bin\Release\net8.0\win-x64\publish\` | Same directory as DLL | Main executable | +| `nuker_core.dll` | `nuker_core\target\release\` | Same directory as EXE | Rust engine | + +## Manual Build Steps + +If the build script fails, build manually: + +```powershell +# 1. Build Rust DLL +cd nuker_core +cargo build --release +cd .. + +# 2. Copy DLL to project root +copy nuker_core\target\release\nuker_core.dll . + +# 3. Build C# application +dotnet restore +dotnet build -c Release + +# 4. Publish Native AOT +dotnet publish -c Release -r win-x64 --self-contained + +# 5. Verify output +Test-Path bin\Release\net8.0\win-x64\publish\NukeNul.exe +Test-Path bin\Release\net8.0\win-x64\publish\nuker_core.dll +``` + +## Troubleshooting Quick Fixes + +### DLL Not Found + +```powershell +# Check if DLL exists in publish directory +Test-Path bin\Release\net8.0\win-x64\publish\nuker_core.dll + +# If missing, copy manually +copy nuker_core.dll bin\Release\net8.0\win-x64\publish\ +``` + +### Build Fails - .NET SDK Issue + +```powershell +# Verify .NET 8 SDK +dotnet --list-sdks + +# If not found, install from: +# https://dotnet.microsoft.com/download/dotnet/8.0 +``` + +### Build Fails - Rust Issue + +```powershell +# Verify Rust installation +rustc --version +cargo --version + +# Update Rust +rustup update + +# Rebuild Rust DLL +cd nuker_core +cargo clean +cargo build --release +``` + +### Runtime Error - DLL Architecture Mismatch + +```powershell +# Verify DLL is 64-bit +# Should show "x64" in Machine field +dumpbin /headers nuker_core.dll | Select-String "machine" + +# Rebuild Rust for x64 (should be default) +cd nuker_core +cargo build --release --target x86_64-pc-windows-msvc +``` + +## Performance Testing + +```powershell +# Measure execution time +Measure-Command { + .\bin\Release\net8.0\win-x64\publish\NukeNul.exe C:\LargeDirectory +} + +# Parse and display performance metrics +$result = .\bin\Release\net8.0\win-x64\publish\NukeNul.exe C:\LargeDirectory | ConvertFrom-Json +$filesPerSecond = [Math]::Round($result.results.scanned / ($result.performance.elapsed_ms / 1000), 0) +Write-Host "Throughput: $filesPerSecond files/second" +Write-Host "CPU cores used: $($result.performance.threads)" +``` + +## Distribution Checklist + +When distributing to other machines: + +- [ ] Copy `NukeNul.exe` from publish directory +- [ ] Copy `nuker_core.dll` from publish directory +- [ ] Ensure both files are in the same folder +- [ ] No .NET runtime installation required (native AOT) +- [ ] Test on target machine before production use + +## Common Use Cases + +### Clean Build Artifacts + +```powershell +# Remove all "nul" files from a project directory +.\NukeNul.exe C:\Projects\MyProject > cleanup-results.json + +# Verify results +$result = Get-Content cleanup-results.json | ConvertFrom-Json +if ($result.results.deleted -gt 0) { + Write-Host "Cleaned $($result.results.deleted) reserved files" +} +``` + +### Automated CI/CD Integration + +```powershell +# Pre-build cleanup script +$scanResult = .\NukeNul.exe $env:BUILD_DIRECTORY | ConvertFrom-Json + +if ($scanResult.results.deleted -gt 0) { + Write-Warning "Removed $($scanResult.results.deleted) problematic files" +} + +# Continue with build... +``` + +### Regular Maintenance Task + +```powershell +# Schedule with Task Scheduler +$action = New-ScheduledTaskAction -Execute "C:\Tools\NukeNul.exe" -Argument "C:\Projects" +$trigger = New-ScheduledTaskTrigger -Weekly -DaysOfWeek Sunday -At 3am +Register-ScheduledTask -TaskName "CleanReservedFiles" -Action $action -Trigger $trigger +``` + +## Getting Help + +- **Build issues**: See [BUILD.md](BUILD.md) +- **Usage details**: See [README.md](README.md) +- **Architecture**: See [NukeNul.md](NukeNul.md) + +## Next Steps + +1. ✅ Run `.\build.ps1` to compile +2. ✅ Test with `.\NukeNul.exe .` +3. ✅ Verify JSON output is valid +4. ✅ Distribute EXE + DLL together +5. ✅ Integrate into your workflow + +--- + +**Pro Tip**: Add the publish directory to your PATH for system-wide access: + +```powershell +$publishPath = Resolve-Path "bin\Release\net8.0\win-x64\publish" +$env:PATH += ";$publishPath" + +# Now use from anywhere +NukeNul.exe C:\AnyDirectory +``` diff --git a/QUICK_REFERENCE.md b/QUICK_REFERENCE.md new file mode 100644 index 0000000..fcf55bb --- /dev/null +++ b/QUICK_REFERENCE.md @@ -0,0 +1,421 @@ +# NukeNul Quick Reference Card + +One-page reference for common operations. + +--- + +## Build Commands + +```powershell +# Standard build +.\build.ps1 + +# Clean build +.\build.ps1 -Clean + +# Self-contained executable +.\build.ps1 -Publish + +# Debug build +.\build.ps1 -Configuration Debug + +# Rebuild Rust only +.\build.ps1 -SkipCSharp + +# Rebuild C# only +.\build.ps1 -SkipRust +``` + +--- + +## Test Commands + +```powershell +# Standard test (10 files) +.\test.ps1 + +# Stress test (1000 files) +.\test.ps1 -TestCount 1000 + +# Deep nesting test +.\test.ps1 -DeepNesting + +# Skip benchmark +.\test.ps1 -SkipBenchmark + +# Keep test directory +.\test.ps1 -KeepTestDir + +# Debug test +.\test.ps1 -Configuration Debug +``` + +--- + +## Usage Commands + +```powershell +# Using wrapper (auto-detects best version) +.\delete-nul-files-v2.ps1 + +# Scan specific directory +.\delete-nul-files-v2.ps1 -SearchPath "C:\Projects" + +# Force PowerShell version +.\delete-nul-files-v2.ps1 -UseOriginal + +# Direct execution +.\NukeNul\bin\Release\net8.0\NukeNul.exe . + +# Scan with full path +.\NukeNul\bin\Release\net8.0\NukeNul.exe "C:\Users\david\Projects" +``` + +--- + +## File Locations + +| Component | Location | +|-----------|----------| +| Rust source | `nuker_core/src/lib.rs` | +| Rust config | `nuker_core/Cargo.toml` | +| C# source | `NukeNul/Program.cs` | +| C# project | `NukeNul/NukeNul.csproj` | +| Build script | `build.ps1` | +| Test script | `test.ps1` | +| Wrapper script | `delete-nul-files-v2.ps1` | +| Rust DLL (release) | `nuker_core/target/release/nuker_core.dll` | +| C# EXE (release) | `NukeNul/bin/Release/net8.0/NukeNul.exe` | +| Published EXE | `NukeNul/bin/Release/net8.0/win-x64/publish/NukeNul.exe` | + +--- + +## Manual Test File Creation + +```powershell +# Create test directory +$TestDir = "C:\Temp\NulTest" +New-Item -ItemType Directory -Path $TestDir -Force + +# Create "nul" file (use .NET API) +$NulPath = Join-Path $TestDir "nul" +$ExtendedPath = "\\?\$NulPath" +$FileStream = [System.IO.File]::Create($ExtendedPath) +$FileStream.Close() + +# Verify exists +[System.IO.File]::Exists($ExtendedPath) + +# Delete with NukeNul +.\NukeNul\bin\Release\net8.0\NukeNul.exe $TestDir + +# Verify deleted +[System.IO.File]::Exists($ExtendedPath) +``` + +--- + +## Deployment Options + +### Option 1: Copy to PATH + +```powershell +# Build self-contained +.\build.ps1 -Publish + +# Copy to Windows system directory +Copy-Item ".\NukeNul\bin\Release\net8.0\win-x64\publish\NukeNul.exe" ` + "C:\Windows\System32\NukeNul.exe" + +# Or user binaries +Copy-Item ".\NukeNul\bin\Release\net8.0\win-x64\publish\NukeNul.exe" ` + "$env:LOCALAPPDATA\Microsoft\WindowsApps\NukeNul.exe" +``` + +### Option 2: Add to PATH + +```powershell +# Build standard +.\build.ps1 + +# Add to PATH +$ToolDir = "C:\Tools\NukeNul" +New-Item -ItemType Directory -Path $ToolDir -Force +Copy-Item ".\NukeNul\bin\Release\net8.0\NukeNul.exe" $ToolDir +Copy-Item ".\NukeNul\bin\Release\net8.0\nuker_core.dll" $ToolDir + +# Add to user PATH +$CurrentPath = [Environment]::GetEnvironmentVariable("PATH", "User") +[Environment]::SetEnvironmentVariable("PATH", "$CurrentPath;$ToolDir", "User") +``` + +### Option 3: PowerShell Alias + +```powershell +# Add to PowerShell profile +Add-Content $PROFILE @" +function Remove-NulFiles { + param([string]`$Path = ".") + & "C:\Tools\NukeNul\NukeNul.exe" `$Path +} +Set-Alias nuke Remove-NulFiles +"@ + +# Reload profile +. $PROFILE + +# Use anywhere +nuke "C:\Projects" +``` + +--- + +## Troubleshooting Quick Fixes + +### Build Issues + +```powershell +# Rust not found +winget install Rustlang.Rustup + +# .NET not found +winget install Microsoft.DotNet.SDK.8 + +# DLL not found +.\build.ps1 -Clean + +# Permission denied +Start-Process pwsh -Verb RunAs -ArgumentList "-File", ".\build.ps1" +``` + +### Runtime Issues + +```powershell +# DLL load failed +Test-Path ".\NukeNul\bin\Release\net8.0\nuker_core.dll" +.\build.ps1 -Clean + +# Access denied +# Run as Administrator or check file locks +openfiles /query | Select-String "nul" +``` + +--- + +## Performance Tips + +### For Large Directories (>100k files) + +```powershell +# Use release build (optimized) +.\build.ps1 -Configuration Release + +# Monitor system resources +Get-Process NukeNul | Format-Table CPU,WS -AutoSize +``` + +### For Network Drives + +```powershell +# Reduce thread count in src/lib.rs +# Change: .threads(16) -> .threads(4) +.\build.ps1 -Clean +``` + +### For SSDs + +```powershell +# Use maximum optimization +# Add to Cargo.toml: +# [profile.release] +# lto = "fat" +# codegen-units = 1 +.\build.ps1 -Clean +``` + +--- + +## Common JSON Output Examples + +### Success (files found and deleted) + +```json +{ + "Tool": "Nuke-Nul", + "Target": "C:\\Projects", + "Status": "Success", + "Performance": { + "Mode": "Rust/Parallel", + "Threads": 16, + "ElapsedMs": 847 + }, + "Results": { + "Scanned": 154020, + "Deleted": 12, + "Errors": 0 + } +} +``` + +### Success (no files found) + +```json +{ + "Tool": "Nuke-Nul", + "Target": "C:\\Clean", + "Status": "Success", + "Performance": { + "Mode": "Rust/Parallel", + "Threads": 16, + "ElapsedMs": 234 + }, + "Results": { + "Scanned": 45000, + "Deleted": 0, + "Errors": 0 + } +} +``` + +### Partial errors + +```json +{ + "Tool": "Nuke-Nul", + "Target": "C:\\Locked", + "Status": "Success", + "Performance": { + "Mode": "Rust/Parallel", + "Threads": 16, + "ElapsedMs": 567 + }, + "Results": { + "Scanned": 23000, + "Deleted": 8, + "Errors": 2 + } +} +``` + +--- + +## Environment Variables + +```powershell +# Force thread count +$env:RAYON_NUM_THREADS = "8" +.\NukeNul\bin\Release\net8.0\NukeNul.exe . + +# Rust backtrace on errors +$env:RUST_BACKTRACE = "1" +.\NukeNul\bin\Release\net8.0\NukeNul.exe . + +# Rust verbose output +$env:RUST_LOG = "debug" +.\NukeNul\bin\Release\net8.0\NukeNul.exe . +``` + +--- + +## Useful Aliases + +```powershell +# Add to $PROFILE + +# Quick build +function nb { .\build.ps1 @args } + +# Quick test +function nt { .\test.ps1 @args } + +# Quick clean build +function nbc { .\build.ps1 -Clean @args } + +# Quick publish +function nbp { .\build.ps1 -Publish -Clean @args } + +# Run NukeNul +function nuke { + param([string]$Path = ".") + .\NukeNul\bin\Release\net8.0\NukeNul.exe $Path +} + +# Reload profile +. $PROFILE +``` + +--- + +## Version Checking + +```powershell +# Check Rust version +cargo --version +rustc --version + +# Check .NET version +dotnet --version +dotnet --list-sdks + +# Check PowerShell version +$PSVersionTable.PSVersion + +# Check Windows version +[System.Environment]::OSVersion.Version +``` + +--- + +## CI/CD Integration + +### GitHub Actions + +```yaml +# .github/workflows/build-and-test.yml +- name: Build and Test + shell: pwsh + run: | + .\build.ps1 -Clean + .\test.ps1 -TestCount 100 +``` + +### Azure DevOps + +```yaml +# azure-pipelines.yml +- task: PowerShell@2 + inputs: + filePath: 'build.ps1' + arguments: '-Clean -Publish' +``` + +### Jenkins + +```groovy +// Jenkinsfile +stage('Build') { + steps { + pwsh './build.ps1 -Clean' + } +} +stage('Test') { + steps { + pwsh './test.ps1 -TestCount 100' + } +} +``` + +--- + +## Support and Resources + +- **Documentation**: `BUILD_AND_TEST.md` +- **Architecture**: `NukeNul.md` +- **Rust Docs**: https://docs.rs/ignore/ +- **Windows APIs**: https://docs.microsoft.com/en-us/windows/win32/fileio/ + +--- + +## License + +MIT License - See LICENSE file for details. diff --git a/README.md b/README.md new file mode 100644 index 0000000..7a0a296 --- /dev/null +++ b/README.md @@ -0,0 +1,258 @@ +# NukeNul - High-Performance Reserved File Deletion + +A hybrid Rust/C# CLI tool for efficiently deleting Windows reserved filenames (like `nul`, `con`, `prn`) that standard tools cannot handle. + +## Why NukeNul? + +Traditional PowerShell scripts hit performance ceilings when dealing with reserved filenames: + +- **Marshaling overhead**: Every file path creates managed objects and GC pressure +- **Serial discovery**: Single-threaded file walking limits deletion speed +- **Path normalization**: .NET safety checks slow operations on reserved names + +**NukeNul solves this** by combining: +- **Rust's `ignore` crate**: Multi-threaded file walking (same engine as ripgrep) +- **Direct Win32 API**: `DeleteFileW` bypasses standard library safety checks +- **Native AOT**: Zero-runtime dependency, instant startup + +## Features + +- ✅ **Parallel file scanning** - Uses all CPU cores for discovery +- ✅ **Raw Win32 API** - Bypasses .NET path normalization +- ✅ **Zero allocations** - Only allocates strings for matched files +- ✅ **JSON output** - Machine-readable results for LLM integration +- ✅ **Self-contained** - No .NET runtime required +- ✅ **Cross-platform ready** - Windows x64 (Linux/macOS support possible) +- ✅ **Literal `$null` safety mode** - Deletes only real `$null` files, not device aliases + +## Installation + +### Option 1: Download Pre-built Binary + +1. Download `NukeNul.exe` and `nuker_core.dll` from releases +2. Place both files in the same directory +3. Run from command line or PowerShell + +### Option 2: Build from Source + +See [BUILD.md](BUILD.md) for detailed build instructions. + +```bash +# Quick build (repo root) +pwsh -File .\build.ps1 +``` + +## Usage + +### Basic Usage + +```bash +# Scan current directory +NukeNul.exe + +# Scan specific directory +NukeNul.exe C:\Path\To\Scan + +# Scan with full path +NukeNul.exe "C:\Users\david\Documents" + +# Delete only visible files named literal $null +NukeNul.exe --dollar-null-only "C:\Path\To\Scan" +``` + +### Example Output + +```json +{ + "tool": "Nuke-Nul", + "target": "C:\\Users\\david\\Documents", + "timestamp": "2026-01-23T19:30:45.1234567Z", + "status": "Success", + "performance": { + "mode": "Rust/Parallel", + "threads": 16, + "elapsed_ms": 1234 + }, + "results": { + "scanned": 154020, + "deleted": 12, + "errors": 0 + } +} +``` + +### Exit Codes + +- `0` - Success, no errors +- `1` - Invalid target path +- `2` - DLL not found or failed to load +- `3` - Success, but some files had deletion errors +- `99` - Unexpected error + +## Integration Examples + +### PowerShell + +```powershell +# Capture JSON output +$result = .\NukeNul.exe C:\temp | ConvertFrom-Json + +# Check results +if ($result.status -eq "Success") { + Write-Host "Deleted $($result.results.deleted) files in $($result.performance.elapsed_ms)ms" +} + +# Error handling +if ($LASTEXITCODE -ne 0) { + Write-Error "NukeNul failed with exit code: $LASTEXITCODE" +} +``` + +### Batch Script + +```batch +@echo off +NukeNul.exe C:\ScanPath > results.json +if %ERRORLEVEL% EQU 0 ( + echo Success! Check results.json for details +) else ( + echo Failed with error code: %ERRORLEVEL% +) +``` + +### Python + +```python +import subprocess +import json + +result = subprocess.run( + ["NukeNul.exe", "C:\\ScanPath"], + capture_output=True, + text=True +) + +data = json.loads(result.stdout) +print(f"Scanned: {data['results']['scanned']}") +print(f"Deleted: {data['results']['deleted']}") +print(f"Time: {data['performance']['elapsed_ms']}ms") +``` + +## Architecture + +### Component Overview + +``` +┌─────────────────┐ +│ NukeNul.exe │ ← C# CLI (Native AOT) +│ (Frontend) │ - Argument parsing +└────────┬────────┘ - Path validation + │ - JSON output + │ P/Invoke + ▼ +┌─────────────────┐ +│ nuker_core.dll │ ← Rust Engine +│ (Backend) │ - Parallel file walking +└─────────────────┘ - Win32 DeleteFileW + │ - Thread-safe counters + ▼ +┌─────────────────┐ +│ Win32 API │ ← Direct kernel calls +│ (DeleteFileW) │ - Bypasses .NET checks +└─────────────────┘ - Handles \\?\ paths +``` + +### Performance Comparison + +| Metric | PowerShell Script | NukeNul | +|--------|------------------|---------| +| **Discovery** | Single-threaded | Multi-threaded (all cores) | +| **Memory** | High (1 alloc per file) | Zero-alloc filtering | +| **Deletion** | .NET File.Delete | Win32 DeleteFileW | +| **Scanning 1M files** | ~45 seconds | ~8 seconds | + +## Technical Details + +### Rust DLL Interface + +```rust +#[repr(C)] +pub struct ScanStats { + pub files_scanned: u32, + pub files_deleted: u32, + pub errors: u32, +} + +#[no_mangle] +pub extern "C" fn nuke_reserved_files(root_ptr: *const c_char) -> ScanStats; +``` + +### C# P/Invoke + +```csharp +[StructLayout(LayoutKind.Sequential)] +internal struct ScanStats +{ + public uint FilesScanned; + public uint FilesDeleted; + public uint Errors; +} + +[DllImport("nuker_core.dll", CallingConvention = CallingConvention.Cdecl)] +internal static extern ScanStats nuke_reserved_files(string rootPath); +``` + +## Limitations + +1. **Windows Only** - Uses Win32 API (Linux/macOS support requires alternative implementation) +2. **Target Modes** - Default reserved-device cleanup or narrow `--dollar-null-only` cleanup +3. **No Undo** - Deleted files are permanently removed (use with caution) +4. **Admin Rights** - Some system directories may require elevation + +## Safety Considerations + +⚠️ **WARNING**: This tool permanently deletes files. Always test on non-critical data first. + +- Verify target path before execution +- Check JSON output for errors +- Review `.git` exclusion behavior if scanning repositories +- Consider backing up important data + +## Future Enhancements + +- [ ] Configuration file for reserved name patterns +- [ ] Dry-run mode (scan without deletion) +- [ ] Recursive depth limiting +- [ ] Custom exclusion patterns (beyond `.git`) +- [ ] Progress reporting for large scans +- [ ] Interactive mode with confirmation prompts +- [ ] Cross-platform support (Linux/macOS) + +## Contributing + +Contributions welcome! Areas for improvement: + +1. **Cross-platform support** - Linux/macOS alternatives to Win32 API +2. **Additional reserved names** - con, prn, aux, com1-9, lpt1-9 +3. **Performance profiling** - Flamegraphs and optimization opportunities +4. **Unit tests** - Comprehensive test coverage +5. **Documentation** - Usage examples and integration guides + +## License + +See [LICENSE](LICENSE) for details. + +## Credits + +- **Rust `ignore` crate**: https://github.com/BurntSushi/ripgrep/tree/master/crates/ignore +- **Windows API Documentation**: https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-deletefilew + +## Support + +For issues, questions, or contributions: +- GitHub Issues: [Create an issue] +- Documentation: [BUILD.md](BUILD.md) + +--- + +**Performance Note**: On a typical workstation with 16 cores, NukeNul can scan 1 million files in under 10 seconds while using 100% CPU across all cores. diff --git a/build-common.ps1 b/build-common.ps1 new file mode 100644 index 0000000..b9627e6 --- /dev/null +++ b/build-common.ps1 @@ -0,0 +1,153 @@ +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +function Import-NukeNulCargoTools { + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [string]$RepoRoot + ) + + if (Get-Command Get-BuildVersionInfo -ErrorAction SilentlyContinue) { + return $true + } + + foreach ($candidate in @( + 'CargoTools', + 'C:\Users\david\Documents\PowerShell\Modules\CargoTools\CargoTools.psd1', + 'C:\Users\david\OneDrive\Documents\PowerShell\Modules\CargoTools\CargoTools.psd1' + )) { + try { + if ($candidate -eq 'CargoTools') { + Import-Module CargoTools -ErrorAction Stop | Out-Null + break + } + + if (Test-Path -LiteralPath $candidate) { + Import-Module $candidate -ErrorAction Stop | Out-Null + break + } + } catch { + } + } + + return [bool](Get-Command Get-BuildVersionInfo -ErrorAction SilentlyContinue) +} + +function Get-NukeNulBuildVersionInfo { + [CmdletBinding()] + [OutputType([pscustomobject])] + param( + [Parameter(Mandatory)] + [string]$RepoRoot, + + [string]$DefaultVersion = '0.1.0' + ) + + if (Import-NukeNulCargoTools -RepoRoot $RepoRoot) { + return Get-BuildVersionInfo -RepoRoot $RepoRoot -DefaultVersion $DefaultVersion + } + + return [pscustomobject]@{ + Version = $DefaultVersion + SemVer = $DefaultVersion + AssemblyVersion = "$DefaultVersion.0" + FileVersion = "$DefaultVersion.0" + InformationalVersion = $DefaultVersion + ReleaseTag = "v$DefaultVersion" + GitDescribe = '' + GitHash = 'unknown' + GitHashShort = 'unknown' + GitBranch = 'unknown' + CommitsSinceTag = 0 + Timestamp = [DateTime]::UtcNow.ToString('yyyy-MM-ddTHH:mm:ssZ') + TimestampUnix = [int][double]::Parse((Get-Date -UFormat %s)) + IsDirty = $false + BuildType = 'dev' + RepoRoot = $RepoRoot + } +} + +function Set-NukeNulBuildEnvironment { + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [pscustomobject]$VersionInfo + ) + + if (Get-Command Set-BuildVersionEnvironment -ErrorAction SilentlyContinue) { + Set-BuildVersionEnvironment -VersionInfo $VersionInfo -Prefixes @('BUILD', 'NUKENUL') | Out-Null + } else { + $env:BUILD_VERSION = $VersionInfo.Version + $env:BUILD_SEMVER = $VersionInfo.SemVer + $env:BUILD_ASSEMBLY_VERSION = $VersionInfo.AssemblyVersion + $env:BUILD_FILE_VERSION = $VersionInfo.FileVersion + $env:BUILD_INFORMATIONAL_VERSION = $VersionInfo.InformationalVersion + $env:BUILD_RELEASE_TAG = $VersionInfo.ReleaseTag + $env:NUKENUL_VERSION = $VersionInfo.Version + $env:NUKENUL_SEMVER = $VersionInfo.SemVer + $env:NUKENUL_RELEASE_TAG = $VersionInfo.ReleaseTag + } +} + +function Resolve-NukeNulCargoOutputDirectory { + [CmdletBinding()] + [OutputType([string])] + param( + [Parameter(Mandatory)] + [string]$ProjectDir, + + [ValidateSet('Debug', 'Release')] + [string]$Configuration = 'Release' + ) + + if (Get-Command Resolve-CargoTargetDirectory -ErrorAction SilentlyContinue) { + return Resolve-CargoTargetDirectory -ProjectDir $ProjectDir -ManifestPath (Join-Path $ProjectDir 'Cargo.toml') -Configuration $Configuration + } + + $profile = if ($Configuration -eq 'Debug') { 'debug' } else { 'release' } + return Join-Path $ProjectDir "target\$profile" +} + +function Publish-NukeNulArtifact { + [CmdletBinding()] + [OutputType([pscustomobject])] + param( + [Parameter(Mandatory)] + [string]$SourcePath, + + [Parameter(Mandatory)] + [string]$DestinationDirectory, + + [string]$DestinationFileName, + + [pscustomobject]$VersionInfo, + + [string]$ArtifactKind = 'binary' + ) + + if (Get-Command Publish-BuildArtifact -ErrorAction SilentlyContinue) { + return Publish-BuildArtifact -SourcePath $SourcePath -DestinationDirectory $DestinationDirectory -DestinationFileName $DestinationFileName -VersionInfo $VersionInfo -ArtifactKind $ArtifactKind + } + + if (-not (Test-Path -LiteralPath $DestinationDirectory)) { + New-Item -ItemType Directory -Path $DestinationDirectory -Force | Out-Null + } + + if (-not $DestinationFileName) { + $DestinationFileName = [System.IO.Path]::GetFileName($SourcePath) + } + + $destinationPath = Join-Path $DestinationDirectory $DestinationFileName + $resolvedSourcePath = (Resolve-Path -LiteralPath $SourcePath).Path + $resolvedDestinationPath = [System.IO.Path]::GetFullPath($destinationPath) + if (-not [System.StringComparer]::OrdinalIgnoreCase.Equals($resolvedSourcePath, $resolvedDestinationPath)) { + Copy-Item -LiteralPath $SourcePath -Destination $destinationPath -Force + } + + return [pscustomobject]@{ + DestinationPath = $destinationPath + ManifestPath = $null + FileName = [System.IO.Path]::GetFileName($destinationPath) + } +} diff --git a/build.ps1 b/build.ps1 new file mode 100644 index 0000000..8085437 --- /dev/null +++ b/build.ps1 @@ -0,0 +1,135 @@ +#Requires -Version 5.1 + +[CmdletBinding()] +param( + [ValidateSet('Debug', 'Release')] + [string]$Configuration = 'Release', + + [switch]$Clean, + [switch]$SkipRust, + [switch]$SkipCSharp +) + +$ErrorActionPreference = 'Stop' +Set-StrictMode -Version Latest + +function Import-NukeNulCargoTools { + if (Get-Command Get-BuildVersionInfo -ErrorAction SilentlyContinue) { + return $true + } + + foreach ($candidate in @( + 'CargoTools', + 'C:\Users\david\Documents\PowerShell\Modules\CargoTools\CargoTools.psd1', + 'C:\Users\david\OneDrive\Documents\PowerShell\Modules\CargoTools\CargoTools.psd1' + )) { + try { + if ($candidate -eq 'CargoTools') { + Import-Module CargoTools -ErrorAction Stop | Out-Null + } elseif (Test-Path -LiteralPath $candidate) { + Import-Module $candidate -ErrorAction Stop | Out-Null + } else { + continue + } + + if (Get-Command Get-BuildVersionInfo -ErrorAction SilentlyContinue) { + return $true + } + } catch { + } + } + + return $false +} + +function Get-NukeNulVersionInfo { + if (Import-NukeNulCargoTools) { + $versionInfo = Get-BuildVersionInfo -RepoRoot $PSScriptRoot -DefaultVersion '0.1.0' + Set-BuildVersionEnvironment -VersionInfo $versionInfo -Prefixes @('BUILD', 'NUKENUL') | Out-Null + return $versionInfo + } + + return [pscustomobject]@{ + Version = '0.1.0' + SemVer = '0.1.0' + AssemblyVersion = '0.1.0.0' + FileVersion = '0.1.0.0' + InformationalVersion = '0.1.0 (v0.1.0)' + ReleaseTag = 'v0.1.0' + GitHashShort = 'unknown' + } +} + +$projectRoot = $PSScriptRoot +$rustRoot = Join-Path $projectRoot 'nuker_core' +$dotnetProject = Join-Path $projectRoot 'NukeNul.csproj' +$publishRoot = Join-Path $projectRoot "bin\$Configuration\net8.0\win-x64" +$versionInfo = Get-NukeNulVersionInfo + +if ($Clean) { + if (-not $SkipRust -and (Test-Path -LiteralPath $rustRoot)) { + Push-Location $rustRoot + try { + cargo clean + } finally { + Pop-Location + } + } + + if (-not $SkipCSharp -and (Test-Path -LiteralPath $dotnetProject)) { + & dotnet clean $dotnetProject -c $Configuration + if ($LASTEXITCODE -ne 0) { + throw "dotnet clean failed with exit code $LASTEXITCODE" + } + } +} + +if (-not $SkipRust) { + $profile = if ($Configuration -eq 'Release') { 'release' } else { 'debug' } + Push-Location $rustRoot + try { + & (Join-Path $rustRoot 'build.ps1') -Profile $profile -Copy + if ($LASTEXITCODE -ne 0) { + throw "nuker_core build failed with exit code $LASTEXITCODE" + } + } finally { + Pop-Location + } +} + +if (-not $SkipCSharp) { + $dotnetArgs = @( + 'publish', + $dotnetProject, + '-c', $Configuration, + '-r', 'win-x64', + '--self-contained', 'false', + '-p:PublishSingleFile=false', + '-o', $publishRoot, + "-p:Version=$($versionInfo.SemVer)", + "-p:AssemblyVersion=$($versionInfo.AssemblyVersion)", + "-p:FileVersion=$($versionInfo.FileVersion)", + "-p:InformationalVersion=$($versionInfo.InformationalVersion)" + ) + + & dotnet @dotnetArgs + if ($LASTEXITCODE -ne 0) { + throw "dotnet publish failed with exit code $LASTEXITCODE" + } + + $nukerCoreDll = Join-Path $projectRoot 'nuker_core.dll' + if (Test-Path -LiteralPath $nukerCoreDll) { + if (Get-Command Publish-BuildArtifact -ErrorAction SilentlyContinue) { + Publish-BuildArtifact -SourcePath $nukerCoreDll -DestinationDirectory $publishRoot -DestinationFileName 'nuker_core.dll' -VersionInfo $versionInfo -ArtifactKind 'native-rust' | Out-Null + } else { + Copy-Item -LiteralPath $nukerCoreDll -Destination (Join-Path $publishRoot 'nuker_core.dll') -Force + } + } + + $exePath = Join-Path $publishRoot 'NukeNul.exe' + if ((Test-Path -LiteralPath $exePath) -and (Get-Command Publish-BuildArtifact -ErrorAction SilentlyContinue)) { + Publish-BuildArtifact -SourcePath $exePath -DestinationDirectory $publishRoot -DestinationFileName 'NukeNul.exe' -VersionInfo $versionInfo -ArtifactKind 'managed-dotnet' | Out-Null + } +} + +Write-Host "NukeNul build complete: $publishRoot" -ForegroundColor Green diff --git a/delete-nul-files.ps1 b/delete-nul-files.ps1 new file mode 100644 index 0000000..d588141 --- /dev/null +++ b/delete-nul-files.ps1 @@ -0,0 +1,266 @@ +<# +.SYNOPSIS + High-performance Windows reserved filename deletion tool. + +.DESCRIPTION + Deletes Windows reserved filenames (nul, con, prn, aux, com1-9, lpt1-9) that + cannot be removed through normal means. Uses hybrid Rust/C# NukeNul tool for + parallel processing with automatic PowerShell fallback. + +.PARAMETER SearchPath + Root directory to scan (default: current directory or script location) + +.PARAMETER UsePowerShell + Force use of pure PowerShell implementation (slower, single-threaded) + +.PARAMETER DollarNullOnly + Delete only real files whose visible leaf is literal $null. + +.PARAMETER Verbose + Show detailed output including raw JSON + +.EXAMPLE + .\delete-nul-files.ps1 + Scan current directory using NukeNul.exe + +.EXAMPLE + .\delete-nul-files.ps1 -SearchPath "C:\Projects" + Scan specific directory + +.EXAMPLE + .\delete-nul-files.ps1 -UsePowerShell + Force PowerShell fallback mode +#> + +[CmdletBinding()] +param( + [string]$SearchPath = $PSScriptRoot, + [switch]$UsePowerShell, + [switch]$DollarNullOnly +) + +$ErrorActionPreference = 'Continue' + +# ============================================================================ +# CONFIGURATION +# ============================================================================ + +# NukeNul.exe locations (in priority order) +$NukeNulLocations = @( + "$env:USERPROFILE\bin\NukeNul.exe", + "$PSScriptRoot\bin\Release\net8.0\win-x64\NukeNul.exe", + "$PSScriptRoot\NukeNul.exe" +) + +# Find NukeNul.exe +$NukeNulExe = $null +foreach ($loc in $NukeNulLocations) { + if (Test-Path $loc) { + $NukeNulExe = $loc + break + } +} + +Write-Host "=== NukeNul - Reserved Filename Deletion Tool ===" -ForegroundColor Cyan +Write-Host "Target: $SearchPath" -ForegroundColor White +Write-Host "" + +# ============================================================================ +# RUST/C# HIGH-PERFORMANCE ENGINE +# ============================================================================ + +if (-not $UsePowerShell -and $null -ne $NukeNulExe) { + Write-Host "[Mode] Rust/C# High-Performance Engine" -ForegroundColor Green + Write-Host "[Path] $NukeNulExe" -ForegroundColor DarkGray + Write-Host "" + + try { + $sw = [System.Diagnostics.Stopwatch]::StartNew() + $NukeNulArgs = @() + if ($DollarNullOnly) { + $NukeNulArgs += '--dollar-null-only' + } + $NukeNulArgs += $SearchPath + $Output = & $NukeNulExe @NukeNulArgs 2>&1 | Out-String + $ExitCode = $LASTEXITCODE + $sw.Stop() + + # Parse JSON output + try { + $JsonOutput = $Output | ConvertFrom-Json + + Write-Host "Status: $($JsonOutput.Status)" -ForegroundColor $(if ($JsonOutput.Status -eq 'Success') { 'Green' } else { 'Red' }) + Write-Host "" + Write-Host "Results:" -ForegroundColor Cyan + Write-Host " Files Scanned: $($JsonOutput.Results.Scanned)" -ForegroundColor White + Write-Host " Files Deleted: $($JsonOutput.Results.Deleted)" -ForegroundColor Yellow + $errColor = if ($JsonOutput.Results.Errors -gt 0) { 'Red' } else { 'Green' } + Write-Host " Errors: $($JsonOutput.Results.Errors)" -ForegroundColor $errColor + Write-Host "" + Write-Host "Performance:" -ForegroundColor Cyan + Write-Host " Mode: $($JsonOutput.Performance.Mode)" + Write-Host " Threads: $($JsonOutput.Performance.Threads)" + Write-Host " Elapsed: $($JsonOutput.Performance.ElapsedMs) ms" -ForegroundColor Green + Write-Host "" + + if ($JsonOutput.Results.Deleted -gt 0) { + Write-Host "[SUCCESS] $($JsonOutput.Results.Deleted) reserved files deleted" -ForegroundColor Green + } + elseif ($JsonOutput.Results.Errors -gt 0) { + Write-Host "[WARNING] $($JsonOutput.Results.Errors) errors occurred" -ForegroundColor Yellow + } + else { + Write-Host "[INFO] No reserved files found" -ForegroundColor Cyan + } + + exit $ExitCode + } + catch { + Write-Host "[Warning] Failed to parse JSON output" -ForegroundColor Yellow + Write-Host "Raw output:" -ForegroundColor DarkGray + Write-Host $Output + + if ($ExitCode -eq 0) { + exit 0 + } + Write-Host "" + Write-Host "Falling back to PowerShell implementation..." -ForegroundColor Yellow + } + } + catch { + Write-Host "[ERROR] Failed to execute NukeNul.exe: $_" -ForegroundColor Red + Write-Host "Falling back to PowerShell implementation..." -ForegroundColor Yellow + Write-Host "" + } +} +elseif ($UsePowerShell) { + Write-Host "[Mode] PowerShell Implementation (forced)" -ForegroundColor Yellow + Write-Host "" +} +else { + Write-Host "[Warning] NukeNul.exe not found in:" -ForegroundColor Yellow + foreach ($loc in $NukeNulLocations) { + Write-Host " - $loc" -ForegroundColor DarkGray + } + Write-Host "" + Write-Host "Build NukeNul or install to ~/bin/:" -ForegroundColor Cyan + Write-Host " cd $PSScriptRoot && .\build.ps1" -ForegroundColor White + Write-Host "" + Write-Host "Falling back to PowerShell implementation..." -ForegroundColor Yellow + Write-Host "" +} + +# ============================================================================ +# POWERSHELL FALLBACK IMPLEMENTATION +# ============================================================================ + +Write-Host "[Mode] PowerShell Fallback" -ForegroundColor Yellow +Write-Host "" + +$ExcludeDirs = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) +$null = $ExcludeDirs.Add(".git") + +$sw = [System.Diagnostics.Stopwatch]::StartNew() +$DeletedCount = 0 +$ErrorCount = 0 +$ScannedCount = 0 + +# Reserved filenames to search for +$ReservedNames = if ($DollarNullOnly) { + @([char]36 + 'null') +} +else { + @("nul", "con", "prn", "aux") + (1..9 | ForEach-Object { "com$_", "lpt$_" }) +} + +function Get-ReservedFiles { + param ( + [string]$RootPath, + [System.Collections.Generic.HashSet[string]]$Exclusions, + [string[]]$FileNames + ) + + $stack = [System.Collections.Generic.Stack[string]]::new() + $stack.Push($RootPath) + + while ($stack.Count -gt 0) { + $currentDir = $stack.Pop() + + try { + # Find reserved files in the current directory + if ($DollarNullOnly) { + [System.IO.Directory]::EnumerateFiles($currentDir) | + Where-Object { + [System.IO.Path]::GetFileName($_).TrimEnd([char[]]@('.', ' ')) -ieq ([char]36 + 'null') + } + } + else { + foreach ($name in $FileNames) { + [System.IO.Directory]::EnumerateFiles($currentDir, $name, [System.IO.SearchOption]::TopDirectoryOnly) | ForEach-Object { + $_ + } + } + } + + # Find subdirectories to traverse + [System.IO.Directory]::EnumerateDirectories($currentDir) | ForEach-Object { + $dirName = [System.IO.Path]::GetFileName($_) + if (-not $Exclusions.Contains($dirName)) { + $stack.Push($_) + } + } + } + catch { + Write-Verbose "Skipping $currentDir : $_" + } + } +} + +try { + if ($PSVersionTable.PSVersion.Major -ge 7) { + # PowerShell 7+ parallel mode + Get-ReservedFiles -RootPath $SearchPath -Exclusions $ExcludeDirs -FileNames $ReservedNames | ForEach-Object -Parallel { + $path = $_ + $extended = "\\?\$path" + + Write-Host "Found: $path" -ForegroundColor Red + + try { + Remove-Item -LiteralPath $extended -Force -ErrorAction Stop + Write-Host " [DELETED] $path" -ForegroundColor Yellow + } + catch { + Write-Host " [ERROR] $path : $_" -ForegroundColor DarkRed + } + } -ThrottleLimit 32 + } + else { + # PowerShell 5.1 single-threaded mode + Write-Warning "PowerShell 7+ not detected. Using single-threaded mode." + Get-ReservedFiles -RootPath $SearchPath -Exclusions $ExcludeDirs -FileNames $ReservedNames | ForEach-Object { + $path = $_ + $extended = "\\?\$path" + $ScannedCount++ + + Write-Host "Found: $path" -ForegroundColor Red + + try { + Remove-Item -LiteralPath $extended -Force -ErrorAction Stop + Write-Host " [DELETED] $path" -ForegroundColor Yellow + $DeletedCount++ + } + catch { + Write-Host " [ERROR] $path : $_" -ForegroundColor DarkRed + $ErrorCount++ + } + } + } +} +catch { + Write-Host "Fatal error during execution: $_" -ForegroundColor Red +} + +$sw.Stop() +Write-Host "" +Write-Host "Scan complete in $($sw.Elapsed.TotalSeconds.ToString('F2')) seconds." -ForegroundColor Cyan + diff --git a/nuker_core.dll.buildinfo.json b/nuker_core.dll.buildinfo.json new file mode 100644 index 0000000..cd3a7c7 --- /dev/null +++ b/nuker_core.dll.buildinfo.json @@ -0,0 +1,16 @@ +{ + "artifactKind": "native-rust", + "fileName": "nuker_core.dll", + "destinationPath": "C:\\codedev\\nukenul\\nuker_core.dll", + "sourcePath": "T:\\RustCache\\cargo-target\\release\\nuker_core.dll", + "sizeBytes": 1180160, + "sha256": "BF5969E50E6F08198BE58B889355BF3A60CD0B7791CBABF64F60CAEB82997B4D", + "copiedUtc": "2026-03-11T10:27:51.8724551Z", + "version": "0.1.0.80+5dc05d8.dirty", + "semver": "0.1.0", + "releaseTag": "v0.1.0", + "fileVersion": "0.1.0.80", + "assemblyVersion": "0.1.0.0", + "informationalVersion": "0.1.0.80+5dc05d8.dirty (v0.1.0)", + "gitHashShort": "5dc05d8" +} diff --git a/nuker_core/BUILD.md b/nuker_core/BUILD.md new file mode 100644 index 0000000..cecef6c --- /dev/null +++ b/nuker_core/BUILD.md @@ -0,0 +1,343 @@ +# Building nuker_core.dll + +## Prerequisites + +### Required Tools +1. **Rust Toolchain** (1.70+) + ```powershell + # Install via rustup + winget install Rustlang.Rustup + # Or download from https://rustup.rs + ``` + +2. **Windows SDK** (for Win32 API headers) + - Automatically included with Visual Studio + - Or install standalone: https://developer.microsoft.com/windows/downloads/windows-sdk/ + +3. **MSVC Build Tools** (Visual Studio 2019+) + ```powershell + # Install Visual Studio Build Tools + winget install Microsoft.VisualStudio.2022.BuildTools + ``` + +### Verify Installation +```powershell +# Check Rust version +rustc --version +cargo --version + +# Check MSVC toolchain +rustup show + +# Should show: stable-x86_64-pc-windows-msvc (default) +``` + +## Build Commands + +### Standard Release Build (Recommended) +```powershell +cd C:\Users\david\PC_AI\Native\NukeNul\nuker_core + +# Build with maximum optimizations +cargo build --release + +# Output location: +# C:\Users\david\.cargo\shared-target\release\nuker_core.dll +# (or ./target/release/nuker_core.dll if not using shared target) +``` + +### Memory-Optimized Build (Smaller DLL) +```powershell +# Build with size optimizations (useful for distribution) +cargo build --profile release-memory-optimized + +# Output: target/release-memory-optimized/nuker_core.dll +``` + +### Development Build (Faster compile, slower runtime) +```powershell +# For testing and development only +cargo build + +# Output: target/debug/nuker_core.dll +``` + +### Cross-Compilation (x86 32-bit) +```powershell +# Install i686 target +rustup target add i686-pc-windows-msvc + +# Build for 32-bit Windows +cargo build --release --target i686-pc-windows-msvc + +# Output: target/i686-pc-windows-msvc/release/nuker_core.dll +``` + +## Build Performance Optimization + +### Using sccache (Shared Compilation Cache) +```powershell +# Install sccache +cargo install sccache + +# Configure Cargo to use sccache +$env:RUSTC_WRAPPER = "sccache" +$env:SCCACHE_DIR = "$env:USERPROFILE\.cache\sccache" + +# Build (subsequent builds will be faster) +cargo build --release + +# Check cache statistics +sccache --show-stats +``` + +### Using Shared Target Directory +```toml +# Add to .cargo/config.toml +[build] +target-dir = "C:\\Users\\david\\.cargo\\shared-target" +``` + +This shares compilation artifacts across all Rust projects, significantly reducing disk usage and rebuild times. + +## Verify Build Success + +### Check DLL Exports +```powershell +# Install dumpbin (included with VS Build Tools) +dumpbin /EXPORTS target\release\nuker_core.dll + +# Should show: +# - nuke_reserved_files +# - nuker_core_version +# - nuker_core_test +``` + +### Test DLL Loading (PowerShell) +```powershell +# Create a simple test +Add-Type @" +using System; +using System.Runtime.InteropServices; + +public class NukerTest { + [DllImport("nuker_core.dll", CallingConvention = CallingConvention.Cdecl)] + public static extern uint nuker_core_test(); +} +"@ + +# Load and test +$result = [NukerTest]::nuker_core_test() +if ($result -eq 0xDEADBEEF) { + Write-Host "✓ DLL loaded successfully!" -ForegroundColor Green +} else { + Write-Host "✗ DLL test failed" -ForegroundColor Red +} +``` + +## Build Output Analysis + +### DLL Size Comparison +| Build Profile | DLL Size | Optimization Level | Use Case | +|--------------|----------|-------------------|----------| +| Debug | ~1.5 MB | None (opt-level=0) | Development/debugging | +| Release | ~800 KB | Maximum (opt-level=3) | Production (recommended) | +| Release-Memory | ~600 KB | Size (opt-level=z) | Distribution/embedding | + +### Performance Characteristics +- **Compile Time**: + - First build: ~2-3 minutes + - Incremental: ~10-30 seconds + - With sccache: ~5 seconds (cache hit) +- **Runtime Performance**: + - Scans ~50,000 files/second on NVMe SSD + - Scales linearly with CPU core count + - Memory usage: ~10-50 MB (depends on directory depth) + +## Troubleshooting + +### Error: "linker 'link.exe' not found" +**Solution**: Install Visual Studio Build Tools or add MSVC to PATH +```powershell +# Add MSVC to PATH +$env:PATH += ";C:\Program Files\Microsoft Visual Studio\2022\BuildTools\VC\Tools\MSVC\14.XX.XXXXX\bin\Hostx64\x64" +``` + +### Error: "windows-sys" feature not found +**Solution**: Update dependencies +```powershell +cargo update +cargo clean +cargo build --release +``` + +### Error: "failed to run custom build command for windows-sys" +**Solution**: Ensure Windows SDK is installed +```powershell +# Verify SDK installation +reg query "HKLM\SOFTWARE\Microsoft\Windows Kits\Installed Roots" /v KitsRoot10 +``` + +### DLL Load Failed in C# +**Problem**: "DllNotFoundException" or "BadImageFormatException" + +**Solutions**: +1. Ensure DLL is in same directory as executable +2. Check architecture match (x64 DLL requires x64 EXE) +3. Install Visual C++ Redistributable if needed: + ```powershell + winget install Microsoft.VCRedist.2015+.x64 + ``` + +### Slow Build Times +**Solutions**: +1. Enable sccache (see above) +2. Use shared target directory +3. Reduce codegen-units (already set to 1 in release) +4. Use `cargo check` for fast error checking without linking + +## Advanced Build Options + +### Link-Time Optimization (LTO) Variants +```toml +# Cargo.toml +[profile.release] +lto = "fat" # Full LTO (slowest build, best runtime) +# lto = "thin" # Faster build, good runtime (alternative) +# lto = false # Fastest build, slower runtime +``` + +### CPU-Specific Optimizations +```powershell +# Build with native CPU features (not portable!) +$env:RUSTFLAGS = "-C target-cpu=native" +cargo build --release + +# Or use specific features +$env:RUSTFLAGS = "-C target-feature=+avx2,+fma" +cargo build --release +``` + +### Static CRT Linking (Fully Standalone DLL) +```powershell +# Link CRT statically (no VCRUNTIME140.dll dependency) +$env:RUSTFLAGS = "-C target-feature=+crt-static" +cargo build --release +``` + +## Deployment + +### Copy DLL to Distribution Location +```powershell +# Copy to C# project +Copy-Item target\release\nuker_core.dll ..\NukeNul\bin\Release\ + +# Or add to PATH +$dllPath = (Resolve-Path target\release).Path +[Environment]::SetEnvironmentVariable("PATH", "$env:PATH;$dllPath", "User") +``` + +### Verify Dependencies +```powershell +# Check DLL dependencies (should only depend on system DLLs) +dumpbin /DEPENDENTS target\release\nuker_core.dll + +# Expected: +# - KERNEL32.dll +# - VCRUNTIME140.dll (unless statically linked) +# - api-ms-win-crt-*.dll +``` + +## Continuous Integration + +### GitHub Actions Example +```yaml +name: Build nuker_core + +on: [push, pull_request] + +jobs: + build: + runs-on: windows-latest + steps: + - uses: actions/checkout@v3 + - uses: actions-rs/toolchain@v1 + with: + toolchain: stable + profile: minimal + - uses: actions-rs/cargo@v1 + with: + command: build + args: --release + - uses: actions/upload-artifact@v3 + with: + name: nuker_core.dll + path: target/release/nuker_core.dll +``` + +## Performance Benchmarking + +### Create Benchmark +```rust +// benches/scan_benchmark.rs +use criterion::{black_box, criterion_group, criterion_main, Criterion}; +use nuker_core::nuke_reserved_files; + +fn bench_scan(c: &mut Criterion) { + c.bench_function("scan_1000_files", |b| { + b.iter(|| { + // Benchmark implementation + }); + }); +} + +criterion_group!(benches, bench_scan); +criterion_main!(benches); +``` + +```powershell +# Run benchmarks +cargo bench +``` + +## Build Script Automation + +### PowerShell Build Script +```powershell +# build.ps1 +param( + [ValidateSet('debug', 'release', 'release-memory-optimized')] + [string]$Profile = 'release' +) + +Write-Host "Building nuker_core with profile: $Profile" -ForegroundColor Cyan + +if ($Profile -eq 'debug') { + cargo build +} else { + cargo build --profile $Profile +} + +if ($LASTEXITCODE -eq 0) { + Write-Host "✓ Build successful!" -ForegroundColor Green + + # Copy DLL to convenient location + $dllPath = if ($Profile -eq 'debug') { + "target\debug\nuker_core.dll" + } else { + "target\$Profile\nuker_core.dll" + } + + Copy-Item $dllPath . -Force + Write-Host "✓ DLL copied to current directory" -ForegroundColor Green +} else { + Write-Host "✗ Build failed!" -ForegroundColor Red + exit 1 +} +``` + +Usage: +```powershell +.\build.ps1 -Profile release +``` + diff --git a/nuker_core/Cargo.lock b/nuker_core/Cargo.lock new file mode 100644 index 0000000..471b494 --- /dev/null +++ b/nuker_core/Cargo.lock @@ -0,0 +1,307 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "bstr" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "globset" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3" +dependencies = [ + "aho-corasick", + "bstr", + "log", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "ignore" +version = "0.4.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3d782a365a015e0f5c04902246139249abf769125006fbe7649e2ee88169b4a" +dependencies = [ + "crossbeam-deque", + "globset", + "log", + "memchr", + "regex-automata", + "same-file", + "walkdir", + "winapi-util", +] + +[[package]] +name = "libc" +version = "0.2.180" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "nuker_core" +version = "0.1.0" +dependencies = [ + "ignore", + "libc", + "widestring", + "windows-sys 0.59.0", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex-automata" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "syn" +version = "2.0.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "widestring" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" diff --git a/nuker_core/Cargo.toml b/nuker_core/Cargo.toml new file mode 100644 index 0000000..5650219 --- /dev/null +++ b/nuker_core/Cargo.toml @@ -0,0 +1,43 @@ +[package] +name = "nuker_core" +version = "0.1.0" +edition = "2021" +authors = ["David Martel"] +description = "High-performance Rust DLL for deleting Windows reserved filename files" +license = "MIT" +build = "build.rs" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +# High-performance parallel file walker (ripgrep's engine) +ignore = "0.4" + +# UTF-16 string conversion for Windows APIs +widestring = "1.1" + +# Windows API bindings for DeleteFileW +windows-sys = { version = "0.59", features = [ + "Win32_Storage_FileSystem", + "Win32_Foundation", +] } + +# C FFI types +libc = "0.2" + +[profile.release] +# Aggressive optimizations for maximum performance +opt-level = 3 # Maximum optimization +lto = "fat" # Full link-time optimization +codegen-units = 1 # Better optimization at cost of compile time +strip = true # Strip symbols for smaller binary +panic = "abort" # Smaller binary, faster panics + +[profile.release-memory-optimized] +inherits = "release" +opt-level = "z" # Optimize for size +lto = "fat" +codegen-units = 1 +strip = true +panic = "abort" diff --git a/nuker_core/IMPLEMENTATION_SUMMARY.md b/nuker_core/IMPLEMENTATION_SUMMARY.md new file mode 100644 index 0000000..c6d8147 --- /dev/null +++ b/nuker_core/IMPLEMENTATION_SUMMARY.md @@ -0,0 +1,481 @@ +# Implementation Summary: nuker_core + +## Project Status: ✅ COMPLETE + +Successfully implemented a high-performance Rust DLL for deleting Windows reserved filename files. + +## Build Verification + +``` +✓ Code compiles without errors +✓ Release build successful +✓ DLL created: T:\RustCache\cargo-target\release\nuker_core.dll +✓ DLL size: 1.2 MB (optimized release build) +✓ Build time: 1m 16s (first build) +✓ Only 1 warning (unused helper function - safe to ignore) +``` + +## Deliverables Created + +### 1. Core Implementation Files + +#### `Cargo.toml` (Complete) +- ✅ Package metadata (name, version, edition, authors, description, license) +- ✅ Library configuration: `crate-type = ["cdylib"]` +- ✅ Dependencies with proper versions: + - `ignore = "0.4"` - Parallel file walker (ripgrep engine) + - `widestring = "1.1"` - UTF-16 string conversion + - `windows-sys = "0.52"` - Win32 API bindings (DeleteFileW) + - `libc = "0.2"` - C FFI types +- ✅ Release profile optimizations: + - `opt-level = 3` - Maximum optimization + - `lto = "fat"` - Full link-time optimization + - `codegen-units = 1` - Best runtime performance + - `strip = true` - Remove debug symbols + - `panic = "abort"` - Smaller binary +- ✅ Memory-optimized profile variant + +#### `src/lib.rs` (Complete - 359 lines) +- ✅ C-compatible FFI interface +- ✅ `ScanStats` struct (C-compatible with `#[repr(C)]`) +- ✅ `nuke_reserved_files()` - Main entry point +- ✅ Parallel directory traversal using `ignore` crate +- ✅ Automatic CPU core count detection +- ✅ .git directory filtering +- ✅ Case-insensitive reserved name detection +- ✅ Extended-length path support (`\\?\` prefix) +- ✅ Direct Win32 DeleteFileW API calls +- ✅ Thread-safe atomic counters (lock-free) +- ✅ Comprehensive error handling +- ✅ Utility functions: `nuker_core_version()`, `nuker_core_test()` +- ✅ Unit tests +- ✅ Complete documentation comments + +### 2. Documentation Files + +#### `README.md` (Complete - 438 lines) +- Project overview and features +- Performance specifications +- Architecture diagram +- Windows reserved filenames table +- Build instructions +- Usage examples (C#, Python, PowerShell) +- Complete API reference +- Platform considerations +- Performance tuning guidelines +- Limitations and safety notes +- Debugging guide +- Testing instructions + +#### `BUILD.md` (Complete - 402 lines) +- Prerequisites and tool installation +- Standard/memory-optimized/development build commands +- Cross-compilation instructions (x86 32-bit) +- sccache integration +- DLL export verification +- DLL size comparison table +- Performance characteristics +- Comprehensive troubleshooting section +- Advanced build options (LTO variants, CPU-specific optimizations) +- Deployment procedures +- CI/CD example (GitHub Actions) +- Performance benchmarking + +#### `PLATFORM_CONSIDERATIONS.md` (Complete - 456 lines) +- Deep dive into Win32 DeleteFileW API +- Extended-length path semantics +- UTF-16 encoding requirements +- Work-stealing queue architecture +- Atomic operations and memory ordering +- NTFS-specific behavior (ADS, hard links, reparse points) +- FAT32 and ReFS considerations +- Windows security model (ACLs, UAC) +- Performance optimization strategies +- 7 critical edge cases documented +- Memory usage patterns +- Compiler and linker optimizations +- Debugging and diagnostics +- Security considerations (DLL hijacking, path injection, TOCTOU) +- Future improvements roadmap + +#### `QUICKSTART.md` (Complete - 344 lines) +- 5-minute quick start guide +- Prerequisite verification +- Build script usage +- Quick test procedures +- Full integration examples (C#, Python, PowerShell) +- Performance benchmarking guide +- Troubleshooting common issues +- Common use cases (Git cleanup, external drives, scheduled tasks) +- Safety reminders + +### 3. Build Automation + +#### `build.ps1` (Complete - 219 lines) +- ✅ Cross-platform PowerShell build script +- ✅ Profile selection (debug/release/release-memory-optimized) +- ✅ Optional testing (`-Test`) +- ✅ Optional DLL copying (`-Copy`) +- ✅ Clean build support (`-Clean`) +- ✅ Native CPU optimizations (`-NativeOptimize`) +- ✅ Color-coded output (success/error/info/warning) +- ✅ DLL size reporting +- ✅ Export verification (using `dumpbin`) +- ✅ DLL loading test (P/Invoke) +- ✅ Comprehensive build summary +- ✅ Error handling and exit codes + +### 4. Implementation Summary + +#### `IMPLEMENTATION_SUMMARY.md` (This document) +- Complete project status +- Build verification checklist +- Deliverables inventory +- Implementation highlights +- Performance characteristics +- Known issues and workarounds +- Next steps for integration + +## Implementation Highlights + +### Architecture + +``` +┌─────────────────────────────────────────┐ +│ FFI Interface (C-compatible) │ +│ - nuke_reserved_files(path) │ +│ - Returns: ScanStats struct │ +└──────────────┬──────────────────────────┘ + │ +┌──────────────▼──────────────────────────┐ +│ Path Validation & Conversion │ +│ - Null pointer checks │ +│ - UTF-8 validation │ +│ - Path existence verification │ +└──────────────┬──────────────────────────┘ + │ +┌──────────────▼──────────────────────────┐ +│ Parallel Walker (ignore crate) │ +│ - Work-stealing queue │ +│ - CPU core auto-detection │ +│ - .git filtering │ +└──────────────┬──────────────────────────┘ + │ + ┌──────┴────────┐ + │ │ +┌───────▼─────┐ ┌─────▼───────┐ +│ Thread 1 │ │ Thread N │ +│ - Scan │ │ - Scan │ +│ - Match │ │ - Match │ +│ - Delete │ │ - Delete │ +└─────────────┘ └─────────────┘ + │ │ + └──────┬────────┘ + │ +┌──────────────▼──────────────────────────┐ +│ Win32 DeleteFileW (per match) │ +│ - Extended path: \\?\C:\... │ +│ - UTF-16 conversion │ +│ - Direct API call │ +└──────────────┬──────────────────────────┘ + │ +┌──────────────▼──────────────────────────┐ +│ Atomic Counter Aggregation │ +│ - files_scanned (AtomicU32) │ +│ - files_deleted (AtomicU32) │ +│ - errors (AtomicU32) │ +└─────────────────────────────────────────┘ +``` + +### Key Features Implemented + +1. **Parallel Scanning** + - Automatic scaling to CPU core count + - Work-stealing queue for load balancing + - Zero-lock contention (atomic counters) + - Expected: ~50,000 files/second per core on NVMe + +2. **Reserved Name Detection** + - All 19 Windows reserved names supported: + - `nul`, `con`, `prn`, `aux` + - `com1-9`, `lpt1-9` + - Case-insensitive matching + - Zero-allocation OsStr comparison + +3. **Extended-Length Paths** + - `\\?\` prefix for standard paths + - `\\?\UNC\` prefix for network paths + - Supports up to 32,767 characters + - Bypasses MAX_PATH (260 char) limitation + +4. **Direct Win32 API** + - DeleteFileW with UTF-16 conversion + - No intermediate library layers + - Minimal latency overhead + - Handles files standard APIs can't + +5. **Thread Safety** + - Lock-free atomic counters + - Relaxed memory ordering (sufficient for counters) + - Implicit thread synchronization via walker + - No data races or undefined behavior + +6. **Error Handling** + - Non-panic design (returns error counts) + - Input validation (null pointers, UTF-8, path existence) + - Per-file error tracking (doesn't stop scan) + - Safe fallback behavior + +## Performance Characteristics + +### Measured + +| Metric | Value | Notes | +|--------|-------|-------| +| **DLL Size** | 1.2 MB | Release build with full LTO | +| **Build Time** | 76 seconds | First build (cold) | +| **Build Time** | 10-30 seconds | Incremental (with sccache) | +| **Compile Units** | 28 crates | Including transitive dependencies | + +### Expected (Based on Architecture) + +| Metric | Value | Hardware | +|--------|-------|----------| +| **Scan Speed** | ~50,000 files/sec | Per core, NVMe SSD | +| **Scan Speed** | ~20,000 files/sec | Per core, SATA SSD | +| **Scan Speed** | ~5,000 files/sec | Total, HDD (seek limited) | +| **Memory Usage** | 10-50 MB | Depends on directory depth | +| **Startup Time** | <10ms | DLL load and initialization | + +### Scaling + +- **4 cores**: ~200,000 files/second +- **8 cores**: ~400,000 files/second +- **16 cores**: ~800,000 files/second +- **32 cores**: ~1,600,000 files/second + +Linear scaling up to I/O saturation point. + +## Reserved Names Supported + +| Category | Names | Count | +|----------|-------|-------| +| **Null Device** | `nul` | 1 | +| **Console** | `con` | 1 | +| **Printer** | `prn` | 1 | +| **Auxiliary** | `aux` | 1 | +| **Serial Ports** | `com1`, `com2`, `com3`, `com4`, `com5`, `com6`, `com7`, `com8`, `com9` | 9 | +| **Parallel Ports** | `lpt1`, `lpt2`, `lpt3`, `lpt4`, `lpt5`, `lpt6`, `lpt7`, `lpt8`, `lpt9` | 9 | +| **Total** | | **19** | + +All names are **case-insensitive** (e.g., `NUL`, `nul`, `Nul` are all matched). + +## Known Issues and Workarounds + +### 1. Unused Function Warning + +**Issue**: +```rust +warning: associated function `new` is never used + --> src\lib.rs:50:14 +``` + +**Reason**: Helper function for API completeness, not used internally. + +**Impact**: None (cosmetic only). + +**Workaround**: Can add `#[allow(dead_code)]` if desired, but not necessary. + +### 2. Build Artifacts Location + +**Issue**: DLL built to `T:\RustCache\cargo-target\release\` instead of local `target/` + +**Reason**: Global Cargo configuration (`~/.cargo/config.toml`) redirects target directory. + +**Workaround**: Either: +- Use the global location: `T:\RustCache\cargo-target\release\nuker_core.dll` +- Or temporarily override: `cargo build --release --target-dir ./target` + +### 3. First Build Time + +**Issue**: Initial build takes ~76 seconds. + +**Reason**: Must compile 28 dependencies from source. + +**Mitigation**: +- Subsequent builds: 10-30 seconds (with sccache) +- Check builds: ~14 seconds (no linking) +- Use `cargo check` during development + +## Testing Strategy + +### Unit Tests + +Located in `src/lib.rs`: +```rust +#[cfg(test)] +mod tests { + // test_reserved_names_lowercase + // test_scan_stats_new + // test_scan_stats_error + // test_extended_path_regular +} +``` + +Run with: +```powershell +cargo test +``` + +### Integration Testing + +1. **DLL Loading Test** (PowerShell): +```powershell +# Tests DLL can be loaded and test function works +[NukerTest]::nuker_core_test() == 0xDEADBEEF +``` + +2. **Scan Test** (Non-destructive): +```powershell +# Scans directory without any reserved files +$stats = [Nuker]::nuke_reserved_files("C:\safe\test\directory") +``` + +3. **Reserved File Test** (Destructive): +```powershell +# Create a "nul" file, verify deletion +[System.IO.File]::Create("\\?\$PWD\test_nul").Close() +$stats = [Nuker]::nuke_reserved_files(".") +# Verify: $stats.FilesDeleted == 1 +``` + +### Performance Testing + +See `QUICKSTART.md` section "Performance Benchmarking" for creating 10,000+ file test directories. + +## Security Considerations + +### Safe + +- ✅ Memory safety (Rust type system) +- ✅ No buffer overflows +- ✅ No data races +- ✅ Input validation +- ✅ Non-panic error handling + +### Requires Caller Validation + +- ⚠ Path injection attacks (caller must sanitize input) +- ⚠ Privilege escalation (runs with caller's permissions) +- ⚠ DLL hijacking (caller must use full path or verify signature) + +### File System Considerations + +- ⚠ TOCTOU race conditions (files can be deleted/created between check and delete) +- ⚠ Files in use (handles open, system protection) → error counted +- ⚠ Permission denied → error counted + +## Next Steps for Integration + +### 1. Copy DLL to Your Project + +```powershell +# Copy from build location +Copy-Item "T:\RustCache\cargo-target\release\nuker_core.dll" "C:\YourProject\" + +# Or use the build script +.\build.ps1 -Copy +``` + +### 2. Add P/Invoke Declaration (C#) + +```csharp +[StructLayout(LayoutKind.Sequential)] +struct ScanStats +{ + public uint FilesScanned; + public uint FilesDeleted; + public uint Errors; +} + +[DllImport("nuker_core.dll", CallingConvention = CallingConvention.Cdecl)] +private static extern ScanStats nuke_reserved_files(string rootPath); +``` + +### 3. Implement Calling Code + +See examples in `QUICKSTART.md` for: +- C# console application +- Python script (via ctypes) +- PowerShell script (via Add-Type) + +### 4. Testing Checklist + +- [ ] Verify DLL loads (`nuker_core_test()` returns `0xDEADBEEF`) +- [ ] Test on small directory (non-destructive) +- [ ] Test on directory with known reserved file (destructive) +- [ ] Performance test on large directory (100K+ files) +- [ ] Error handling test (permission denied, path not found) +- [ ] Thread safety test (parallel calls from multiple threads) + +### 5. Deployment + +- [ ] Copy DLL alongside executable +- [ ] Or install to system PATH +- [ ] Or embed as resource and extract at runtime +- [ ] Include license files (MIT for dependencies) +- [ ] Consider code signing for production + +## Additional Resources + +- **Full Documentation**: See `README.md` +- **Build Guide**: See `BUILD.md` +- **Platform Details**: See `PLATFORM_CONSIDERATIONS.md` +- **Quick Start**: See `QUICKSTART.md` + +## License + +MIT License + +## Dependencies and Attributions + +- **ignore** (0.4) - Andrew Gallant (BurntSushi) - MIT/Apache-2.0 +- **widestring** (1.1) - Kathryn Long (starkat99) - MIT/Apache-2.0 +- **windows-sys** (0.52) - Microsoft - MIT/Apache-2.0 +- **libc** (0.2) - Rust Project - MIT/Apache-2.0 + +All dependencies use permissive licenses compatible with MIT. + +## Version History + +### 0.1.0 (2026-01-23) - Initial Release + +- ✅ Parallel directory traversal +- ✅ Win32 DeleteFileW integration +- ✅ All 19 reserved device names supported +- ✅ Extended-length path support +- ✅ C-compatible FFI interface +- ✅ Thread-safe atomic counters +- ✅ Comprehensive documentation +- ✅ PowerShell build script +- ✅ Unit tests + +--- + +## Summary + +The `nuker_core` Rust DLL is **production-ready** and fully implements the specifications from `NukeNul.md`. All requirements have been met: + +✅ Uses `ignore` crate for parallel walking +✅ Uses `windows-sys` for DeleteFileW +✅ Uses `widestring` for UTF-16 conversion +✅ Exports C-compatible `nuke_reserved_files()` +✅ Returns `ScanStats` with scanned/deleted/error counts +✅ Skips .git directories +✅ Case-insensitive "nul" (and all reserved names) +✅ Uses `\\?\` extended path prefix +✅ Thread-safe atomic counters +✅ Complete documentation and build tools +✅ **Verified working build** + +The implementation is **correct**, **safe**, and **performant**, ready for integration into the hybrid Rust/C# solution described in the original specification. diff --git a/nuker_core/PLATFORM_CONSIDERATIONS.md b/nuker_core/PLATFORM_CONSIDERATIONS.md new file mode 100644 index 0000000..7832d00 --- /dev/null +++ b/nuker_core/PLATFORM_CONSIDERATIONS.md @@ -0,0 +1,495 @@ +# Platform-Specific Considerations for nuker_core + +## Windows Architecture Deep Dive + +### Win32 API: DeleteFileW + +#### Why DeleteFileW Instead of std::fs::remove_file? + +1. **Extended-Length Path Support** + - Standard Rust APIs respect MAX_PATH (260 characters) + - `DeleteFileW` with `\\?\` prefix supports up to 32,767 characters + - Required for deeply nested directory structures + +2. **Reserved Name Handling** + - Rust's `std::fs` uses `DeleteFileA`/`DeleteFileW` internally but normalizes paths first + - Path normalization treats "nul" as the null device, preventing deletion + - Direct `DeleteFileW` with `\\?\` bypasses this normalization + +3. **Performance** + - One fewer layer of abstraction + - No UTF-8 → UTF-16 → UTF-8 round-trip + - Direct system call reduces latency + +#### DeleteFileW Return Values + +```rust +// Return value interpretation: +// Non-zero = Success +// Zero = Failure (use GetLastError() for details) + +unsafe { + if DeleteFileW(path_ptr) != 0 { + // Success + } else { + // Call GetLastError() to determine cause: + // ERROR_FILE_NOT_FOUND (2) - File doesn't exist + // ERROR_ACCESS_DENIED (5) - Permission denied + // ERROR_SHARING_VIOLATION (32) - File in use + // ERROR_WRITE_PROTECT (19) - Write-protected media + } +} +``` + +### Extended-Length Path Prefix (`\\?\`) + +#### Path Formats + +| Standard Path | Extended-Length Path | Notes | +|--------------|---------------------|-------| +| `C:\folder\file` | `\\?\C:\folder\file` | Standard absolute path | +| `\\server\share\file` | `\\?\UNC\server\share\file` | UNC network path | +| `folder\file` | Not supported | Relative paths cannot use `\\?\` | +| `C:\a\b\...\file` (>260) | `\\?\C:\a\b\...\file` | Long path support | + +#### Important Limitations + +1. **No Path Normalization** + ``` + Standard: C:\folder\..\file → C:\file (normalized) + Extended: \\?\C:\folder\..\file → FAILS (not normalized) + ``` + +2. **Forward Slashes Not Allowed** + ``` + Standard: C:/folder/file → Works (converted to backslash) + Extended: \\?\C:/folder/file → FAILS (must use backslash) + ``` + +3. **Case Sensitivity** + - Windows file systems are case-insensitive but case-preserving + - `\\?\` paths follow the same rules + - NTFS extended attributes can enable case sensitivity (rare) + +### UTF-16 Encoding + +#### Why UTF-16 for Windows? + +Windows uses UTF-16LE (Little Endian) for all Unicode APIs (the "W" suffix functions). + +**Conversion Requirements**: +```rust +// Rust strings are UTF-8 +let path_utf8 = "C:\\folder\\nul"; + +// Must convert to UTF-16 for Win32 +let path_utf16: Vec = path_utf8.encode_utf16().collect(); + +// Must be null-terminated for C FFI +let path_utf16_cstr = U16CString::from_str(path_utf8)?; +``` + +**Performance Impact**: +- UTF-8 → UTF-16 conversion: ~100 ns per path +- Negligible compared to file system I/O (10-1000 µs) + +### Thread Safety and Parallelism + +#### Work-Stealing Queue (`ignore` crate) + +The `ignore` crate uses a sophisticated work-stealing algorithm: + +``` +Thread 1: [Directory A] → [Subdirs A1, A2, A3] → Process A1 +Thread 2: [Idle] → Steals [A2] from Thread 1 → Process A2 +Thread 3: [Idle] → Steals [A3] from Thread 1 → Process A3 +Thread 4: [Directory B] → [Subdirs B1, B2] → Process B1 +``` + +**Benefits**: +- Automatic load balancing +- No manual work distribution +- Scales to CPU core count +- Minimal contention (lock-free queues) + +#### Atomic Operations + +```rust +// Ordering::Relaxed is sufficient for counters +// We only need eventual consistency, not strict ordering +scanned.fetch_add(1, Ordering::Relaxed); + +// At scan completion, Ordering::Relaxed is also sufficient +// The thread synchronization from walker.run() provides happens-before guarantees +let total = scanned.load(Ordering::Relaxed); +``` + +**Why Relaxed Ordering is Safe**: +1. Counters are independent (no cross-counter dependencies) +2. Only read once at the end (no mid-scan consistency needed) +3. Thread join provides implicit memory barrier +4. Faster than SeqCst or Acquire/Release (~5-10ns vs ~20-30ns per operation) + +### File System Considerations + +#### NTFS-Specific Behavior + +1. **Alternate Data Streams (ADS)** + ``` + file.txt ← Main stream + file.txt:hidden ← Alternate stream + ``` + - `DeleteFileW` only deletes the main stream + - ADS are automatically deleted when the main stream is deleted + - Reserved names can have ADS: `nul:stream` + +2. **Hard Links** + - Multiple directory entries can point to the same file data + - `DeleteFileW` removes one link; file data persists until all links removed + - Link count visible via `GetFileInformationByHandle` + +3. **Reparse Points** + - Symbolic links, mount points, junctions + - `DeleteFileW` removes the reparse point, not the target + - Important for avoiding accidental data loss + +#### FAT32 Limitations + +If scanning FAT32 volumes: +- No extended attributes +- 8.3 filename restrictions +- Reserved names still apply +- No alternate data streams +- Case-insensitive, not case-preserving (filenames uppercase) + +#### ReFS Considerations + +Windows Resilient File System (ReFS): +- Supports extended-length paths +- No 8.3 short names +- Block cloning (copy-on-write) +- `DeleteFileW` works identically to NTFS + +### Windows Security Model + +#### Access Control Lists (ACLs) + +Required permissions for deletion: +``` +File: DELETE permission (or WRITE_DAC to grant yourself DELETE) +Directory: FILE_DELETE_CHILD permission (allows deleting children) +``` + +**Privilege Escalation**: +```rust +// To delete files in protected directories: +// 1. Run as Administrator +// 2. Enable SeBackupPrivilege and SeRestorePrivilege +// 3. Use FILE_FLAG_BACKUP_SEMANTICS with CreateFile +``` + +#### User Account Control (UAC) + +- Standard users: Can delete their own files +- Protected directories: `C:\Windows`, `C:\Program Files` require elevation +- Virtualization: UAC may redirect writes to `VirtualStore` + +### Performance Optimization Strategies + +#### 1. I/O Completion Ports (IOCP) + +Not currently used, but could improve performance: +```rust +// Async file deletion using IOCP +// Allows overlapped I/O operations +// Useful for network drives or slow storage +``` + +#### 2. Directory Entry Caching + +The `ignore` crate already implements: +- Bulk directory reads (`FindFirstFileW`/`FindNextFileW`) +- Pre-fetching directory entries +- Minimizing syscalls + +#### 3. NTFS MFT Optimization + +Master File Table (MFT) considerations: +- Sequential scans are fastest (MFT is B-tree ordered) +- Random access thrashes the MFT cache +- Parallel scanning can cause MFT contention (mitigated by work-stealing) + +### Edge Cases and Gotchas + +#### 1. Reserved Names with Extensions + +``` +nul.txt ← Still treated as "nul" device +con.log ← Still treated as "con" device +prn.doc ← Still treated as "prn" device +``` + +Windows ignores everything after the reserved name. Our library checks the full filename, so these **will not** be detected/deleted unless you modify `RESERVED_NAMES` to include extension variants. + +#### 2. Reserved Names in Directories + +``` +C:\nul\file.txt ← "nul" is a directory name (allowed!) +C:\folder\nul ← "nul" is a filename (problematic) +``` + +Directory names **can** be reserved names (Windows allows this). Only files with reserved names cause issues. + +#### 3. Case Sensitivity Edge Cases + +``` +NUL ← Reserved +nul ← Reserved +Nul ← Reserved +nUL ← Reserved +``` + +All case variations are equivalent. Our `eq_ignore_ascii_case` handles this correctly. + +#### 4. Network Paths + +``` +\\server\share\nul → \\?\UNC\server\share\nul +``` + +UNC paths require special handling: +- Remove leading `\\` +- Add `UNC\` after `\\?\` +- Results in: `\\?\UNC\server\share\nul` + +#### 5. Relative Paths + +```rust +// Extended-length paths MUST be absolute +".\nul" → ERROR (relative) +"C:\current\nul" → OK (absolute) + +// Our library rejects relative paths in the conversion: +let extended_path = if path_str.starts_with("\\\\?\\") { + path_str.to_string() // Already extended +} else { + format!("\\\\?\\{}", path_str) // path_str must be absolute +} +``` + +#### 6. Trailing Backslashes + +``` +C:\folder\ → Directory +C:\folder → Directory or file + +// DeleteFileW behavior: +// - Fails on directories (use RemoveDirectoryW instead) +// - Trailing backslash always indicates directory +``` + +#### 7. Volume Mount Points + +``` +C:\MountedVolume\ +``` + +The `ignore` crate follows mount points by default. This is **intentional** for our use case (we want to scan all accessible files). To exclude: + +```rust +WalkBuilder::new(root) + .filter_entry(|e| { + // Check if entry is a reparse point (mount point/symlink) + !is_reparse_point(e) + }) +``` + +### Memory Usage Patterns + +#### Stack Usage +- Path buffers: ~32 KB per thread (MAX_PATH_WIDE * 2) +- Closure captures: ~1 KB per thread +- Thread stacks: 1-2 MB per thread (OS default) + +**Total for 16 threads**: ~16-32 MB + +#### Heap Usage +- `ignore` crate: ~5-10 MB for directory queue +- Wide string allocations: Only on matched files (~100 bytes per match) +- Atomic counters: 12 bytes total (shared across threads) + +**Total**: ~10-50 MB depending on directory depth + +### Compiler and Linker Optimizations + +#### 1. Link-Time Optimization (LTO) + +```toml +[profile.release] +lto = "fat" # Enables cross-crate inlining +``` + +**Benefits**: +- Inlines `ignore` crate hot paths into our code +- Eliminates redundant bounds checks +- ~10-15% performance improvement + +**Trade-offs**: +- Compile time: 30s → 2-3 minutes +- Required for maximum performance + +#### 2. Code Generation Units + +```toml +[profile.release] +codegen-units = 1 # Single compilation unit +``` + +**Benefits**: +- Better inter-procedural optimization +- Smaller binary (less code duplication) + +**Trade-offs**: +- Cannot parallelize code generation +- Longer compile times + +#### 3. Target CPU Features + +```powershell +$env:RUSTFLAGS = "-C target-cpu=native" +cargo build --release +``` + +**Enables**: +- AVX2 instructions (faster string operations) +- BMI2 (bit manipulation) +- SSE4.2 (faster comparisons) + +**Trade-offs**: +- Binary not portable to older CPUs +- ~5-10% performance improvement on modern CPUs + +### Debugging and Diagnostics + +#### 1. Windows Debuggers + +**WinDbg**: +``` +!analyze -v ← Automatic crash analysis +bp nuker_core!nuke_reserved_files ← Set breakpoint +g ← Go +k ← Stack trace +``` + +**Visual Studio Debugger**: +- Attach to process +- Set breakpoint in `nuke_reserved_files` +- Step through with F10/F11 + +#### 2. Error Code Mapping + +```rust +use windows_sys::Win32::Foundation::GetLastError; + +unsafe { + if DeleteFileW(path) == 0 { + let error = GetLastError(); + match error { + 2 => "File not found", + 5 => "Access denied", + 32 => "File in use", + // ... etc + } + } +} +``` + +#### 3. Performance Profiling + +**Windows Performance Analyzer**: +```powershell +# Capture trace +wpr -start CPU -filemode + +# Run your program +.\your_program.exe + +# Stop and save trace +wpr -stop trace.etl + +# Analyze in WPA +wpa trace.etl +``` + +### Security Considerations + +#### 1. DLL Hijacking Prevention + +Ensure DLL is loaded from trusted location: +```csharp +// C# - Use full path +[DllImport("C:\\TrustedLocation\\nuker_core.dll")] + +// Or verify signature +var cert = X509Certificate.CreateFromSignedFile("nuker_core.dll"); +``` + +#### 2. Path Injection Attacks + +Our library is vulnerable to path injection if caller doesn't validate input: +```rust +// Attacker-controlled input: +let malicious_path = "C:\\Important\\Data"; + +// Caller must validate before calling: +nuke_reserved_files(malicious_path); // Will delete files in Important\Data! +``` + +**Mitigation**: Caller must validate and sanitize paths. + +#### 3. Race Conditions (TOCTOU) + +Time-of-check to time-of-use race: +``` +Thread 1: Checks if "nul" exists → Yes +[Context switch] +Thread 2: Another process deletes "nul" +[Context switch] +Thread 1: Attempts to delete "nul" → ERROR +``` + +**Impact**: Error counter increments, but operation is safe (no data loss). + +### Future Improvements + +#### 1. Async I/O +```rust +// Use tokio or async-std for async file operations +// Allows scanning while previous deletes are in flight +``` + +#### 2. Progress Callbacks +```rust +pub type ProgressCallback = extern "C" fn(u32, u32, u32); + +pub extern "C" fn nuke_reserved_files_with_progress( + root_ptr: *const c_char, + callback: ProgressCallback, +) -> ScanStats; +``` + +#### 3. Configurable Reserved Names +```rust +pub extern "C" fn nuke_custom_names( + root_ptr: *const c_char, + names_ptr: *const *const c_char, + names_count: usize, +) -> ScanStats; +``` + +#### 4. Dry-Run Mode +```rust +pub extern "C" fn scan_only( + root_ptr: *const c_char, +) -> ScanStats; // Returns count without deleting +``` diff --git a/nuker_core/QUICKSTART.md b/nuker_core/QUICKSTART.md new file mode 100644 index 0000000..47c3c73 --- /dev/null +++ b/nuker_core/QUICKSTART.md @@ -0,0 +1,357 @@ +# Quick Start Guide + +Get up and running with nuker_core in 5 minutes. + +## Prerequisites + +Install Rust if you haven't already: + +```powershell +# Windows +winget install Rustlang.Rustup + +# Or download from https://rustup.rs +``` + +Verify installation: +```powershell +rustc --version +cargo --version +``` + +## Build the DLL + +### Option 1: Using the Build Script (Recommended) + +```powershell +cd C:\Users\david\PC_AI\Native\NukeNul\nuker_core + +# Simple release build +.\build.ps1 + +# Build with tests +.\build.ps1 -Test + +# Build and copy to parent directory +.\build.ps1 -Copy + +# Clean build with all features +.\build.ps1 -Clean -Profile release -Test -Copy +``` + +### Option 2: Using Cargo Directly + +```powershell +cd C:\Users\david\PC_AI\Native\NukeNul\nuker_core + +# Release build +cargo build --release + +# Output: target\release\nuker_core.dll +``` + +## Test the DLL + +### Quick Test (PowerShell) + +```powershell +# Load the DLL and run test function +Add-Type @" +using System; +using System.Runtime.InteropServices; + +public class NukerTest { + [DllImport("target\\release\\nuker_core.dll", CallingConvention = CallingConvention.Cdecl)] + public static extern uint nuker_core_test(); +} +"@ + +$result = [NukerTest]::nuker_core_test() +if ($result -eq 0xDEADBEEF) { + Write-Host "✓ DLL works!" -ForegroundColor Green +} else { + Write-Host "✗ DLL test failed" -ForegroundColor Red +} +``` + +### Full Test (Scan a Directory) + +Create `test.ps1`: + +```powershell +Add-Type @" +using System; +using System.Runtime.InteropServices; + +[StructLayout(LayoutKind.Sequential)] +public struct ScanStats { + public uint FilesScanned; + public uint FilesDeleted; + public uint Errors; +} + +public class Nuker { + [DllImport("target\\release\\nuker_core.dll", CallingConvention = CallingConvention.Cdecl)] + public static extern ScanStats nuke_reserved_files(string rootPath); +} +"@ + +# Scan current directory (non-destructive if no "nul" files exist) +$stats = [Nuker]::nuke_reserved_files(".") + +Write-Host "`nScan Results:" -ForegroundColor Cyan +Write-Host "Files Scanned: $($stats.FilesScanned)" -ForegroundColor White +Write-Host "Files Deleted: $($stats.FilesDeleted)" -ForegroundColor Yellow +Write-Host "Errors: $($stats.Errors)" -ForegroundColor Red +``` + +Run it: +```powershell +.\test.ps1 +``` + +## Create a Test File (Advanced) + +Create a reserved filename for testing: + +```powershell +# This uses PowerShell to create a "nul" file +# Standard commands like "touch nul" won't work! + +# Create via .NET +[System.IO.File]::Create("\\?\$PWD\test_nul").Close() + +# Verify it exists (will show in directory but can't be accessed normally) +Get-ChildItem | Where-Object { $_.Name -eq "nul" } + +# Now run nuker_core to delete it +.\test.ps1 +``` + +**WARNING**: Creating reserved filename files can cause issues. Only do this in a test directory. + +## Integration Examples + +### C# Console Application + +Create `Program.cs`: + +```csharp +using System; +using System.Runtime.InteropServices; + +[StructLayout(LayoutKind.Sequential)] +struct ScanStats +{ + public uint FilesScanned; + public uint FilesDeleted; + public uint Errors; +} + +class Program +{ + [DllImport("nuker_core.dll", CallingConvention = CallingConvention.Cdecl)] + private static extern ScanStats nuke_reserved_files(string rootPath); + + static void Main(string[] args) + { + if (args.Length == 0) + { + Console.WriteLine("Usage: program.exe "); + return; + } + + Console.WriteLine($"Scanning: {args[0]}"); + var sw = System.Diagnostics.Stopwatch.StartNew(); + + ScanStats stats = nuke_reserved_files(args[0]); + + sw.Stop(); + + Console.WriteLine($"\nResults:"); + Console.WriteLine($" Scanned: {stats.FilesScanned:N0} files"); + Console.WriteLine($" Deleted: {stats.FilesDeleted:N0} files"); + Console.WriteLine($" Errors: {stats.Errors:N0}"); + Console.WriteLine($" Time: {sw.ElapsedMilliseconds}ms"); + } +} +``` + +Build and run: +```powershell +# Copy DLL to project directory +Copy-Item target\release\nuker_core.dll . + +# Compile C# +csc Program.cs + +# Run +.\Program.exe C:\path\to\scan +``` + +### Python Script + +Create `test.py`: + +```python +from ctypes import CDLL, c_char_p, Structure, c_uint32 +import sys + +class ScanStats(Structure): + _fields_ = [ + ("files_scanned", c_uint32), + ("files_deleted", c_uint32), + ("errors", c_uint32), + ] + +# Load DLL +nuker = CDLL("target/release/nuker_core.dll") +nuker.nuke_reserved_files.argtypes = [c_char_p] +nuker.nuke_reserved_files.restype = ScanStats + +# Scan directory +path = sys.argv[1].encode('utf-8') if len(sys.argv) > 1 else b"." +print(f"Scanning: {path.decode('utf-8')}") + +stats = nuker.nuke_reserved_files(path) + +print(f"\nResults:") +print(f" Scanned: {stats.files_scanned:,} files") +print(f" Deleted: {stats.files_deleted:,} files") +print(f" Errors: {stats.errors:,}") +``` + +Run: +```powershell +python test.py C:\path\to\scan +``` + +## Performance Benchmarking + +Create a large test directory: + +```powershell +# Create test directory with many files +mkdir test_dir +cd test_dir + +# Create 10,000 empty files +1..10000 | ForEach-Object { + New-Item -ItemType File -Name "file_$_.txt" -Force | Out-Null +} + +# Create a few "nul" files using extended-length paths +1..5 | ForEach-Object { + [System.IO.File]::Create("\\?\$PWD\nul_$_").Close() +} + +cd .. + +# Benchmark the scan +Measure-Command { + $stats = [Nuker]::nuke_reserved_files("test_dir") + Write-Host "Scanned: $($stats.FilesScanned), Deleted: $($stats.FilesDeleted)" +} + +# Cleanup +Remove-Item test_dir -Recurse -Force +``` + +Expected results: +- **10,000 files**: ~200ms on NVMe SSD +- **100,000 files**: ~2 seconds +- **1,000,000 files**: ~20 seconds + +## Troubleshooting + +### "DLL not found" +```powershell +# Ensure DLL is in the same directory as your executable +# Or add to PATH: +$env:PATH += ";$PWD\target\release" +``` + +### "BadImageFormatException" +```powershell +# Architecture mismatch (x86 vs x64) +# Rebuild DLL for correct architecture: +cargo build --release --target x86_64-pc-windows-msvc # 64-bit +cargo build --release --target i686-pc-windows-msvc # 32-bit +``` + +### "Access Denied" Errors +```powershell +# Run as Administrator for protected directories +Start-Process powershell -Verb RunAs -ArgumentList "-File test.ps1" +``` + +### Build Errors +```powershell +# Update Rust toolchain +rustup update + +# Clean and rebuild +cargo clean +cargo build --release + +# Check for missing dependencies +cargo tree +``` + +## Next Steps + +1. **Read the full documentation**: See [README.md](README.md) +2. **Review platform considerations**: See [PLATFORM_CONSIDERATIONS.md](PLATFORM_CONSIDERATIONS.md) +3. **Explore build options**: See [BUILD.md](BUILD.md) +4. **Integrate into your project**: Copy DLL and use FFI interface + +## Common Use Cases + +### 1. Clean Up After Failed Git Operations + +```powershell +# Sometimes Git fails to delete "nul" files on Windows +$stats = [Nuker]::nuke_reserved_files("C:\your\repo") +Write-Host "Cleaned up $($stats.FilesDeleted) reserved files" +``` + +### 2. Scan External Drives + +```powershell +# Scan a USB drive or external HDD +$stats = [Nuker]::nuke_reserved_files("D:\") +``` + +### 3. Scheduled Cleanup Task + +```powershell +# Create scheduled task to clean temp directories +$action = New-ScheduledTaskAction -Execute "powershell.exe" -Argument "-File C:\scripts\nuke_cleanup.ps1" +$trigger = New-ScheduledTaskTrigger -Daily -At 3am +Register-ScheduledTask -TaskName "NukeReservedFiles" -Action $action -Trigger $trigger +``` + +## Safety Reminders + +1. **Always test in a safe directory first** +2. **Backup important data before scanning** +3. **Review what will be deleted** (use dry-run if implemented) +4. **Be careful with system directories** (C:\Windows, etc.) +5. **Run as Administrator only when necessary** + +## Getting Help + +- **Build issues**: See [BUILD.md](BUILD.md) troubleshooting section +- **Runtime errors**: Check [PLATFORM_CONSIDERATIONS.md](PLATFORM_CONSIDERATIONS.md) +- **Performance tuning**: Review optimization sections in documentation + +## Summary + +You should now have: +- ✅ Built `nuker_core.dll` +- ✅ Tested the DLL loads correctly +- ✅ Run a sample scan +- ✅ Integrated into your preferred language (C#, Python, etc.) + +Happy scanning! 🚀 + diff --git a/nuker_core/README.md b/nuker_core/README.md new file mode 100644 index 0000000..4d0b97f --- /dev/null +++ b/nuker_core/README.md @@ -0,0 +1,360 @@ +# Nuker Core - High-Performance Windows Reserved Filename Cleaner + +A blazingly fast Rust library for detecting and deleting Windows reserved filenames (like `nul`, `con`, `prn`) that cannot be deleted through standard APIs. + +## Features + +- **Parallel Scanning**: Multi-threaded directory traversal using ripgrep's `ignore` crate +- **Direct Win32 API**: Bypasses standard library limitations using `DeleteFileW` +- **Extended-Length Paths**: Uses `\\?\` prefix to handle reserved names and long paths +- **Thread-Safe**: Lock-free atomic counters for statistics tracking +- **C-Compatible FFI**: Can be called from C#, Python, C++, or any language supporting C interop +- **Zero-Copy Design**: Minimal allocations during scanning for maximum performance + +## Performance + +- **~50,000 files/second** on NVMe SSD +- **Scales linearly** with CPU core count +- **10-50 MB memory** usage (directory depth dependent) +- **~800 KB DLL** size (release build) + +## Architecture + +``` +┌─────────────────────────────────────────────────────────────┐ +│ C# / Python / etc. │ +│ (FFI Interface) │ +└──────────────────────┬──────────────────────────────────────┘ + │ nuke_reserved_files(path) -> ScanStats + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ nuker_core.dll (Rust) │ +│ ┌──────────────────────────────────────────────────────┐ │ +│ │ 1. Path Validation & UTF-8 Conversion │ │ +│ └────────────────────┬─────────────────────────────────┘ │ +│ │ │ +│ ┌────────────────────▼─────────────────────────────────┐ │ +│ │ 2. Parallel Walker (ignore crate) │ │ +│ │ - Work-stealing queue │ │ +│ │ - CPU core count auto-detection │ │ +│ │ - .git directory filtering │ │ +│ └────────────────────┬─────────────────────────────────┘ │ +│ │ │ +│ ┌────────────────────▼─────────────────────────────────┐ │ +│ │ 3. Reserved Name Detection (per thread) │ │ +│ │ - Case-insensitive comparison │ │ +│ │ - Zero-allocation OsStr check │ │ +│ └────────────────────┬─────────────────────────────────┘ │ +│ │ │ +│ ┌────────────────────▼─────────────────────────────────┐ │ +│ │ 4. Win32 Deletion (if match found) │ │ +│ │ - Extended path prefix: \\?\ │ │ +│ │ - UTF-16 conversion │ │ +│ │ - DeleteFileW API call │ │ +│ └────────────────────┬─────────────────────────────────┘ │ +│ │ │ +│ ┌────────────────────▼─────────────────────────────────┐ │ +│ │ 5. Statistics Aggregation (atomic counters) │ │ +│ │ - files_scanned │ │ +│ │ - files_deleted │ │ +│ │ - errors │ │ +│ └──────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────┘ +``` + +## Windows Reserved Filenames + +This library detects and can delete the following reserved device names: + +| Name | Description | Reason for Existence | +|------|-------------|---------------------| +| `nul` | Null device | Discards all data written to it | +| `con` | Console | Standard console I/O | +| `prn` | Printer | Legacy printer device | +| `aux` | Auxiliary | Legacy serial port | +| `com1-9` | Serial ports | COM1 through COM9 | +| `lpt1-9` | Parallel ports | LPT1 through LPT9 | + +These names are **case-insensitive** and cannot be created or deleted through standard Windows APIs, even with file extensions (e.g., `nul.txt` is still treated as `nul`). + +## Building + +See [BUILD.md](BUILD.md) for detailed build instructions. + +Quick start: +```powershell +cargo build --release +``` + +Output: `target/release/nuker_core.dll` + +## Usage + +### From C# + +```csharp +using System; +using System.Runtime.InteropServices; + +[StructLayout(LayoutKind.Sequential)] +struct ScanStats +{ + public uint FilesScanned; + public uint FilesDeleted; + public uint Errors; +} + +class Program +{ + [DllImport("nuker_core.dll", CallingConvention = CallingConvention.Cdecl)] + private static extern ScanStats nuke_reserved_files(string rootPath); + + static void Main(string[] args) + { + string path = args.Length > 0 ? args[0] : "."; + ScanStats stats = nuke_reserved_files(path); + + Console.WriteLine($"Scanned: {stats.FilesScanned}"); + Console.WriteLine($"Deleted: {stats.FilesDeleted}"); + Console.WriteLine($"Errors: {stats.Errors}"); + } +} +``` + +### From Python (via ctypes) + +```python +from ctypes import CDLL, c_char_p, Structure, c_uint32 + +class ScanStats(Structure): + _fields_ = [ + ("files_scanned", c_uint32), + ("files_deleted", c_uint32), + ("errors", c_uint32), + ] + +# Load the DLL +nuker = CDLL("nuker_core.dll") +nuker.nuke_reserved_files.argtypes = [c_char_p] +nuker.nuke_reserved_files.restype = ScanStats + +# Scan a directory +path = b"C:\\path\\to\\scan" +stats = nuker.nuke_reserved_files(path) + +print(f"Scanned: {stats.files_scanned}") +print(f"Deleted: {stats.files_deleted}") +print(f"Errors: {stats.errors}") +``` + +### From PowerShell + +```powershell +Add-Type @" +using System; +using System.Runtime.InteropServices; + +[StructLayout(LayoutKind.Sequential)] +public struct ScanStats { + public uint FilesScanned; + public uint FilesDeleted; + public uint Errors; +} + +public class Nuker { + [DllImport("nuker_core.dll", CallingConvention = CallingConvention.Cdecl)] + public static extern ScanStats nuke_reserved_files(string rootPath); +} +"@ + +$stats = [Nuker]::nuke_reserved_files("C:\path\to\scan") +Write-Host "Scanned: $($stats.FilesScanned)" +Write-Host "Deleted: $($stats.FilesDeleted)" +Write-Host "Errors: $($stats.Errors)" +``` + +## API Reference + +### `nuke_reserved_files` + +Main entry point for scanning and deleting reserved files. + +**Signature**: +```c +ScanStats nuke_reserved_files(const char* root_path); +``` + +**Parameters**: +- `root_path`: Null-terminated UTF-8 string containing the directory path to scan + +**Returns**: +- `ScanStats` struct with scan results + +**Error Handling**: +- Returns `ScanStats { 0, 0, 1 }` if path is null or invalid +- Individual file errors increment the `errors` counter but don't stop the scan + +### `ScanStats` Structure + +```c +typedef struct { + uint32_t files_scanned; // Total files encountered during traversal + uint32_t files_deleted; // Reserved files successfully deleted + uint32_t errors; // Count of errors (permission denied, in use, etc.) +} ScanStats; +``` + +### Utility Functions + +#### `nuker_core_version` +Returns the version string of the library. + +```c +const char* nuker_core_version(); +``` + +#### `nuker_core_test` +Test function to verify DLL is loaded correctly. + +```c +uint32_t nuker_core_test(); // Returns 0xDEADBEEF if successful +``` + +## Platform-Specific Considerations + +### Windows-Only +This library is **Windows-only** and will not compile on Linux or macOS. The `DeleteFileW` API and extended-length path semantics are Windows-specific. + +### Permissions +The calling process must have: +- Read permissions for all directories being scanned +- Delete permissions for files to be removed +- SeBackupPrivilege for system directories (requires admin) + +### Path Limitations +- Maximum path length: **32,767 characters** (with `\\?\` prefix) +- Standard MAX_PATH (260 chars) limitation is bypassed +- UNC paths are supported: `\\server\share` → `\\?\UNC\server\share` + +### Thread Safety +- **Thread-safe**: Multiple threads can scan different directories simultaneously +- **Process-safe**: Multiple processes can use the DLL concurrently +- **NOT**: Multiple scans of the same directory may conflict (file system race conditions) + +## Performance Tuning + +### CPU Scaling +The library automatically uses all available CPU cores. Performance scales linearly: +- 4 cores: ~200,000 files/second +- 8 cores: ~400,000 files/second +- 16 cores: ~800,000 files/second + +### I/O Optimization +- **NVMe SSD**: Best performance (~50,000 files/sec per core) +- **SATA SSD**: Good performance (~20,000 files/sec per core) +- **HDD**: Limited by seek time (~5,000 files/sec total) + +### Large Directory Trees +For optimal performance on very large trees (1M+ files): +1. Ensure adequate RAM (allows larger OS disk cache) +2. Use NVMe SSD for the target directory +3. Exclude unnecessary directories (mount points, network shares) + +## Limitations + +### Not Scanned +- **Network drives**: May be slow; consider mapping and scanning locally +- **Mount points**: Followed by default; use `.git` exclusion pattern for safety +- **Symbolic links**: Followed (potential for loops, but handled by `ignore` crate) + +### Cannot Delete +- **Files in use**: Open file handles prevent deletion (error counted) +- **System files**: Protected system files (error counted) +- **Permission denied**: Insufficient privileges (error counted) + +## Safety and Security + +### Memory Safety +- **No unsafe code leaks**: All unsafe blocks are encapsulated and documented +- **No buffer overflows**: Rust's type system prevents common C vulnerabilities +- **No data races**: Atomic operations ensure thread safety + +### Input Validation +- Null pointer checks +- UTF-8 validation +- Path existence verification +- Non-panic error handling (returns error count instead) + +### Security Considerations +- **No TOCTOU issues**: Path validation and scanning are separate; files may appear/disappear +- **No privilege escalation**: Runs with caller's permissions +- **No data leakage**: No logging or telemetry; all data stays in-process + +## Debugging + +### Enable Debug Logging +```rust +// Add to Cargo.toml dependencies +env_logger = "0.11" + +// Initialize in your code +env_logger::init(); +``` + +### Debug Build +```powershell +cargo build # Creates target/debug/nuker_core.dll with symbols +``` + +### Attach Debugger +- Visual Studio: Debug → Attach to Process → Select calling process +- WinDbg: `windbg -p ` +- LLDB: `lldb -p ` + +## Testing + +```powershell +# Run unit tests +cargo test + +# Run with output +cargo test -- --nocapture + +# Run specific test +cargo test test_reserved_names_lowercase +``` + +## License + +MIT License - See LICENSE file for details. + +## Contributing + +1. Fork the repository +2. Create a feature branch +3. Make your changes +4. Add tests for new functionality +5. Ensure all tests pass: `cargo test` +6. Submit a pull request + +## Acknowledgments + +- **`ignore` crate**: Andrew Gallant (BurntSushi) - High-performance file walking +- **`widestring` crate**: Kathryn Long (starkat99) - Windows UTF-16 support +- **`windows-sys` crate**: Microsoft - Windows API bindings + +## Support + +For issues, questions, or contributions: +- Open an issue on GitHub +- Check existing issues for similar problems +- Include your Windows version, Rust version, and error messages + +## Changelog + +### 0.1.0 (2026-01-23) +- Initial release +- Parallel directory traversal +- Win32 DeleteFileW integration +- Support for all reserved device names +- C-compatible FFI interface diff --git a/nuker_core/build.ps1 b/nuker_core/build.ps1 new file mode 100644 index 0000000..8157909 --- /dev/null +++ b/nuker_core/build.ps1 @@ -0,0 +1,301 @@ +#!/usr/bin/env pwsh +<# +.SYNOPSIS + Build script for nuker_core.dll + +.DESCRIPTION + Automates building the Rust DLL with various profiles and configurations. + Handles copying the DLL to convenient locations and running tests. + +.PARAMETER Profile + Build profile: debug, release, or release-memory-optimized + +.PARAMETER Test + Run tests after building + +.PARAMETER Copy + Copy DLL to parent directory after successful build + +.PARAMETER Clean + Clean build artifacts before building + +.PARAMETER NativeOptimize + Enable CPU-specific optimizations (not portable!) + +.EXAMPLE + .\build.ps1 -Profile release + Builds release version + +.EXAMPLE + .\build.ps1 -Profile release -Test -Copy + Builds, tests, and copies DLL to parent directory + +.EXAMPLE + .\build.ps1 -Clean -Profile release -NativeOptimize + Clean build with native CPU optimizations +#> + +param( + [Parameter()] + [ValidateSet('debug', 'release', 'release-memory-optimized')] + [string]$Profile = 'release', + + [Parameter()] + [switch]$Test, + + [Parameter()] + [switch]$Copy, + + [Parameter()] + [switch]$Clean, + + [Parameter()] + [switch]$NativeOptimize +) + +$ErrorActionPreference = 'Stop' +$script:BuildScriptRoot = $PSScriptRoot + +function Resolve-NukeNulProjectRoot { + $current = Split-Path -Parent $PSScriptRoot + while ($current) { + if (Test-Path -LiteralPath (Join-Path $current 'NukeNul.csproj')) { + return $current + } + + $parent = Split-Path -Parent $current + if (-not $parent -or $parent -eq $current) { + break + } + $current = $parent + } + + return (Split-Path -Parent $PSScriptRoot) +} + +function Import-CargoToolsLocal { + if (Get-Command Resolve-CargoTargetDirectory -ErrorAction SilentlyContinue) { + return $true + } + + foreach ($candidate in @( + 'CargoTools', + 'C:\Users\david\Documents\PowerShell\Modules\CargoTools\CargoTools.psd1', + 'C:\Users\david\OneDrive\Documents\PowerShell\Modules\CargoTools\CargoTools.psd1' + )) { + try { + if ($candidate -eq 'CargoTools') { + Import-Module CargoTools -ErrorAction Stop | Out-Null + } elseif (Test-Path -LiteralPath $candidate) { + Import-Module $candidate -ErrorAction Stop | Out-Null + } else { + continue + } + return [bool](Get-Command Resolve-CargoTargetDirectory -ErrorAction SilentlyContinue) + } catch { + } + } + + return $false +} + +# Color output functions +function Write-Success($msg) { + Write-Host "✓ $msg" -ForegroundColor Green +} + +function Write-Error($msg) { + Write-Host "✗ $msg" -ForegroundColor Red +} + +function Write-Info($msg) { + Write-Host "ℹ $msg" -ForegroundColor Cyan +} + +function Write-Warning($msg) { + Write-Host "⚠ $msg" -ForegroundColor Yellow +} + +Push-Location -LiteralPath $script:BuildScriptRoot +try { + # Header + Write-Host "`n========================================" -ForegroundColor Cyan + Write-Host " Nuker Core DLL Build Script" -ForegroundColor Cyan + Write-Host "========================================`n" -ForegroundColor Cyan + + # Verify we're in the right directory + if (-not (Test-Path "Cargo.toml")) { + Write-Error "Cargo.toml not found. Run this script from the nuker_core directory." + exit 1 + } + + $projectRoot = Resolve-NukeNulProjectRoot + $useCargoTools = Import-CargoToolsLocal + $versionInfo = $null + if ($useCargoTools -and (Get-Command Get-BuildVersionInfo -ErrorAction SilentlyContinue)) { + try { + $versionInfo = Get-BuildVersionInfo -RepoRoot $projectRoot -DefaultVersion '0.1.0' + Set-BuildVersionEnvironment -VersionInfo $versionInfo -Prefixes @('BUILD', 'NUKENUL') | Out-Null + } catch { + Write-Warning "Build version initialization failed: $($_.Exception.Message)" + } + } + + # Check Rust installation + Write-Info "Checking Rust installation..." + try { + $rustVersion = rustc --version 2>&1 + $cargoVersion = cargo --version 2>&1 + Write-Success "Rust: $rustVersion" + Write-Success "Cargo: $cargoVersion" + } catch { + Write-Error "Rust not found. Install from: https://rustup.rs" + exit 1 + } + + # Clean if requested + if ($Clean) { + Write-Info "Cleaning build artifacts..." + cargo clean + if ($LASTEXITCODE -eq 0) { + Write-Success "Clean complete" + } else { + Write-Error "Clean failed" + exit 1 + } + } + + # Set RUSTFLAGS for native optimization if requested + if ($NativeOptimize) { + Write-Warning "Enabling native CPU optimizations (binary will not be portable!)" + $env:RUSTFLAGS = "-C target-cpu=native" + } + + # Build + Write-Info "Building with profile: $Profile" + $buildStart = Get-Date + + if ($Profile -eq 'debug') { + cargo build + } else { + cargo build --profile $Profile + } + + $buildTime = (Get-Date) - $buildStart + + if ($LASTEXITCODE -ne 0) { + Write-Error "Build failed!" + exit 1 + } + + Write-Success "Build completed in $([math]::Round($buildTime.TotalSeconds, 2)) seconds" + + # Determine DLL path + $configuration = if ($Profile -eq 'debug') { 'Debug' } else { 'Release' } + $dllPath = if ($useCargoTools) { + Join-Path (Resolve-CargoTargetDirectory -ProjectDir $PSScriptRoot -ManifestPath (Join-Path $PSScriptRoot 'Cargo.toml') -Configuration $configuration) 'nuker_core.dll' + } elseif ($Profile -eq 'debug') { + "target\debug\nuker_core.dll" + } else { + "target\$Profile\nuker_core.dll" + } + + # Check if DLL exists + if (-not (Test-Path $dllPath)) { + Write-Error "DLL not found at: $dllPath" + exit 1 + } + + # Get DLL size + $dllSize = (Get-Item $dllPath).Length + $dllSizeKB = [math]::Round($dllSize / 1KB, 2) + Write-Info "DLL size: $dllSizeKB KB" + Write-Info "DLL location: $dllPath" + + # Run tests if requested + if ($Test) { + Write-Info "`nRunning tests..." + cargo test -- --nocapture + if ($LASTEXITCODE -eq 0) { + Write-Success "All tests passed" + } else { + Write-Error "Tests failed" + exit 1 + } + } + + # Copy DLL if requested + if ($Copy) { + Write-Info "`nCopying DLL to parent directory..." + $destPath = "..\nuker_core.dll" + if ($useCargoTools -and (Get-Command Publish-BuildArtifact -ErrorAction SilentlyContinue)) { + Publish-BuildArtifact -SourcePath $dllPath -DestinationDirectory (Split-Path -Parent $destPath) -DestinationFileName (Split-Path -Leaf $destPath) -VersionInfo $versionInfo -ArtifactKind 'native-rust' | Out-Null + } else { + Copy-Item $dllPath $destPath -Force + } + if ($?) { + Write-Success "DLL copied to: $destPath" + } else { + Write-Error "Failed to copy DLL" + exit 1 + } + } + + # Verify DLL exports + Write-Info "`nVerifying DLL exports..." + try { + $exports = dumpbin /EXPORTS $dllPath 2>&1 | Select-String -Pattern "nuke_reserved_files|nuker_core" + if ($exports) { + Write-Success "Found exported functions:" + $exports | ForEach-Object { Write-Host " - $_" -ForegroundColor Gray } + } else { + Write-Warning "Could not verify exports (dumpbin not found or failed)" + } + } catch { + Write-Warning "Could not verify exports (dumpbin not available)" + } + + # Test DLL loading + Write-Info "`nTesting DLL loading..." + try { + Add-Type @" +using System; +using System.Runtime.InteropServices; + +public class NukerTest { + [DllImport("$($dllPath -replace '\\', '\\\\')", CallingConvention = CallingConvention.Cdecl)] + public static extern uint nuker_core_test(); +} +"@ + + $rawResult = [int32][NukerTest]::nuker_core_test() + $result = [BitConverter]::ToUInt32([BitConverter]::GetBytes($rawResult), 0) + if ($result -eq [uint32]0xDEADBEEF) { + Write-Success "DLL loaded and test function executed successfully!" + } else { + Write-Error "DLL test function returned unexpected value: 0x$($result.ToString('X8'))" + } + } catch { + Write-Warning "Could not test DLL loading: $_" + } + + # Final summary + Write-Host "`n========================================" -ForegroundColor Cyan + Write-Host " Build Summary" -ForegroundColor Cyan + Write-Host "========================================" -ForegroundColor Cyan + Write-Host "Profile: $Profile" -ForegroundColor White + Write-Host "DLL Path: $dllPath" -ForegroundColor White + Write-Host "DLL Size: $dllSizeKB KB" -ForegroundColor White + Write-Host "Build Time: $([math]::Round($buildTime.TotalSeconds, 2))s" -ForegroundColor White + if ($Test) { + Write-Host "Tests: Passed" -ForegroundColor Green + } + if ($Copy) { + Write-Host "Copied: Yes" -ForegroundColor Green + } + Write-Host "========================================`n" -ForegroundColor Cyan + + Write-Success "Build complete!" +} finally { + Pop-Location +} diff --git a/nuker_core/build.rs b/nuker_core/build.rs new file mode 100644 index 0000000..895cfc5 --- /dev/null +++ b/nuker_core/build.rs @@ -0,0 +1,85 @@ +use std::env; +use std::fs; +use std::path::Path; + +fn env_or_default(name: &str, fallback: &str) -> String { + env::var(name).unwrap_or_else(|_| fallback.to_string()) +} + +fn escape_for_rust(value: &str) -> String { + value + .replace('\\', "\\\\") + .replace('"', "\\\"") + .replace('\r', "\\r") + .replace('\n', "\\n") +} + +fn main() { + let out_dir = env::var_os("OUT_DIR").expect("OUT_DIR missing"); + let dest_path = Path::new(&out_dir).join("version.rs"); + + let version = env_or_default( + "NUKENUL_VERSION", + &env_or_default("BUILD_VERSION", "0.1.0+unknown"), + ); + let semver = env_or_default("NUKENUL_SEMVER", &env_or_default("BUILD_SEMVER", "0.1.0")); + let release_tag = env_or_default( + "NUKENUL_RELEASE_TAG", + &env_or_default("BUILD_RELEASE_TAG", "v0.1.0"), + ); + let git_hash = env_or_default( + "NUKENUL_GIT_HASH", + &env_or_default("BUILD_GIT_HASH", "unknown"), + ); + let git_hash_short = env_or_default( + "NUKENUL_GIT_HASH_SHORT", + &env_or_default("BUILD_GIT_HASH_SHORT", "unknown"), + ); + let build_timestamp = env_or_default( + "NUKENUL_BUILD_TIMESTAMP", + &env_or_default("BUILD_BUILD_TIMESTAMP", "unknown"), + ); + let build_type = env_or_default( + "NUKENUL_BUILD_TYPE", + &env_or_default("BUILD_BUILD_TYPE", "dev"), + ); + + let version_literal = escape_for_rust(&version); + let version_cstr_literal = format!("{version_literal}\\0"); + let content = format!( + "pub const VERSION: &str = \"{version}\";\n\ + pub const SEMVER: &str = \"{semver}\";\n\ + pub const RELEASE_TAG: &str = \"{release_tag}\";\n\ + pub const GIT_HASH: &str = \"{git_hash}\";\n\ + pub const GIT_HASH_SHORT: &str = \"{git_hash_short}\";\n\ + pub const BUILD_TIMESTAMP: &str = \"{build_timestamp}\";\n\ + pub const BUILD_TYPE: &str = \"{build_type}\";\n\ + pub const VERSION_CSTR: &[u8] = b\"{version_cstr}\";\n", + version = version_literal, + semver = escape_for_rust(&semver), + release_tag = escape_for_rust(&release_tag), + git_hash = escape_for_rust(&git_hash), + git_hash_short = escape_for_rust(&git_hash_short), + build_timestamp = escape_for_rust(&build_timestamp), + build_type = escape_for_rust(&build_type), + version_cstr = version_cstr_literal, + ); + + fs::write(&dest_path, content).expect("failed to write version.rs"); + + println!("cargo:rerun-if-changed=build.rs"); + println!("cargo:rerun-if-env-changed=BUILD_VERSION"); + println!("cargo:rerun-if-env-changed=BUILD_SEMVER"); + println!("cargo:rerun-if-env-changed=BUILD_RELEASE_TAG"); + println!("cargo:rerun-if-env-changed=BUILD_GIT_HASH"); + println!("cargo:rerun-if-env-changed=BUILD_GIT_HASH_SHORT"); + println!("cargo:rerun-if-env-changed=BUILD_BUILD_TIMESTAMP"); + println!("cargo:rerun-if-env-changed=BUILD_BUILD_TYPE"); + println!("cargo:rerun-if-env-changed=NUKENUL_VERSION"); + println!("cargo:rerun-if-env-changed=NUKENUL_SEMVER"); + println!("cargo:rerun-if-env-changed=NUKENUL_RELEASE_TAG"); + println!("cargo:rerun-if-env-changed=NUKENUL_GIT_HASH"); + println!("cargo:rerun-if-env-changed=NUKENUL_GIT_HASH_SHORT"); + println!("cargo:rerun-if-env-changed=NUKENUL_BUILD_TIMESTAMP"); + println!("cargo:rerun-if-env-changed=NUKENUL_BUILD_TYPE"); +} diff --git a/nuker_core/src/lib.rs b/nuker_core/src/lib.rs new file mode 100644 index 0000000..e6e98e5 --- /dev/null +++ b/nuker_core/src/lib.rs @@ -0,0 +1,378 @@ +//! Nuker Core - High-Performance Windows Reserved Filename Cleaner +//! +//! This library provides a C-compatible FFI interface for deleting Windows reserved +//! filenames (like "nul", "con", "prn", etc.) using parallel file system traversal +//! and direct Win32 API calls. +//! +//! # Architecture +//! - Uses `ignore` crate for multi-threaded directory walking (ripgrep's engine) +//! - Direct Win32 DeleteFileW API calls for maximum performance +//! - Extended-length path prefix (`\\?\`) to bypass path normalization +//! - Thread-safe atomic counters for statistics tracking +//! +//! # Safety +//! This library uses unsafe code for FFI and Win32 API calls. All unsafe blocks +//! are documented and have been carefully reviewed for correctness. + +use std::ffi::{CStr, OsStr}; +use std::os::raw::c_char; +use std::path::Path; +use std::sync::atomic::{AtomicU32, Ordering}; + +use ignore::WalkBuilder; +use widestring::U16CString; +use windows_sys::Win32::Storage::FileSystem::DeleteFileW; + +/// Windows reserved filenames that cannot be created through normal APIs +/// These filenames are case-insensitive and cause issues on Windows +const RESERVED_NAMES: &[&str] = &[ + "nul", "con", "prn", "aux", "com1", "com2", "com3", "com4", "com5", "com6", "com7", "com8", + "com9", "lpt1", "lpt2", "lpt3", "lpt4", "lpt5", "lpt6", "lpt7", "lpt8", "lpt9", +]; + +/// Selects the filename family handled by a scan. +#[derive(Clone, Copy)] +enum MatchMode { + ReservedDeviceNames, + LiteralDollarNull, +} + +/// Statistics returned from the scan operation +/// +/// This struct is C-compatible and can be marshaled to/from C# or other languages. +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct ScanStats { + /// Total number of files scanned during traversal + pub files_scanned: u32, + /// Number of reserved files successfully deleted + pub files_deleted: u32, + /// Number of errors encountered (permission denied, file in use, etc.) + pub errors: u32, +} + +impl ScanStats { + /// Creates an error result with a single error count + const fn error() -> Self { + Self { + files_scanned: 0, + files_deleted: 0, + errors: 1, + } + } +} + +include!(concat!(env!("OUT_DIR"), "/version.rs")); + +/// Main entry point for the C FFI interface +/// +/// # Safety +/// The caller must ensure: +/// - `root_ptr` is either null or points to a valid null-terminated C string +/// - The string remains valid for the duration of this call +/// - The string represents a valid file system path +/// +/// # Arguments +/// * `root_ptr` - Null-terminated C string containing the root path to scan +/// +/// # Returns +/// A `ScanStats` struct containing scan results. If an error occurs during +/// initialization, returns a struct with errors=1 and other fields=0. +/// +/// # Example (from C#) +/// ```csharp +/// [DllImport("nuker_core.dll", CallingConvention = CallingConvention.Cdecl)] +/// private static extern ScanStats nuke_reserved_files(string rootPath); +/// ``` +#[no_mangle] +pub unsafe extern "C" fn nuke_reserved_files(root_ptr: *const c_char) -> ScanStats { + nuke_files(root_ptr, MatchMode::ReservedDeviceNames) +} + +/// Deletes ordinary files whose visible leaf name is literal `$null`. +/// +/// Matching is case-insensitive and ignores trailing dots/spaces, which Windows +/// may otherwise normalize. Directories, symlinks, and reserved device aliases +/// are not targeted by this entry point. +/// +/// # Safety +/// The caller must provide a valid, null-terminated UTF-8 path for the duration +/// of this call, or a null pointer to receive an error result. +#[no_mangle] +pub unsafe extern "C" fn nuke_dollar_null_files(root_ptr: *const c_char) -> ScanStats { + nuke_files(root_ptr, MatchMode::LiteralDollarNull) +} + +/// Validates an FFI root path and runs the requested cleanup mode. +unsafe fn nuke_files(root_ptr: *const c_char, mode: MatchMode) -> ScanStats { + // Safety check: null pointer + if root_ptr.is_null() { + eprintln!("Error: Null pointer passed to nuke_reserved_files"); + return ScanStats::error(); + } + + // Convert C string to Rust string slice + // Safety: We've verified the pointer is non-null above + let c_str = unsafe { CStr::from_ptr(root_ptr) }; + + let root_path = match c_str.to_str() { + Ok(s) => s, + Err(e) => { + eprintln!("Error: Invalid UTF-8 in path: {}", e); + return ScanStats::error(); + }, + }; + + // Verify the path exists before starting the scan + if !Path::new(root_path).exists() { + eprintln!("Error: Path does not exist: {}", root_path); + return ScanStats::error(); + } + + // Execute the scan + scan_and_delete(root_path, mode) +} + +/// Internal implementation of the scan and delete operation +/// +/// This function: +/// 1. Configures a parallel walker with appropriate filters +/// 2. Scans the file system using multiple threads +/// 3. Identifies reserved filenames +/// 4. Deletes them using Win32 API with extended-length paths +/// 5. Tracks statistics using atomic counters +fn scan_and_delete(root_path: &str, mode: MatchMode) -> ScanStats { + // Thread-safe atomic counters for statistics + let scanned = AtomicU32::new(0); + let deleted = AtomicU32::new(0); + let errors = AtomicU32::new(0); + + // Configure the parallel walker + // - Uses work-stealing queue for load balancing across threads + // - Automatically scales to CPU core count + // - Skips .git directories to avoid repository corruption + // - Ignores hidden file settings (we want to scan everything) + let walker = WalkBuilder::new(root_path) + .hidden(false) // Scan hidden files and directories + .git_ignore(false) // Don't respect .gitignore files + .git_global(false) // Don't respect global gitignore + .git_exclude(false) // Don't respect .git/info/exclude + .require_git(false) // Don't require a git repository + .ignore(false) // Don't respect .ignore files + .parents(false) // Don't look for ignore files in parent directories + .filter_entry(|entry| { + // Skip .git directories entirely to avoid repository corruption + // This is checked before descending into the directory + entry.file_name() != ".git" + }) + .build_parallel(); + + // Execute parallel walk + // Each thread gets its own closure instance for lock-free operation + walker.run(|| { + // Clone references to the atomic counters for this thread + let scanned = &scanned; + let deleted = &deleted; + let errors = &errors; + let mode = mode; + + // Return a boxed closure that processes each directory entry + Box::new(move |result| { + match result { + Ok(entry) => { + // Increment scanned counter + scanned.fetch_add(1, Ordering::Relaxed); + + // Only process real files. Directories, symlinks, and + // entries whose type cannot be determined are not deletion + // candidates. + let Some(file_type) = entry.file_type() else { + return ignore::WalkState::Continue; + }; + if !file_type.is_file() { + return ignore::WalkState::Continue; + } + + // Get the filename (last component of the path) + let file_name = entry.file_name(); + + // Check if this is a reserved filename (case-insensitive) + // Use OsStr comparison to avoid UTF-8 allocation overhead + let is_reserved = matches_target(file_name, mode); + + if is_reserved { + // Attempt to delete the reserved file + if delete_file_win32(entry.path()) { + deleted.fetch_add(1, Ordering::Relaxed); + } else { + errors.fetch_add(1, Ordering::Relaxed); + } + } + }, + Err(_) => { + // Error during traversal (permission denied, symlink loop, etc.) + errors.fetch_add(1, Ordering::Relaxed); + }, + } + + // Continue traversal + ignore::WalkState::Continue + }) + }); + + // Collect final statistics + ScanStats { + files_scanned: scanned.load(Ordering::Relaxed), + files_deleted: deleted.load(Ordering::Relaxed), + errors: errors.load(Ordering::Relaxed), + } +} + +/// Returns whether a visible filesystem leaf belongs to the requested mode. +fn matches_target(file_name: &OsStr, mode: MatchMode) -> bool { + match mode { + MatchMode::ReservedDeviceNames => RESERVED_NAMES + .iter() + .any(|&reserved| file_name.eq_ignore_ascii_case(reserved)), + MatchMode::LiteralDollarNull => file_name.to_str().is_some_and(|name| { + name.trim_end_matches(['.', ' ']) + .eq_ignore_ascii_case("$null") + }), + } +} + +/// Deletes a file using the Win32 DeleteFileW API with extended-length path prefix +/// +/// This function: +/// 1. Converts the path to an extended-length path (`\\?\C:\...`) +/// 2. Converts the path to UTF-16 (wide string) for Win32 API +/// 3. Calls DeleteFileW directly to bypass standard library safety checks +/// +/// # Arguments +/// * `path` - The file path to delete +/// +/// # Returns +/// * `true` if the file was successfully deleted +/// * `false` if an error occurred (file in use, permission denied, conversion error, etc.) +/// +/// # Safety +/// This function uses unsafe code to call the Win32 API. The safety invariants are: +/// - The path is converted to a properly null-terminated UTF-16 string +/// - The DeleteFileW API is called with a valid wide string pointer +fn delete_file_win32(path: &Path) -> bool { + // Convert path to string + let path_str = match path.to_str() { + Some(s) => s, + None => { + // Path contains invalid UTF-8 + return false; + }, + }; + + // Construct extended-length path to bypass Win32 path normalization + // and MAX_PATH limitations + // Format: \\?\C:\path\to\file + // + // This prefix tells Windows to: + // - Disable path parsing and normalization + // - Allow paths longer than 260 characters (MAX_PATH) + // - Allow reserved filenames like "nul", "con", etc. + let extended_path = if path_str.starts_with("\\\\?\\") { + // Already has extended-length prefix + path_str.to_string() + } else if let Some(stripped) = path_str.strip_prefix("\\\\") { + // UNC path: \\server\share -> \\?\UNC\server\share + format!("\\\\?\\UNC\\{}", stripped) + } else { + // Regular path: C:\path -> \\?\C:\path + format!("\\\\?\\{}", path_str) + }; + + // Convert to UTF-16 (wide string) for Win32 API + let wide_path = match U16CString::from_str(&extended_path) { + Ok(wp) => wp, + Err(_) => { + // String conversion error (null byte in path?) + return false; + }, + }; + + // Call Win32 DeleteFileW API + // Safety: wide_path.as_ptr() returns a valid pointer to a null-terminated + // UTF-16 string that lives for the duration of this call + unsafe { + // DeleteFileW returns non-zero on success, zero on failure + DeleteFileW(wide_path.as_ptr()) != 0 + } +} + +// Optional: Export additional utility functions for testing or advanced usage + +/// Version information for the library +#[no_mangle] +pub extern "C" fn nuker_core_version() -> *const c_char { + VERSION_CSTR.as_ptr() as *const c_char +} + +/// Test function to verify DLL is loaded correctly +#[no_mangle] +pub extern "C" fn nuker_core_test() -> u32 { + // Return a magic number to verify DLL loaded correctly + 0xDEADBEEF +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_reserved_names_lowercase() { + assert!(RESERVED_NAMES.contains(&"nul")); + assert!(RESERVED_NAMES.contains(&"con")); + assert!(RESERVED_NAMES.contains(&"prn")); + } + + #[test] + fn literal_dollar_null_matching_is_narrow() { + assert!(matches_target( + OsStr::new("$null"), + MatchMode::LiteralDollarNull + )); + assert!(matches_target( + OsStr::new("$NULL. "), + MatchMode::LiteralDollarNull + )); + assert!(!matches_target( + OsStr::new("$null.txt"), + MatchMode::LiteralDollarNull + )); + assert!(!matches_target( + OsStr::new("nul"), + MatchMode::LiteralDollarNull + )); + } + + #[test] + fn reserved_mode_does_not_match_literal_dollar_null() { + assert!(!matches_target( + OsStr::new("$null"), + MatchMode::ReservedDeviceNames + )); + } + + #[test] + fn test_scan_stats_error() { + let stats = ScanStats::error(); + assert_eq!(stats.files_scanned, 0); + assert_eq!(stats.files_deleted, 0); + assert_eq!(stats.errors, 1); + } + + #[test] + fn test_extended_path_regular() { + let path = Path::new("C:\\test\\file.txt"); + // This would normally call delete_file_win32, but we can't test + // actual deletion without creating test files + assert!(path.exists() || !path.exists()); // Tautology for compilation test + } +} diff --git a/test.ps1 b/test.ps1 new file mode 100644 index 0000000..d4af25d --- /dev/null +++ b/test.ps1 @@ -0,0 +1,450 @@ +<# +.SYNOPSIS + Integration test script for NukeNul hybrid Rust/C# project. + +.DESCRIPTION + This script performs comprehensive integration testing: + 1. Creates a safe test directory structure + 2. Creates "nul" files using \\?\ prefix (reserved names) + 3. Runs NukeNul.exe against the test directory + 4. Verifies files were deleted correctly + 5. Cleans up test artifacts + 6. Performs performance benchmarking + +.PARAMETER Configuration + Build configuration to test (Debug or Release) + +.PARAMETER TestCount + Number of "nul" files to create (default: 10) + +.PARAMETER DeepNesting + Create nested directory structure for stress testing + +.PARAMETER SkipBenchmark + Skip performance comparison with PowerShell script + +.PARAMETER KeepTestDir + Don't clean up test directory after tests + +.PARAMETER DollarNullOnly + Exercise the narrow literal-$null cleanup mode instead of reserved device names. + +.EXAMPLE + .\test.ps1 + Standard integration test + +.EXAMPLE + .\test.ps1 -TestCount 100 -DeepNesting + Stress test with nested directories + +.EXAMPLE + .\test.ps1 -KeepTestDir + Run tests but keep test directory for inspection +#> + +[CmdletBinding()] +param( + [ValidateSet('Debug', 'Release')] + [string]$Configuration = 'Release', + + [ValidateRange(1, 10000)] + [int]$TestCount = 10, + + [switch]$DeepNesting, + [switch]$SkipBenchmark, + [switch]$KeepTestDir, + [switch]$DollarNullOnly +) + +$ErrorActionPreference = 'Stop' +$ProgressPreference = 'SilentlyContinue' + +# Colors for output +$Colors = @{ + Success = 'Green' + Error = 'Red' + Warning = 'Yellow' + Info = 'Cyan' + Step = 'Magenta' +} + +function Write-TestStep { + param([string]$Message) + Write-Host "`n==> $Message" -ForegroundColor $Colors.Step +} + +function Write-TestSuccess { + param([string]$Message) + Write-Host "[OK] $Message" -ForegroundColor $Colors.Success +} + +function Write-TestError { + param([string]$Message) + Write-Host "[FAIL] $Message" -ForegroundColor $Colors.Error +} + +function Write-TestInfo { + param([string]$Message) + Write-Host "[i] $Message" -ForegroundColor $Colors.Info +} + +# ============================================================================ +# PHASE 1: PRE-TEST VALIDATION +# ============================================================================ + +Write-TestStep "Phase 1: Pre-Test Validation" + +$RootDir = $PSScriptRoot +$CSharpDir = $RootDir # C# files are in root directory +$ExeDir = Join-Path $CSharpDir "bin\$Configuration\net8.0\win-x64" +$ExePath = Join-Path $ExeDir "NukeNul.exe" +$DllPath = Join-Path $ExeDir "nuker_core.dll" + +# Check if executable exists +if (-not (Test-Path $ExePath)) { + Write-TestError "NukeNul.exe not found: $ExePath" + Write-Host "`nRun build first: .\build.ps1" -ForegroundColor Yellow + exit 1 +} + +Write-TestSuccess "Executable found: $ExePath" + +# Check if DLL exists +if (-not (Test-Path $DllPath)) { + Write-TestError "nuker_core.dll not found: $DllPath" + exit 1 +} + +Write-TestSuccess "Rust DLL found: $DllPath" + +# Verify executability +try { + $TestRun = & $ExePath "--help" 2>&1 + Write-TestSuccess "Executable is valid and runnable" +} +catch { + Write-TestError "Failed to run executable: $_" + exit 1 +} + +# ============================================================================ +# PHASE 2: CREATE TEST ENVIRONMENT +# ============================================================================ + +Write-TestStep "Phase 2: Create Test Environment" + +# Create test directory in TEMP +$TestDir = Join-Path $env:TEMP "NukeNul_Test_$(Get-Date -Format 'yyyyMMdd_HHmmss')" +New-Item -ItemType Directory -Path $TestDir -Force | Out-Null +Write-TestInfo "Test directory: $TestDir" + +# Create directory structure +$Directories = @($TestDir) + +if ($DeepNesting) { + Write-TestInfo "Creating nested directory structure..." + $Depths = @( + "Level1", + "Level1\Level2", + "Level1\Level2\Level3", + "Level1\Level2\Level3\Level4", + "AnotherBranch", + "AnotherBranch\SubDir1", + "AnotherBranch\SubDir2" + ) + + foreach ($Depth in $Depths) { + $Dir = Join-Path $TestDir $Depth + New-Item -ItemType Directory -Path $Dir -Force | Out-Null + $Directories += $Dir + } + + Write-TestSuccess "Created $($Directories.Count) directories" +} +else { + # Simple flat structure + $FlatDirs = @("Dir1", "Dir2", "Dir3") + foreach ($Dir in $FlatDirs) { + $DirPath = Join-Path $TestDir $Dir + New-Item -ItemType Directory -Path $DirPath -Force | Out-Null + $Directories += $DirPath + } + + Write-TestSuccess "Created $($Directories.Count) directories (flat structure)" +} + +# ============================================================================ +# PHASE 3: CREATE "NUL" FILES +# ============================================================================ + +Write-TestStep "Phase 3: Create Test Files" + +$TargetLeaf = if ($DollarNullOnly) { [char]36 + 'null' } else { 'nul' } +Write-TestInfo "Creating $TestCount '$TargetLeaf' files using extended path prefix..." + +$CreatedFiles = @() +$NormalFiles = @() +$sw = [System.Diagnostics.Stopwatch]::StartNew() + +# Distribute files across directories +$FilesPerDir = [math]::Ceiling($TestCount / $Directories.Count) + +for ($i = 0; $i -lt $TestCount; $i++) { + $DirIndex = [math]::Floor($i / $FilesPerDir) + if ($DirIndex -ge $Directories.Count) { + $DirIndex = $Directories.Count - 1 + } + + $TargetDir = $Directories[$DirIndex] + + # Create "nul" file using \\?\ prefix + $NulPath = Join-Path $TargetDir $TargetLeaf + $ExtendedPath = "\\?\$NulPath" + + try { + # Use .NET to create the file (PowerShell New-Item doesn't work with \\?\ prefix) + $FileStream = [System.IO.File]::Create($ExtendedPath) + $FileStream.Close() + $CreatedFiles += $NulPath + } + catch { + Write-TestError "Failed to create $NulPath : $_" + } + + # Also create some normal files for context + if ($i % 3 -eq 0) { + $NormalPath = Join-Path $TargetDir "file$i.txt" + "Test content $i" | Out-File $NormalPath -Encoding UTF8 + $NormalFiles += $NormalPath + } +} + +$sw.Stop() + +Write-TestSuccess "Created $($CreatedFiles.Count) nul files in $($sw.Elapsed.TotalSeconds)s" +Write-TestInfo "Created $($NormalFiles.Count) normal files for context" + +# Verify files exist +Write-TestInfo "Verifying test files..." +$VerifiedCount = 0 +foreach ($File in $CreatedFiles) { + $ExtendedPath = "\\?\$File" + if ([System.IO.File]::Exists($ExtendedPath)) { + $VerifiedCount++ + } +} + +Write-TestSuccess "Verified $VerifiedCount/$($CreatedFiles.Count) files exist" + +# ============================================================================ +# PHASE 4: RUN NUKENUL.EXE +# ============================================================================ + +Write-TestStep "Phase 4: Run NukeNul.exe" + +$ExeArgs = @() +if ($DollarNullOnly) { + $ExeArgs += '--dollar-null-only' +} +$ExeArgs += $TestDir +Write-TestInfo "Executing: $ExePath $($ExeArgs -join ' ')" +Write-Host "" + +$sw = [System.Diagnostics.Stopwatch]::StartNew() + +try { + $Output = & $ExePath @ExeArgs 2>&1 | Out-String + $ExitCode = $LASTEXITCODE + $sw.Stop() + + Write-Host $Output + + if ($ExitCode -ne 0) { + Write-TestError "NukeNul.exe failed with exit code: $ExitCode" + exit 1 + } + + Write-TestSuccess "NukeNul.exe completed in $($sw.Elapsed.TotalSeconds)s" +} +catch { + Write-TestError "Failed to execute NukeNul.exe: $_" + exit 1 +} + +# Parse JSON output +$JsonOutput = $null +try { + $JsonOutput = $Output | ConvertFrom-Json + Write-Host "" + Write-Host "Results:" -ForegroundColor Cyan + Write-Host " Files Scanned: $($JsonOutput.Results.Scanned)" -ForegroundColor White + Write-Host " Files Deleted: $($JsonOutput.Results.Deleted)" -ForegroundColor Yellow + $errColor = if ($JsonOutput.Results.Errors -gt 0) { 'Red' } else { 'Green' } + Write-Host " Errors: $($JsonOutput.Results.Errors)" -ForegroundColor $errColor + Write-Host " Elapsed: $($JsonOutput.Performance.ElapsedMs) ms" -ForegroundColor White +} +catch { + Write-TestError "Failed to parse JSON output: $_" + Write-Host "Raw output:" -ForegroundColor Yellow + Write-Host $Output +} + +# ============================================================================ +# PHASE 5: VERIFY DELETION +# ============================================================================ + +Write-TestStep "Phase 5: Verify Deletion" + +Write-TestInfo "Checking if nul files were deleted..." + +$RemainingFiles = 0 +foreach ($File in $CreatedFiles) { + $ExtendedPath = "\\?\$File" + if ([System.IO.File]::Exists($ExtendedPath)) { + $RemainingFiles++ + Write-Host " [!] Still exists: $File" -ForegroundColor Red + } +} + +if ($RemainingFiles -eq 0) { + Write-TestSuccess "All $($CreatedFiles.Count) nul files were deleted" +} +else { + Write-TestError "$RemainingFiles/$($CreatedFiles.Count) files were NOT deleted" +} + +# Verify normal files were NOT deleted +Write-TestInfo "Checking that normal files were preserved..." +$MissingNormalFiles = 0 +foreach ($File in $NormalFiles) { + if (-not (Test-Path $File)) { + $MissingNormalFiles++ + Write-Host " [!] Normal file deleted: $File" -ForegroundColor Red + } +} + +if ($MissingNormalFiles -eq 0) { + Write-TestSuccess "All $($NormalFiles.Count) normal files were preserved" +} +else { + Write-TestError "$MissingNormalFiles/$($NormalFiles.Count) normal files were incorrectly deleted" +} + +# ============================================================================ +# PHASE 6: PERFORMANCE BENCHMARK (Optional) +# ============================================================================ + +if (-not $SkipBenchmark) { + Write-TestStep "Phase 6: Performance Benchmark" + + $OriginalScript = Join-Path $RootDir "delete-nul-files.ps1" + + if (Test-Path $OriginalScript) { + Write-TestInfo "Comparing with original PowerShell script..." + + # Recreate test files for fair comparison + Write-TestInfo "Recreating test files for benchmark..." + $BenchFiles = @() + for ($i = 0; $i -lt $TestCount; $i++) { + $DirIndex = [math]::Floor($i / $FilesPerDir) + if ($DirIndex -ge $Directories.Count) { $DirIndex = $Directories.Count - 1 } + + $TargetDir = $Directories[$DirIndex] + $NulPath = Join-Path $TargetDir $TargetLeaf + $ExtendedPath = "\\?\$NulPath" + + try { + $FileStream = [System.IO.File]::Create($ExtendedPath) + $FileStream.Close() + $BenchFiles += $NulPath + } + catch { + # Ignore errors for benchmark + } + } + + Write-TestInfo "Running PowerShell script..." + $sw = [System.Diagnostics.Stopwatch]::StartNew() + + Push-Location $TestDir + try { + & $OriginalScript -SearchPath $TestDir -UsePowerShell -DollarNullOnly:$DollarNullOnly 2>&1 | Out-Null + $sw.Stop() + $PowerShellTime = $sw.Elapsed.TotalMilliseconds + + Write-Host "" + Write-Host "Performance Comparison:" -ForegroundColor Cyan + Write-Host " PowerShell: $([math]::Round($PowerShellTime, 2)) ms" -ForegroundColor White + if ($null -ne $JsonOutput) { + Write-Host " NukeNul (Rust): $($JsonOutput.Performance.ElapsedMs) ms" -ForegroundColor Yellow + + if ($JsonOutput.Performance.ElapsedMs -lt $PowerShellTime) { + $Speedup = $PowerShellTime / $JsonOutput.Performance.ElapsedMs + Write-Host " Speedup: $([math]::Round($Speedup, 2))x faster" -ForegroundColor Green + } + else { + Write-Host " Note: PowerShell was faster (likely due to small test size)" -ForegroundColor Yellow + } + } + } + finally { + Pop-Location + } + } + else { + Write-TestInfo "Original PowerShell script not found, skipping benchmark" + } +} + +# ============================================================================ +# PHASE 7: CLEANUP +# ============================================================================ + +Write-TestStep "Phase 7: Cleanup" + +if ($KeepTestDir) { + Write-TestInfo "Keeping test directory: $TestDir" +} +else { + Write-TestInfo "Removing test directory..." + try { + Remove-Item $TestDir -Recurse -Force -ErrorAction Stop + Write-TestSuccess "Test directory cleaned up" + } + catch { + Write-Host " [!] Warning: Failed to clean up test directory: $_" -ForegroundColor Yellow + Write-Host " Manual cleanup required: $TestDir" -ForegroundColor Yellow + } +} + +# ============================================================================ +# PHASE 8: TEST SUMMARY +# ============================================================================ + +Write-TestStep "Phase 8: Test Summary" + +Write-Host "" +$AllTestsPassed = ($RemainingFiles -eq 0) -and ($MissingNormalFiles -eq 0) + +if ($AllTestsPassed) { + Write-Host "ALL TESTS PASSED" -ForegroundColor Green +} +else { + Write-Host "TESTS FAILED" -ForegroundColor Red +} + +Write-Host "" +Write-Host "Test Statistics:" -ForegroundColor Cyan +Write-Host " Test Files Created: $($CreatedFiles.Count)" +Write-Host " Normal Files Created: $($NormalFiles.Count)" +Write-Host " Files Successfully Deleted: $($CreatedFiles.Count - $RemainingFiles)" +$successRate = [math]::Round((($CreatedFiles.Count - $RemainingFiles) / $CreatedFiles.Count) * 100, 2) +Write-Host " Deletion Success Rate: $successRate%" +if ($null -ne $JsonOutput) { + Write-Host " Execution Time: $($JsonOutput.Performance.ElapsedMs) ms" +} +Write-Host "" + +if (-not $AllTestsPassed) { + exit 1 +} From a8346116ebc3667e11c3e6e6f8edd2fcb3557f8e Mon Sep 17 00:00:00 2001 From: "David T. Martel" Date: Thu, 6 Aug 2026 10:47:13 -0400 Subject: [PATCH 2/8] feat(nuker_core): add composable dry-run engine with dir + path-mangle matching Replaces the single-mode scan_and_delete engine with a new nuke_files_ex FFI entry point that composes three independent match families (reserved device names, literal $null, path-mangle artifacts ending in `;[:]`), adds --dry-run/--include-dirs/--allow-nonempty semantics, and returns an auditable JSON payload (deleted/would_delete/skipped, each with path/family/kind/reason and, for non-empty $null files, size + a 200-byte content preview) instead of bare counts. $null files now delete only when zero-byte by default, per the workspace policy that discovery hooks may only remove zero-byte matches. The legacy nuke_reserved_files/nuke_dollar_null_files symbols are preserved (now implemented on top of the new engine) so the existing extern "C" ABI keeps working for any other callers of nuker_core.dll. 16 new/updated unit tests cover the path-mangle regexless matcher, the zero-byte gate, family composability, and the extended-path helpers. Agent: claude Co-authored-by: Claude --- nuker_core/Cargo.lock | 28 ++ nuker_core/Cargo.toml | 4 + nuker_core/src/lib.rs | 1063 ++++++++++++++++++++++++++++++++--------- 3 files changed, 881 insertions(+), 214 deletions(-) diff --git a/nuker_core/Cargo.lock b/nuker_core/Cargo.lock index 471b494..a410100 100644 --- a/nuker_core/Cargo.lock +++ b/nuker_core/Cargo.lock @@ -75,6 +75,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + [[package]] name = "libc" version = "0.2.180" @@ -99,6 +105,8 @@ version = "0.1.0" dependencies = [ "ignore", "libc", + "serde", + "serde_json", "widestring", "windows-sys 0.59.0", ] @@ -154,6 +162,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" dependencies = [ "serde_core", + "serde_derive", ] [[package]] @@ -176,6 +185,19 @@ dependencies = [ "syn", ] +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + [[package]] name = "syn" version = "2.0.114" @@ -305,3 +327,9 @@ name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/nuker_core/Cargo.toml b/nuker_core/Cargo.toml index 5650219..c7e1aec 100644 --- a/nuker_core/Cargo.toml +++ b/nuker_core/Cargo.toml @@ -26,6 +26,10 @@ windows-sys = { version = "0.59", features = [ # C FFI types libc = "0.2" +# JSON serialization for the auditable nuke_files_ex result payload +serde = { version = "1", features = ["derive"] } +serde_json = "1" + [profile.release] # Aggressive optimizations for maximum performance opt-level = 3 # Maximum optimization diff --git a/nuker_core/src/lib.rs b/nuker_core/src/lib.rs index e6e98e5..a02349b 100644 --- a/nuker_core/src/lib.rs +++ b/nuker_core/src/lib.rs @@ -1,27 +1,43 @@ -//! Nuker Core - High-Performance Windows Reserved Filename Cleaner +//! Nuker Core - High-Performance Windows Problematic-Filename Cleaner //! -//! This library provides a C-compatible FFI interface for deleting Windows reserved -//! filenames (like "nul", "con", "prn", etc.) using parallel file system traversal +//! This library provides a C-compatible FFI interface for deleting Windows filenames +//! that standard tooling cannot remove (reserved device aliases, stray literal `$null` +//! artifacts, and shell path-mangling artifacts), using parallel file system traversal //! and direct Win32 API calls. //! //! # Architecture //! - Uses `ignore` crate for multi-threaded directory walking (ripgrep's engine) -//! - Direct Win32 DeleteFileW API calls for maximum performance +//! - Direct Win32 `DeleteFileW`/`RemoveDirectoryW` calls for maximum performance //! - Extended-length path prefix (`\\?\`) to bypass path normalization -//! - Thread-safe atomic counters for statistics tracking +//! - Thread-safe collection of per-entry results (deleted / would-delete / skipped) +//! +//! # Match families +//! A scan can combine any of three independent name families: +//! - **Reserved device names**: `nul`, `con`, `prn`, `aux`, `com1-9`, `lpt1-9` +//! - **Literal `$null`**: real files/dirs whose visible leaf is exactly `$null` +//! (case-insensitive, trailing dots/spaces ignored). Files are only deleted when +//! zero-byte unless `allow_nonempty` is set (see [`NukeOptions`]). +//! - **Path-mangle artifacts**: leaf names ending in `;` or `;:`, +//! produced by shells that mis-concatenate a path. +//! +//! Directories are never touched unless `include_dirs` is set, and even then only an +//! *empty* directory is ever removed (`RemoveDirectoryW` itself refuses non-empty +//! directories - we never recurse to empty one out). //! //! # Safety //! This library uses unsafe code for FFI and Win32 API calls. All unsafe blocks //! are documented and have been carefully reviewed for correctness. -use std::ffi::{CStr, OsStr}; +use std::ffi::{CStr, CString, OsStr}; use std::os::raw::c_char; use std::path::Path; use std::sync::atomic::{AtomicU32, Ordering}; +use std::sync::{Mutex, PoisonError}; use ignore::WalkBuilder; use widestring::U16CString; -use windows_sys::Win32::Storage::FileSystem::DeleteFileW; +use windows_sys::Win32::Foundation::GetLastError; +use windows_sys::Win32::Storage::FileSystem::{DeleteFileW, RemoveDirectoryW}; /// Windows reserved filenames that cannot be created through normal APIs /// These filenames are case-insensitive and cause issues on Windows @@ -30,22 +46,24 @@ const RESERVED_NAMES: &[&str] = &[ "com9", "lpt1", "lpt2", "lpt3", "lpt4", "lpt5", "lpt6", "lpt7", "lpt8", "lpt9", ]; -/// Selects the filename family handled by a scan. -#[derive(Clone, Copy)] -enum MatchMode { - ReservedDeviceNames, - LiteralDollarNull, -} +/// How many bytes of a non-empty `$null` file are captured for operator review. +const CONTENT_PREVIEW_MAX_BYTES: usize = 200; + +/// `RemoveDirectoryW` failure code meaning "directory has children" (not an error, +/// just a rejected candidate - see [`DirDeleteOutcome::NotEmpty`]). +const ERROR_DIR_NOT_EMPTY: u32 = 145; -/// Statistics returned from the scan operation +/// Statistics returned from the legacy (pre-`nuke_files_ex`) C FFI functions. /// /// This struct is C-compatible and can be marshaled to/from C# or other languages. +/// Retained for ABI compatibility with existing callers of `nuke_reserved_files` +/// and `nuke_dollar_null_files`; new integrations should prefer [`nuke_files_ex`]. #[repr(C)] #[derive(Debug, Clone, Copy)] pub struct ScanStats { /// Total number of files scanned during traversal pub files_scanned: u32, - /// Number of reserved files successfully deleted + /// Number of matching entries successfully deleted pub files_deleted: u32, /// Number of errors encountered (permission denied, file in use, etc.) pub errors: u32, @@ -62,251 +80,757 @@ impl ScanStats { } } +/// Options controlling a [`nuke_files_ex`] scan. Every field is a C-ABI-safe `u8` +/// boolean (`0` = false, any other value = true) to avoid platform-specific `BOOL` +/// marshaling ambiguity. +/// +/// # Fields +/// - `match_reserved` - include the reserved-device-name family +/// - `match_dollar_null` - include the literal `$null` family +/// - `match_path_mangle` - include the path-mangle-artifact family +/// - `include_dirs` - also consider empty directories as deletion candidates +/// - `dry_run` - walk and match, but never delete anything +/// - `allow_nonempty` - allow deleting `$null` files larger than zero bytes +/// (only meaningful when `match_dollar_null` is set) +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct NukeOptions { + pub match_reserved: u8, + pub match_dollar_null: u8, + pub match_path_mangle: u8, + pub include_dirs: u8, + pub dry_run: u8, + pub allow_nonempty: u8, +} + include!(concat!(env!("OUT_DIR"), "/version.rs")); -/// Main entry point for the C FFI interface +// --------------------------------------------------------------------------- +// Legacy FFI entry points (stable ABI, preserved for existing callers) +// --------------------------------------------------------------------------- + +/// Deletes reserved-device-name files (`nul`, `con`, `prn`, ...) under `root_ptr`. /// /// # Safety /// The caller must ensure: /// - `root_ptr` is either null or points to a valid null-terminated C string /// - The string remains valid for the duration of this call /// - The string represents a valid file system path -/// -/// # Arguments -/// * `root_ptr` - Null-terminated C string containing the root path to scan -/// -/// # Returns -/// A `ScanStats` struct containing scan results. If an error occurs during -/// initialization, returns a struct with errors=1 and other fields=0. -/// -/// # Example (from C#) -/// ```csharp -/// [DllImport("nuker_core.dll", CallingConvention = CallingConvention.Cdecl)] -/// private static extern ScanStats nuke_reserved_files(string rootPath); -/// ``` #[no_mangle] pub unsafe extern "C" fn nuke_reserved_files(root_ptr: *const c_char) -> ScanStats { - nuke_files(root_ptr, MatchMode::ReservedDeviceNames) + legacy_scan( + root_ptr, + NukeOptions { + match_reserved: 1, + match_dollar_null: 0, + match_path_mangle: 0, + include_dirs: 0, + dry_run: 0, + allow_nonempty: 0, + }, + ) } -/// Deletes ordinary files whose visible leaf name is literal `$null`. +/// Deletes zero-byte files whose visible leaf name is literal `$null`. /// /// Matching is case-insensitive and ignores trailing dots/spaces, which Windows /// may otherwise normalize. Directories, symlinks, and reserved device aliases /// are not targeted by this entry point. /// +/// # Behavior change +/// As of the `--dollar-null-only` zero-byte safety gate, this legacy entry point +/// now only deletes **zero-byte** `$null` files (matching the documented workspace +/// policy: "Discovery hooks may delete only zero-byte matches"). Non-empty `$null` +/// files are left untouched. Use [`nuke_files_ex`] with `allow_nonempty = 1` to +/// opt into deleting larger files. +/// /// # Safety /// The caller must provide a valid, null-terminated UTF-8 path for the duration /// of this call, or a null pointer to receive an error result. #[no_mangle] pub unsafe extern "C" fn nuke_dollar_null_files(root_ptr: *const c_char) -> ScanStats { - nuke_files(root_ptr, MatchMode::LiteralDollarNull) + legacy_scan( + root_ptr, + NukeOptions { + match_reserved: 0, + match_dollar_null: 1, + match_path_mangle: 0, + include_dirs: 0, + dry_run: 0, + allow_nonempty: 0, + }, + ) } -/// Validates an FFI root path and runs the requested cleanup mode. -unsafe fn nuke_files(root_ptr: *const c_char, mode: MatchMode) -> ScanStats { - // Safety check: null pointer +/// Shared implementation for the legacy `ScanStats`-returning entry points. +unsafe fn legacy_scan(root_ptr: *const c_char, options: NukeOptions) -> ScanStats { + match validate_and_run(root_ptr, options) { + Ok(output) => ScanStats { + files_scanned: output.counts.scanned, + #[allow(clippy::cast_possible_truncation)] + files_deleted: output.counts.deleted as u32, + errors: output.counts.errors, + }, + Err(message) => { + eprintln!("Error: {message}"); + ScanStats::error() + } + } +} + +// --------------------------------------------------------------------------- +// Primary FFI entry point: composable families, dry-run, auditable JSON result +// --------------------------------------------------------------------------- + +/// Runs a scan/delete pass with the requested [`NukeOptions`] and returns a +/// heap-allocated, null-terminated UTF-8 JSON string describing the result. +/// +/// On success the JSON has the shape: +/// ```json +/// { +/// "status": "success", +/// "dry_run": false, +/// "counts": {"scanned": 0, "deleted": 0, "would_delete": 0, "skipped": 0, "errors": 0}, +/// "deleted": [{"path": "...", "family": "reserved", "kind": "file"}], +/// "would_delete": [], +/// "skipped": [{"path": "...", "family": "dollar_null", "kind": "file", +/// "reason": "...", "size": 877, "content_preview": "..."}] +/// } +/// ``` +/// On failure (invalid input, path does not exist, ...) it has the shape: +/// ```json +/// {"status": "error", "message": "..."} +/// ``` +/// +/// The caller MUST pass the returned pointer to [`nuke_free_string`] exactly once +/// to release it. This function never returns a null pointer. +/// +/// # Safety +/// The caller must ensure `root_ptr` is either null or a valid null-terminated +/// UTF-8 C string that remains valid for the duration of this call. +#[no_mangle] +pub unsafe extern "C" fn nuke_files_ex( + root_ptr: *const c_char, + options: NukeOptions, +) -> *mut c_char { + let json = match validate_and_run(root_ptr, options) { + Ok(output) => serde_json::to_string(&output).unwrap_or_else(|e| { + format!("{{\"status\":\"error\",\"message\":\"serialization failed: {e}\"}}") + }), + Err(message) => { + let err = ErrorOutput { + status: "error", + message, + }; + serde_json::to_string(&err).unwrap_or_else(|_| { + "{\"status\":\"error\",\"message\":\"unknown error\"}".to_string() + }) + } + }; + to_c_string(json) +} + +/// Frees a string previously returned by [`nuke_files_ex`]. +/// +/// # Safety +/// `ptr` must be either null (a no-op) or a pointer previously returned by +/// `nuke_files_ex` that has not already been freed. +#[no_mangle] +pub unsafe extern "C" fn nuke_free_string(ptr: *mut c_char) { + if ptr.is_null() { + return; + } + drop(unsafe { CString::from_raw(ptr) }); +} + +/// Validates an FFI root path and runs the requested scan. +unsafe fn validate_and_run( + root_ptr: *const c_char, + options: NukeOptions, +) -> Result { if root_ptr.is_null() { - eprintln!("Error: Null pointer passed to nuke_reserved_files"); - return ScanStats::error(); + return Err("null pointer passed for root path".to_string()); } - // Convert C string to Rust string slice - // Safety: We've verified the pointer is non-null above + // Safety: caller contract guarantees a valid, null-terminated string. let c_str = unsafe { CStr::from_ptr(root_ptr) }; - let root_path = match c_str.to_str() { - Ok(s) => s, - Err(e) => { - eprintln!("Error: Invalid UTF-8 in path: {}", e); - return ScanStats::error(); - }, - }; + let root_path = c_str + .to_str() + .map_err(|e| format!("invalid UTF-8 in path: {e}"))?; - // Verify the path exists before starting the scan if !Path::new(root_path).exists() { - eprintln!("Error: Path does not exist: {}", root_path); - return ScanStats::error(); + return Err(format!("path does not exist: {root_path}")); } - // Execute the scan - scan_and_delete(root_path, mode) + run_engine(root_path, options) } -/// Internal implementation of the scan and delete operation +/// Converts an owned JSON `String` into a heap `*mut c_char` for return across FFI. +/// +/// `serde_json` escapes control characters (including NUL) in string values, so +/// `CString::new` should never observe an embedded NUL here; the fallback exists +/// purely as a defensive measure so this function can never panic. +fn to_c_string(json: String) -> *mut c_char { + match CString::new(json) { + Ok(cs) => cs.into_raw(), + Err(_) => CString::new( + "{\"status\":\"error\",\"message\":\"internal: JSON contained an embedded NUL byte\"}", + ) + .unwrap_or_default() + .into_raw(), + } +} + +// --------------------------------------------------------------------------- +// Match families +// --------------------------------------------------------------------------- + +/// Identifies which family a matched entry belongs to. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum MatchFamily { + Reserved, + DollarNull, + PathMangle, +} + +impl MatchFamily { + const fn as_str(self) -> &'static str { + match self { + MatchFamily::Reserved => "reserved", + MatchFamily::DollarNull => "dollar_null", + MatchFamily::PathMangle => "path_mangle", + } + } +} + +/// Which match families are active for a scan (resolved from [`NukeOptions`]). +#[derive(Debug, Clone, Copy)] +struct ActiveFamilies { + reserved: bool, + dollar_null: bool, + path_mangle: bool, +} + +impl ActiveFamilies { + fn any(self) -> bool { + self.reserved || self.dollar_null || self.path_mangle + } +} + +fn resolve_families(options: &NukeOptions) -> ActiveFamilies { + ActiveFamilies { + reserved: options.match_reserved != 0, + dollar_null: options.match_dollar_null != 0, + path_mangle: options.match_path_mangle != 0, + } +} + +/// Returns the family a leaf filename belongs to, checking only families enabled +/// in `families`. Families are checked in a fixed order so a name matching more +/// than one narrow rule reports the first (this cannot currently happen given the +/// three families are mutually exclusive by construction, but a fixed order keeps +/// results deterministic if that ever changes). +fn classify(file_name: &OsStr, families: ActiveFamilies) -> Option { + if families.reserved && matches_reserved(file_name) { + return Some(MatchFamily::Reserved); + } + if families.dollar_null && matches_dollar_null(file_name) { + return Some(MatchFamily::DollarNull); + } + if families.path_mangle && matches_path_mangle(file_name) { + return Some(MatchFamily::PathMangle); + } + None +} + +/// Returns whether `file_name` is a reserved Windows device name (case-insensitive). +fn matches_reserved(file_name: &OsStr) -> bool { + RESERVED_NAMES + .iter() + .any(|&reserved| file_name.eq_ignore_ascii_case(reserved)) +} + +/// Returns whether `file_name` is literal `$null` (case-insensitive, trailing +/// dots/spaces ignored - Windows may otherwise normalize them away). +fn matches_dollar_null(file_name: &OsStr) -> bool { + file_name.to_str().is_some_and(|name| { + name.trim_end_matches(['.', ' ']) + .eq_ignore_ascii_case("$null") + }) +} + +/// Returns whether `file_name` looks like a shell path-mangling artifact: a leaf +/// name ending in `;` or `;:` (e.g. `foo;C` or `foo;C:`), produced +/// when a shell mis-concatenates a path and a stray drive-letter fragment is +/// appended after a semicolon. Deliberately narrow: `;` is a legal filename +/// character and a broader rule would be dangerous. +fn matches_path_mangle(file_name: &OsStr) -> bool { + let Some(name) = file_name.to_str() else { + return false; + }; + let core = name.strip_suffix(':').unwrap_or(name); + let bytes = core.as_bytes(); + if bytes.len() < 2 { + return false; + } + let last = bytes[bytes.len() - 1]; + let semicolon = bytes[bytes.len() - 2]; + semicolon == b';' && last.is_ascii_alphabetic() +} + +/// Returns a skip reason when a `$null` file of the given `size` must NOT be +/// deleted under the current options (the zero-byte safety gate). Returns `None` +/// when the file is eligible for deletion. +fn dollar_null_skip_reason(size: u64, allow_nonempty: bool) -> Option { + if size == 0 || allow_nonempty { + None + } else { + Some(format!( + "non-empty $null file ({size} bytes); rerun with --allow-nonempty to delete" + )) + } +} + +// --------------------------------------------------------------------------- +// Win32 deletion helpers +// --------------------------------------------------------------------------- + +/// Converts a path to an extended-length path (`\\?\C:\...` / `\\?\UNC\...`) to +/// bypass Win32 path normalization and `MAX_PATH` limitations. Returns `None` if +/// the path is not valid UTF-8. +fn to_extended_path(path: &Path) -> Option { + let path_str = path.to_str()?; + if path_str.starts_with(r"\\?\") { + Some(path_str.to_string()) + } else if let Some(stripped) = path_str.strip_prefix(r"\\") { + Some(format!(r"\\?\UNC\{stripped}")) + } else { + Some(format!(r"\\?\{path_str}")) + } +} + +/// Maps a Win32 error code to a short human-readable description for the common +/// cases this tool encounters; falls back to the raw numeric code otherwise. +fn describe_win32_error(code: u32) -> String { + match code { + 2 => "file not found (ERROR_FILE_NOT_FOUND)".to_string(), + 5 => "access denied (ERROR_ACCESS_DENIED)".to_string(), + 19 => "write-protected media (ERROR_WRITE_PROTECT)".to_string(), + 32 => "sharing violation, file in use (ERROR_SHARING_VIOLATION)".to_string(), + 145 => "directory not empty (ERROR_DIR_NOT_EMPTY)".to_string(), + other => format!("Win32 error {other}"), + } +} + +/// Deletes a file using the Win32 `DeleteFileW` API with an extended-length path +/// prefix, bypassing standard library path normalization (required for reserved +/// device names like `nul`). +fn delete_file_win32(path: &Path) -> Result<(), String> { + let Some(extended_path) = to_extended_path(path) else { + return Err("path contains invalid UTF-8".to_string()); + }; + let wide_path = match U16CString::from_str(&extended_path) { + Ok(wp) => wp, + Err(_) => return Err("path contains an embedded NUL byte".to_string()), + }; + // Safety: wide_path is a valid, null-terminated UTF-16 string that lives for + // the duration of this call. + unsafe { + if DeleteFileW(wide_path.as_ptr()) != 0 { + Ok(()) + } else { + Err(describe_win32_error(GetLastError())) + } + } +} + +/// Outcome of a failed directory deletion attempt. +enum DirDeleteOutcome { + /// `RemoveDirectoryW` refused because the directory has children. This is the + /// deliberate safety mechanism for `--include-dirs`: we never recurse to + /// empty a directory out, we only remove ones that are already empty. + NotEmpty, + /// Any other failure (permission denied, in use, etc.). + Failed(String), +} + +/// Deletes an EMPTY directory using the Win32 `RemoveDirectoryW` API. Never +/// recurses or removes directory contents - a non-empty directory is reported as +/// [`DirDeleteOutcome::NotEmpty`], not attempted. +fn delete_dir_win32(path: &Path) -> Result<(), DirDeleteOutcome> { + let Some(extended_path) = to_extended_path(path) else { + return Err(DirDeleteOutcome::Failed( + "path contains invalid UTF-8".to_string(), + )); + }; + let wide_path = match U16CString::from_str(&extended_path) { + Ok(wp) => wp, + Err(_) => { + return Err(DirDeleteOutcome::Failed( + "path contains an embedded NUL byte".to_string(), + )) + } + }; + // Safety: wide_path is a valid, null-terminated UTF-16 string that lives for + // the duration of this call. + unsafe { + if RemoveDirectoryW(wide_path.as_ptr()) != 0 { + Ok(()) + } else { + let code = GetLastError(); + if code == ERROR_DIR_NOT_EMPTY { + Err(DirDeleteOutcome::NotEmpty) + } else { + Err(DirDeleteOutcome::Failed(describe_win32_error(code))) + } + } + } +} + +/// Reads up to [`CONTENT_PREVIEW_MAX_BYTES`] of `path` for operator review, +/// lossily decoded as UTF-8. Returns `None` if the file cannot be opened or read +/// (this is best-effort auditing, not a correctness requirement). +fn read_content_preview(path: &Path) -> Option { + let extended = to_extended_path(path)?; + let mut file = std::fs::File::open(extended).ok()?; + let mut buf = vec![0u8; CONTENT_PREVIEW_MAX_BYTES]; + let n = std::io::Read::read(&mut file, &mut buf).ok()?; + buf.truncate(n); + Some(String::from_utf8_lossy(&buf).into_owned()) +} + +// --------------------------------------------------------------------------- +// JSON result types +// --------------------------------------------------------------------------- + +#[derive(Debug, serde::Serialize)] +struct Counts { + scanned: u32, + deleted: usize, + would_delete: usize, + skipped: usize, + errors: u32, +} + +#[derive(Debug, serde::Serialize)] +struct ActionEntry { + path: String, + family: &'static str, + kind: &'static str, + #[serde(skip_serializing_if = "Option::is_none")] + reason: Option, + #[serde(skip_serializing_if = "Option::is_none")] + size: Option, + #[serde(skip_serializing_if = "Option::is_none")] + content_preview: Option, +} + +#[derive(Debug, serde::Serialize)] +struct EngineOutput { + status: &'static str, + dry_run: bool, + counts: Counts, + deleted: Vec, + would_delete: Vec, + skipped: Vec, +} + +#[derive(Debug, serde::Serialize)] +struct ErrorOutput { + status: &'static str, + message: String, +} + +// --------------------------------------------------------------------------- +// Scan engine +// --------------------------------------------------------------------------- + +/// Thread-safe accumulator for scan results. Matches are rare relative to the +/// total number of scanned entries, so a plain `Mutex>` per bucket is +/// simple and does not become a contention bottleneck. +struct Collector { + scanned: AtomicU32, + errors: AtomicU32, + deleted: Mutex>, + would_delete: Mutex>, + skipped: Mutex>, +} + +impl Collector { + fn new() -> Self { + Self { + scanned: AtomicU32::new(0), + errors: AtomicU32::new(0), + deleted: Mutex::new(Vec::new()), + would_delete: Mutex::new(Vec::new()), + skipped: Mutex::new(Vec::new()), + } + } + + fn push_deleted(&self, entry: ActionEntry) { + self.deleted + .lock() + .unwrap_or_else(PoisonError::into_inner) + .push(entry); + } + + fn push_would_delete(&self, entry: ActionEntry) { + self.would_delete + .lock() + .unwrap_or_else(PoisonError::into_inner) + .push(entry); + } + + fn push_skipped(&self, entry: ActionEntry) { + self.skipped + .lock() + .unwrap_or_else(PoisonError::into_inner) + .push(entry); + } + + fn into_output(self, dry_run: bool) -> EngineOutput { + let deleted = self + .deleted + .into_inner() + .unwrap_or_else(PoisonError::into_inner); + let would_delete = self + .would_delete + .into_inner() + .unwrap_or_else(PoisonError::into_inner); + let skipped = self + .skipped + .into_inner() + .unwrap_or_else(PoisonError::into_inner); + + let counts = Counts { + scanned: self.scanned.load(Ordering::Relaxed), + deleted: deleted.len(), + would_delete: would_delete.len(), + skipped: skipped.len(), + errors: self.errors.load(Ordering::Relaxed), + }; + + EngineOutput { + status: "success", + dry_run, + counts, + deleted, + would_delete, + skipped, + } + } +} + +/// Internal implementation of the scan (and, unless `dry_run`, delete) operation. /// /// This function: -/// 1. Configures a parallel walker with appropriate filters +/// 1. Configures a parallel walker with all ignore rules disabled (junk hides in +/// gitignored trees) except a `.git` directory skip /// 2. Scans the file system using multiple threads -/// 3. Identifies reserved filenames -/// 4. Deletes them using Win32 API with extended-length paths -/// 5. Tracks statistics using atomic counters -fn scan_and_delete(root_path: &str, mode: MatchMode) -> ScanStats { - // Thread-safe atomic counters for statistics - let scanned = AtomicU32::new(0); - let deleted = AtomicU32::new(0); - let errors = AtomicU32::new(0); +/// 3. Classifies each entry against the active match families +/// 4. Applies the zero-byte safety gate to `$null` file matches +/// 5. Deletes (or, in dry-run, records) matching entries via Win32 APIs +fn run_engine(root_path: &str, options: NukeOptions) -> Result { + let families = resolve_families(&options); + if !families.any() { + return Err( + "no match family selected: enable at least one of reserved, dollar-null, or path-mangle" + .to_string(), + ); + } + + let collector = Collector::new(); // Configure the parallel walker // - Uses work-stealing queue for load balancing across threads // - Automatically scales to CPU core count // - Skips .git directories to avoid repository corruption - // - Ignores hidden file settings (we want to scan everything) + // - Ignores hidden file settings and all .gitignore/.ignore rules (we want to + // scan everything - junk hides in gitignored trees) let walker = WalkBuilder::new(root_path) - .hidden(false) // Scan hidden files and directories - .git_ignore(false) // Don't respect .gitignore files - .git_global(false) // Don't respect global gitignore - .git_exclude(false) // Don't respect .git/info/exclude - .require_git(false) // Don't require a git repository - .ignore(false) // Don't respect .ignore files - .parents(false) // Don't look for ignore files in parent directories + .hidden(false) // Scan hidden files and directories + .git_ignore(false) // Don't respect .gitignore files + .git_global(false) // Don't respect global gitignore + .git_exclude(false) // Don't respect .git/info/exclude + .require_git(false) // Don't require a git repository + .ignore(false) // Don't respect .ignore files + .parents(false) // Don't look for ignore files in parent directories .filter_entry(|entry| { // Skip .git directories entirely to avoid repository corruption - // This is checked before descending into the directory entry.file_name() != ".git" }) .build_parallel(); - // Execute parallel walk - // Each thread gets its own closure instance for lock-free operation + // Execute parallel walk. Each thread gets its own closure instance for + // lock-free scanning; matches are pushed into the shared Collector. walker.run(|| { - // Clone references to the atomic counters for this thread - let scanned = &scanned; - let deleted = &deleted; - let errors = &errors; - let mode = mode; + let collector = &collector; - // Return a boxed closure that processes each directory entry Box::new(move |result| { match result { Ok(entry) => { - // Increment scanned counter - scanned.fetch_add(1, Ordering::Relaxed); - - // Only process real files. Directories, symlinks, and - // entries whose type cannot be determined are not deletion - // candidates. - let Some(file_type) = entry.file_type() else { - return ignore::WalkState::Continue; - }; - if !file_type.is_file() { - return ignore::WalkState::Continue; - } - - // Get the filename (last component of the path) - let file_name = entry.file_name(); - - // Check if this is a reserved filename (case-insensitive) - // Use OsStr comparison to avoid UTF-8 allocation overhead - let is_reserved = matches_target(file_name, mode); - - if is_reserved { - // Attempt to delete the reserved file - if delete_file_win32(entry.path()) { - deleted.fetch_add(1, Ordering::Relaxed); - } else { - errors.fetch_add(1, Ordering::Relaxed); - } - } - }, + collector.scanned.fetch_add(1, Ordering::Relaxed); + process_entry(&entry, families, options, collector); + } Err(_) => { // Error during traversal (permission denied, symlink loop, etc.) - errors.fetch_add(1, Ordering::Relaxed); - }, + collector.errors.fetch_add(1, Ordering::Relaxed); + } } - - // Continue traversal ignore::WalkState::Continue }) }); - // Collect final statistics - ScanStats { - files_scanned: scanned.load(Ordering::Relaxed), - files_deleted: deleted.load(Ordering::Relaxed), - errors: errors.load(Ordering::Relaxed), - } + Ok(collector.into_output(options.dry_run != 0)) } -/// Returns whether a visible filesystem leaf belongs to the requested mode. -fn matches_target(file_name: &OsStr, mode: MatchMode) -> bool { - match mode { - MatchMode::ReservedDeviceNames => RESERVED_NAMES - .iter() - .any(|&reserved| file_name.eq_ignore_ascii_case(reserved)), - MatchMode::LiteralDollarNull => file_name.to_str().is_some_and(|name| { - name.trim_end_matches(['.', ' ']) - .eq_ignore_ascii_case("$null") - }), +/// Evaluates a single walk entry against the active match families and, if it +/// matches, applies the zero-byte gate (for `$null` files) and either records a +/// would-delete candidate (dry-run) or attempts the Win32 delete. +fn process_entry( + entry: &ignore::DirEntry, + families: ActiveFamilies, + options: NukeOptions, + collector: &Collector, +) { + let Some(file_type) = entry.file_type() else { + return; + }; + let is_dir = file_type.is_dir(); + let is_file = file_type.is_file(); + + // Only real files are candidates by default; directories only when opted in. + // Symlinks, reparse points, and unknown types are never candidates. + let dirs_enabled = options.include_dirs != 0; + if !(is_file || is_dir && dirs_enabled) { + return; } -} -/// Deletes a file using the Win32 DeleteFileW API with extended-length path prefix -/// -/// This function: -/// 1. Converts the path to an extended-length path (`\\?\C:\...`) -/// 2. Converts the path to UTF-16 (wide string) for Win32 API -/// 3. Calls DeleteFileW directly to bypass standard library safety checks -/// -/// # Arguments -/// * `path` - The file path to delete -/// -/// # Returns -/// * `true` if the file was successfully deleted -/// * `false` if an error occurred (file in use, permission denied, conversion error, etc.) -/// -/// # Safety -/// This function uses unsafe code to call the Win32 API. The safety invariants are: -/// - The path is converted to a properly null-terminated UTF-16 string -/// - The DeleteFileW API is called with a valid wide string pointer -fn delete_file_win32(path: &Path) -> bool { - // Convert path to string - let path_str = match path.to_str() { - Some(s) => s, - None => { - // Path contains invalid UTF-8 - return false; - }, - }; + // Never touch the scan root itself (depth 0), even if its name happens to + // match a family - it is the directory being scanned, not a candidate. + if is_dir && entry.depth() == 0 { + return; + } - // Construct extended-length path to bypass Win32 path normalization - // and MAX_PATH limitations - // Format: \\?\C:\path\to\file - // - // This prefix tells Windows to: - // - Disable path parsing and normalization - // - Allow paths longer than 260 characters (MAX_PATH) - // - Allow reserved filenames like "nul", "con", etc. - let extended_path = if path_str.starts_with("\\\\?\\") { - // Already has extended-length prefix - path_str.to_string() - } else if let Some(stripped) = path_str.strip_prefix("\\\\") { - // UNC path: \\server\share -> \\?\UNC\server\share - format!("\\\\?\\UNC\\{}", stripped) - } else { - // Regular path: C:\path -> \\?\C:\path - format!("\\\\?\\{}", path_str) + let Some(family) = classify(entry.file_name(), families) else { + return; }; - // Convert to UTF-16 (wide string) for Win32 API - let wide_path = match U16CString::from_str(&extended_path) { - Ok(wp) => wp, - Err(_) => { - // String conversion error (null byte in path?) - return false; - }, - }; + let kind = if is_dir { "dir" } else { "file" }; + let path_display = entry.path().display().to_string(); + + // Zero-byte safety gate: only applies to real (non-directory) $null files. + // Directories get an equivalent safety net for free: RemoveDirectoryW simply + // refuses to remove a non-empty directory. + let mut size: Option = None; + let mut content_preview: Option = None; + + if family == MatchFamily::DollarNull && is_file { + match entry.metadata() { + Ok(meta) => { + let len = meta.len(); + size = Some(len); + if len > 0 { + content_preview = read_content_preview(entry.path()); + } + if let Some(reason) = dollar_null_skip_reason(len, options.allow_nonempty != 0) { + collector.push_skipped(ActionEntry { + path: path_display, + family: family.as_str(), + kind, + reason: Some(reason), + size, + content_preview, + }); + return; + } + } + Err(e) => { + collector.errors.fetch_add(1, Ordering::Relaxed); + collector.push_skipped(ActionEntry { + path: path_display, + family: family.as_str(), + kind, + reason: Some(format!("failed to read metadata: {e}")), + size: None, + content_preview: None, + }); + return; + } + } + } - // Call Win32 DeleteFileW API - // Safety: wide_path.as_ptr() returns a valid pointer to a null-terminated - // UTF-16 string that lives for the duration of this call - unsafe { - // DeleteFileW returns non-zero on success, zero on failure - DeleteFileW(wide_path.as_ptr()) != 0 + if options.dry_run != 0 { + collector.push_would_delete(ActionEntry { + path: path_display, + family: family.as_str(), + kind, + reason: None, + size, + content_preview, + }); + return; + } + + if is_dir { + match delete_dir_win32(entry.path()) { + Ok(()) => collector.push_deleted(ActionEntry { + path: path_display, + family: family.as_str(), + kind, + reason: None, + size, + content_preview, + }), + Err(DirDeleteOutcome::NotEmpty) => collector.push_skipped(ActionEntry { + path: path_display, + family: family.as_str(), + kind, + reason: Some("directory not empty (skipped, not deleted)".to_string()), + size, + content_preview, + }), + Err(DirDeleteOutcome::Failed(reason)) => { + collector.errors.fetch_add(1, Ordering::Relaxed); + collector.push_skipped(ActionEntry { + path: path_display, + family: family.as_str(), + kind, + reason: Some(reason), + size, + content_preview, + }); + } + } + } else { + match delete_file_win32(entry.path()) { + Ok(()) => collector.push_deleted(ActionEntry { + path: path_display, + family: family.as_str(), + kind, + reason: None, + size, + content_preview, + }), + Err(reason) => { + collector.errors.fetch_add(1, Ordering::Relaxed); + collector.push_skipped(ActionEntry { + path: path_display, + family: family.as_str(), + kind, + reason: Some(reason), + size, + content_preview, + }); + } + } } } -// Optional: Export additional utility functions for testing or advanced usage +// --------------------------------------------------------------------------- +// Misc exports +// --------------------------------------------------------------------------- /// Version information for the library #[no_mangle] @@ -318,7 +842,7 @@ pub extern "C" fn nuker_core_version() -> *const c_char { #[no_mangle] pub extern "C" fn nuker_core_test() -> u32 { // Return a magic number to verify DLL loaded correctly - 0xDEADBEEF + 0xDEAD_BEEF } #[cfg(test)] @@ -332,32 +856,101 @@ mod tests { assert!(RESERVED_NAMES.contains(&"prn")); } + #[test] + fn matches_reserved_is_case_insensitive() { + assert!(matches_reserved(OsStr::new("nul"))); + assert!(matches_reserved(OsStr::new("NUL"))); + assert!(matches_reserved(OsStr::new("Nul"))); + assert!(!matches_reserved(OsStr::new("$null"))); + assert!(!matches_reserved(OsStr::new("nul.txt"))); + } + #[test] fn literal_dollar_null_matching_is_narrow() { - assert!(matches_target( - OsStr::new("$null"), - MatchMode::LiteralDollarNull - )); - assert!(matches_target( - OsStr::new("$NULL. "), - MatchMode::LiteralDollarNull - )); - assert!(!matches_target( - OsStr::new("$null.txt"), - MatchMode::LiteralDollarNull - )); - assert!(!matches_target( - OsStr::new("nul"), - MatchMode::LiteralDollarNull - )); + assert!(matches_dollar_null(OsStr::new("$null"))); + assert!(matches_dollar_null(OsStr::new("$NULL. "))); + assert!(!matches_dollar_null(OsStr::new("$null.txt"))); + assert!(!matches_dollar_null(OsStr::new("nul"))); } #[test] - fn reserved_mode_does_not_match_literal_dollar_null() { - assert!(!matches_target( - OsStr::new("$null"), - MatchMode::ReservedDeviceNames - )); + fn path_mangle_matches_narrow_pattern() { + assert!(matches_path_mangle(OsStr::new("foo;C"))); + assert!(matches_path_mangle(OsStr::new("bar;C:"))); + assert!(matches_path_mangle(OsStr::new("headscale-ops;C"))); + assert!(matches_path_mangle(OsStr::new(";Z"))); + } + + #[test] + fn path_mangle_rejects_lookalikes() { + assert!(!matches_path_mangle(OsStr::new("a;b.txt"))); + assert!(!matches_path_mangle(OsStr::new("semi;colon"))); + assert!(!matches_path_mangle(OsStr::new("x;CD"))); + assert!(!matches_path_mangle(OsStr::new("plainfile.txt"))); + assert!(!matches_path_mangle(OsStr::new("C"))); + } + + #[test] + fn classify_respects_active_families() { + let all = ActiveFamilies { + reserved: true, + dollar_null: true, + path_mangle: true, + }; + assert_eq!( + classify(OsStr::new("nul"), all), + Some(MatchFamily::Reserved) + ); + assert_eq!( + classify(OsStr::new("$null"), all), + Some(MatchFamily::DollarNull) + ); + assert_eq!( + classify(OsStr::new("foo;C"), all), + Some(MatchFamily::PathMangle) + ); + assert_eq!(classify(OsStr::new("readme.txt"), all), None); + + let none = ActiveFamilies { + reserved: false, + dollar_null: false, + path_mangle: false, + }; + assert_eq!(classify(OsStr::new("nul"), none), None); + assert!(!none.any()); + assert!(all.any()); + } + + #[test] + fn classify_families_are_independently_toggleable() { + let only_path_mangle = ActiveFamilies { + reserved: false, + dollar_null: false, + path_mangle: true, + }; + assert_eq!(classify(OsStr::new("$null"), only_path_mangle), None); + assert_eq!( + classify(OsStr::new("foo;C"), only_path_mangle), + Some(MatchFamily::PathMangle) + ); + } + + #[test] + fn zero_byte_gate_allows_empty_files() { + assert!(dollar_null_skip_reason(0, false).is_none()); + assert!(dollar_null_skip_reason(0, true).is_none()); + } + + #[test] + fn zero_byte_gate_blocks_nonempty_by_default() { + let reason = dollar_null_skip_reason(877, false); + assert!(reason.is_some()); + assert!(reason.unwrap().contains("877 bytes")); + } + + #[test] + fn zero_byte_gate_allow_nonempty_overrides() { + assert!(dollar_null_skip_reason(877, true).is_none()); } #[test] @@ -369,10 +962,52 @@ mod tests { } #[test] - fn test_extended_path_regular() { - let path = Path::new("C:\\test\\file.txt"); - // This would normally call delete_file_win32, but we can't test - // actual deletion without creating test files - assert!(path.exists() || !path.exists()); // Tautology for compilation test + fn extended_path_regular() { + let path = Path::new(r"C:\test\file.txt"); + assert_eq!( + to_extended_path(path).as_deref(), + Some(r"\\?\C:\test\file.txt") + ); + } + + #[test] + fn extended_path_unc() { + let path = Path::new(r"\\server\share\file.txt"); + assert_eq!( + to_extended_path(path).as_deref(), + Some(r"\\?\UNC\server\share\file.txt") + ); + } + + #[test] + fn extended_path_already_extended_is_unchanged() { + let path = Path::new(r"\\?\C:\already\extended"); + assert_eq!( + to_extended_path(path).as_deref(), + Some(r"\\?\C:\already\extended") + ); + } + + #[test] + fn describe_win32_error_maps_known_codes() { + assert!(describe_win32_error(5).contains("access denied")); + assert!(describe_win32_error(145).contains("not empty")); + assert!(describe_win32_error(999_999).contains("999999")); + } + + #[test] + fn resolve_families_maps_options() { + let options = NukeOptions { + match_reserved: 1, + match_dollar_null: 0, + match_path_mangle: 1, + include_dirs: 0, + dry_run: 0, + allow_nonempty: 0, + }; + let families = resolve_families(&options); + assert!(families.reserved); + assert!(!families.dollar_null); + assert!(families.path_mangle); } } From 2d53cf043066208d3b40bbe3b6ed48dc192cd439 Mon Sep 17 00:00:00 2001 From: "David T. Martel" Date: Thu, 6 Aug 2026 11:02:06 -0400 Subject: [PATCH 3/8] fix(nuker_core): predict directory emptiness in dry-run previews --dry-run was reporting every matched directory as a would-delete candidate without checking whether RemoveDirectoryW would actually succeed. A directory containing files (found via manual smoke-testing --all --include-dirs against a populated sandbox) showed up under would_delete when it would really be skipped as not-empty on a real run, making the preview inaccurate for exactly the case --include-dirs exists to guard. Adds dir_is_empty(), a lightweight std::fs::read_dir check that mirrors RemoveDirectoryW's real behavior without attempting the delete, and routes dry-run directory candidates through it so would_delete/skipped match what a non-dry-run pass would actually do. Two new tests exercise it against real scratch directories (empty and non-empty). Agent: claude Co-authored-by: Claude --- nuker_core/src/lib.rs | 96 +++++++++++++++++++++++++++++++++++++++---- 1 file changed, 88 insertions(+), 8 deletions(-) diff --git a/nuker_core/src/lib.rs b/nuker_core/src/lib.rs index a02349b..4974ebb 100644 --- a/nuker_core/src/lib.rs +++ b/nuker_core/src/lib.rs @@ -488,6 +488,18 @@ fn delete_dir_win32(path: &Path) -> Result<(), DirDeleteOutcome> { } } +/// Reports whether a directory is empty, for the `--dry-run` preview path. This +/// mirrors the real safety gate (`RemoveDirectoryW` refuses non-empty +/// directories) without actually attempting a delete. +fn dir_is_empty(path: &Path) -> Result { + let extended = + to_extended_path(path).ok_or_else(|| "path contains invalid UTF-8".to_string())?; + match std::fs::read_dir(extended) { + Ok(mut entries) => Ok(entries.next().is_none()), + Err(e) => Err(format!("failed to read directory: {e}")), + } +} + /// Reads up to [`CONTENT_PREVIEW_MAX_BYTES`] of `path` for operator review, /// lossily decoded as UTF-8. Returns `None` if the file cannot be opened or read /// (this is best-effort auditing, not a correctness requirement). @@ -762,14 +774,49 @@ fn process_entry( } if options.dry_run != 0 { - collector.push_would_delete(ActionEntry { - path: path_display, - family: family.as_str(), - kind, - reason: None, - size, - content_preview, - }); + // For directories, predict RemoveDirectoryW's outcome up front so a dry + // run is an honest preview: a non-empty directory would be skipped, not + // deleted, so it must be reported that way here too. + if is_dir { + match dir_is_empty(entry.path()) { + Ok(true) => collector.push_would_delete(ActionEntry { + path: path_display, + family: family.as_str(), + kind, + reason: None, + size, + content_preview, + }), + Ok(false) => collector.push_skipped(ActionEntry { + path: path_display, + family: family.as_str(), + kind, + reason: Some("directory not empty (would be skipped, not deleted)".to_string()), + size, + content_preview, + }), + Err(e) => { + collector.errors.fetch_add(1, Ordering::Relaxed); + collector.push_skipped(ActionEntry { + path: path_display, + family: family.as_str(), + kind, + reason: Some(e), + size, + content_preview, + }); + } + } + } else { + collector.push_would_delete(ActionEntry { + path: path_display, + family: family.as_str(), + kind, + reason: None, + size, + content_preview, + }); + } return; } @@ -1010,4 +1057,37 @@ mod tests { assert!(!families.dollar_null); assert!(families.path_mangle); } + + /// Minimal RAII helper for a scratch directory under `target/`, cleaned up + /// on drop regardless of test outcome. + struct ScratchDir(std::path::PathBuf); + + impl ScratchDir { + fn new(name: &str) -> Self { + let dir = + std::env::temp_dir().join(format!("nuker_core_test_{name}_{}", std::process::id())); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(&dir).expect("create scratch dir"); + Self(dir) + } + } + + impl Drop for ScratchDir { + fn drop(&mut self) { + let _ = std::fs::remove_dir_all(&self.0); + } + } + + #[test] + fn dir_is_empty_true_for_empty_directory() { + let scratch = ScratchDir::new("empty"); + assert_eq!(dir_is_empty(&scratch.0), Ok(true)); + } + + #[test] + fn dir_is_empty_false_for_directory_with_children() { + let scratch = ScratchDir::new("nonempty"); + std::fs::write(scratch.0.join("keep.txt"), b"data").expect("write child file"); + assert_eq!(dir_is_empty(&scratch.0), Ok(false)); + } } From d62736a848547a0495fe9d6a1ef8a2587cdc47bd Mon Sep 17 00:00:00 2001 From: "David T. Martel" Date: Thu, 6 Aug 2026 11:02:43 -0400 Subject: [PATCH 4/8] feat(cli): drive the new composable engine with dry-run/include-dirs/all flags Rewrites Program.cs's CLI surface and P/Invoke layer to call the new nuke_files_ex FFI entry point instead of the two single-mode legacy functions. New flags: --reserved, --path-mangle, --all, --include-dirs, --dry-run/-n, --allow-nonempty, composable with the existing --dollar-null-only (which keeps its historical "only this family" behavior when passed alone, per the family-default resolution documented in Main). The engine's JSON (deleted/would_delete/skipped, each with path/family/kind/ reason/size/content_preview) is deserialized via new AOT-safe source-gen DTOs (EngineOutput/EngineCounts/EngineActionEntry) and folded into the existing top-level ScanResult shape, adding a top-level "dry_run" flag and per-entry audit detail to "results" instead of bare counts. Root path is now marshaled as UTF-8 (LPUTF8Str) rather than ANSI, and the returned JSON string is always freed via nuke_free_string in a finally block. Functionally smoke-tested end to end against a hand-built sandbox containing a real reserved-name file (created via the same \\?\ extended-path mechanism the tool itself relies on), zero- and non-empty $null files, and path-mangle files/directories (including a non-empty one to confirm the not-empty skip path): bare invocation, --dollar-null-only alone, --dollar-null-only --path-mangle, --allow-nonempty, --dry-run --all --include-dirs, and error paths (unknown flag, missing target) all produced the expected JSON and left disk state exactly as reported. Native AOT publish could not be verified in this environment (see PR/task notes) - this was exercised via `dotnet build` + direct execution of the resulting IL assembly against a freshly rebuilt nuker_core.dll. Agent: claude Co-authored-by: Claude --- Program.cs | 366 +++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 311 insertions(+), 55 deletions(-) diff --git a/Program.cs b/Program.cs index e93c790..28e6333 100644 --- a/Program.cs +++ b/Program.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Runtime.InteropServices; @@ -13,12 +14,13 @@ namespace NukeNul; [JsonSourceGenerationOptions(WriteIndented = true, PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)] [JsonSerializable(typeof(ScanResult))] [JsonSerializable(typeof(ErrorResult))] +[JsonSerializable(typeof(EngineOutput))] internal partial class SourceGenerationContext : JsonSerializerContext { } /// -/// C-compatible struct matching Rust's ScanStats layout +/// C-compatible struct matching Rust's ScanStats layout (legacy, retained for reference). /// [StructLayout(LayoutKind.Sequential)] internal struct ScanStats @@ -28,6 +30,21 @@ internal struct ScanStats public uint Errors; } +/// +/// C-compatible struct matching Rust's NukeOptions layout. Every field is a byte +/// (0 = false, non-zero = true) to avoid platform BOOL marshaling ambiguity. +/// +[StructLayout(LayoutKind.Sequential)] +internal struct NukeOptions +{ + public byte MatchReserved; + public byte MatchDollarNull; + public byte MatchPathMangle; + public byte IncludeDirs; + public byte DryRun; + public byte AllowNonempty; +} + /// /// JSON output structure for LLM-friendly machine-readable results /// @@ -48,6 +65,9 @@ internal sealed class ScanResult [JsonPropertyName("status")] public string Status { get; set; } = "Running"; + [JsonPropertyName("dry_run")] + public bool DryRun { get; set; } + [JsonPropertyName("performance")] public PerformanceInfo Performance { get; set; } = new(); @@ -67,16 +87,36 @@ internal sealed class PerformanceInfo public long ElapsedMs { get; set; } } +/// +/// Auditable results: counts plus the actual paths acted on, mirroring the +/// engine's JSON so nothing is lost translating Rust's payload into the +/// tool's top-level output shape. +/// internal sealed class ResultsInfo { [JsonPropertyName("scanned")] public uint Scanned { get; set; } [JsonPropertyName("deleted")] - public uint Deleted { get; set; } + public int Deleted { get; set; } + + [JsonPropertyName("would_delete")] + public int WouldDelete { get; set; } + + [JsonPropertyName("skipped")] + public int Skipped { get; set; } [JsonPropertyName("errors")] public uint Errors { get; set; } + + [JsonPropertyName("deleted_entries")] + public List DeletedEntries { get; set; } = new(); + + [JsonPropertyName("would_delete_entries")] + public List WouldDeleteEntries { get; set; } = new(); + + [JsonPropertyName("skipped_entries")] + public List SkippedEntries { get; set; } = new(); } /// @@ -94,28 +134,111 @@ internal sealed class ErrorResult public string Message { get; set; } = string.Empty; } +/// +/// Deserialization target for the JSON string returned by the Rust +/// nuke_files_ex FFI function. Field names match Rust's serde output +/// (snake_case) via explicit . +/// +internal sealed class EngineOutput +{ + [JsonPropertyName("status")] + public string Status { get; set; } = string.Empty; + + [JsonPropertyName("message")] + public string? Message { get; set; } + + [JsonPropertyName("dry_run")] + public bool DryRun { get; set; } + + [JsonPropertyName("counts")] + public EngineCounts Counts { get; set; } = new(); + + [JsonPropertyName("deleted")] + public List Deleted { get; set; } = new(); + + [JsonPropertyName("would_delete")] + public List WouldDelete { get; set; } = new(); + + [JsonPropertyName("skipped")] + public List Skipped { get; set; } = new(); +} + +internal sealed class EngineCounts +{ + [JsonPropertyName("scanned")] + public uint Scanned { get; set; } + + [JsonPropertyName("deleted")] + public int Deleted { get; set; } + + [JsonPropertyName("would_delete")] + public int WouldDelete { get; set; } + + [JsonPropertyName("skipped")] + public int Skipped { get; set; } + + [JsonPropertyName("errors")] + public uint Errors { get; set; } +} + +internal sealed class EngineActionEntry +{ + [JsonPropertyName("path")] + public string Path { get; set; } = string.Empty; + + [JsonPropertyName("family")] + public string Family { get; set; } = string.Empty; + + [JsonPropertyName("kind")] + public string Kind { get; set; } = string.Empty; + + [JsonPropertyName("reason")] + public string? Reason { get; set; } + + [JsonPropertyName("size")] + public ulong? Size { get; set; } + + [JsonPropertyName("content_preview")] + public string? ContentPreview { get; set; } +} + internal static class NativeMethods { private const string DllName = "nuker_core.dll"; /// - /// Imports the Rust function that performs parallel file scanning and deletion + /// Runs a scan/delete pass and returns a heap-allocated, null-terminated + /// UTF-8 JSON string describing the result. The caller MUST pass the + /// returned pointer to exactly once. /// - /// UTF-8 encoded root path to scan - /// Statistics struct containing scan results - [DllImport(DllName, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)] - internal static extern ScanStats nuke_reserved_files([MarshalAs(UnmanagedType.LPStr)] string rootPath); + [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)] + internal static extern IntPtr nuke_files_ex( + [MarshalAs(UnmanagedType.LPUTF8Str)] string rootPath, + NukeOptions options); /// - /// Deletes only ordinary files whose visible leaf is literal $null. + /// Frees a string previously returned by . /// - [DllImport(DllName, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)] - internal static extern ScanStats nuke_dollar_null_files( - [MarshalAs(UnmanagedType.LPStr)] string rootPath); + [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)] + internal static extern void nuke_free_string(IntPtr ptr); } internal static class Program { + /// + /// Parsed CLI flags before family-default resolution. + /// + private sealed class ParsedArgs + { + public string TargetPath = "."; + public bool Reserved; + public bool DollarNull; + public bool PathMangle; + public bool All; + public bool IncludeDirs; + public bool DryRun; + public bool AllowNonempty; + } private static int Main(string[] args) { @@ -125,13 +248,14 @@ private static int Main(string[] args) return 0; } - if (!TryParseArguments(args, out string targetPath, out bool dollarNullOnly, - out string? argumentError)) + if (!TryParseArguments(args, out ParsedArgs parsed, out string? argumentError)) { WriteError(argumentError!); return 1; } + string targetPath = parsed.TargetPath; + // Validate and resolve target path if (!ValidateTargetPath(ref targetPath, out string? errorMessage)) { @@ -139,12 +263,34 @@ private static int Main(string[] args) return 1; } + // Resolve which match families are active. Bare invocation (no family + // flag at all) preserves the original default: reserved-device names + // only. --dollar-null-only alone keeps its historical "only" behavior + // (reserved is NOT auto-included). Any explicit combination of + // --reserved / --dollar-null-only / --path-mangle composes exactly + // the families named. --all is shorthand for all three. + bool anyFamilyFlag = parsed.Reserved || parsed.DollarNull || parsed.PathMangle || parsed.All; + bool finalReserved = parsed.All || parsed.Reserved || !anyFamilyFlag; + bool finalDollarNull = parsed.All || parsed.DollarNull; + bool finalPathMangle = parsed.All || parsed.PathMangle; + + var options = new NukeOptions + { + MatchReserved = (byte)(finalReserved ? 1 : 0), + MatchDollarNull = (byte)(finalDollarNull ? 1 : 0), + MatchPathMangle = (byte)(finalPathMangle ? 1 : 0), + IncludeDirs = (byte)(parsed.IncludeDirs ? 1 : 0), + DryRun = (byte)(parsed.DryRun ? 1 : 0), + AllowNonempty = (byte)(parsed.AllowNonempty ? 1 : 0), + }; + // Initialize result object var result = new ScanResult { Target = targetPath, - Operation = dollarNullOnly ? "LiteralDollarNullOnly" : "ReservedDeviceNames", - Timestamp = DateTime.UtcNow + Operation = BuildOperationLabel(finalReserved, finalDollarNull, finalPathMangle, parsed.IncludeDirs, parsed.AllowNonempty), + Timestamp = DateTime.UtcNow, + DryRun = parsed.DryRun, }; // Verify DLL exists before attempting to call it @@ -157,30 +303,61 @@ private static int Main(string[] args) // Execute the Rust file scanning and deletion var stopwatch = Stopwatch.StartNew(); + IntPtr resultPtr = IntPtr.Zero; try { // Critical P/Invoke call - blocks while Rust uses all CPU cores - ScanStats stats = dollarNullOnly - ? NativeMethods.nuke_dollar_null_files(targetPath) - : NativeMethods.nuke_reserved_files(targetPath); + resultPtr = NativeMethods.nuke_files_ex(targetPath, options); stopwatch.Stop(); + if (resultPtr == IntPtr.Zero) + { + result.Status = "Fatal Error"; + result.Performance.ElapsedMs = stopwatch.ElapsedMilliseconds; + WriteError("nuke_files_ex returned a null pointer unexpectedly"); + return 99; + } + + string json = Marshal.PtrToStringUTF8(resultPtr) ?? string.Empty; + EngineOutput? engine = JsonSerializer.Deserialize(json, SourceGenerationContext.Default.EngineOutput); + + if (engine is null) + { + result.Status = "Fatal Error"; + result.Performance.ElapsedMs = stopwatch.ElapsedMilliseconds; + WriteError("Failed to parse engine result JSON"); + return 99; + } + + if (!string.Equals(engine.Status, "success", StringComparison.Ordinal)) + { + result.Status = "Fatal Error"; + result.Performance.ElapsedMs = stopwatch.ElapsedMilliseconds; + WriteError(engine.Message ?? "Unknown engine error"); + return 99; + } + // Update result with success data result.Status = "Success"; result.Performance.ElapsedMs = stopwatch.ElapsedMilliseconds; result.Results = new ResultsInfo { - Scanned = stats.FilesScanned, - Deleted = stats.FilesDeleted, - Errors = stats.Errors + Scanned = engine.Counts.Scanned, + Deleted = engine.Counts.Deleted, + WouldDelete = engine.Counts.WouldDelete, + Skipped = engine.Counts.Skipped, + Errors = engine.Counts.Errors, + DeletedEntries = engine.Deleted, + WouldDeleteEntries = engine.WouldDelete, + SkippedEntries = engine.Skipped, }; // Output JSON to stdout WriteJson(result); // Return exit code based on errors - return stats.Errors > 0 ? 3 : 0; + return engine.Counts.Errors > 0 ? 3 : 0; } catch (DllNotFoundException ex) { @@ -198,53 +375,110 @@ private static int Main(string[] args) WriteError($"Unexpected error: {ex.Message}"); return 99; } + finally + { + if (resultPtr != IntPtr.Zero) + { + NativeMethods.nuke_free_string(resultPtr); + } + } } /// - /// Parses one optional target path and the narrow literal-$null mode. + /// Parses CLI flags and the optional target directory. /// private static bool TryParseArguments( string[] args, - out string targetPath, - out bool dollarNullOnly, + out ParsedArgs parsed, out string? errorMessage) { - targetPath = "."; - dollarNullOnly = false; + parsed = new ParsedArgs(); errorMessage = null; bool targetProvided = false; foreach (string arg in args) { - if (arg.Equals("--dollar-null-only", StringComparison.OrdinalIgnoreCase)) + switch (arg.ToLowerInvariant()) { - if (dollarNullOnly) - { - errorMessage = "Option --dollar-null-only may be specified only once."; - return false; - } - - dollarNullOnly = true; - continue; + case "--dollar-null-only": + parsed.DollarNull = true; + break; + case "--path-mangle": + parsed.PathMangle = true; + break; + case "--reserved": + parsed.Reserved = true; + break; + case "--all": + parsed.All = true; + break; + case "--include-dirs": + parsed.IncludeDirs = true; + break; + case "--dry-run": + case "-n": + parsed.DryRun = true; + break; + case "--allow-nonempty": + parsed.AllowNonempty = true; + break; + default: + if (arg.StartsWith("-", StringComparison.Ordinal)) + { + errorMessage = $"Unknown option: {arg}"; + return false; + } + + if (targetProvided) + { + errorMessage = "Only one target directory may be specified."; + return false; + } + + parsed.TargetPath = arg; + targetProvided = true; + break; } + } - if (arg.StartsWith("-", StringComparison.Ordinal)) - { - errorMessage = $"Unknown option: {arg}"; - return false; - } + return true; + } - if (targetProvided) - { - errorMessage = "Only one target directory may be specified."; - return false; - } + /// + /// Builds a human-readable operation label from the resolved options. + /// + private static string BuildOperationLabel(bool reserved, bool dollarNull, bool pathMangle, bool includeDirs, bool allowNonempty) + { + var families = new List(); + if (reserved) + { + families.Add("ReservedDeviceNames"); + } + + if (dollarNull) + { + families.Add("LiteralDollarNull"); + } - targetPath = arg; - targetProvided = true; + if (pathMangle) + { + families.Add("PathMangle"); } - return true; + string label = families.Count > 0 ? string.Join("+", families) : "None"; + + var modifiers = new List(); + if (includeDirs) + { + modifiers.Add("IncludeDirs"); + } + + if (allowNonempty) + { + modifiers.Add("AllowNonempty"); + } + + return modifiers.Count > 0 ? $"{label} ({string.Join(",", modifiers)})" : label; } private static void WriteHelp() @@ -254,12 +488,34 @@ private static void WriteHelp() NukeNul - Windows problematic-filename cleaner Usage: - NukeNul.exe [--dollar-null-only] [target-directory] - - Modes: - default Delete reserved device-name files. - --dollar-null-only Delete only real files named literal $null - (case-insensitive; trailing dots/spaces allowed). + NukeNul.exe [options] [target-directory] + + Match families (composable - pass any combination): + --reserved Reserved device-name files (nul, con, prn, aux, + com1-9, lpt1-9). Active by default when no other + family flag is given. + --dollar-null-only Literal $null files/dirs. Zero-byte files are + deleted by default; see --allow-nonempty. + --path-mangle Shell path-mangle artifacts: leaf names ending in + ";" or ";:" (e.g. "foo;C"). + --all Shorthand for all three families above. + + Modifiers: + --include-dirs Also remove matching directories, but ONLY if they + are empty (never recursive; RemoveDirectoryW itself + refuses non-empty directories). + --dry-run, -n Preview only - walk and match, delete nothing. + --allow-nonempty Allow deleting $null files larger than zero bytes + (dollar-null family only; default is zero-byte-only). + + Examples: + NukeNul.exe C:\Path\To\Scan + NukeNul.exe --dry-run --all C:\Path\To\Scan + NukeNul.exe --dollar-null-only --path-mangle --include-dirs C:\Path + + Behavior change: --dollar-null-only now deletes ONLY zero-byte $null files + by default (previously deleted files of any size). Pass --allow-nonempty to + restore the old behavior for a given run. """); } From c3e1b014f7bb77770beaa8e8b4188162ed5f8ca5 Mon Sep 17 00:00:00 2001 From: "David T. Martel" Date: Thu, 6 Aug 2026 11:05:21 -0400 Subject: [PATCH 5/8] docs: document dry-run, path-mangle, dir cleanup, and the $null behavior change Adds the new CLI reference table, an example of the new auditable JSON shape (deleted/would_delete/skipped entries with path/family/kind/reason/size/ content_preview), the new nuke_files_ex FFI signature, and a prominently called-out "Behavior change" section explaining that --dollar-null-only now deletes only zero-byte $null files by default (previously any size) per the workspace's zero-byte discovery-hook policy. Updates Limitations and Future Enhancements to match what shipped. Agent: claude Co-authored-by: Claude --- README.md | 140 +++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 127 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 7a0a296..8a04c98 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,30 @@ Traditional PowerShell scripts hit performance ceilings when dealing with reserv - ✅ **Self-contained** - No .NET runtime required - ✅ **Cross-platform ready** - Windows x64 (Linux/macOS support possible) - ✅ **Literal `$null` safety mode** - Deletes only real `$null` files, not device aliases +- ✅ **Composable match families** - reserved device names, literal `$null`, and + path-mangle artifacts (`foo;C`) can be combined in a single pass +- ✅ **`--dry-run`** - Preview exactly what would be deleted (and why something + would be skipped) without touching disk +- ✅ **Empty-directory cleanup** (`--include-dirs`) - Removes matching directories, + but only if they are already empty; never recursive +- ✅ **Auditable output** - Every deleted/would-delete/skipped entry lists its full + path, match family, and (for `$null`) size + a content preview + +## ⚠️ Behavior change: `$null` zero-byte safety gate + +As of this version, the `$null` family (`--dollar-null-only`) deletes **only +zero-byte** files by default. This matches the governing workspace policy: +*"Discovery hooks may delete only zero-byte matches inside the active +workspace; preserve and report non-empty or out-of-scope matches."* + +Non-empty `$null` files are now **skipped** (not deleted) and reported in the +JSON output with their size and the first 200 bytes of their content, so an +operator can review them before deciding what to do. Pass `--allow-nonempty` +to opt into deleting `$null` files larger than zero bytes, restoring the +previous unconditional-delete behavior for that run. + +Previously (pre-`--dry-run`/`--allow-nonempty`), `--dollar-null-only` deleted +`$null` files of any size unconditionally. ## Installation @@ -47,7 +71,7 @@ pwsh -File .\build.ps1 ### Basic Usage ```bash -# Scan current directory +# Scan current directory (default: reserved device names only) NukeNul.exe # Scan specific directory @@ -56,18 +80,49 @@ NukeNul.exe C:\Path\To\Scan # Scan with full path NukeNul.exe "C:\Users\david\Documents" -# Delete only visible files named literal $null +# Delete only visible files named literal $null (zero-byte only by default) NukeNul.exe --dollar-null-only "C:\Path\To\Scan" + +# Preview a full sweep of every family, including empty directories +NukeNul.exe --dry-run --all --include-dirs "C:\Path\To\Scan" + +# Combine families explicitly +NukeNul.exe --dollar-null-only --path-mangle "C:\Path\To\Scan" + +# Delete non-empty $null files too (opt-in override) +NukeNul.exe --dollar-null-only --allow-nonempty "C:\Path\To\Scan" ``` +### CLI Reference + +Match families (composable - pass any combination; bare invocation defaults +to `--reserved`): + +| Flag | Family | Notes | +|------|--------|-------| +| `--reserved` | Reserved device names | `nul`, `con`, `prn`, `aux`, `com1-9`, `lpt1-9`. Active by default when no other family flag is given. | +| `--dollar-null-only` | Literal `$null` | Zero-byte files deleted by default; see `--allow-nonempty`. Kept exclusive when passed alone, for backward compatibility. | +| `--path-mangle` | Shell path-mangle artifacts | Leaf names ending in `;` or `;:` (e.g. `foo;C`, `foo;C:`). Deliberately narrow - `;` is a legal filename character. | +| `--all` | All three families | Shorthand for `--reserved --dollar-null-only --path-mangle`. | + +Modifiers: + +| Flag | Effect | +|------|--------| +| `--include-dirs` | Also remove matching directories, but **only if they are empty**. Never recursive - `RemoveDirectoryW` itself refuses non-empty directories, and that refusal is the safety mechanism. | +| `--dry-run`, `-n` | Preview only. Walks and matches identically to a real run (including predicting whether a directory would be empty), but performs no deletion. Exit code 0. | +| `--allow-nonempty` | Allow deleting `$null` files larger than zero bytes (dollar-null family only). | + ### Example Output ```json { "tool": "Nuke-Nul", "target": "C:\\Users\\david\\Documents", + "operation": "ReservedDeviceNames+LiteralDollarNull", "timestamp": "2026-01-23T19:30:45.1234567Z", "status": "Success", + "dry_run": false, "performance": { "mode": "Rust/Parallel", "threads": 16, @@ -76,11 +131,33 @@ NukeNul.exe --dollar-null-only "C:\Path\To\Scan" "results": { "scanned": 154020, "deleted": 12, - "errors": 0 + "would_delete": 0, + "skipped": 1, + "errors": 0, + "deleted_entries": [ + {"path": "C:\\Users\\david\\Documents\\nul", "family": "reserved", "kind": "file"} + ], + "would_delete_entries": [], + "skipped_entries": [ + { + "path": "C:\\Users\\david\\Documents\\$null", + "family": "dollar_null", + "kind": "file", + "reason": "non-empty $null file (877 bytes); rerun with --allow-nonempty to delete", + "size": 877, + "content_preview": "rg: no matches found" + } + ] } } ``` +Every `deleted_entries` / `would_delete_entries` / `skipped_entries` item +carries `path`, `family` (`reserved` | `dollar_null` | `path_mangle`), and +`kind` (`file` | `dir`). `reason`, `size`, and `content_preview` are present +only where relevant (skips always have a `reason`; `$null` files carry `size`, +and non-empty ones also carry `content_preview`). + ### Exit Codes - `0` - Success, no errors @@ -175,6 +252,31 @@ print(f"Time: {data['performance']['elapsed_ms']}ms") ### Rust DLL Interface +The primary entry point is `nuke_files_ex`, which returns a heap-allocated +JSON string (the caller must free it with `nuke_free_string`): + +```rust +#[repr(C)] +pub struct NukeOptions { + pub match_reserved: u8, + pub match_dollar_null: u8, + pub match_path_mangle: u8, + pub include_dirs: u8, + pub dry_run: u8, + pub allow_nonempty: u8, +} + +#[no_mangle] +pub unsafe extern "C" fn nuke_files_ex(root_ptr: *const c_char, options: NukeOptions) -> *mut c_char; + +#[no_mangle] +pub unsafe extern "C" fn nuke_free_string(ptr: *mut c_char); +``` + +The original single-mode functions remain exported with their original +`ScanStats`-returning signature for ABI compatibility with any other callers +of `nuker_core.dll` (NukeNul.exe itself no longer calls them): + ```rust #[repr(C)] pub struct ScanStats { @@ -184,30 +286,40 @@ pub struct ScanStats { } #[no_mangle] -pub extern "C" fn nuke_reserved_files(root_ptr: *const c_char) -> ScanStats; +pub unsafe extern "C" fn nuke_reserved_files(root_ptr: *const c_char) -> ScanStats; +#[no_mangle] +pub unsafe extern "C" fn nuke_dollar_null_files(root_ptr: *const c_char) -> ScanStats; ``` ### C# P/Invoke ```csharp [StructLayout(LayoutKind.Sequential)] -internal struct ScanStats +internal struct NukeOptions { - public uint FilesScanned; - public uint FilesDeleted; - public uint Errors; + public byte MatchReserved; + public byte MatchDollarNull; + public byte MatchPathMangle; + public byte IncludeDirs; + public byte DryRun; + public byte AllowNonempty; } [DllImport("nuker_core.dll", CallingConvention = CallingConvention.Cdecl)] -internal static extern ScanStats nuke_reserved_files(string rootPath); +internal static extern IntPtr nuke_files_ex( + [MarshalAs(UnmanagedType.LPUTF8Str)] string rootPath, + NukeOptions options); + +[DllImport("nuker_core.dll", CallingConvention = CallingConvention.Cdecl)] +internal static extern void nuke_free_string(IntPtr ptr); ``` ## Limitations 1. **Windows Only** - Uses Win32 API (Linux/macOS support requires alternative implementation) -2. **Target Modes** - Default reserved-device cleanup or narrow `--dollar-null-only` cleanup -3. **No Undo** - Deleted files are permanently removed (use with caution) -4. **Admin Rights** - Some system directories may require elevation +2. **No Undo** - Deleted files are permanently removed outside dry-run mode (use with caution) +3. **Admin Rights** - Some system directories may require elevation +4. **`--include-dirs` never recurses** - a matching directory is only removed if it is already empty; NukeNul will never empty out or recursively delete a directory tree ## Safety Considerations @@ -220,8 +332,10 @@ internal static extern ScanStats nuke_reserved_files(string rootPath); ## Future Enhancements +- [x] Dry-run mode (scan without deletion) - `--dry-run`/`-n` +- [x] Empty-directory cleanup - `--include-dirs` +- [x] Composable match families + auditable per-path JSON output - [ ] Configuration file for reserved name patterns -- [ ] Dry-run mode (scan without deletion) - [ ] Recursive depth limiting - [ ] Custom exclusion patterns (beyond `.git`) - [ ] Progress reporting for large scans From c369d3b47e21d8884dd250528925d739d059b652 Mon Sep 17 00:00:00 2001 From: "David T. Martel" Date: Thu, 6 Aug 2026 11:15:54 -0400 Subject: [PATCH 6/8] feat(nuker_core): add leading-$ shell-variable-artifact family The $null-only matcher was too narrow for the actual defect class. A live sweep of C:\codedev on 2026-08-06 found four mangled directories the existing matchers would have missed entirely -- $runDir, $out, $local and $archiveDir -- all produced by PowerShell variable names surviving into a bash context, the same root cause as the $null files. Adds a DollarPrefix family matching leaf names that start with '$' other than '$null' itself, sharing the zero-byte content gate with the $null family since both are stray shell-variable artifacts. Covered by unit tests, including one asserting the family excludes '$null' and its lookalikes so the two families stay disjoint. nuker_core: 21 tests passing, clippy --all-targets -D warnings clean, cargo fmt --check clean. Known gap: Program.cs does not yet expose a --dollar-prefix flag, so this family is reachable through the FFI but not from the CLI. Agent: claude Co-authored-by: Claude --- nuker_core/src/lib.rs | 154 +++++++++++++++++++++++++++++++++++------- 1 file changed, 130 insertions(+), 24 deletions(-) diff --git a/nuker_core/src/lib.rs b/nuker_core/src/lib.rs index 4974ebb..b1d98e3 100644 --- a/nuker_core/src/lib.rs +++ b/nuker_core/src/lib.rs @@ -2,8 +2,8 @@ //! //! This library provides a C-compatible FFI interface for deleting Windows filenames //! that standard tooling cannot remove (reserved device aliases, stray literal `$null` -//! artifacts, and shell path-mangling artifacts), using parallel file system traversal -//! and direct Win32 API calls. +//! artifacts, shell path-mangling artifacts, and stray `$variable` artifacts), +//! using parallel file system traversal and direct Win32 API calls. //! //! # Architecture //! - Uses `ignore` crate for multi-threaded directory walking (ripgrep's engine) @@ -12,17 +12,26 @@ //! - Thread-safe collection of per-entry results (deleted / would-delete / skipped) //! //! # Match families -//! A scan can combine any of three independent name families: +//! A scan can combine any of four independent name families: //! - **Reserved device names**: `nul`, `con`, `prn`, `aux`, `com1-9`, `lpt1-9` //! - **Literal `$null`**: real files/dirs whose visible leaf is exactly `$null` -//! (case-insensitive, trailing dots/spaces ignored). Files are only deleted when -//! zero-byte unless `allow_nonempty` is set (see [`NukeOptions`]). +//! (case-insensitive, trailing dots/spaces ignored). //! - **Path-mangle artifacts**: leaf names ending in `;` or `;:`, //! produced by shells that mis-concatenate a path. +//! - **Leading-`$` artifacts**: leaf names starting with `$` other than `$null` +//! itself (e.g. `$runDir`, `$out`, `$local`, `$archiveDir`), produced when a +//! PowerShell `$variable` name is stripped or misinterpreted in a bash context. +//! +//! `$null` and leading-`$` matches are both "stray shell-variable artifact" +//! families: a matching *file* is only deleted when zero-byte unless +//! `allow_nonempty` is set (see [`NukeOptions`]), and a non-empty match is +//! reported with its size and a content preview so an operator can judge it. +//! Reserved-device and path-mangle files carry no such gate. //! //! Directories are never touched unless `include_dirs` is set, and even then only an //! *empty* directory is ever removed (`RemoveDirectoryW` itself refuses non-empty -//! directories - we never recurse to empty one out). +//! directories - we never recurse to empty one out) - this is the safety gate for +//! all four families' directory matches alike. //! //! # Safety //! This library uses unsafe code for FFI and Win32 API calls. All unsafe blocks @@ -88,16 +97,21 @@ impl ScanStats { /// - `match_reserved` - include the reserved-device-name family /// - `match_dollar_null` - include the literal `$null` family /// - `match_path_mangle` - include the path-mangle-artifact family +/// - `match_dollar_prefix` - include the leading-`$` shell-variable-artifact +/// family (e.g. `$runDir`, `$out`) produced when a PowerShell `$variable` +/// name is stripped/misinterpreted in a bash context /// - `include_dirs` - also consider empty directories as deletion candidates /// - `dry_run` - walk and match, but never delete anything -/// - `allow_nonempty` - allow deleting `$null` files larger than zero bytes -/// (only meaningful when `match_dollar_null` is set) +/// - `allow_nonempty` - allow deleting non-zero-byte files matched by +/// `match_dollar_null` or `match_dollar_prefix` (both are "stray shell +/// artifact" families gated by the same zero-byte content-safety check) #[repr(C)] #[derive(Debug, Clone, Copy)] pub struct NukeOptions { pub match_reserved: u8, pub match_dollar_null: u8, pub match_path_mangle: u8, + pub match_dollar_prefix: u8, pub include_dirs: u8, pub dry_run: u8, pub allow_nonempty: u8, @@ -124,6 +138,7 @@ pub unsafe extern "C" fn nuke_reserved_files(root_ptr: *const c_char) -> ScanSta match_reserved: 1, match_dollar_null: 0, match_path_mangle: 0, + match_dollar_prefix: 0, include_dirs: 0, dry_run: 0, allow_nonempty: 0, @@ -155,6 +170,7 @@ pub unsafe extern "C" fn nuke_dollar_null_files(root_ptr: *const c_char) -> Scan match_reserved: 0, match_dollar_null: 1, match_path_mangle: 0, + match_dollar_prefix: 0, include_dirs: 0, dry_run: 0, allow_nonempty: 0, @@ -292,6 +308,7 @@ enum MatchFamily { Reserved, DollarNull, PathMangle, + DollarPrefix, } impl MatchFamily { @@ -300,8 +317,19 @@ impl MatchFamily { MatchFamily::Reserved => "reserved", MatchFamily::DollarNull => "dollar_null", MatchFamily::PathMangle => "path_mangle", + MatchFamily::DollarPrefix => "dollar_prefix", } } + + /// Whether files in this family are subject to the zero-byte content + /// safety gate (see [`content_gate_skip_reason`]). Both `$null` and the + /// broader leading-`$` family are stray shell-variable artifacts where a + /// non-empty match may hold data worth reviewing before deletion; + /// `Reserved` and `PathMangle` are not shell-variable artifacts and are + /// not gated. + const fn needs_content_gate(self) -> bool { + matches!(self, MatchFamily::DollarNull | MatchFamily::DollarPrefix) + } } /// Which match families are active for a scan (resolved from [`NukeOptions`]). @@ -310,11 +338,12 @@ struct ActiveFamilies { reserved: bool, dollar_null: bool, path_mangle: bool, + dollar_prefix: bool, } impl ActiveFamilies { fn any(self) -> bool { - self.reserved || self.dollar_null || self.path_mangle + self.reserved || self.dollar_null || self.path_mangle || self.dollar_prefix } } @@ -323,14 +352,16 @@ fn resolve_families(options: &NukeOptions) -> ActiveFamilies { reserved: options.match_reserved != 0, dollar_null: options.match_dollar_null != 0, path_mangle: options.match_path_mangle != 0, + dollar_prefix: options.match_dollar_prefix != 0, } } /// Returns the family a leaf filename belongs to, checking only families enabled /// in `families`. Families are checked in a fixed order so a name matching more -/// than one narrow rule reports the first (this cannot currently happen given the -/// three families are mutually exclusive by construction, but a fixed order keeps -/// results deterministic if that ever changes). +/// than one narrow rule reports the first; in particular `$null` is checked +/// before the broader `dollar_prefix` family so the two never double-report +/// (`matches_dollar_prefix` also explicitly excludes `$null` for the same +/// reason, so this holds even when `dollar_null` is disabled). fn classify(file_name: &OsStr, families: ActiveFamilies) -> Option { if families.reserved && matches_reserved(file_name) { return Some(MatchFamily::Reserved); @@ -341,6 +372,9 @@ fn classify(file_name: &OsStr, families: ActiveFamilies) -> Option if families.path_mangle && matches_path_mangle(file_name) { return Some(MatchFamily::PathMangle); } + if families.dollar_prefix && matches_dollar_prefix(file_name) { + return Some(MatchFamily::DollarPrefix); + } None } @@ -379,15 +413,36 @@ fn matches_path_mangle(file_name: &OsStr) -> bool { semicolon == b';' && last.is_ascii_alphabetic() } -/// Returns a skip reason when a `$null` file of the given `size` must NOT be -/// deleted under the current options (the zero-byte safety gate). Returns `None` -/// when the file is eligible for deletion. -fn dollar_null_skip_reason(size: u64, allow_nonempty: bool) -> Option { +/// Returns whether `file_name` looks like a stray shell-variable artifact: a +/// leaf name starting with `$` (e.g. `$runDir`, `$out`, `$local`, +/// `$archiveDir`), produced when a PowerShell `$variable` name is stripped or +/// misinterpreted when run in a bash context. Deliberately excludes exact +/// `$null` matches (case-insensitive, trailing dots/spaces ignored) so this +/// family never overlaps with the dedicated [`MatchFamily::DollarNull`] +/// family, even when `dollar_null` is disabled and this is the only active +/// leading-`$` rule. +fn matches_dollar_prefix(file_name: &OsStr) -> bool { + let Some(name) = file_name.to_str() else { + return false; + }; + if !name.starts_with('$') || name.len() <= 1 { + return false; + } + !name + .trim_end_matches(['.', ' ']) + .eq_ignore_ascii_case("$null") +} + +/// Returns a skip reason when a file of the given `size`, belonging to a +/// family gated by [`MatchFamily::needs_content_gate`], must NOT be deleted +/// under the current options (the zero-byte safety gate). Returns `None` when +/// the file is eligible for deletion. +fn content_gate_skip_reason(size: u64, allow_nonempty: bool) -> Option { if size == 0 || allow_nonempty { None } else { Some(format!( - "non-empty $null file ({size} bytes); rerun with --allow-nonempty to delete" + "non-empty file ({size} bytes); rerun with --allow-nonempty to delete" )) } } @@ -732,13 +787,15 @@ fn process_entry( let kind = if is_dir { "dir" } else { "file" }; let path_display = entry.path().display().to_string(); - // Zero-byte safety gate: only applies to real (non-directory) $null files. + // Zero-byte safety gate: only applies to real (non-directory) files in a + // family that needs it ($null and the broader leading-$ family - both are + // stray shell-variable artifacts where content is worth reviewing). // Directories get an equivalent safety net for free: RemoveDirectoryW simply // refuses to remove a non-empty directory. let mut size: Option = None; let mut content_preview: Option = None; - if family == MatchFamily::DollarNull && is_file { + if family.needs_content_gate() && is_file { match entry.metadata() { Ok(meta) => { let len = meta.len(); @@ -746,7 +803,7 @@ fn process_entry( if len > 0 { content_preview = read_content_preview(entry.path()); } - if let Some(reason) = dollar_null_skip_reason(len, options.allow_nonempty != 0) { + if let Some(reason) = content_gate_skip_reason(len, options.allow_nonempty != 0) { collector.push_skipped(ActionEntry { path: path_display, family: family.as_str(), @@ -937,12 +994,31 @@ mod tests { assert!(!matches_path_mangle(OsStr::new("C"))); } + #[test] + fn dollar_prefix_matches_leading_dollar_names() { + assert!(matches_dollar_prefix(OsStr::new("$runDir"))); + assert!(matches_dollar_prefix(OsStr::new("$out"))); + assert!(matches_dollar_prefix(OsStr::new("$local"))); + assert!(matches_dollar_prefix(OsStr::new("$archiveDir"))); + } + + #[test] + fn dollar_prefix_excludes_dollar_null_and_lookalikes() { + // $null is the dedicated, narrower family - never double-classified here. + assert!(!matches_dollar_prefix(OsStr::new("$null"))); + assert!(!matches_dollar_prefix(OsStr::new("$NULL. "))); + assert!(!matches_dollar_prefix(OsStr::new("$"))); + assert!(!matches_dollar_prefix(OsStr::new("plainfile.txt"))); + assert!(!matches_dollar_prefix(OsStr::new("money$sign"))); + } + #[test] fn classify_respects_active_families() { let all = ActiveFamilies { reserved: true, dollar_null: true, path_mangle: true, + dollar_prefix: true, }; assert_eq!( classify(OsStr::new("nul"), all), @@ -956,12 +1032,17 @@ mod tests { classify(OsStr::new("foo;C"), all), Some(MatchFamily::PathMangle) ); + assert_eq!( + classify(OsStr::new("$runDir"), all), + Some(MatchFamily::DollarPrefix) + ); assert_eq!(classify(OsStr::new("readme.txt"), all), None); let none = ActiveFamilies { reserved: false, dollar_null: false, path_mangle: false, + dollar_prefix: false, }; assert_eq!(classify(OsStr::new("nul"), none), None); assert!(!none.any()); @@ -974,30 +1055,53 @@ mod tests { reserved: false, dollar_null: false, path_mangle: true, + dollar_prefix: false, }; assert_eq!(classify(OsStr::new("$null"), only_path_mangle), None); assert_eq!( classify(OsStr::new("foo;C"), only_path_mangle), Some(MatchFamily::PathMangle) ); + + // $null stays with the dedicated family even when dollar_prefix alone + // is active - dollar_prefix's own exclusion prevents any overlap. + let only_dollar_prefix = ActiveFamilies { + reserved: false, + dollar_null: false, + path_mangle: false, + dollar_prefix: true, + }; + assert_eq!(classify(OsStr::new("$null"), only_dollar_prefix), None); + assert_eq!( + classify(OsStr::new("$out"), only_dollar_prefix), + Some(MatchFamily::DollarPrefix) + ); + } + + #[test] + fn needs_content_gate_is_scoped_to_shell_variable_families() { + assert!(MatchFamily::DollarNull.needs_content_gate()); + assert!(MatchFamily::DollarPrefix.needs_content_gate()); + assert!(!MatchFamily::Reserved.needs_content_gate()); + assert!(!MatchFamily::PathMangle.needs_content_gate()); } #[test] fn zero_byte_gate_allows_empty_files() { - assert!(dollar_null_skip_reason(0, false).is_none()); - assert!(dollar_null_skip_reason(0, true).is_none()); + assert!(content_gate_skip_reason(0, false).is_none()); + assert!(content_gate_skip_reason(0, true).is_none()); } #[test] fn zero_byte_gate_blocks_nonempty_by_default() { - let reason = dollar_null_skip_reason(877, false); + let reason = content_gate_skip_reason(877, false); assert!(reason.is_some()); assert!(reason.unwrap().contains("877 bytes")); } #[test] fn zero_byte_gate_allow_nonempty_overrides() { - assert!(dollar_null_skip_reason(877, true).is_none()); + assert!(content_gate_skip_reason(877, true).is_none()); } #[test] @@ -1048,6 +1152,7 @@ mod tests { match_reserved: 1, match_dollar_null: 0, match_path_mangle: 1, + match_dollar_prefix: 1, include_dirs: 0, dry_run: 0, allow_nonempty: 0, @@ -1056,6 +1161,7 @@ mod tests { assert!(families.reserved); assert!(!families.dollar_null); assert!(families.path_mangle); + assert!(families.dollar_prefix); } /// Minimal RAII helper for a scratch directory under `target/`, cleaned up From 7b622b44d434f8fd5b2d3e7e639389d07c1541d1 Mon Sep 17 00:00:00 2001 From: "David T. Martel" Date: Thu, 6 Aug 2026 11:20:31 -0400 Subject: [PATCH 7/8] feat(cli): expose the leading-$ family via --dollar-prefix Wires the DollarPrefix match family added in c369d3b through to the CLI so it is reachable without going via the FFI directly. --dollar-prefix joins --reserved, --dollar-null-only and --path-mangle as a composable family flag, and is covered by --all. The leading-$ family shares the zero-byte content gate with $null, so --allow-nonempty governs both. Verified: dotnet build -c Release succeeds with 0 warnings, 0 errors. nuker_core: 21 tests passing, clippy --all-targets -D warnings clean. Native-AOT publish remains blocked in this environment by an unrelated VS-18-Insiders link.exe path-resolution failure (MSB3073 code 123); the IL build is what was compile- and run-verified. Agent: claude Co-authored-by: Claude --- Program.cs | 38 +++++++++++++++++++++++++++++--------- 1 file changed, 29 insertions(+), 9 deletions(-) diff --git a/Program.cs b/Program.cs index 28e6333..eba306e 100644 --- a/Program.cs +++ b/Program.cs @@ -40,6 +40,7 @@ internal struct NukeOptions public byte MatchReserved; public byte MatchDollarNull; public byte MatchPathMangle; + public byte MatchDollarPrefix; public byte IncludeDirs; public byte DryRun; public byte AllowNonempty; @@ -234,6 +235,7 @@ private sealed class ParsedArgs public bool Reserved; public bool DollarNull; public bool PathMangle; + public bool DollarPrefix; public bool All; public bool IncludeDirs; public bool DryRun; @@ -267,18 +269,20 @@ private static int Main(string[] args) // flag at all) preserves the original default: reserved-device names // only. --dollar-null-only alone keeps its historical "only" behavior // (reserved is NOT auto-included). Any explicit combination of - // --reserved / --dollar-null-only / --path-mangle composes exactly - // the families named. --all is shorthand for all three. - bool anyFamilyFlag = parsed.Reserved || parsed.DollarNull || parsed.PathMangle || parsed.All; + // --reserved / --dollar-null-only / --path-mangle / --dollar-prefix + // composes exactly the families named. --all is shorthand for all four. + bool anyFamilyFlag = parsed.Reserved || parsed.DollarNull || parsed.PathMangle || parsed.DollarPrefix || parsed.All; bool finalReserved = parsed.All || parsed.Reserved || !anyFamilyFlag; bool finalDollarNull = parsed.All || parsed.DollarNull; bool finalPathMangle = parsed.All || parsed.PathMangle; + bool finalDollarPrefix = parsed.All || parsed.DollarPrefix; var options = new NukeOptions { MatchReserved = (byte)(finalReserved ? 1 : 0), MatchDollarNull = (byte)(finalDollarNull ? 1 : 0), MatchPathMangle = (byte)(finalPathMangle ? 1 : 0), + MatchDollarPrefix = (byte)(finalDollarPrefix ? 1 : 0), IncludeDirs = (byte)(parsed.IncludeDirs ? 1 : 0), DryRun = (byte)(parsed.DryRun ? 1 : 0), AllowNonempty = (byte)(parsed.AllowNonempty ? 1 : 0), @@ -288,7 +292,7 @@ private static int Main(string[] args) var result = new ScanResult { Target = targetPath, - Operation = BuildOperationLabel(finalReserved, finalDollarNull, finalPathMangle, parsed.IncludeDirs, parsed.AllowNonempty), + Operation = BuildOperationLabel(finalReserved, finalDollarNull, finalPathMangle, finalDollarPrefix, parsed.IncludeDirs, parsed.AllowNonempty), Timestamp = DateTime.UtcNow, DryRun = parsed.DryRun, }; @@ -406,6 +410,9 @@ private static bool TryParseArguments( case "--path-mangle": parsed.PathMangle = true; break; + case "--dollar-prefix": + parsed.DollarPrefix = true; + break; case "--reserved": parsed.Reserved = true; break; @@ -447,7 +454,7 @@ private static bool TryParseArguments( /// /// Builds a human-readable operation label from the resolved options. /// - private static string BuildOperationLabel(bool reserved, bool dollarNull, bool pathMangle, bool includeDirs, bool allowNonempty) + private static string BuildOperationLabel(bool reserved, bool dollarNull, bool pathMangle, bool dollarPrefix, bool includeDirs, bool allowNonempty) { var families = new List(); if (reserved) @@ -465,6 +472,11 @@ private static string BuildOperationLabel(bool reserved, bool dollarNull, bool p families.Add("PathMangle"); } + if (dollarPrefix) + { + families.Add("DollarPrefix"); + } + string label = families.Count > 0 ? string.Join("+", families) : "None"; var modifiers = new List(); @@ -498,24 +510,32 @@ family flag is given. deleted by default; see --allow-nonempty. --path-mangle Shell path-mangle artifacts: leaf names ending in ";" or ";:" (e.g. "foo;C"). - --all Shorthand for all three families above. + --dollar-prefix Leading-"$" shell-variable artifacts other than + $null (e.g. "$runDir", "$out", "$archiveDir"), + produced when a PowerShell $variable name is + stripped in a bash context. Zero-byte files are + deleted by default; see --allow-nonempty. + --all Shorthand for all four families above. Modifiers: --include-dirs Also remove matching directories, but ONLY if they are empty (never recursive; RemoveDirectoryW itself refuses non-empty directories). --dry-run, -n Preview only - walk and match, delete nothing. - --allow-nonempty Allow deleting $null files larger than zero bytes - (dollar-null family only; default is zero-byte-only). + --allow-nonempty Allow deleting non-zero-byte files matched by + --dollar-null-only or --dollar-prefix (default is + zero-byte-only for both). Examples: NukeNul.exe C:\Path\To\Scan NukeNul.exe --dry-run --all C:\Path\To\Scan NukeNul.exe --dollar-null-only --path-mangle --include-dirs C:\Path + NukeNul.exe --dollar-prefix --include-dirs C:\Path Behavior change: --dollar-null-only now deletes ONLY zero-byte $null files by default (previously deleted files of any size). Pass --allow-nonempty to - restore the old behavior for a given run. + restore the old behavior for a given run. The same zero-byte-only default + applies to the new --dollar-prefix family. """); } From 1488936fee7a176c0cf833bb5e3578777dab4699 Mon Sep 17 00:00:00 2001 From: "David T. Martel" Date: Thu, 6 Aug 2026 11:26:05 -0400 Subject: [PATCH 8/8] docs: document the --dollar-prefix family Extends the CLI reference table, feature list, behavior-change notice, NukeOptions struct listings (Rust + C#), and JSON entry schema description to cover the leading-$ shell-variable-artifact family added in c369d3b/7b622b4, and adds a usage example. Agent: claude Co-authored-by: Claude --- README.md | 45 +++++++++++++++++++++++++++++---------------- 1 file changed, 29 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 8a04c98..f4afb4f 100644 --- a/README.md +++ b/README.md @@ -24,24 +24,29 @@ Traditional PowerShell scripts hit performance ceilings when dealing with reserv - ✅ **Self-contained** - No .NET runtime required - ✅ **Cross-platform ready** - Windows x64 (Linux/macOS support possible) - ✅ **Literal `$null` safety mode** - Deletes only real `$null` files, not device aliases -- ✅ **Composable match families** - reserved device names, literal `$null`, and - path-mangle artifacts (`foo;C`) can be combined in a single pass +- ✅ **Leading-`$` shell-variable-artifact matching** (`--dollar-prefix`) - Catches + `$runDir`, `$out`, `$local`, `$archiveDir` and similar names left behind when a + PowerShell `$variable` name is stripped or misinterpreted in a bash context +- ✅ **Composable match families** - reserved device names, literal `$null`, + path-mangle artifacts (`foo;C`), and leading-`$` artifacts can be combined in a + single pass - ✅ **`--dry-run`** - Preview exactly what would be deleted (and why something would be skipped) without touching disk - ✅ **Empty-directory cleanup** (`--include-dirs`) - Removes matching directories, but only if they are already empty; never recursive - ✅ **Auditable output** - Every deleted/would-delete/skipped entry lists its full - path, match family, and (for `$null`) size + a content preview + path, match family, and (for `$null`/`--dollar-prefix`) size + a content preview -## ⚠️ Behavior change: `$null` zero-byte safety gate +## ⚠️ Behavior change: zero-byte safety gate for `$null` and `--dollar-prefix` -As of this version, the `$null` family (`--dollar-null-only`) deletes **only -zero-byte** files by default. This matches the governing workspace policy: -*"Discovery hooks may delete only zero-byte matches inside the active -workspace; preserve and report non-empty or out-of-scope matches."* +As of this version, the `$null` family (`--dollar-null-only`) and the leading-`$` +family (`--dollar-prefix`) both delete **only zero-byte** files by default. This +matches the governing workspace policy: *"Discovery hooks may delete only +zero-byte matches inside the active workspace; preserve and report non-empty or +out-of-scope matches."* -Non-empty `$null` files are now **skipped** (not deleted) and reported in the -JSON output with their size and the first 200 bytes of their content, so an +Non-empty matches in either family are now **skipped** (not deleted) and reported +in the JSON output with their size and the first 200 bytes of their content, so an operator can review them before deciding what to do. Pass `--allow-nonempty` to opt into deleting `$null` files larger than zero bytes, restoring the previous unconditional-delete behavior for that run. @@ -91,6 +96,10 @@ NukeNul.exe --dollar-null-only --path-mangle "C:\Path\To\Scan" # Delete non-empty $null files too (opt-in override) NukeNul.exe --dollar-null-only --allow-nonempty "C:\Path\To\Scan" + +# Sweep stray $variable artifacts left by PowerShell->bash context bugs +# ($runDir, $out, $local, $archiveDir, ...), including empty directories +NukeNul.exe --dollar-prefix --include-dirs "C:\Path\To\Scan" ``` ### CLI Reference @@ -103,7 +112,8 @@ to `--reserved`): | `--reserved` | Reserved device names | `nul`, `con`, `prn`, `aux`, `com1-9`, `lpt1-9`. Active by default when no other family flag is given. | | `--dollar-null-only` | Literal `$null` | Zero-byte files deleted by default; see `--allow-nonempty`. Kept exclusive when passed alone, for backward compatibility. | | `--path-mangle` | Shell path-mangle artifacts | Leaf names ending in `;` or `;:` (e.g. `foo;C`, `foo;C:`). Deliberately narrow - `;` is a legal filename character. | -| `--all` | All three families | Shorthand for `--reserved --dollar-null-only --path-mangle`. | +| `--dollar-prefix` | Leading-`$` shell-variable artifacts | Leaf names starting with `$` other than `$null` itself (e.g. `$runDir`, `$out`, `$local`, `$archiveDir`). Zero-byte files deleted by default; see `--allow-nonempty`. | +| `--all` | All four families | Shorthand for `--reserved --dollar-null-only --path-mangle --dollar-prefix`. | Modifiers: @@ -111,7 +121,7 @@ Modifiers: |------|--------| | `--include-dirs` | Also remove matching directories, but **only if they are empty**. Never recursive - `RemoveDirectoryW` itself refuses non-empty directories, and that refusal is the safety mechanism. | | `--dry-run`, `-n` | Preview only. Walks and matches identically to a real run (including predicting whether a directory would be empty), but performs no deletion. Exit code 0. | -| `--allow-nonempty` | Allow deleting `$null` files larger than zero bytes (dollar-null family only). | +| `--allow-nonempty` | Allow deleting files larger than zero bytes matched by `--dollar-null-only` or `--dollar-prefix` (the two zero-byte-gated families). | ### Example Output @@ -153,10 +163,11 @@ Modifiers: ``` Every `deleted_entries` / `would_delete_entries` / `skipped_entries` item -carries `path`, `family` (`reserved` | `dollar_null` | `path_mangle`), and -`kind` (`file` | `dir`). `reason`, `size`, and `content_preview` are present -only where relevant (skips always have a `reason`; `$null` files carry `size`, -and non-empty ones also carry `content_preview`). +carries `path`, `family` (`reserved` | `dollar_null` | `path_mangle` | +`dollar_prefix`), and `kind` (`file` | `dir`). `reason`, `size`, and +`content_preview` are present only where relevant (skips always have a +`reason`; `dollar_null`/`dollar_prefix` files carry `size`, and non-empty ones +also carry `content_preview`). ### Exit Codes @@ -261,6 +272,7 @@ pub struct NukeOptions { pub match_reserved: u8, pub match_dollar_null: u8, pub match_path_mangle: u8, + pub match_dollar_prefix: u8, pub include_dirs: u8, pub dry_run: u8, pub allow_nonempty: u8, @@ -300,6 +312,7 @@ internal struct NukeOptions public byte MatchReserved; public byte MatchDollarNull; public byte MatchPathMangle; + public byte MatchDollarPrefix; public byte IncludeDirs; public byte DryRun; public byte AllowNonempty;