Private/Core/CatalogParser.ps1

function Expand-DATCabinet {
    <#
    .SYNOPSIS
        Expands a .cab file to a destination directory.
    .PARAMETER CabPath
        Path to the .cab file.
    .PARAMETER DestinationPath
        Directory to extract contents to.
    .PARAMETER Filter
        Optional file filter for extraction (e.g., '*.xml').
    .OUTPUTS
        Returns the list of extracted file paths.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$CabPath,

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

        [string]$Filter = '*'
    )

    if (-not (Test-Path $CabPath)) {
        throw "Cabinet file not found: $CabPath"
    }

    # Validate the cab file isn't suspiciously small (likely an HTML error page)
    $CabSize = (Get-Item $CabPath).Length
    if ($CabSize -lt 1024) {
        throw "Cabinet file is only $CabSize bytes - likely an invalid download (HTML error page). Delete cache and retry."
    }

    if (-not (Test-Path $DestinationPath)) {
        New-Item -Path $DestinationPath -ItemType Directory -Force | Out-Null
    }

    Write-DATLog -Message "Expanding cabinet: $(Split-Path $CabPath -Leaf) ($([math]::Round($CabSize / 1KB)) KB) to $DestinationPath" -Severity 1

    # Snapshot existing files before extraction so we can diff after
    $BeforeFiles = @(Get-ChildItem -Path $DestinationPath -File -ErrorAction SilentlyContinue |
        Select-Object -ExpandProperty FullName)

    try {
        # Use expand.exe with proper argument quoting
        # -F:filter selects which files to extract, -R renames/replaces existing files
        $ExpandExe = Join-Path $env:SystemRoot 'System32\expand.exe'
        $FilterArg = "-F:$Filter"
        $Output = & $ExpandExe "$CabPath" $FilterArg "$DestinationPath" -R 2>&1

        if ($LASTEXITCODE -ne 0) {
            throw "expand.exe failed with exit code $LASTEXITCODE`: $Output"
        }

        # Find newly extracted files by comparing directory before/after
        $AfterFiles = @(Get-ChildItem -Path $DestinationPath -File -ErrorAction SilentlyContinue |
            Select-Object -ExpandProperty FullName)
        $ExtractedFiles = @($AfterFiles | Where-Object { $_ -notin $BeforeFiles })

        Write-DATLog -Message "Extracted $($ExtractedFiles.Count) file(s) from cabinet" -Severity 1

        if ($ExtractedFiles.Count -eq 0) {
            # Log expand.exe output for diagnostics
            Write-DATLog -Message "expand.exe output: $($Output -join ' ')" -Severity 2
        }

        return $ExtractedFiles
    } catch {
        Write-DATLog -Message "Failed to expand cabinet $CabPath`: $($_.Exception.Message)" -Severity 3
        throw
    }
}

function Read-DATXml {
    <#
    .SYNOPSIS
        Safely loads an XML file with error handling and encoding detection.
    .PARAMETER Path
        Path to the XML file.
    .OUTPUTS
        Returns the parsed [xml] object.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$Path
    )

    if (-not (Test-Path $Path)) {
        throw "XML file not found: $Path"
    }

    try {
        $Content = Get-Content -Path $Path -Raw -Encoding UTF8
        [xml]$Xml = $Content
        return $Xml
    } catch {
        # Try default encoding as fallback
        try {
            [xml]$Xml = Get-Content -Path $Path -Raw
            return $Xml
        } catch {
            Write-DATLog -Message "Failed to parse XML file $Path`: $($_.Exception.Message)" -Severity 3
            throw
        }
    }
}

function Get-DATTempPath {
    <#
    .SYNOPSIS
        Returns a unique temporary directory for the current operation.
    .PARAMETER Prefix
        Prefix for the temp directory name.
    #>

    [CmdletBinding()]
    param(
        [string]$Prefix = 'DAT'
    )

    $TempBase = Join-Path $env:TEMP 'DriverAutomationTool'
    if (-not (Test-Path $TempBase)) {
        New-Item -Path $TempBase -ItemType Directory -Force | Out-Null
    }

    $TempDir = Join-Path $TempBase ('{0}_{1}' -f $Prefix, [guid]::NewGuid().ToString('N').Substring(0, 8))
    New-Item -Path $TempDir -ItemType Directory -Force | Out-Null

    return $TempDir
}

function Remove-DATTempPath {
    <#
    .SYNOPSIS
        Removes a temporary directory created by Get-DATTempPath.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$Path
    )

    if ((Test-Path $Path) -and $Path -like "*DriverAutomationTool*") {
        Remove-Item -Path $Path -Recurse -Force -ErrorAction SilentlyContinue
    }
}

function ConvertTo-DATStandardModel {
    <#
    .SYNOPSIS
        Normalizes a model name for consistent matching and package naming.
    .DESCRIPTION
        Removes common suffixes, trims whitespace, and standardizes capitalization.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$ModelName
    )

    $Normalized = $ModelName.Trim()

    # Remove common noise suffixes
    $RemovePatterns = @(
        '\s+AIO$'
        '\s+All-In-One$'
        '\s+Desktop$'
        '\s+Notebook$'
        '\s+Laptop$'
        '\s+Tower$'
        '\s+SFF$'
        '\s+Small Form Factor$'
        '\s+Micro$'
        '\s+Mini$'
    )

    foreach ($Pattern in $RemovePatterns) {
        $Normalized = $Normalized -replace $Pattern, ''
    }

    # Collapse multiple spaces
    $Normalized = $Normalized -replace '\s+', ' '

    return $Normalized.Trim()
}