Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 129 additions & 1 deletion functions/private/Invoke-WinUtilISOScript.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,121 @@ function Invoke-WinUtilISOScript {
}
}

function Test-WinUtilISODriverExtensionClass {
param ([Parameter(Mandatory)][System.IO.FileInfo]$InfFile)

try {
return (Get-Content -LiteralPath $InfFile.FullName -Raw -ErrorAction Stop) -match '(?im)^\s*Class\s*=\s*Extension\s*(?:;.*)?$'
Comment thread
mewclouds marked this conversation as resolved.
Outdated
} catch {
& $Logger "Warning: could not classify driver '$($InfFile.FullName)': $_"
return $false
}
}

function Get-WinUtilISODriverPackageVersion {
param ([Parameter(Mandatory)][System.IO.FileInfo]$InfFile)

try {
$infText = Get-Content -LiteralPath $InfFile.FullName -Raw -ErrorAction Stop
} catch {
& $Logger "Warning: could not read '$($InfFile.FullName)' to determine its driver version: $_"
return $null
}

$match = [regex]::Match($infText, '(?im)^\s*DriverVer\s*=\s*(?<date>\d{1,2}/\d{1,2}/\d{4})\s*,\s*(?<version>\d+(?:\.\d+){0,3})\s*(?:;.*)?$')
if (-not $match.Success) {
return $null
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

try {
$date = [datetime]::ParseExact($match.Groups['date'].Value, 'M/d/yyyy', [System.Globalization.CultureInfo]::InvariantCulture)
$versionText = $match.Groups['version'].Value
if (($versionText.Split('.')).Count -lt 2) {
$versionText = "$versionText.0"
}
$version = [version]$versionText
} catch {
& $Logger "Warning: could not parse DriverVer '$($match.Value.Trim())' in '$($InfFile.FullName)': $_"
return $null
}

return [pscustomobject]@{
Date = $date
Version = $version
Raw = "$($match.Groups['date'].Value),$($match.Groups['version'].Value)"
}
}

function Select-WinUtilISOStagedDriverPackages {
param (
[Parameter(Mandatory)][AllowEmptyCollection()][object[]]$DriverFolderGroups,
[scriptblock]$Logger
)

$survivingFolders = [System.Collections.Generic.List[string]]::new()
$dedupGroups = @{}

foreach ($driverFolderGroup in $DriverFolderGroups) {
$driverFolder = [string]$driverFolderGroup.Name
$isExtension = [bool]@($driverFolderGroup.Group | Where-Object { Test-WinUtilISODriverExtensionClass -InfFile $_ }).Count
Comment thread
mewclouds marked this conversation as resolved.

if ($isExtension) {
& $Logger "Excluding extension-class driver package '$driverFolder' from Add-Driver (Class=Extension is not a serviceable hardware driver)."
continue
Comment thread
mewclouds marked this conversation as resolved.
}

# DISM names exported package folders <infname>_<arch>_<hash>; grouping on infname+arch
# (dropping the hash) is what lets us recognize two exports of the same driver.
$leafName = Split-Path -Path $driverFolder -Leaf
$dedupKey = $leafName
$nameMatch = [regex]::Match($leafName, '(?i)^(?<infname>.+)_(?<arch>x86|amd64|arm64|arm|wow)_[0-9a-f]{16}$')
if ($nameMatch.Success) {
$dedupKey = "$($nameMatch.Groups['infname'].Value.ToLowerInvariant())_$($nameMatch.Groups['arch'].Value.ToLowerInvariant())"
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Comment thread
mewclouds marked this conversation as resolved.

if (-not $dedupGroups.ContainsKey($dedupKey)) {
$dedupGroups[$dedupKey] = [System.Collections.Generic.List[object]]::new()
}
$dedupGroups[$dedupKey].Add($driverFolderGroup)
}

foreach ($dedupKey in $dedupGroups.Keys) {
$candidates = $dedupGroups[$dedupKey]
if ($candidates.Count -eq 1) {
$survivingFolders.Add([string]$candidates[0].Name)
continue
}

$ranked = @($candidates | ForEach-Object {
$primaryVersion = ($_.Group | ForEach-Object { Get-WinUtilISODriverPackageVersion -InfFile $_ } | Where-Object { $_ }) |
Sort-Object -Property Date, Version -Descending | Select-Object -First 1
[pscustomobject]@{ Folder = [string]$_.Name; Version = $primaryVersion }
})

$withVersion = @($ranked | Where-Object { $_.Version })
if ($withVersion.Count -eq 0) {
& $Logger "Warning: could not determine DriverVer for any duplicate of '$dedupKey'; keeping all $($ranked.Count) package(s) rather than guessing."
foreach ($candidate in $ranked) {
$survivingFolders.Add($candidate.Folder)
}
continue
}

$kept = $withVersion | Sort-Object -Property @{ Expression = { $_.Version.Date } }, @{ Expression = { $_.Version.Version } } -Descending | Select-Object -First 1
$survivingFolders.Add($kept.Folder)

foreach ($candidate in $ranked) {
if ($candidate.Folder -eq $kept.Folder) {
continue
}
$droppedVersion = if ($candidate.Version) { $candidate.Version.Raw } else { 'unknown' }
& $Logger "Excluding stale duplicate driver package '$($candidate.Folder)' (DriverVer $droppedVersion) superseded by '$($kept.Folder)' (DriverVer $($kept.Version.Raw))."
}
}

return @($survivingFolders)
}

function Invoke-WinUtilISODism {
param (
[Parameter(Mandatory)][string[]]$Arguments,
Expand Down Expand Up @@ -192,7 +307,20 @@ function Invoke-WinUtilISOScript {
throw "Failed to stage $copyFailures boot-storage driver package folders."
}

& $Logger "Exported $($driverInfs.Count) driver INF files across $($driverFolders.Count) package folders; staged $storageCount boot-storage packages for WinPE."
$stagedDriverFolders = @(Select-WinUtilISOStagedDriverPackages -DriverFolderGroups $driverFolders -Logger $Logger)
Comment thread
mewclouds marked this conversation as resolved.
if ($stagedDriverFolders.Count -eq 0) {
throw 'All exported driver packages were excluded (Extension class or stale duplicates); nothing left to inject.'
Comment thread
mewclouds marked this conversation as resolved.
Outdated
}
$excludedFolders = @($driverFolders.Name | Where-Object { $_ -notin $stagedDriverFolders })
foreach ($excludedFolder in $excludedFolders) {
try {
Remove-Item -LiteralPath $excludedFolder -Recurse -Force -ErrorAction Stop
} catch {
throw "Failed to remove excluded driver package '$excludedFolder' before injection: $_"
}
}

& $Logger "Exported $($stagedDriverFolders.Count) of $($driverFolders.Count) driver packages ($storageCount staged for WinPE, $($excludedFolders.Count) excluded)."
$metadataBefore = Get-WinUtilISOWimMetadata -ImagePath $InstallImagePath -Index $InstallImageIndex
Assert-WinUtilISOWimMetadata -Before $metadataBefore

Expand Down
160 changes: 157 additions & 3 deletions pester/win11creator.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -357,13 +357,19 @@ Describe "Win11 Creator setup media" {
@{ Path = 'scsi_pkg'; Name = 'controller.inf'; Class = 'SCSIAdapter' },
@{ Path = 'net_pkg'; Name = 'network.inf'; Class = 'Net' },
@{ Path = 'group_a\duplicate'; Name = 'audio.inf'; Class = 'Media' },
@{ Path = 'group_b\duplicate'; Name = 'extension.inf'; Class = 'Extension' }
@{ Path = 'hdx_asusext_apot_g5-tse.inf_amd64_aabbccddeeff0011'; Name = 'hdx_asusext_apot_g5-tse.inf'; Class = 'Extension' },
@{ Path = 'ntprint.inf_x86_7426e1b60aa62272'; Name = 'ntprint.inf'; Class = 'Printer'; DriverVer = '1/1/2023,10.0.26100.8875' },
@{ Path = 'ntprint.inf_x86_58e7118cdecb935e'; Name = 'ntprint.inf'; Class = 'Printer'; DriverVer = '6/1/2024,10.0.26100.9168' }
)

foreach ($fixture in $fixtures) {
$fixturePath = Join-Path $exportRoot $fixture.Path
New-Item -Path $fixturePath -ItemType Directory -Force | Out-Null
Set-Content -Path (Join-Path $fixturePath $fixture.Name) -Value "[Version]`r`nClass=$($fixture.Class)" -Encoding ASCII
$infContent = "[Version]`r`nClass=$($fixture.Class)"
if ($fixture.DriverVer) {
$infContent += "`r`nDriverVer=$($fixture.DriverVer)"
}
Set-Content -Path (Join-Path $fixturePath $fixture.Name) -Value $infContent -Encoding ASCII
}

return [pscustomobject]@{ ExitCode = 0 }
Expand Down Expand Up @@ -395,7 +401,9 @@ Describe "Win11 Creator setup media" {
$nsMgr = New-Object System.Xml.XmlNamespaceManager($answerFile.NameTable)
$nsMgr.AddNamespace('sg', 'https://schneegans.de/windows/unattend-generator/')
$answerFile.SelectSingleNode('//sg:File[@path="C:\Windows\Setup\Scripts\WinUtil-InstallDrivers.ps1"]', $nsMgr) | Should -BeNullOrEmpty
($logs -join '|') | Should -Match 'staged 2 boot-storage packages for WinPE'
($logs -join '|') | Should -Match 'Exported 6 of 8 driver packages \(2 staged for WinPE, 2 excluded\)'
($logs -join '|') | Should -Match "Excluding extension-class driver package '.*hdx_asusext_apot_g5-tse.*'"
($logs -join '|') | Should -Match "Excluding stale duplicate driver package '.*ntprint\.inf_x86_7426e1b60aa62272' \(DriverVer 1/1/2023,10\.0\.26100\.8875\) superseded by '.*ntprint\.inf_x86_58e7118cdecb935e' \(DriverVer 6/1/2024,10\.0\.26100\.9168\)"
($logs -join '|') | Should -Match 'install.wim metadata validation passed'
($logs -join '|') | Should -Match 'DISM mount completed.'
($logs -join '|') | Should -Not -Match '100.0%'
Expand All @@ -405,6 +413,152 @@ Describe "Win11 Creator setup media" {
}
}

It "excludes Class=Extension driver packages from Add-Driver regardless of vendor or case" {
$contentRoot = Join-Path ([IO.Path]::GetTempPath()) "WinUtilIsoExtensionExclude_$([guid]::NewGuid())"
$installWim = Join-Path $contentRoot 'sources\install.wim'
$template = Get-Content -Path $script:autoUnattendPath -Raw
$logs = [System.Collections.Generic.List[string]]::new()
$script:dismCalls = [System.Collections.Generic.List[string]]::new()

function dism.exe {
param([Parameter(ValueFromRemainingArguments)][string[]]$Arguments)

$script:dismCalls.Add(($Arguments -join '|'))
$global:LASTEXITCODE = 0
if ($Arguments -contains '/Get-WimInfo') {
'Languages : en-US'
'Installation : Client'
'Edition : Professional'
'ProductSuite : Terminal Server'
'ProductType : WinNT'
} elseif ($Arguments -contains '/Mount-Image') {
'[==========================100.0%==========================]'
}
}

Mock Start-Process {
param($FilePath, $ArgumentList)

if ($FilePath -ne 'dism.exe') {
throw "Unexpected process in driver export mock: $FilePath"
}

$destinationMatch = [regex]::Match([string]$ArgumentList, '/destination:"([^"]+)"')
$exportRoot = $destinationMatch.Groups[1].Value
$fixtures = @(
@{ Path = 'net_pkg'; Name = 'network.inf'; Class = 'Net' },
@{ Path = 'ext_pkg_lower'; Name = 'lowercase_extension.inf'; Class = 'extension' }
)

foreach ($fixture in $fixtures) {
$fixturePath = Join-Path $exportRoot $fixture.Path
New-Item -Path $fixturePath -ItemType Directory -Force | Out-Null
Set-Content -Path (Join-Path $fixturePath $fixture.Name) -Value "[Version]`r`nClass=$($fixture.Class)" -Encoding ASCII
}

return [pscustomobject]@{ ExitCode = 0 }
} -ParameterFilter { $FilePath -eq 'dism.exe' }

try {
New-Item -Path (Split-Path $installWim -Parent) -ItemType Directory -Force | Out-Null
Set-Content -Path $installWim -Value 'mock-wim'
. $script:isoScriptPath
Invoke-WinUtilISOScript -ISOContentsDir $contentRoot -AutoUnattendXml $template -InjectCurrentSystemDrivers $true -InstallImagePath $installWim -InstallImageIndex 6 -InstallEditionId 'Professional' -Log {
param($message)
$logs.Add([string]$message)
}

@($script:dismCalls | Where-Object { $_ -match '/Add-Driver' }).Count | Should -Be 1
($logs -join '|') | Should -Match 'Exported 1 of 2 driver packages \(0 staged for WinPE, 1 excluded\)'
($logs -join '|') | Should -Match "Excluding extension-class driver package '.*ext_pkg_lower'"
($logs -join '|') | Should -Not -Match "Excluding extension-class driver package '.*net_pkg'"
} finally {
Remove-Item Function:\dism.exe -ErrorAction SilentlyContinue
Remove-Item -Path $contentRoot -Recurse -Force -ErrorAction SilentlyContinue
}
}

It "drops stale duplicate driver versions and keeps only the highest DriverVer" {
$contentRoot = Join-Path ([IO.Path]::GetTempPath()) "WinUtilIsoStaleDedup_$([guid]::NewGuid())"
$installWim = Join-Path $contentRoot 'sources\install.wim'
$template = Get-Content -Path $script:autoUnattendPath -Raw
$logs = [System.Collections.Generic.List[string]]::new()
$script:dismCalls = [System.Collections.Generic.List[string]]::new()

function dism.exe {
param([Parameter(ValueFromRemainingArguments)][string[]]$Arguments)

$script:dismCalls.Add(($Arguments -join '|'))
$global:LASTEXITCODE = 0
if ($Arguments -contains '/Get-WimInfo') {
'Languages : en-US'
'Installation : Client'
'Edition : Professional'
'ProductSuite : Terminal Server'
'ProductType : WinNT'
} elseif ($Arguments -contains '/Mount-Image') {
'[==========================100.0%==========================]'
}
}

Mock Start-Process {
param($FilePath, $ArgumentList)

if ($FilePath -ne 'dism.exe') {
throw "Unexpected process in driver export mock: $FilePath"
}

$destinationMatch = [regex]::Match([string]$ArgumentList, '/destination:"([^"]+)"')
$exportRoot = $destinationMatch.Groups[1].Value
$fixtures = @(
# Three-way duplicate mirroring the real ntprint.inf report: only the newest DriverVer should survive.
@{ Path = 'ntprint.inf_x86_7426e1b60aa62272'; Name = 'ntprint.inf'; Class = 'Printer'; DriverVer = '1/1/2023,10.0.26100.8875' },
@{ Path = 'ntprint.inf_x86_6688e7b66f8d9fb5'; Name = 'ntprint.inf'; Class = 'Printer'; DriverVer = '1/1/2024,10.0.26100.8972' },
@{ Path = 'ntprint.inf_x86_58e7118cdecb935e'; Name = 'ntprint.inf'; Class = 'Printer'; DriverVer = '6/1/2024,10.0.26100.9168' },
# A duplicate pair where one package is missing DriverVer entirely: the parseable one must win.
@{ Path = 'sample.inf_amd64_11111111aaaaaaaa'; Name = 'sample.inf'; Class = 'Net' },
@{ Path = 'sample.inf_amd64_22222222bbbbbbbb'; Name = 'sample.inf'; Class = 'Net'; DriverVer = '3/1/2024,1.2.3.4' },
# A duplicate pair keyed entirely on case-insensitive DriverVer parsing: the uppercase
# DRIVERVER on the newer package must still be read and win the comparison.
@{ Path = 'caps.inf_amd64_33333333cccccccc'; Name = 'caps.inf'; Class = 'Net'; DriverVer = '1/1/2020,1.0.0.0'; VersionKeyword = 'driverver' },
@{ Path = 'caps.inf_amd64_44444444dddddddd'; Name = 'caps.inf'; Class = 'Net'; DriverVer = '1/1/2021,2.0.0.0'; VersionKeyword = 'DRIVERVER' }
)

foreach ($fixture in $fixtures) {
$fixturePath = Join-Path $exportRoot $fixture.Path
New-Item -Path $fixturePath -ItemType Directory -Force | Out-Null
$infContent = "[Version]`r`nClass=$($fixture.Class)"
if ($fixture.DriverVer) {
$versionKeyword = if ($fixture.VersionKeyword) { $fixture.VersionKeyword } else { 'DriverVer' }
$infContent += "`r`n$versionKeyword=$($fixture.DriverVer)"
}
Set-Content -Path (Join-Path $fixturePath $fixture.Name) -Value $infContent -Encoding ASCII
}

return [pscustomobject]@{ ExitCode = 0 }
} -ParameterFilter { $FilePath -eq 'dism.exe' }

try {
New-Item -Path (Split-Path $installWim -Parent) -ItemType Directory -Force | Out-Null
Set-Content -Path $installWim -Value 'mock-wim'
. $script:isoScriptPath
Invoke-WinUtilISOScript -ISOContentsDir $contentRoot -AutoUnattendXml $template -InjectCurrentSystemDrivers $true -InstallImagePath $installWim -InstallImageIndex 6 -InstallEditionId 'Professional' -Log {
param($message)
$logs.Add([string]$message)
}

@($script:dismCalls | Where-Object { $_ -match '/Add-Driver' }).Count | Should -Be 1
($logs -join '|') | Should -Match 'Exported 3 of 7 driver packages \(0 staged for WinPE, 4 excluded\)'
($logs -join '|') | Should -Match "Excluding stale duplicate driver package '.*ntprint\.inf_x86_7426e1b60aa62272' \(DriverVer 1/1/2023,10\.0\.26100\.8875\) superseded by '.*ntprint\.inf_x86_58e7118cdecb935e'"
($logs -join '|') | Should -Match "Excluding stale duplicate driver package '.*ntprint\.inf_x86_6688e7b66f8d9fb5' \(DriverVer 1/1/2024,10\.0\.26100\.8972\) superseded by '.*ntprint\.inf_x86_58e7118cdecb935e'"
($logs -join '|') | Should -Match "Excluding stale duplicate driver package '.*sample\.inf_amd64_11111111aaaaaaaa' \(DriverVer unknown\) superseded by '.*sample\.inf_amd64_22222222bbbbbbbb' \(DriverVer 3/1/2024,1\.2\.3\.4\)"
($logs -join '|') | Should -Match "Excluding stale duplicate driver package '.*caps\.inf_amd64_33333333cccccccc' \(DriverVer 1/1/2020,1\.0\.0\.0\) superseded by '.*caps\.inf_amd64_44444444dddddddd' \(DriverVer 1/1/2021,2\.0\.0\.0\)"
} finally {
Remove-Item Function:\dism.exe -ErrorAction SilentlyContinue
Remove-Item -Path $contentRoot -Recurse -Force -ErrorAction SilentlyContinue
}
}

It "discards a partially mounted install.wim after mount failure" {
$contentRoot = Join-Path ([IO.Path]::GetTempPath()) "WinUtilIsoMountFailure_$([guid]::NewGuid())"
$installWim = Join-Path $contentRoot 'sources\install.wim'
Expand Down