Support/ExecutionCore/Eigenverft.Manifested.Sandbox.ExecutionCore.FileSystem.ps1

<#
    Eigenverft.Manifested.Sandbox.ExecutionEngine.FileSystem
#>


function Remove-PathIfExists {
<#
.SYNOPSIS
Removes a file or directory when it exists.
 
.DESCRIPTION
Returns `$false` when the path is already missing. Otherwise removes the path
recursively and forcefully, then returns `$true`.
#>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$Path
    )

    if (-not (Test-Path -LiteralPath $Path)) {
        return $false
    }

    Remove-Item -LiteralPath $Path -Recurse -Force -ErrorAction Stop
    if (Test-Path -LiteralPath $Path) {
        throw "Could not remove path '$Path'."
    }

    return $true
}

function Get-EmptyParentPruneCeilingDirectory {
<#
.SYNOPSIS
Resolves the directory ceiling for empty-parent pruning after deleting an inventory install path.
 
.DESCRIPTION
If InstallLeafPath lies under PreferredInstallRootDirectory (Inst), returns that Inst root so pruning
never leaves empty version folders above the package but stays inside the managed install tree.
 
If the leaf is outside Inst (for example an adopted path on another drive), returns the volume or
UNC share root of InstallLeafPath. Remove-EmptyParentDirectoryChain only removes empty directories
and never removes the ceiling directory itself, so pruning stays on that volume/share and stops at
the first non-empty parent.
#>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$InstallLeafPath,

        [Parameter(Mandatory = $false)]
        [AllowEmptyString()]
        [string]$PreferredInstallRootDirectory
    )

    if ([string]::IsNullOrWhiteSpace($InstallLeafPath)) {
        return $null
    }

    $leafFull = [System.IO.Path]::GetFullPath($InstallLeafPath)

    if (-not [string]::IsNullOrWhiteSpace($PreferredInstallRootDirectory)) {
        $trimmed = $PreferredInstallRootDirectory.TrimEnd(
            [System.IO.Path]::DirectorySeparatorChar,
            [System.IO.Path]::AltDirectorySeparatorChar)
        if (-not [string]::IsNullOrWhiteSpace($trimmed)) {
            $instFull = [System.IO.Path]::GetFullPath($trimmed)
            if ([string]::Equals($leafFull, $instFull, [System.StringComparison]::OrdinalIgnoreCase)) {
                return $instFull
            }
            $instPrefix = $instFull + [System.IO.Path]::DirectorySeparatorChar
            if ($leafFull.StartsWith($instPrefix, [System.StringComparison]::OrdinalIgnoreCase)) {
                return $instFull
            }
        }
    }

    $root = [System.IO.Path]::GetPathRoot($leafFull)
    if ([string]::IsNullOrWhiteSpace($root)) {
        return $null
    }

    return [System.IO.Path]::GetFullPath($root)
}

function Remove-EmptyParentDirectoryChain {
<#
.SYNOPSIS
Removes empty parent directories from a deleted path up to a ceiling directory.
 
.DESCRIPTION
Walks from the immediate parent of DeletedLeafPath upward. Each directory is removed
only if it exists, is empty (no child files or directories), and is a strict
descendant of AncestorCeilingDirectory. The ceiling directory itself is never removed.
Stops at the first non-empty directory or when the path is no longer under the ceiling.
#>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$DeletedLeafPath,

        [Parameter(Mandatory = $true)]
        [string]$AncestorCeilingDirectory
    )

    if ([string]::IsNullOrWhiteSpace($DeletedLeafPath) -or [string]::IsNullOrWhiteSpace($AncestorCeilingDirectory)) {
        return
    }

    $ceilingFull = [System.IO.Path]::GetFullPath($AncestorCeilingDirectory.TrimEnd(
            [System.IO.Path]::DirectorySeparatorChar,
            [System.IO.Path]::AltDirectorySeparatorChar))
    $resolvedLeaf = [System.IO.Path]::GetFullPath($DeletedLeafPath)
    $current = Split-Path -Path $resolvedLeaf -Parent

    while (-not [string]::IsNullOrWhiteSpace($current)) {
        $currentFull = [System.IO.Path]::GetFullPath($current)

        if ([string]::Equals($currentFull, $ceilingFull, [System.StringComparison]::OrdinalIgnoreCase)) {
            break
        }

        $ceilingPrefix = $ceilingFull + [System.IO.Path]::DirectorySeparatorChar
        if (-not $currentFull.StartsWith($ceilingPrefix, [System.StringComparison]::OrdinalIgnoreCase)) {
            break
        }

        if (-not (Test-Path -LiteralPath $currentFull -PathType Container)) {
            break
        }

        $children = @(Get-ChildItem -LiteralPath $currentFull -Force -ErrorAction SilentlyContinue)
        if ($children.Count -gt 0) {
            break
        }

        Remove-PathIfExists -Path $currentFull | Out-Null
        $current = Split-Path -Path $currentFull -Parent
    }
}

