Public/Get-NewestFileVersionCompatible.ps1

function Get-NewestFileVersion {
    <#
    .SYNOPSIS
    Analyzes file version information and identifies the newest version of each unique filename.
 
    .DESCRIPTION
    The Get-NewestFileVersion function processes file paths or FileInfo objects to extract version
    information and group files by their filename. For each unique filename, it identifies which
    instance has the newest version (or most recent LastWriteTime if version info is unavailable).
 
    This function is designed to work seamlessly with Find-FileRecursive or Get-ChildItem to help
    identify version conflicts when multiple copies of the same DLL or executable exist across
    different directories.
 
    .PARAMETER Path
    Specifies the path(s) to files to analyze. Accepts:
    - String paths (e.g., "C:\Windows\System32\kernel32.dll")
    - Arrays of string paths
    - FileInfo objects from Get-ChildItem or Find-FileRecursive
    - Pipeline input from any command that outputs file paths or FileInfo objects
 
    This parameter accepts pipeline input and can be bound by property name (FullName, PSPath).
 
    .PARAMETER SkipFilesWithoutVersion
    When specified, files that do not contain embedded version information will be excluded from
    the results. By default, files without version info are included and sorted by LastWriteTime.
 
    .PARAMETER AsHashTable
    When specified, returns the results as a hashtable instead of an array of PSCustomObjects.
    The filenames are stored as keys, and the full details are stored as PSCustomObjects in the
    hashtable item values.
 
    This format is useful for quick lookups by filename or for programmatic access to specific
    file version data.
 
    .INPUTS
    System.String[]
    System.IO.FileInfo[]
 
    You can pipe file paths (strings) or FileInfo objects to Get-NewestFileVersion.
 
    .OUTPUTS
    PSCustomObject[] (default)
    System.Collections.Hashtable (when -AsHashTable is specified)
 
    Default output is an array of custom objects with the following properties:
    - FileName: The name of the file (e.g., "kernel32.dll")
    - NewestVersion: The file version of the newest instance (e.g., "10.0.19041.1")
    - NewestPath: Full path to the file with the newest version
    - LastWriteTime: The LastWriteTime of the newest file
    - HasVersionInfo: Boolean indicating if the newest file has version information
    - TotalInstancesFound: Count of all instances of this filename found
    - FilesWithVersion: Count of instances that have version information
    - FilesWithoutVersion: Count of instances without version information
    - AllVersions: Array of all unique versions found, sorted descending
    - AllPaths: Array of full paths to all instances of this filename
 
    When -AsHashTable is specified, returns a hashtable where keys are filenames and values
    are the PSCustomObjects described above.
 
    .EXAMPLE
    Find-FileRecursive -Path "C:\Program Files" -FilePattern "Microsoft.Identity.Client.dll" -Parallel | Get-NewestFileVersion
 
    Searches for all instances of a DLL across 'Program Files' and identifies the newest version.
 
    .EXAMPLE
    $ModulePaths = $env:PSModulePath -split [System.IO.Path]::PathSeparator
    $Results = Find-FileRecursive -Path $ModulePaths `
        -FilePattern @('Microsoft.Identity*.dll', 'Microsoft.IdentityModel*.dll') -Parallel | Get-NewestFileVersion
 
    $Results | Format-Table FileName, NewestVersion, TotalInstancesFound, NewestPath -AutoSize
 
    Finds all Microsoft Identity-related DLLs across PowerShell module paths, groups them by
    filename, and displays a summary table showing the newest version of each.
 
    .EXAMPLE
    $ModulePaths = $env:PSModulePath -split [System.IO.Path]::PathSeparator
    $VersionMap = Find-FileRecursive -Path $ModulePaths -FilePattern 'Microsoft.Identity*.dll' -Parallel |
        Get-NewestFileVersion -AsHashTable
 
    $VersionMap['Microsoft.Identity.Client.dll']
    $VersionMap['Microsoft.Identity.Client.dll'].NewestVersion
    $VersionMap['Microsoft.Identity.Client.dll'].NewestPath
 
    Returns results as a hashtable for quick lookups. You can access version information for any specific filename directly
    using the hashtable key.
 
    .NOTES
    Requires: PowerShell 5.1+
 
    The function uses FileVersionInfo to read embedded version information from executables and
    DLLs. If a file does not have version information, it falls back to using LastWriteTime for
    comparison unless -SkipFilesWithoutVersion is specified.
 
    When comparing versions, the function attempts to parse FileVersion as a [version] object
    for accurate semantic version comparison. If parsing fails, it falls back to LastWriteTime.
 
    The -AsHashTable parameter is useful when you need O(1) lookup performance for specific
    filenames or when building version comparison logic in scripts.
    #>


    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [Alias('FullName', 'PSPath')]
        [object[]]$Path,

        [Parameter(Mandatory = $false)]
        [switch]$SkipFilesWithoutVersion,

        [Parameter(Mandatory = $false)]
        [switch]$AsHashTable
    )

    begin {
        $FileGroups = @{}
    }

    process {
        foreach ($Item in $Path) {
            # Determine file path from input type
            if ($Item -is [System.IO.FileInfo]) {
                $FilePath = $Item.FullName
            } elseif ($Item -is [string]) {
                $FilePath = $Item
            } else {
                $FilePath = $Item.ToString()
            }

            try {
                # Get FileInfo object
                if ($Item -is [System.IO.FileInfo]) {
                    $FileInfo = $Item
                } else {
                    $FileInfo = Get-Item -LiteralPath $FilePath -ErrorAction Stop
                }

                $FileName = $FileInfo.Name

                try {
                    $VersionInfo = [System.Diagnostics.FileVersionInfo]::GetVersionInfo($FileInfo.FullName)

                    $HasVersion = -not [string]::IsNullOrWhiteSpace($VersionInfo.FileVersion)

                    if ($SkipFilesWithoutVersion -and -not $HasVersion) {
                        Write-Verbose "Skipping file without version: $($FileInfo.FullName)"
                        continue
                    }

                    $FileData = [PSCustomObject]@{
                        FullName       = $FileInfo.FullName
                        Name           = $FileName
                        FileVersion    = if ($HasVersion) { $VersionInfo.FileVersion } else { $null }
                        ProductVersion = if (-not [string]::IsNullOrWhiteSpace($VersionInfo.ProductVersion)) { $VersionInfo.ProductVersion } else { $null }
                        LastWriteTime  = $FileInfo.LastWriteTime
                        Length         = $FileInfo.Length
                        HasVersionInfo = $HasVersion
                    }

                    if (-not $FileGroups.ContainsKey($FileName)) {
                        $FileGroups[$FileName] = New-Object System.Collections.Generic.List[object]
                    }
                    $FileGroups[$FileName].Add($FileData)
                } catch {
                    Write-Warning "Could not read version info for: $($FileInfo.FullName) - $($_.Exception.Message)"

                    if (-not $SkipFilesWithoutVersion) {
                        $FileData = [PSCustomObject]@{
                            FullName       = $FileInfo.FullName
                            Name           = $FileName
                            FileVersion    = $null
                            ProductVersion = $null
                            LastWriteTime  = $FileInfo.LastWriteTime
                            Length         = $FileInfo.Length
                            HasVersionInfo = $false
                        }

                        if (-not $FileGroups.ContainsKey($FileName)) {
                            $FileGroups[$FileName] = New-Object System.Collections.Generic.List[object]
                        }
                        $FileGroups[$FileName].Add($FileData)
                    }
                }
            } catch {
                Write-Warning "Could not access file: $FilePath - $($_.Exception.Message)"
            }
        }
    }

    end {
        if ($AsHashTable) {
            $ResultHashTable = @{}

            foreach ($FileName in $FileGroups.Keys) {
                $Files = $FileGroups[$FileName]

                $Newest = $Files | Sort-Object -Descending -Property {
                    if ($_.HasVersionInfo -and $_.FileVersion) {
                        try {
                            [version]$_.FileVersion
                        } catch {
                            $_.LastWriteTime
                        }
                    } else {
                        $_.LastWriteTime
                    }
                } | Select-Object -First 1

                $ResultHashTable[$FileName] = [PSCustomObject]@{
                    FileName            = $FileName
                    NewestVersion       = $Newest.FileVersion
                    NewestPath          = $Newest.FullName
                    LastWriteTime       = $Newest.LastWriteTime
                    HasVersionInfo      = $Newest.HasVersionInfo
                    TotalInstancesFound = $Files.Count
                    FilesWithVersion    = @($Files | Where-Object { $_.HasVersionInfo }).Count
                    FilesWithoutVersion = @($Files | Where-Object { -not $_.HasVersionInfo }).Count
                    AllVersions         = @($Files | Where-Object { $_.HasVersionInfo } | Select-Object -ExpandProperty FileVersion -Unique | Sort-Object -Descending)
                    AllPaths            = $Files.FullName
                }
            }

            $ResultHashTable
        } else {
            foreach ($FileName in ($FileGroups.Keys | Sort-Object)) {
                $Files = $FileGroups[$FileName]

                $Newest = $Files | Sort-Object -Descending -Property {
                    if ($_.HasVersionInfo -and $_.FileVersion) {
                        try {
                            [version]$_.FileVersion
                        } catch {
                            $_.LastWriteTime
                        }
                    } else {
                        $_.LastWriteTime
                    }
                } | Select-Object -First 1

                [PSCustomObject]@{
                    FileName            = $FileName
                    NewestVersion       = $Newest.FileVersion
                    NewestPath          = $Newest.FullName
                    LastWriteTime       = $Newest.LastWriteTime
                    HasVersionInfo      = $Newest.HasVersionInfo
                    TotalInstancesFound = $Files.Count
                    FilesWithVersion    = @($Files | Where-Object { $_.HasVersionInfo }).Count
                    FilesWithoutVersion = @($Files | Where-Object { -not $_.HasVersionInfo }).Count
                    AllVersions         = @($Files | Where-Object { $_.HasVersionInfo } | Select-Object -ExpandProperty FileVersion -Unique | Sort-Object -Descending)
                    AllPaths            = $Files.FullName
                }
            }
        }
    }
}