Private/Get-PathHash.ps1

function Get-PathHash
{
    <#
    .SYNOPSIS
        Calculates SHA-256 hash for a file or directory.

    .DESCRIPTION
        Computes cryptographic hash values for files and directories to enable
        backup integrity verification and change detection. For directories,
        creates a composite hash based on all contained files and their paths.

    .PARAMETER Path
        The file or directory path to hash.

    .PARAMETER Algorithm
        The hash algorithm to use. Defaults to SHA256.

    .OUTPUTS
        String containing the computed hash value.

    .NOTES
        For directories, the hash is computed by:
        1. Getting all files recursively with their relative paths
        2. Computing hash for each file
        3. Creating sorted list of "path:hash" entries
        4. Hashing the concatenated string

        This provides meaningful change detection for directory structures.

    .EXAMPLE
        PS > Get-PathHash -Path 'C:\Documents\report.pdf'
        Returns SHA-256 hash of the file

    .EXAMPLE
        PS > Get-PathHash -Path 'C:\Documents'
        Returns composite SHA-256 hash of the directory contents
    #>

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

        [Parameter(Mandatory = $false)]
        [ValidateSet('SHA1', 'SHA256', 'SHA384', 'SHA512', 'MD5')]
        [string] $Algorithm = 'SHA256'
    )

    try
    {
        if (-not (Test-Path -Path $Path))
        {
            Write-Warning "Get-PathHash> Path does not exist: $Path"
            return $null
        }

        if (Test-Path -Path $Path -PathType Leaf)
        {
            # File hash - straightforward
            Write-Verbose "Get-PathHash> Computing $Algorithm hash for file: $Path"
            $hash = Get-FileHash -Path $Path -Algorithm $Algorithm
            return $hash.Hash
        }
        else
        {
            # Directory hash - composite approach
            Write-Verbose "Get-PathHash> Computing $Algorithm composite hash for directory: $Path"

            $files = Get-ChildItem -Path $Path -File -Recurse | Sort-Object FullName
            if (-not $files)
            {
                Write-Verbose "Get-PathHash> Empty directory, returning hash of empty string"
                $emptyHash = [System.Security.Cryptography.HashAlgorithm]::Create($Algorithm)
                $hashBytes = $emptyHash.ComputeHash([System.Text.Encoding]::UTF8.GetBytes(''))
                $emptyHash.Dispose()
                return [System.BitConverter]::ToString($hashBytes) -replace '-', ''
            }

            $hashEntries = @()
            $basePath = (Resolve-Path $Path).Path

            foreach ($file in $files)
            {
                try
                {
                    $relativePath = $file.FullName.Substring($basePath.Length).TrimStart('\', '/')
                    $fileHash = Get-FileHash -Path $file.FullName -Algorithm $Algorithm
                    $hashEntries += "${relativePath}:$($fileHash.Hash)"
                }
                catch
                {
                    Write-Warning "Get-PathHash> Failed to hash file $($file.FullName): $_"
                    # Include path with error marker for consistency
                    $relativePath = $file.FullName.Substring($basePath.Length).TrimStart('\', '/')
                    $hashEntries += "${relativePath}:ERROR"
                }
            }

            # Create composite hash from sorted entries
            $sortedEntries = $hashEntries | Sort-Object
            $compositeString = $sortedEntries -join "`n"

            $hashAlgorithm = [System.Security.Cryptography.HashAlgorithm]::Create($Algorithm)
            $hashBytes = $hashAlgorithm.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($compositeString))
            $hashAlgorithm.Dispose()

            $finalHash = [System.BitConverter]::ToString($hashBytes) -replace '-', ''
            Write-Verbose "Get-PathHash> Computed composite hash from $($files.Count) files"
            return $finalHash
        }
    }
    catch
    {
        Write-Warning "Get-PathHash> Failed to compute hash for $Path : $_"
        return $null
    }
}