function Copy-FileToPath {
<#
.SYNOPSIS
Copies one file to a target path.
 
.DESCRIPTION
Provides a thin execution seam over `Copy-Item` so callers do not depend on a
specific file-copy backend.
#>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$SourcePath,

        [Parameter(Mandatory = $true)]
        [string]$TargetPath,

        [switch]$Overwrite
    )

    if ([string]::IsNullOrWhiteSpace($SourcePath)) {
        throw 'A source file path is required.'
    }

    if ([string]::IsNullOrWhiteSpace($TargetPath)) {
        throw 'A target file path is required.'
    }

    if ($Overwrite) {
        Copy-Item -LiteralPath $SourcePath -Destination $TargetPath -Force
    }
    else {
        Copy-Item -LiteralPath $SourcePath -Destination $TargetPath
    }

    return (Resolve-Path -LiteralPath $TargetPath -ErrorAction Stop).Path
}

function New-CommandShim {
<#
.SYNOPSIS
Creates a small command shim script.
 
.DESCRIPTION
Writes a `.cmd` shim that forwards all arguments to a concrete target command.
The helper is intentionally generic so the Package layer can decide ownership,
name collision, and lifecycle policy.
#>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$ShimPath,

        [Parameter(Mandatory = $true)]
        [string]$TargetPath,

        [string[]]$HeaderLines = @(),

        [switch]$Overwrite
    )

    if ([string]::IsNullOrWhiteSpace($ShimPath)) {
        throw 'A shim path is required.'
    }

    if ([string]::IsNullOrWhiteSpace($TargetPath)) {
        throw 'A shim target path is required.'
    }

    $resolvedShimPath = [System.IO.Path]::GetFullPath($ShimPath)
    $resolvedTargetPath = [System.IO.Path]::GetFullPath($TargetPath)
    $shimDirectory = Split-Path -Parent $resolvedShimPath
    if (-not [string]::IsNullOrWhiteSpace($shimDirectory) -and -not (Test-Path -LiteralPath $shimDirectory -PathType Container)) {
        $null = New-Item -ItemType Directory -Path $shimDirectory -Force
    }

    if ((Test-Path -LiteralPath $resolvedShimPath -PathType Leaf) -and -not $Overwrite.IsPresent) {
        throw "Command shim '$resolvedShimPath' already exists."
    }

    $lines = New-Object System.Collections.Generic.List[string]
    $lines.Add('@echo off') | Out-Null
    foreach ($headerLine in @($HeaderLines)) {
        if (-not [string]::IsNullOrWhiteSpace($headerLine)) {
            $lines.Add(("rem {0}" -f [string]$headerLine)) | Out-Null
        }
    }
    $lines.Add(('call "{0}" %*' -f $resolvedTargetPath)) | Out-Null
    $lines.Add('exit /b %ERRORLEVEL%') | Out-Null

    $encoding = New-Object System.Text.UTF8Encoding($false)
    [System.IO.File]::WriteAllText($resolvedShimPath, (@($lines.ToArray()) -join "`r`n") + "`r`n", $encoding)

    return $resolvedShimPath
}