Desktop/Private/Filtering/Test-TreeItemFilterMatch.ps1

# src/Private/Filtering/Test-TreeItemFilterMatch.ps1

<#
.SYNOPSIS
    Tests if an item matches a specific glob pattern.
 
.DESCRIPTION
    Test-TreeItemFilterMatch performs the actual string or path matching for a single
    pattern. It handles both simple wildcards and complex path-based patterns,
    accounting for the traversal root path.
#>

function Test-TreeItemFilterMatch
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [object]$Item,

        [Parameter(Mandatory)]
        [string]$Pattern,

        [string]$RootPath
    )

    if (-not $PSBoundParameters.ContainsKey('Debug') -and $PSCmdlet)
    {
        $DebugPreference = $PSCmdlet.GetVariableValue('DebugPreference')
    }
    if (-not $PSBoundParameters.ContainsKey('Verbose') -and $PSCmdlet)
    {
        $VerbosePreference = $PSCmdlet.GetVariableValue('VerbosePreference')
    }

    $filter = ConvertTo-TreeFilterPattern -Pattern $Pattern -RootPath $RootPath

    if ($filter.DirectoryOnly -and -not $Item.IsContainer)
    {
        return $false
    }

    $itemPath = [System.IO.Path]::GetFullPath($Item.FullPath).TrimEnd([System.IO.Path]::DirectorySeparatorChar)

    # If the pattern contains separators, treat it as a path match
    $isPathMatch = $Pattern.Contains([System.IO.Path]::DirectorySeparatorChar)

    if ($isPathMatch)
    {
        # Canonicalize the pattern path
        $patternPath = $filter.Pattern

        # If the pattern is not rooted, we check if it matches as a relative suffix
        # or a specific segment in the path.
        if (-not [System.IO.Path]::IsPathRooted($patternPath))
        {
            $sep = [System.IO.Path]::DirectorySeparatorChar
            $normPattern = $sep + $patternPath.TrimStart($sep)
            $normItem = $sep + $itemPath.Replace($RootPath, '').TrimStart($sep)

            Write-Verbose " Test-Match (Relative): normItem='$normItem' normPattern='$normPattern'"

            if ($normItem.Contains($normPattern + $sep) -or $normItem.EndsWith($normPattern))
            {
                Write-Verbose " True"
                return $true
            }
            Write-Verbose " False"
            return $false
        }

        $patternPath = [System.IO.Path]::GetFullPath($patternPath).TrimEnd([System.IO.Path]::DirectorySeparatorChar)

        $hasWildcard = $patternPath.Contains('*') -or $patternPath.Contains('?')
        if ($hasWildcard)
        {
            Write-Verbose " Test-Match (Wildcard): itemPath='$itemPath' patternPath='$patternPath'"
            return $itemPath -like $patternPath -or $itemPath -like ($patternPath + [System.IO.Path]::DirectorySeparatorChar + '*')
        }

        Write-Verbose " Test-Match (Relative): itemPath='$itemPath' patternPath='$patternPath'"
        return $itemPath -eq $patternPath -or $itemPath.StartsWith($patternPath + [System.IO.Path]::DirectorySeparatorChar, [System.StringComparison]::OrdinalIgnoreCase)
    }

    # Name-based match: An item matches if its name matches
    if ($Item.Name -like $filter.Pattern) {
        Write-Verbose " Test-Match (Name): result='True'"
        return $true
    }

    # Recursive segment check: only for exclusions (where RootPath is provided)
    if ($RootPath) {
        $relPath = $itemPath.Replace($RootPath, '').TrimStart([System.IO.Path]::DirectorySeparatorChar)
        if ($relPath) {
            foreach ($segment in $relPath.Split([System.IO.Path]::DirectorySeparatorChar)) {
                if ($segment -like $filter.Pattern) {
                    Write-Verbose " Test-Match (Segment): result='True' (matched $segment)"
                    return $true
                }
            }
        }
    }

    Write-Verbose " Test-Match (Name): result='False'"
    return $false
}