Eigenverft.Manifested.Drydock.PathLimits.ps1
|
function Test-WindowsPathLimits { <# .SYNOPSIS Diagnose Windows long-path policy and runtime path create behavior. .DESCRIPTION Public standalone Drydock diagnostic. Reports: - OS LongPathsEnabled registry policy - Current PowerShell / .NET host identity - Empirical FileStream create probes around and above 260 characters - Optional analysis of concrete paths from error logs (e.g. Package depot partial paths) Use this when logs show opaque FileStream ".ctor" failures such as "Could not find a part of the path" to distinguish missing parent directories from classic MAX_PATH limits and per-component length limits. Does not call other Drydock module functions. .PARAMETER Path Optional concrete path(s) to classify (from an error log). No file is created for these unless -TryOpenPath is also specified. .PARAMETER TryOpenPath When set with -Path, attempts a real FileStream create for each analyzed path (under the existing parent only; does not create missing parents). .PARAMETER ProbeRoot Root directory for empirical length probes. Default: %TEMP%\Evf.PathLimitProbe .PARAMETER SkipProbe Skip empirical create probes; only report policy and optional -Path analysis. .EXAMPLE Test-WindowsPathLimits .EXAMPLE Test-WindowsPathLimits -Path 'C:\Users\Administrator\AppData\Local\Programs\Evf.Package\PkgDepot\evf\CursorCli\stable\2026.07.16-899851b\win32-x64\agent-cli-package-2026.07.16-899851b-win32-x64.zip.partial.5a7bbeca2c6478936978e25da0939d453aec63120c22d21836ce1d3f112ba328.ca28a3d1399349bd98422a12b102cdb3' .EXAMPLE Test-WindowsPathLimits -Path $failedPartial -TryOpenPath .NOTES Self-contained public command. Helpers are local to this function. #> [CmdletBinding(PositionalBinding = $false)] param( [Parameter()] [string[]]$Path, [Parameter()] [switch]$TryOpenPath, [Parameter()] [string]$ProbeRoot = (Join-Path $env:TEMP 'Evf.PathLimitProbe'), [Parameter()] [switch]$SkipProbe ) function local:Get-WindowsLongPathsPolicy { $keyPath = 'HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem' $valueName = 'LongPathsEnabled' $result = [ordered]@{ RegistryPath = $keyPath ValueName = $valueName ValuePresent = $false LongPathsEnabled = $false LongPathsEnabledRaw = $null ReadError = $null } try { $item = Get-ItemProperty -LiteralPath $keyPath -Name $valueName -ErrorAction Stop $raw = [int]$item.$valueName $result.ValuePresent = $true $result.LongPathsEnabledRaw = $raw $result.LongPathsEnabled = ($raw -ne 0) } catch { $result.ReadError = $_.Exception.Message } [pscustomobject]$result } function local:Resolve-PathIoFailureHint { param( [Parameter(Mandatory)] [string]$TargetPath, [Parameter()] [System.Exception]$Exception ) $normalized = $TargetPath.Trim() if ($normalized.StartsWith('\\?\', [StringComparison]::Ordinal)) { $normalized = $normalized.Substring(4) } $parent = [System.IO.Path]::GetDirectoryName($normalized) $fileName = [System.IO.Path]::GetFileName($normalized) $parentExists = if ([string]::IsNullOrWhiteSpace($parent)) { $false } else { Test-Path -LiteralPath $parent -PathType Container } $pathLength = $normalized.Length $fileNameLength = if ($null -eq $fileName) { 0 } else { $fileName.Length } $parentLength = if ($null -eq $parent) { 0 } else { $parent.Length } $exceptionText = if ($Exception) { if ($Exception.InnerException) { $Exception.InnerException.Message } else { $Exception.Message } } else { $null } $hints = New-Object System.Collections.Generic.List[string] $likelyCause = 'Unknown' $secondaryRisks = New-Object System.Collections.Generic.List[string] if ($fileNameLength -gt 255) { $likelyCause = 'ComponentTooLong' [void]$hints.Add(("File name length {0} exceeds Windows per-component limit 255." -f $fileNameLength)) } elseif (-not $parentExists) { $likelyCause = 'MissingParentDirectory' [void]$hints.Add(("Parent directory does not exist: '{0}'." -f $parent)) [void]$hints.Add('Same Win32/HRESULT text often appears for missing parents and for overlong paths.') if ($pathLength -gt 259) { [void]$secondaryRisks.Add('ClassicMaxPathOverflow') [void]$hints.Add(("Secondary risk: full path length {0} also exceeds classic MAX_PATH (~259 usable). After creating the parent, create may still fail on non-long-path-aware hosts." -f $pathLength)) } } elseif ($pathLength -gt 259) { $likelyCause = 'ClassicMaxPathOverflow' [void]$hints.Add(("Full path length {0} exceeds classic MAX_PATH usable limit (~259 chars, API often reports 260 including NUL)." -f $pathLength)) [void]$hints.Add('Parent exists, so missing-folder is unlikely; host may still be limited to legacy MAX_PATH.') } else { $likelyCause = 'OtherIoFailure' [void]$hints.Add('Path length and parent look OK; failure is likely ACL, sharing, invalid chars, or another IO condition.') } [pscustomobject]@{ Path = $normalized PathLength = $pathLength ParentDirectory = $parent ParentDirectoryLength = $parentLength ParentExists = $parentExists FileName = $fileName FileNameLength = $fileNameLength ExceedsClassicMaxPath = ($pathLength -gt 259) ExceedsComponentLimit = ($fileNameLength -gt 255) LikelyCause = $likelyCause SecondaryRisks = @($secondaryRisks.ToArray()) Hints = @($hints.ToArray()) ExceptionMessage = $exceptionText } } function local:New-ProbeFilePath { param( [Parameter(Mandatory)] [string]$Root, [Parameter(Mandatory)] [int]$TargetFullLength ) $prefix = Join-Path $Root 'probe' $prefixWithSep = $prefix + [System.IO.Path]::DirectorySeparatorChar $remaining = $TargetFullLength - $prefixWithSep.Length if ($remaining -lt 8) { throw ("ProbeRoot too long for target length {0}: '{1}'" -f $TargetFullLength, $Root) } $stemLen = $remaining - 4 if ($stemLen -lt 1) { throw ("Cannot build probe name for target length {0}." -f $TargetFullLength) } $stem = ('a' * $stemLen) $candidate = Join-Path $prefix ($stem + '.bin') if ($candidate.Length -ne $TargetFullLength) { $delta = $TargetFullLength - $candidate.Length if ($delta -gt 0) { $stem = $stem + ('b' * $delta) } elseif ($delta -lt 0 -and $stem.Length -gt -$delta) { $stem = $stem.Substring(0, $stem.Length + $delta) } $candidate = Join-Path $prefix ($stem + '.bin') } return $candidate } function local:Test-FileStreamCreate { param( [Parameter(Mandatory)] [string]$TargetPath, [Parameter()] [switch]$UseExtendedSyntax ) $openPath = if ($UseExtendedSyntax -and -not $TargetPath.StartsWith('\\?\', [StringComparison]::Ordinal)) { '\\?\' + $TargetPath } else { $TargetPath } $created = $false $errorMessage = $null try { $parent = [System.IO.Path]::GetDirectoryName($TargetPath) if (-not [string]::IsNullOrWhiteSpace($parent) -and -not (Test-Path -LiteralPath $parent -PathType Container)) { New-Item -ItemType Directory -Force -Path $parent | Out-Null } $fs = [System.IO.FileStream]::new( $openPath, [System.IO.FileMode]::Create, [System.IO.FileAccess]::Write, [System.IO.FileShare]::None, 4096, [System.IO.FileOptions]::None ) try { $fs.WriteByte(1) } finally { $fs.Dispose() } $created = $true } catch { $errorMessage = if ($_.Exception.InnerException) { $_.Exception.InnerException.Message } else { $_.Exception.Message } } finally { if (Test-Path -LiteralPath $TargetPath -PathType Leaf) { Remove-Item -LiteralPath $TargetPath -Force -ErrorAction SilentlyContinue } } $exceptionForHint = $null if ($errorMessage) { $exceptionForHint = [System.IO.IOException]::new($errorMessage) } $hint = Resolve-PathIoFailureHint -TargetPath $TargetPath -Exception $exceptionForHint [pscustomobject]@{ TargetPath = $TargetPath OpenPath = $openPath TargetPathLength = $TargetPath.Length UseExtendedSyntax = [bool]$UseExtendedSyntax CreateSucceeded = $created ErrorMessage = $errorMessage LikelyCause = if ($created) { 'None' } else { $hint.LikelyCause } Hints = if ($created) { @() } else { $hint.Hints } } } $policy = Get-WindowsLongPathsPolicy $runtime = [ordered]@{ PSVersion = $PSVersionTable.PSVersion.ToString() PSEdition = [string]$PSVersionTable.PSEdition OSVersion = [System.Environment]::OSVersion.VersionString FrameworkDescription = $null IsWindows = $true ClassicMaxPathLimit = 260 ClassicMaxPathUsable = 259 ComponentLengthLimit = 255 } try { $runtime.FrameworkDescription = [System.Runtime.InteropServices.RuntimeInformation]::FrameworkDescription } catch { $runtime.FrameworkDescription = [System.Runtime.InteropServices.RuntimeEnvironment]::GetRuntimeDirectory() } try { $runtime.IsWindows = [System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform( [System.Runtime.InteropServices.OSPlatform]::Windows ) } catch { $runtime.IsWindows = ($env:OS -eq 'Windows_NT') } $pathAnalyses = @() foreach ($candidate in @($Path)) { if ([string]::IsNullOrWhiteSpace($candidate)) { continue } $analysis = Resolve-PathIoFailureHint -TargetPath $candidate $openResult = $null if ($TryOpenPath) { $parent = $analysis.ParentDirectory if (-not $analysis.ParentExists) { $openResult = [pscustomobject]@{ CreateSucceeded = $false UseExtendedSyntax = $false ErrorMessage = ("Skipped create: parent directory missing ('{0}')." -f $parent) LikelyCause = 'MissingParentDirectory' } } else { $openResult = Test-FileStreamCreate -TargetPath $analysis.Path if (-not $openResult.CreateSucceeded) { $openExt = Test-FileStreamCreate -TargetPath $analysis.Path -UseExtendedSyntax $openResult = [pscustomobject]@{ CreateSucceeded = $openResult.CreateSucceeded ErrorMessage = $openResult.ErrorMessage LikelyCause = $openResult.LikelyCause ExtendedCreateSucceeded = $openExt.CreateSucceeded ExtendedErrorMessage = $openExt.ErrorMessage ExtendedLikelyCause = $openExt.LikelyCause } } } } $pathAnalyses += [pscustomobject]@{ Analysis = $analysis OpenProbe = $openResult Summary = @( ("PathLength={0}" -f $analysis.PathLength) ("ParentExists={0}" -f $analysis.ParentExists) ("LikelyCause={0}" -f $analysis.LikelyCause) ($analysis.Hints -join ' ') ) -join '; ' } } $probes = @() if (-not $SkipProbe) { New-Item -ItemType Directory -Force -Path $ProbeRoot | Out-Null try { foreach ($len in 200, 240, 259, 260, 261, 273, 300) { try { $target = New-ProbeFilePath -Root $ProbeRoot -TargetFullLength $len } catch { $probes += [pscustomobject]@{ TargetPathLength = $len CreateSucceeded = $false UseExtendedSyntax = $false ErrorMessage = $_.Exception.Message LikelyCause = 'ProbeSetupFailed' Hints = @($_.Exception.Message) OpenPath = $null TargetPath = $null } continue } $normal = Test-FileStreamCreate -TargetPath $target $probes += $normal if (-not $normal.CreateSucceeded) { $extended = Test-FileStreamCreate -TargetPath $target -UseExtendedSyntax $probes += $extended } } } finally { if (Test-Path -LiteralPath $ProbeRoot -PathType Container) { Remove-Item -LiteralPath $ProbeRoot -Recurse -Force -ErrorAction SilentlyContinue } } } $probeVerdict = 'Skipped' if (-not $SkipProbe) { $over = @($probes | Where-Object { $_.TargetPathLength -ge 261 -and -not $_.UseExtendedSyntax }) $overOk = @($over | Where-Object { $_.CreateSucceeded }) $overFail = @($over | Where-Object { -not $_.CreateSucceeded }) if ($overOk.Count -gt 0 -and $overFail.Count -eq 0) { $probeVerdict = 'RuntimeAllowsPathsBeyondClassicMaxPath' } elseif ($overOk.Count -gt 0) { $probeVerdict = 'RuntimePartiallyAllowsLongPaths' } elseif ($overFail.Count -gt 0) { $probeVerdict = 'RuntimeBlockedByClassicMaxPath' } else { $probeVerdict = 'Inconclusive' } } $policyVerdict = if ($policy.LongPathsEnabled) { 'OsPolicyAllowsLongPaths' } elseif ($policy.ValuePresent) { 'OsPolicyDisallowsLongPaths' } else { 'OsPolicyUnset(defaultsToClassicMaxPath)' } $combinedParts = New-Object System.Collections.Generic.List[string] [void]$combinedParts.Add($policyVerdict) [void]$combinedParts.Add($probeVerdict) if ($policy.LongPathsEnabled -and $probeVerdict -eq 'RuntimeBlockedByClassicMaxPath') { [void]$combinedParts.Add('IMPORTANT: LongPathsEnabled=1 but this host still cannot create >260 paths (app/runtime not long-path aware).') } elseif (-not $policy.LongPathsEnabled -and $probeVerdict -eq 'RuntimeAllowsPathsBeyondClassicMaxPath') { [void]$combinedParts.Add('NOTE: OS policy off/unset, but this runtime still created long paths (unusual; verify probe results).') } [pscustomobject]@{ Policy = $policy Runtime = [pscustomobject]$runtime PolicyVerdict = $policyVerdict ProbeVerdict = $probeVerdict CombinedVerdict = ($combinedParts.ToArray() -join ' | ') PathAnalyses = $pathAnalyses Probes = $probes } } |