Private/PathUtilities.ps1
|
# ShowTree\Private\PathUtilities.ps1 #region Path Utilities <# .SYNOPSIS Resolves a user-supplied path into a fully qualified provider path with correct caller-relative behavior, normalization, and mode-specific error handling. .DESCRIPTION Resolve-TreePath converts user input (relative paths, absolute paths, and mixed-case paths) into a canonical provider path suitable for tree rendering. The function performs three key operations: • Caller-relative resolution Relative paths such as '.', '..', and '.\foo' are resolved against the caller's working directory, not the module's import location. • Normalization The resulting path is normalized segment-by-segment to match actual filesystem casing and to collapse constructs like '..' and redundant separators. • Mode-specific error behavior In Normal and List modes, nonexistent paths produce a PowerShell-style ItemNotFound error. In Tree mode, nonexistent paths are returned verbatim so that the caller can reproduce tree.com’s error messages exactly. The returned value is always a fully qualified provider path unless the path does not exist and Tree mode is active. #> function Resolve-TreePath { [CmdletBinding()] param( [Parameter(Mandatory)] [string]$Path, [ValidateSet('Normal','Tree','List')] [string]$Mode = 'Normal' ) try { # Caller’s working directory, not module’s $cwd = $ExecutionContext.SessionState.Path.CurrentLocation.ProviderPath if (-not [System.IO.Path]::IsPathRooted($Path)) { $Path = Join-Path -Path $cwd -ChildPath $Path } # Normalize casing/segments $Path = Get-NormalizedPath -Path $Path -ErrorAction Stop # Resolve to provider path $resolved = Resolve-Path -LiteralPath $Path -ErrorAction Stop return $resolved.ProviderPath } catch { if ($Mode -ne 'Tree') { $msg = "Cannot find path '$Path' because it does not exist." $exception = New-Object System.Management.Automation.ItemNotFoundException $msg $category = [System.Management.Automation.ErrorCategory]::ObjectNotFound $errorRecord = New-Object System.Management.Automation.ErrorRecord ` $exception, 'ItemNotFound', $category, $Path $PSCmdlet.WriteError($errorRecord) return $null } return $Path } } <# .SYNOPSIS Enumerates all set file attributes on an item. .DESCRIPTION Used by Get-ItemStyle to apply attribute overlays. #> function Get-SetFileAttributes { param([IO.FileAttributes]$Attributes) foreach ($flag in [System.Enum]::GetValues([IO.FileAttributes])) { if ($Attributes -band $flag) { $flag } } } <# .SYNOPSIS Normalizes a path to match actual filesystem casing. .DESCRIPTION Walks each segment and resolves its real casing using Get-ChildItem. Ensures consistent display even when user input is lowercase/mixed. #> function Get-NormalizedPath { param([string]$Path) # Assume absolute path $absPath = [System.IO.Path]::GetFullPath($Path) # Trim trailing slash unless root if ($absPath.Length -gt 3 -and $absPath.EndsWith("\")) { $absPath = $absPath.TrimEnd('\') } $segments = $absPath -split '\\' $normalized = @() $current = $segments[0] + "\" $normalized += $segments[0] for ($i = 1; $i -lt $segments.Count; $i++) { $segment = $segments[$i] try { $entries = Get-ChildItem -LiteralPath $current -ErrorAction Stop | Select-Object -ExpandProperty Name $match = $entries | Where-Object { $_.ToLower() -eq $segment.ToLower() } if ($match) { $normalized += $match $current = Join-Path $current $match } else { $normalized += $segment $current = Join-Path $current $segment } } catch { # Parent doesn't exist — keep original casing $normalized += $segment $current = Join-Path $current $segment } } ($normalized -join '\') } <# .SYNOPSIS Finds the nearest existing parent directory. .DESCRIPTION Used for Tree.com header generation when the target path does not fully exist. #> function Get-NearestExistingParent { param([string]$Path) $current = [System.IO.Path]::GetFullPath($Path) while (-not (Test-Path $current)) { $parent = [System.IO.Directory]::GetParent($current) if ($null -eq $parent) { return $null } $current = $parent.FullName } $current } <# .SYNOPSIS Returns the filesystem label for a drive. .DESCRIPTION Used only in Tree.com compatibility mode. #> function Get-VolumeName { param([string]$Path = ".") $driveLetter = (Get-Item $Path).PSDrive.Name $volume = Get-Volume -DriveLetter $driveLetter $volume.FileSystemLabel } <# .SYNOPSIS Retrieves the volume serial number using Win32 API. .DESCRIPTION Matches Tree.com output exactly. #> function Get-VolumeSerialNumber { param ( [string]$Path = "." ) if (-not ([System.Management.Automation.PSTypeName]'VolumeInfo').Type) { $definition = @" using System; using System.Runtime.InteropServices; public class VolumeInfo { [DllImport("kernel32.dll", CharSet = CharSet.Auto)] public static extern bool GetVolumeInformation( string lpRootPathName, System.Text.StringBuilder lpVolumeNameBuffer, int nVolumeNameSize, out uint lpVolumeSerialNumber, out uint lpMaximumComponentLength, out uint lpFileSystemFlags, System.Text.StringBuilder lpFileSystemNameBuffer, int nFileSystemNameSize); } "@ Add-Type -TypeDefinition $definition -ErrorAction SilentlyContinue | Out-Null } $root = [System.IO.Path]::GetPathRoot((Resolve-Path $Path).Path) $serial = 0 $null1 = 0 $null2 = 0 $volName = New-Object System.Text.StringBuilder 261 $fsName = New-Object System.Text.StringBuilder 261 [VolumeInfo]::GetVolumeInformation( $root, $volName, $volName.Capacity, [ref]$serial, [ref]$null1, [ref]$null2, $fsName, $fsName.Capacity ) | Out-Null $serialHigh = ($serial -shr 16) $serialLow = ($serial -band 0xFFFF) "{0:X4}-{1:X4}" -f $serialHigh, $serialLow } #endregion |