private/Get-WinPEDriverPackageVMware.ps1

#Requires -PSEdition Core

function Get-WinPEDriverPackageVMware {
    <#
    .SYNOPSIS
        Gets VMware Tools ISO package metadata
 
    .DESCRIPTION
        Reads the VMware UpdateUri from the global WinPE driver configuration, downloads its
        package directory listing, and locates Windows VMware Tools ISOs for amd64 and arm64.
        File names provide version and package identifiers; Get-VMwareListingRow parses each
        row's release date and file size. Missing architecture entries, request failures,
        and parsing failures produce a warning and no objects.
 
    .EXAMPLE
        PS> Get-WinPEDriverPackageVMware
 
        Returns amd64 and arm64 VMware Tools ISO metadata from the configured package index.
 
    .INPUTS
        None. This function does not accept pipeline input.
 
    .OUTPUTS
        System.Management.Automation.PSCustomObject. Returns two objects ordered amd64 then
        arm64, each with Architecture, ReadmeUri, PackageId, Version, ReleaseDate, FileName,
        FileSizeMB, DownloadUri, and Checksums properties, or no objects on failure.
 
    .NOTES
        Author: David Segura
        Company: Recast Software
        Version: 1.0.0
        Date: 2026-08-28
 
        Dependencies:
          Module Functions: Get-VMwareListingRow
          Network Resources: Configured VMware UpdateUri
          DotNet Classes: System.Management.Automation.PSCustomObject
    #>

    [CmdletBinding()]
    [OutputType([PSCustomObject[]])]
    param ()

    try {
        $searchUri = $global:OSDeployModule.WinPEDrivers.'vmware'.UpdateUri
        if ([string]::IsNullOrWhiteSpace($searchUri)) {
            throw "vmware UpdateUri is not defined in winpedrivers.json."
        }

        Write-Verbose "[$($MyInvocation.MyCommand.Name)] Fetching VMware packages directory listing"
        $response = Invoke-WebRequest -Uri $searchUri -UseBasicParsing -ErrorAction Stop
        $html = $response.Content

        # Parse both ISO entries from an Apache autoindex table
        # amd64: VMware-tools-windows-13.0.10-25056151.iso
        # arm64: VMware-tools-windows-arm-13.0.10-25056151.iso
        $amd64Match = [regex]::Match($html,
            '(VMware-tools-windows-(\d+\.\d+\.\d+)-(\d+)\.iso)')
        $arm64Match = [regex]::Match($html,
            '(VMware-tools-windows-arm-(\d+\.\d+\.\d+)-(\d+)\.iso)')

        if (-not $amd64Match.Success) {
            Write-Warning "[$(Get-Date -Format s)] Could not find amd64 ISO filename on '$searchUri'. Page format may have changed."
            return
        }
        if (-not $arm64Match.Success) {
            Write-Warning "[$(Get-Date -Format s)] Could not find arm64 ISO filename on '$searchUri'. Page format may have changed."
            return
        }

        $amd64File    = $amd64Match.Groups[1].Value
        $amd64Version = $amd64Match.Groups[2].Value
        $amd64Id      = $amd64Match.Groups[3].Value

        $arm64File    = $arm64Match.Groups[1].Value
        $arm64Version = $arm64Match.Groups[2].Value
        $arm64Id      = $arm64Match.Groups[3].Value

        Write-Verbose "[$($MyInvocation.MyCommand.Name)] amd64='$amd64File' arm64='$arm64File'"

        # Parse Last-Modified and Size from the directory listing row for each file
        # Apache autoindex rows: <td align="right">2026-01-20 10:30 </td><td align="right"> 113M</td>
        $amd64Meta   = Get-VMwareListingRow -Html $html -FileName $amd64File
        $arm64Meta   = Get-VMwareListingRow -Html $html -FileName $arm64File

        $baseUri = $searchUri.TrimEnd('/')

        @(
            [PSCustomObject]@{
                Architecture = 'amd64'
            ReadmeUri    = $searchUri
                PackageId    = $amd64Id
                Version      = $amd64Version
                ReleaseDate  = $amd64Meta.ReleaseDate
                FileName     = $amd64File
                FileSizeMB   = $amd64Meta.FileSizeMB
                DownloadUri  = "$baseUri/$amd64File"
                Checksums    = [PSCustomObject]@{}
            },
            [PSCustomObject]@{
                Architecture = 'arm64'
                ReadmeUri    = $searchUri
                PackageId    = $arm64Id
                Version      = $arm64Version
                ReleaseDate  = $arm64Meta.ReleaseDate
                FileName     = $arm64File
                FileSizeMB   = $arm64Meta.FileSizeMB
                DownloadUri  = "$baseUri/$arm64File"
                Checksums    = [PSCustomObject]@{}
            }
        )
    }
    catch {
        Write-Warning "[$(Get-Date -Format s)] $($_.Exception.Message)"
    }
}

