Private/Show-TreeInternal.ps1

#region Entry Point
<#
.SYNOPSIS
    Core recursive engine for Show-Tree.
 
.DESCRIPTION
    This function renders a directory tree using graphical connectors,
    optional color, optional file inclusion, and optional gap logic.
    It is called once from Show-Tree.ps1 and then recursively by itself.
 
    Responsibilities:
      • Normalize and validate the root path
      • Initialize gap state
      • Enumerate directories/files (raw Win32 or PowerShell)
      • Apply filtering (hidden/system)
      • Render files, directories, and gap lines
      • Manage recursion depth and prefix construction
      • Maintain gap-mode state machine (Internal, Tail, Sibling)
 
    This function is internal-only and not exported.
#>

enum GapMode {
    None
    Internal
    Tail
    Sibling
}

function Show-TreeInternal {
    [CmdletBinding()]
    param (
        # Absolute or relative path to render
        [string]$Path,

        # Tree.com compatibility mode
        [switch]$Tree,

        # Listing mode (indentation only)
        [switch]$List,

        # Maximum recursion depth (-1 = unlimited)
        [int]$MaxDepth,

        # Enable color output
        [switch]$Colorize,

        # Include files in output
        [switch]$IncludeFiles,

        # Hide hidden items
        [switch]$HideHidden,

        # Hide system items
        [switch]$HideSystem,

        # Show reparse point targets
        [switch]$ShowTargets,

        # Enable gap logic (blank lines between blocks)
        [switch]$Gap,

        # Use ASCII connectors instead of Unicode
        [switch]$Ascii,

        # Show attribute debug info
        [switch]$DebugAttributes,

        # Current recursion depth (internal)
        [int]$CurrentDepth = 0,

        # Prefix string built from parent connectors
        [string]$Prefix = "",

        # Whether the parent directory was the last sibling
        [bool]$IsLastParent = $false
    )

    # Precompute ANSI sequences
    $esc        = [char]27
    $colorReset = $Colorize ? "${esc}[0m"  : ""
    $colorGap   = $Colorize ? "${esc}[90m" : ""

    # Precompute gap connector once per directory block
    $gapConnector = Get-Connector -Type Gap -Tree:$Tree -List:$List -Ascii:$Ascii

    #
    # Root-level initialization
    #
    if ($CurrentDepth -eq 0) {

        # Initialize gap state machine
        $script:GapState = [PSCustomObject]@{
            LastGapMode = [GapMode]::None
        }

        # Normalize path casing and separators
        $Path = Get-NormalizedPath -Path $Path

        # Validate existence
        if (-not (Test-Path $Path)) {
            $invalidPath = $true
        }

        # Tree.com header
        if ($Tree) {
            $nearestExistingParent = Get-NearestExistingParent -Path $Path
            $fileSystemLabel       = Get-VolumeName -Path $nearestExistingParent
            $serialNumber          = Get-VolumeSerialNumber -Path $nearestExistingParent

            Write-Output "Folder PATH listing for volume $fileSystemLabel"
            Write-Output "Volume serial number is $serialNumber"
        }

        # Render root directory name
        $root = Get-Item $Path
        $dir  = [PSCustomObject]@{
            FullName      = $root.FullName
            Name          = $root.Name
            Attributes    = $root.Attributes
            PSIsContainer = $true
        }
        $dir.PSObject.TypeNames.Insert(0, 'System.IO.DirectoryInfo')

        $style = Get-ItemStyle -Item $dir -Colorize:$Colorize

        # Optional attribute debug
        if ($DebugAttributes) {
            $styleName = $style.Name
            $attrHex   = ('0x{0:X8}' -f [uint32]$dir.Attributes)
            $attrNames = $dir.Attributes.ToString()
            $debug     = " [$attrHex $attrNames | $styleName]"
        }

        Write-Output "$($style.Ansi)$Path${colorReset}${debug}"

        if ($invalidPath) {
            Write-Output "Invalid path - \$($Path -Split '\\' | Select-Object -Last 1)"
        }
    }

    #
    # Depth cap enforcement
    #
    if ($MaxDepth -ne -1 -and $CurrentDepth -ge $MaxDepth) {
        return
    }

    #
    # Directory enumeration
    #
    if ($Tree) {
        # Raw Win32 enumeration for Tree.com compatibility
        $raw   = Get-RawDirectoryEntries -Path $Path
        $dirs  = $raw.Directories
        $files = $IncludeFiles ? $raw.Files : @()
    }
    else {
        # Standard PowerShell enumeration
        $dirs  = Get-ChildItem -Path $Path -Directory -Force -ErrorAction SilentlyContinue
        $files = $IncludeFiles ? (Get-ChildItem -Path $Path -File -Force -ErrorAction SilentlyContinue) : @()
    }

    #
    # Filtering
    #
    if ($HideHidden) {
        $dirs  = $dirs  | Where-Object { -not ($_.Attributes -band [IO.FileAttributes]::Hidden) }
        $files = $files | Where-Object { -not ($_.Attributes -band [IO.FileAttributes]::Hidden) }
    }

    if ($HideSystem) {
        $dirs  = $dirs  | Where-Object { -not ($_.Attributes -band [IO.FileAttributes]::System) }
        $files = $files | Where-Object { -not ($_.Attributes -band [IO.FileAttributes]::System) }
    }

    $fileCount = $files.Count
    $dirCount  = $dirs.Count

    # Tree.com: suppress file connectors when no subdirectories exist
    $noSpan = $Tree -and $dirCount -eq 0

    #
    # FILE RENDERING
    #
    for ($j = 0; $j -lt $fileCount; $j++) {
        $file       = $files[$j]
        $isLastFile = ($j -eq $fileCount - 1) -and ($dirCount -eq 0)

        Write-TreeItem `
            -Item          $file `
            -Type          File `
            -Prefix        $Prefix `
            -IsLast        $isLastFile `
            -Tree:$Tree `
            -List:$List `
            -Ascii:$Ascii `
            -Colorize:$Colorize `
            -ShowTargets:$ShowTargets `
            -DebugAttributes:$DebugAttributes `
            -Recurse:$false `
            -NoSpan        $noSpan `
            -MaxDepth      $MaxDepth `
            -CurrentDepth  $CurrentDepth `
            -IncludeFiles:$IncludeFiles `
            -Gap:$Gap `
            -HideHidden:$HideHidden `
            -HideSystem:$HideSystem
    }

    #
    # Early exit for empty directories
    #
    if ($invalidPath -or ($CurrentDepth -eq 0 -and $dirCount -eq 0)) {
        if ($Tree) {
            if (-not $invalidPath) { Write-Output "" }
            Write-Output "No subfolders exist"
        }
        Write-Output ""
        return
    }

    #
    # INTERNAL GAP (files → directories)
    #
    if ($Gap -and
        $script:GapState.LastGapMode -eq [GapMode]::None -and
        $IncludeFiles -and
        $fileCount -gt 0) {

        if ($dirCount -gt 0) {
            # Files + directories → connector gap
            Write-Gap $colorGap $Prefix $gapConnector $colorReset ([GapMode]::Internal)
        }
        else {
            # Files only → tail gap
            if ($Tree -or (-not $IsLastParent)) {
                Write-Gap $colorGap $Prefix $null $colorReset ([GapMode]::Tail)
            }
        }
    }

    #
    # DIRECTORY RENDERING
    #
    for ($i = 0; $i -lt $dirCount; $i++) {
        $dir       = $dirs[$i]
        $isLastDir = ($i -eq $dirCount - 1)

        Write-TreeItem `
            -Item           $dir `
            -Type           Directory `
            -Prefix         $Prefix `
            -IsLast         $isLastDir `
            -Tree:$Tree `
            -List:$List `
            -Ascii:$Ascii `
            -Colorize:$Colorize `
            -ShowTargets:$ShowTargets `
            -DebugAttributes:$DebugAttributes `
            -Recurse `
            -NoSpan         $false `
            -MaxDepth       $MaxDepth `
            -CurrentDepth   $CurrentDepth `
            -IncludeFiles:$IncludeFiles `
            -Gap:$Gap `
            -HideHidden:$HideHidden `
            -HideSystem:$HideSystem

        #
        # SIBLING / COUSIN GAP LOGIC
        #
        if ($Gap -and $i -lt $dirCount - 1) {

            # Tail gap suppresses immediate sibling gap
            if ($script:GapState.LastGapMode -eq [GapMode]::Tail) {
                $script:GapState.LastGapMode = [GapMode]::None
                continue
            }

            # Prevent consecutive gaps
            if ($script:GapState.LastGapMode -ne [GapMode]::None) {
                continue
            }

            # Normal mode: only if left sibling has visible children
            if (-not $Tree) {
                if (Test-HasChildrenForGap -Dir $dirs[$i] -CurrentDepth $CurrentDepth -MaxDepth $MaxDepth) {
                    Write-Gap $colorGap $Prefix $gapConnector $colorReset ([GapMode]::Sibling)
                }
            }
        }
    }

    #
    # Final newline for normal mode root
    #
    if ($CurrentDepth -eq 0 -and -not $Tree) {
        Write-Output ""
    }
}
#endregion

