I want to share a script I created to extract WIM and PSF files from MSU Windows Update files. You might find it useful, as there are no existing tools that can extract PSF files!
This script is designed for simplicity. To use it, you will need to have 7-Zip installed. The default installation path is C:\Program Files\7-Zip\7z.exe, but you can easily change this in the script if your 7-Zip is located elsewhere (line 24).
How to Use the Script
Extract the MSU file to any folder of your choice.
Place the script inside the same folder. (I will not explain how to run a script in detail here; please refer to my previous thread Reduce the size of the uupdump ISO by approximately 2GB)
Run the script. It will automatically extract the WIM and PSF files into the folder.
After extraction, the script will delete the original WIM and PSF files.
It will also automatically install the SSU and prompt you to install the new KB.
I created this script to extract update files once and then install them quickly and easily in my VMs. Others may find different uses for it.
I want to clarify that I take no credit for the PSF extraction method, as I adapted that part from a script by uupdump. My goal was to make it more user-friendly and accessible for everyone.
I hope this script helps you as much as it has helped me! Feel free to ask any questions or provide feedback.
This script is designed for simplicity. To use it, you will need to have 7-Zip installed. The default installation path is C:\Program Files\7-Zip\7z.exe, but you can easily change this in the script if your 7-Zip is located elsewhere (line 24).
How to Use the Script
Extract the MSU file to any folder of your choice.
Place the script inside the same folder. (I will not explain how to run a script in detail here; please refer to my previous thread Reduce the size of the uupdump ISO by approximately 2GB)
Run the script. It will automatically extract the WIM and PSF files into the folder.
After extraction, the script will delete the original WIM and PSF files.
It will also automatically install the SSU and prompt you to install the new KB.
I created this script to extract update files once and then install them quickly and easily in my VMs. Others may find different uses for it.
I want to clarify that I take no credit for the PSF extraction method, as I adapted that part from a script by uupdump. My goal was to make it more user-friendly and accessible for everyone.
I hope this script helps you as much as it has helped me! Feel free to ask any questions or provide feedback.
Powershell:
# =========================
# PSFX Unified Extractor
# =========================
$Base = $PSScriptRoot
Write-Host "Base folder: $Base"
# Detect WIM + PSF
$WimFile = Get-ChildItem $Base -Filter *.wim | Select-Object -First 1
$PsfFile = Get-ChildItem $Base -Filter *.psf | Select-Object -First 1
if (-not $WimFile -or -not $PsfFile) {
Write-Error "Could not find both .wim and .psf files in $Base"
exit 1
}
Write-Host "Found WIM: $($WimFile.Name)"
Write-Host "Found PSF: $($PsfFile.Name)"
# Create subfolder for extracted update (same name as WIM without extension)
$SubFolder = Join-Path $Base ($WimFile.BaseName)
if (-not (Test-Path $SubFolder)) {
New-Item -ItemType Directory -Path $SubFolder | Out-Null
}
# --- Extract WIM with 7-Zip ---
$SevenZip = "C:\Program Files\7-Zip\7z.exe"
Write-Host "Extracting WIM into $SubFolder ..."
Start-Process -FilePath $SevenZip -ArgumentList "x `"$($WimFile.FullName)`" -o`"$SubFolder`" -y" -Wait -NoNewWindow
Remove-Item $WimFile.FullName -Force
# =========================
# Inline PSFX functions (from psfx.txt)
# =========================
function Native($DllFile){
$Lib = [IO.Path]::GetFileName($DllFile)
$Marshal = [System.Runtime.InteropServices.Marshal]
$Module = [AppDomain]::CurrentDomain.DefineDynamicAssembly((Get-Random), 'Run').DefineDynamicModule((Get-Random))
$Struct = $Module.DefineType('DI', 1048841, [ValueType], 0)
[void]$Struct.DefineField('lpStart', [IntPtr], 6)
[void]$Struct.DefineField('uSize', [UIntPtr], 6)
[void]$Struct.DefineField('Editable', [Boolean], 6)
$GLOBAL:DELTA_INPUT = $Struct.CreateType()
$Struct = $Module.DefineType('DO', 1048841, [ValueType], 0)
[void]$Struct.DefineField('lpStart', [IntPtr], 6)
[void]$Struct.DefineField('uSize', [UIntPtr], 6)
$GLOBAL:DELTA_OUTPUT = $Struct.CreateType()
$Class = $Module.DefineType('PSFE', 1048961, [Object], 0)
[void]$Class.DefinePInvokeMethod('LoadLibraryW','kernel32.dll',22,1,[IntPtr],@([String]),1,3).SetImplementationFlags(128)
[void]$Class.DefinePInvokeMethod('ApplyDeltaB',$Lib,22,1,[Int32],@([Int64],[Type]$DELTA_INPUT,[Type]$DELTA_INPUT,[Type]$DELTA_OUTPUT.MakeByRefType()),1,3)
[void]$Class.DefinePInvokeMethod('DeltaFree',$Lib,22,1,[Int32],@([IntPtr]),1,3)
$GLOBAL:Win32 = $Class.CreateType()
}
function ApplyDelta($dBuffer,$dFile){
$trg = [Activator]::CreateInstance($DELTA_OUTPUT)
$src = [Activator]::CreateInstance($DELTA_INPUT)
$dlt = [Activator]::CreateInstance($DELTA_INPUT)
$dlt.lpStart = [System.Runtime.InteropServices.Marshal]::AllocHGlobal($dBuffer.Length)
$dlt.uSize = [Activator]::CreateInstance([UIntPtr], @([UInt32]$dBuffer.Length))
$dlt.Editable = $true
[System.Runtime.InteropServices.Marshal]::Copy($dBuffer,0,$dlt.lpStart,$dBuffer.Length)
[void]$Win32::ApplyDeltaB(0,$src,$dlt,[ref]$trg)
if ($trg.lpStart -eq [IntPtr]::Zero){ return }
$out = New-Object byte[] $trg.uSize.ToUInt32()
[System.Runtime.InteropServices.Marshal]::Copy($trg.lpStart,$out,0,$out.Length)
[IO.File]::WriteAllBytes($dFile,$out)
if ($dlt.lpStart -ne [IntPtr]::Zero){ [System.Runtime.InteropServices.Marshal]::FreeHGlobal($dlt.lpStart) }
if ($trg.lpStart -ne [IntPtr]::Zero){ [void]$Win32::DeltaFree($trg.lpStart) }
}
function G($DirectoryName){
$DeltaList = [ordered]@{}
$doc = New-Object xml
$doc.Load($DirectoryName + "\express.psf.cix.xml")
$child = $doc.FirstChild.NextSibling.FirstChild
while(!$child.LocalName.Equals("Files")){ $child = $child.NextSibling }
$FileList = $child.ChildNodes
foreach($file in $FileList){
$fileChild = $file.FirstChild
while(!$fileChild.LocalName.Equals("Delta")){ $fileChild = $fileChild.NextSibling }
$deltaChild = $fileChild.FirstChild
while(!$deltaChild.LocalName.Equals("Source")){ $deltaChild = $deltaChild.NextSibling }
$DeltaList[$($file.id)] = @{
name=$file.name;
time=$file.time;
stype=$deltaChild.type;
offset=$deltaChild.offset;
length=$deltaChild.length
}
}
return $DeltaList
}
function P($CabFile,$DllFile='msdelta.dll'){
if ($DllFile -eq 'msdelta.dll' -and (Test-Path "$env:SystemRoot\System32\UpdateCompression.dll")){
$DllFile = "$env:SystemRoot\System32\UpdateCompression.dll"
}
Native $DllFile
[void]$Win32::LoadLibraryW($DllFile)
$DirectoryName = $CabFile.Substring(0,$CabFile.LastIndexOf('.'))
$PSFFile = $DirectoryName + ".psf"
$null = [IO.Directory]::CreateDirectory($DirectoryName)
$DeltaList = G $DirectoryName
$PSFFileStream = [IO.File]::OpenRead([IO.Path]::GetFullPath($PSFFile))
$cwd = [IO.Path]::GetFullPath($DirectoryName)
[Environment]::CurrentDirectory = $cwd
$null = [IO.Directory]::CreateDirectory("000")
foreach($DeltaFile in $DeltaList.Values){
$FullFileName = $DeltaFile.name
if (Test-Path $FullFileName){ continue }
$ShortFold = [IO.Path]::GetDirectoryName($FullFileName)
$ShortFile = [IO.Path]::GetFileName($FullFileName)
$UseRobo = (($cwd + '\' + $FullFileName).Length -gt 255) -or (($cwd + '\' + $ShortFold).Length -gt 248)
if ($UseRobo -eq 0 -and $ShortFold.IndexOf("_") -ne -1){ $null = [IO.Directory]::CreateDirectory($ShortFold) }
if ($UseRobo -eq 0){ $WhereFile = $FullFileName } else { $WhereFile = "000\" + $ShortFile }
try { [void]$PSFFileStream.Seek($DeltaFile.offset,0) } catch {}
$Buffer = New-Object byte[] $DeltaFile.length
try { [void]$PSFFileStream.Read($Buffer,0,$DeltaFile.length) } catch {}
$OutputFileStream = [IO.File]::Create($WhereFile)
try { [void]$OutputFileStream.Write($Buffer,0,$DeltaFile.length) } catch {}
[void]$OutputFileStream.Close()
if ($DeltaFile.stype -eq "PA30" -or $DeltaFile.stype -eq "PA31"){ ApplyDelta $Buffer $WhereFile }
$null = [IO.File]::SetLastWriteTimeUtc($WhereFile,[DateTime]::FromFileTimeUtc($DeltaFile.time))
if ($UseRobo -eq 0){ continue }
Start-Process robocopy.exe -NoNewWindow -Wait -ArgumentList ('"' + $cwd + '\000' + '"' + ' ' + '"' + $cwd + '\' + $ShortFold + '"' + ' ' + $ShortFile + ' /MOV /R:1 /W:1 /NS /NC /NFL /NDL /NP /NJH /NJS')
}
[void]$PSFFileStream.Close()
$null = [IO.Directory]::Delete("000",$True)
}
# =========================
# Run patch
# =========================
Write-Host "Applying PSF patch into $SubFolder ..."
P $WimFile.FullName "msdelta.dll"
Remove-Item $PsfFile.FullName -Force
# =========================
# Prompt to Install
# =========================
# --- Install SSU first if present ---
$SSU = Get-ChildItem $Base -Filter *SSU*.cab | Select-Object -First 1
if ($SSU) {
Write-Host "Installing Servicing Stack Update: $($SSU.Name)"
Start-Process dism.exe -ArgumentList "/online /add-package /packagepath:`"$($SSU.FullName)`"" -Wait -NoNewWindow
Write-Host "SSU installation complete."
} else {
Write-Host "No SSU CAB found in $Base, skipping SSU install."
}
# --- Prompt to Install LCU ---
$answer = Read-Host "Do you want to install the cumulative update now? (Y/N)"
if ($answer -match '^[Yy]$') {
Write-Host "Installing LCU via DISM..."
Start-Process dism.exe -ArgumentList "/online /add-package /packagepath:`"$SubFolder`"" -Wait -NoNewWindow
} else {
Write-Host "Update prepared in $SubFolder, you can install later with DISM."
}
My Computer
At a glance
Windows 11 vmwareI9 13950hx128GBNVIDIA 4090
- OS
- Windows 11 vmware
- Computer type
- Laptop
- Manufacturer/Model
- Dell
- CPU
- I9 13950hx
- Memory
- 128GB
- Graphics Card(s)
- NVIDIA 4090
- Sound Card
- Realtek