function Get-VMwareListingRow {
    <#
    .SYNOPSIS
        Gets metadata from a VMware package listing row
 
    .DESCRIPTION
        Matches an exact VMware ISO file name in an Apache-style HTML table row and parses
        its date and size. Sizes expressed in gigabytes, megabytes, kilobytes, or bytes are
        converted to megabytes. When no exact row matches, ReleaseDate is an empty string
        and FileSizeMB is null.
 
    .PARAMETER Html
        Specifies the HTML package listing to search.
 
    .PARAMETER FileName
        Specifies the exact linked file name whose table row is parsed. The value is escaped
        before it is included in the regular expression.
 
    .EXAMPLE
        PS> Get-VMwareListingRow -Html $response.Content -FileName 'VMware-tools-windows-13.0.10-25056151.iso'
 
        Returns the release date and size in megabytes parsed from the matching ISO row.
 
    .INPUTS
        None. This function does not accept pipeline input.
 
    .OUTPUTS
        System.Management.Automation.PSCustomObject. Returns one object with ReleaseDate and
        FileSizeMB properties. ReleaseDate is empty and FileSizeMB is null when no row
        matches.
 
    .NOTES
        Author: David Segura
        Company: Recast Software
        Version: 1.0.0
        Date: 2026-08-28
 
        Dependencies:
          DotNet Classes: System.Management.Automation.PSCustomObject
    #>

    param ([string]$Html, [string]$FileName)

    $releaseDate = ''
    $fileSizeMB  = $null

    # Match the exact ISO row in the autoindex table; avoid adjacent .sha and .sig rows.
    $rowPattern = '(?is)<tr[^>]*>\s*<td[^>]*>.*?<a href="' + [regex]::Escape($FileName) + '">' + [regex]::Escape($FileName) + '</a></td>\s*<td[^>]*>\s*(?<Date>\d{4}-\d{2}-\d{2})(?:\s+\d{2}:\d{2}Z?)?\s*</td>\s*<td[^>]*>\s*(?<Size>\d+(?:\.\d+)?)\s*(?<Unit>[MGK]B?|B)?\s*</td>'
    $rowMatch = [regex]::Match($Html, $rowPattern)
    if ($rowMatch.Success) {
        $releaseDate = $rowMatch.Groups['Date'].Value

        $rawSize = [double]$rowMatch.Groups['Size'].Value
        $unit    = $rowMatch.Groups['Unit'].Value.ToUpperInvariant()
        $fileSizeMB = switch ($unit) {
            'GB' { [math]::Round($rawSize * 1024, 2) }
            'G'  { [math]::Round($rawSize * 1024, 2) }
            'MB' { [math]::Round($rawSize, 2) }
            'M'  { [math]::Round($rawSize, 2) }
            'KB' { [math]::Round($rawSize / 1024, 2) }
            'K'  { [math]::Round($rawSize / 1024, 2) }
            'B'  { [math]::Round($rawSize / 1MB, 2) }
            default { [math]::Round($rawSize, 2) }
        }
    }

    [PSCustomObject]@{
        ReleaseDate = $releaseDate
        FileSizeMB  = $fileSizeMB
    }
}