#region Rendering (TreeItem, Gap)
<#
.SYNOPSIS
    Renders a single file or directory entry.
 
.DESCRIPTION
    Handles:
      • Connector selection
      • Style/color application
      • Reparse target display
      • Attribute debug output
      • Recursion into subdirectories
      • Gap-state reset
#>

function Write-TreeItem {
    param(
        [Parameter(Mandatory)]
        $Item,

        [Parameter(Mandatory)]
        [ValidateSet('File','Directory')]
        [string]$Type,

        # Prefix inherited from parent
        [string]$Prefix = "",

        # Whether this item is the last sibling
        [bool]$IsLast,

        # Mode switches
        [switch]$Tree,
        [switch]$List,
        [switch]$Ascii,
        [switch]$Colorize,
        [switch]$ShowTargets,
        [switch]$DebugAttributes,
        [switch]$Recurse,

        # Whether to suppress file connector span
        [bool]$NoSpan = $false,

        # Recursion state
        [int]$MaxDepth,
        [int]$CurrentDepth,

        # Additional flags
        [switch]$IncludeFiles,
        [switch]$Gap,
        [switch]$HideHidden,
        [switch]$HideSystem
    )

    # Compute connector for this item
    $connector = Get-Connector `
        -Type   $Type `
        -Tree:$Tree `
        -List:$List `
        -Ascii:$Ascii `
        -IsLast $IsLast `
        -NoSpan $NoSpan

    # Compute style
    $style = Get-ItemStyle -Item $Item -Colorize:$Colorize

    #
    # Reparse target resolution
    #
    $target = $null
    if ($ShowTargets -and ($Item.Attributes -band [IO.FileAttributes]::ReparsePoint)) {
        $info = Get-Item -LiteralPath $Item.FullName -Force -ErrorAction SilentlyContinue
        if ($info -and $info.PSObject.Properties.Match('Target')) {
            $target = $info.Target
        }
    }

    #
    # Output formatting
    #
    $esc   = [char]27
    $reset = $Colorize ? "${esc}[0m"  : ""
    $dim   = $Colorize ? "${esc}[90m" : ""

    $targetText = $target ? " ${dim}->${reset} $target" : ""

    # Optional attribute debug
    $debug = ""
    if ($DebugAttributes) {
        $styleName = $style.Name
        $attrHex   = ('0x{0:X8}' -f [uint32]$Item.Attributes)
        $attrNames = $Item.Attributes.ToString()
        $debug     = " [$attrHex $attrNames | $styleName]"
    }

    Write-Output "${dim}${Prefix}${dim}${connector}$($style.Ansi)$($Item.Name)$reset$targetText $debug"

    #
    # Reset gap state unless tail gap was printed
    #
    if ($script:GapState.LastGapMode -ne [GapMode]::Tail) {
        $script:GapState.LastGapMode = [GapMode]::None
    }

    #
    # Recursion into subdirectories
    #
    if ($Recurse -and
        $Type -eq 'Directory' -and
        -not ($Item.Attributes -band [IO.FileAttributes]::ReparsePoint)) {

        # Build next-level prefix
        $newPrefix = $Prefix + (Get-Connector `
            -Type   Prefix `
            -Tree:$Tree `
            -List:$List `
            -Ascii:$Ascii `
            -IsLast $IsLast)

        # Recurse
        Show-TreeInternal `
            -Path          $Item.FullName `
            -Tree:$Tree `
            -List:$List `
            -MaxDepth      $MaxDepth `
            -Colorize:$Colorize `
            -IncludeFiles:$IncludeFiles `
            -HideHidden:$HideHidden `
            -HideSystem:$HideSystem `
            -ShowTargets:$ShowTargets `
            -Gap:$Gap `
            -Ascii:$Ascii `
            -DebugAttributes:$DebugAttributes `
            -CurrentDepth  ($CurrentDepth + 1) `
            -Prefix        $newPrefix `
            -IsLastParent  $IsLast
    }

    #
    # Reset gap state again after recursion
    #
    if ($script:GapState.LastGapMode -ne [GapMode]::Tail) {
        $script:GapState.LastGapMode = [GapMode]::None
    }
}

<#
.SYNOPSIS
    Writes a gap line between blocks.
 
.DESCRIPTION
    Handles Internal, Tail, and Sibling gap modes.
    Updates the global gap-state machine.
#>

function Write-Gap {
    param(
        $colorGap,
        $Prefix,
        $GapConnector,
        $colorReset,
        [GapMode]$Mode
    )

    $connector = $GapConnector ? $GapConnector : ""
    Write-Output "${colorGap}${Prefix}${connector}${colorReset}"
    $script:GapState.LastGapMode = $Mode
}
#endregion

#region Gap Logic Helpers
<#
.SYNOPSIS
    Determines whether a directory has visible children.
 
.DESCRIPTION
    Used to decide whether to print a sibling/cousin gap.
    Respects MaxDepth and treats reparse points as leaf nodes.
#>

function Test-HasChildrenForGap {
    param(
        $Dir,
        [int]$CurrentDepth,
        [int]$MaxDepth
    )

    if (Test-IsReparsePoint $Dir) {
        return $false
    }

    # Depth cap: treat as empty if recursion would stop here
    if ($MaxDepth -ne -1 -and $CurrentDepth + 1 -ge $MaxDepth) {
        return $false
    }

    $children = Get-ChildItem -LiteralPath $Dir.FullName -Force -ErrorAction SilentlyContinue
    return $children.Count -gt 0
}

<#
.SYNOPSIS
    Checks whether an item is a reparse point.
 
.DESCRIPTION
    Reparse points (symlinks/junctions) are treated as leaf nodes
    for recursion and gap logic.
#>

function Test-IsReparsePoint {
    param($Item)
    [bool]($Item.Attributes -band [IO.FileAttributes]::ReparsePoint)
}
#endregion

#region Connector Rendering
<#
.SYNOPSIS
    Returns the connector string for a given item type.
 
.DESCRIPTION
    Handles:
      • Tree.com ASCII mode
      • Unicode graphical mode
      • Prefix vs File vs Directory vs Gap
      • Last-sibling logic
      • NoSpan suppression for Tree.com file connectors
#>

function Get-Connector {
    param(
        [Parameter(Mandatory)]
        [ValidateSet('File','Directory','Gap','Prefix')]
        [string]$Type,

        [switch]$Tree,
        [switch]$List,
        [switch]$Ascii,

        [bool]$IsLast = $false,
        [bool]$NoSpan = $false
    )

    #
    # Listing mode: indentation only
    #
    if ($List) {
        return ' '
    }

    #
    # Tree.com compatibility mode
    #
    if ($Tree) {
        if ($Type -eq 'File' -and $NoSpan) {
            return ' '
        }

        switch ($Type) {
            'File'      { return $Ascii ? '| '  : '│ ' }
            'Directory' {
                if ($IsLast) { return $Ascii ? '\---' : '└───' }
                else         { return $Ascii ? '+---' : '├───' }
            }
            'Gap'       { return $Ascii ? '|'    : '│' }
            'Prefix'    {
                if ($IsLast) { return ' ' }
                else         { return $Ascii ? '| ' : '│ ' }
            }
        }
    }

    #
    # Graphical Unicode mode (Show‑Tree default)
    #
    switch ($Type) {
        'File' {
            if ($IsLast) { return $Ascii ? '\-- ' : '╙── ' }
            else         { return $Ascii ? '+-- ' : '╟── ' }
        }

        'Directory' {
            if ($IsLast) { return $Ascii ? '\== ' : '╚══ ' }
            else         { return $Ascii ? '+== ' : '╠══ ' }
        }

        'Gap' {
            return $Ascii ? '|' : '║'
        }

        'Prefix' {
            if ($IsLast) { return ' ' }
            else         { return $Ascii ? '| ' : '║ ' }
        }
    }
}
#endregion

#region Style Rendering
<#
.SYNOPSIS
    Computes the ANSI style for a file or directory.
 
.DESCRIPTION
    Applies:
      • Base style (directory/file/symlink/junction)
      • Attribute overlays (hidden, system, temporary, etc.)
      • Foreground overrides
      • Combined ANSI escape sequence
 
    Returns an object:
      @{ Name = "..."; Ansi = "..."; }
#>

function Get-ItemStyle {
    param(
        $Item,
        $Colorize = $script:Colorize,
        $StyleProfile = $script:StyleProfile
    )

    $esc = [char]27

    $isDir     = $Item.PSIsContainer
    $attrs     = $Item.Attributes
    $isReparse = [bool]($attrs -band [IO.FileAttributes]::ReparsePoint)

    #
    # Determine base style
    #
    if ($isReparse -and $isDir) {
        $styleName = "Junction"
        $base      = $StyleProfile.Base.Junction
    }
    elseif ($isReparse -and -not $isDir) {
        $styleName = "Symlink"
        $base      = $StyleProfile.Base.File
    }
    elseif ($isDir) {
        $styleName = "Directory"
        $base      = $StyleProfile.Base.Directory
    }
    else {
        $styleName = "File"
        $base      = $StyleProfile.Base.File
    }

    #
    # No color mode
    #
    if (-not $Colorize) {
        return [PSCustomObject]@{
            Name = $styleName
            Ansi = ""
        }
    }

    #
    # Parse base style codes
    #
    $codes = @() + ($base -split ';' | Where-Object { $_ -ne '' })

    # Extract foreground codes (30–37, 90–97)
    $fg    = @() + ($codes | Where-Object { $_ -match '^(3[0-7]|9[0-7])$' })
    $codes = @() + ($codes | Where-Object { $_ -notmatch '^(3[0-7]|9[0-7])$' })

    #
    # Apply attribute overlays
    #
    foreach ($flag in Get-SetFileAttributes $attrs) {
        $flagName = $flag.ToString()

        if ($StyleProfile.Attributes.ContainsKey($flagName)) {
            $overlay = $StyleProfile.Attributes[$flagName]

            # Add overlay attributes
            if ($overlay.Attributes) {
                $codes += ($overlay.Attributes -split ';')
            }

            # Foreground override
            if ($overlay.OverrideForeground) {
                if ($overlay.OverrideForeground -is [string]) {
                    $fg = $overlay.OverrideForeground
                }
                elseif ($overlay.OverrideForeground.ContainsKey($styleName)) {
                    $fg = $overlay.OverrideForeground[$styleName]
                }
            }
        }
    }

    #
    # Build final ANSI sequence
    #
    $final = @()
    if ($fg) { $final += $fg }
    $final += $codes

    $ansi = "${esc}[$($final -join ';')m"

    [PSCustomObject]@{
        Name = $styleName
        Ansi = $ansi
    }
}
#endregion

#region Path Utilities
<#
.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 = ".")

    $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 | 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

