Private/Resolve-KadenContainedPath.ps1

function Resolve-KadenContainedPath {
    <#
    .SYNOPSIS
        Resolve a relative managed path and reject escapes outside a root.
    #>

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

        [Parameter(Mandatory = $true)]
        [AllowEmptyString()]
        [string]$RelativePath
    )

    if ([string]::IsNullOrWhiteSpace($RelativePath)) {
        throw 'KDN-TXN-PATH-ESCAPE: managed path is empty.'
    }
    if ([System.IO.Path]::IsPathRooted($RelativePath)) {
        throw "KDN-TXN-PATH-ESCAPE: managed path must stay relative ('$RelativePath')."
    }

    foreach ($segment in ($RelativePath -split '[\\/]')) {
        if ($segment -eq '..') {
            throw "KDN-TXN-PATH-ESCAPE: managed path escapes its root ('$RelativePath')."
        }
    }

    $rootFull = [System.IO.Path]::GetFullPath($Root)
    $candidate = [System.IO.Path]::GetFullPath((Join-Path $rootFull $RelativePath))
    $final = Resolve-KadenFinalPath -Path $candidate
    foreach ($check in @($candidate, $final)) {
        $separator = [System.IO.Path]::DirectorySeparatorChar
        $rootPrefix = $rootFull.TrimEnd([char]'\', [char]'/') + $separator
        $inside = $check.StartsWith($rootPrefix, [System.StringComparison]::OrdinalIgnoreCase) `
            -or $check.Equals($rootFull, [System.StringComparison]::OrdinalIgnoreCase)
        if (-not $inside) {
            throw "KDN-TXN-PATH-ESCAPE: managed path escapes its root ('$RelativePath')."
        }
    }

    return $candidate
}