#region Raw Directory Enumeration
<#
.SYNOPSIS
    Enumerates directory entries using Win32 FindFirstFile.
 
.DESCRIPTION
    Used in Tree.com mode to match exact ordering and behavior.
    Returns PSCustomObject with:
      • Directories = [...]
      • Files = [...]
#>

function Get-RawDirectoryEntries {
    param([string]$Path)

    #
    # Load RawEnum type once
    #
    if (-not ([System.Management.Automation.PSTypeName]'RawEnum').Type) {
        $definition = @"
using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Collections.Generic;
 
public class RawEnum {
    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
    public struct WIN32_FIND_DATA {
        public uint dwFileAttributes;
        public System.Runtime.InteropServices.ComTypes.FILETIME ftCreationTime;
        public System.Runtime.InteropServices.ComTypes.FILETIME ftLastAccessTime;
        public System.Runtime.InteropServices.ComTypes.FILETIME ftLastWriteTime;
        public uint nFileSizeHigh;
        public uint nFileSizeLow;
        public uint dwReserved0;
        public uint dwReserved1;
        [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)]
        public string cFileName;
        [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 14)]
        public string cAlternateFileName;
    }
 
    [DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
    static extern IntPtr FindFirstFile(string lpFileName, out WIN32_FIND_DATA lpFindFileData);
 
    [DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
    static extern bool FindNextFile(IntPtr hFindFile, out WIN32_FIND_DATA lpFindFileData);
 
    [DllImport("kernel32.dll")]
    static extern bool FindClose(IntPtr hFindFile);
 
    public static IEnumerable<WIN32_FIND_DATA> Enum(string path) {
        WIN32_FIND_DATA data;
        IntPtr handle = FindFirstFile(Path.Combine(path, "*"), out data);
        if (handle == new IntPtr(-1)) yield break;
 
        do {
            string name = data.cFileName;
            if (name != "." && name != "..")
                yield return data;
        }
        while (FindNextFile(handle, out data));
 
        FindClose(handle);
    }
}
"@

        Add-Type -TypeDefinition $definition -ErrorAction SilentlyContinue | Out-Null
    }

    #
    # Enumerate entries
    #
    $entries = [RawEnum]::Enum($Path)

    $dirs  = @()
    $files = @()

    foreach ($e in $entries) {
        $isDir = ($e.dwFileAttributes -band [IO.FileAttributes]::Directory) -ne 0

        $root = Get-Item (Join-Path $Path $e.cFileName) -Force

        $item = [PSCustomObject]@{
            FullName      = $root.FullName
            Name          = $root.Name
            Attributes    = $root.Attributes
            PSIsContainer = $isDir
        }

        if ($isDir) {
            $item.PSObject.TypeNames.Insert(0, 'System.IO.DirectoryInfo')
            $dirs += $item
        }
        else {
            $item.PSObject.TypeNames.Insert(0, 'System.IO.FileInfo')
            $files += $item
        }
    }

    #
    # Return structured result
    #
    [PSCustomObject]@{
        Directories = $dirs
        Files       = $files
    }
}
#endregion