PowerShell 解压 PSF 格式的更新包

PowerShell 解压 PSF 格式的更新包
狂犬主子发现 W10UI 中有一段 PowerShell 代码,能够解压 PSF 格式的更新包,支持最新 PA31,通过原生 C# + Win32 方法实现,无需 PSFExtractor.exe。提取出来发现可以直接使用,特此分享。
使用方法
-
准备文件:
Windows11.0-KB5046732-x64.wim、Windows11.0-KB5046732-x64.psf。- 获取方式:
- UUPDUMP 下载
- 解压 MSU 格式更新包得到
- 两个文件名相同,此处KB号仅供参考
- 第一个文件之前为 CAB 格式,现在改成了 WIM 格式
- 获取方式:
-
打开 PowerShell。
-
导入函数:将下方 函数内容 中的代码复制粘贴进去,然后回车。
-
(可选)如果不想看见满屏报错,PowerShell 中执行以下命令:
1
$ErrorActionPreference = 'SilentlyContinue' -
解压
Windows11.0-KB5046732-x64.wim或者 CAB 格式的补丁到Windows11.0-KB5046732-x64文件夹。- 文件夹要与更新包位于同一目录
- 可以直接右键
Windows11.0-KB5046732-x64.wim=>7-Zip=>解压到 "Windows11.0-KB5046732-x64\"进行解压,报错“有效数据外包含额外数据”为正常现象
-
PowerShell 中调用函数
P解压 PSF(建议输入完整路径):1
P "D:\TEMP\Windows11.0-KB5046732-x64.psf" -
通过解压后的文件夹大小判断是否解压正确。
- 正常 1 GB 以上,如果只有几百兆肯定有问题
-
此时可使用 DISM 添加包
Windows11.0-KB5046732-x64\update.mum来更新系统。
特别说明
这个 P 函数的 第二个参数 $DllFile 貌似有坑,具体见这个 issue:
https://github.com/Secant1006/PSFExtractor/issues/4
如果有需要,请手动提取 msdelta.dll 或者 UpdateCompression.dll,然后指定路径。
UpdateCompression.dll 可从 DesktopDeployment.cab 中提取。
然后调用方式如下:
1 | P "D:\TEMP\Windows11.0-KB5046732-x64.psf" "D:\TEMP\UpdateCompression.dll" |
函数内容
方法来自:
- https://www.elevenforum.com/t/a-script-to-extract-wim-and-psf-from-msu-windows-update-files.39262/
- https://forums.mydigitallife.net/posts/1216064
- https://github.com/abbodi1406/BatUtil/blob/master/W10UI/W10UI.cmd
1 | 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) $DELTA_INPUT = $Struct.CreateType() $Struct = $Module.DefineType('DO', 1048841, [ValueType], 0) [void]$Struct.DefineField('lpStart', [IntPtr], 6) [void]$Struct.DefineField('uSize', [UIntPtr], 6) $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) $Win32 = $Class.CreateType() } function ApplyDelta($dBuffer, $dFile) { $trg = [Activator]::CreateInstance($DELTA_OUTPUT) $src = [Activator]::CreateInstance($DELTA_INPUT) $dlt = [Activator]::CreateInstance($DELTA_INPUT) $dlt.lpStart = $Marshal::AllocHGlobal($dBuffer.Length) $dlt.uSize = [Activator]::CreateInstance([UIntPtr], @([UInt32]$dBuffer.Length)) $dlt.Editable = $true $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() $Marshal::Copy($trg.lpStart, $out, 0, $out.Length) [IO.File]::WriteAllBytes($dFile, $out) if ($dlt.lpStart -ne [IntPtr]::Zero) { $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) [bool]$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) } |
功能解释
(以下内容由AI生成)这段PowerShell代码是一个完整的PSF文件(Patch Streaming Format)解包工具,核心功能是从PSF/CAB文件中提取并还原被差分压缩(delta compression)的文件。下面我会分模块、分函数详细解释每一部分的作用和核心逻辑。
整体功能概述
这段代码实现了以下核心流程:
- 解析PSF文件配套的XML配置文件(
express.psf.cix.xml),获取差分文件的元数据(偏移、长度、压缩类型等)。 - 动态调用系统的差分压缩库(
msdelta.dll/UpdateCompression.dll),通过P/Invoke方式封装底层Windows API。 - 从PSF文件中读取差分数据,调用API还原原始文件。
- 处理长路径问题(超过255字符),通过
robocopy完成文件移动,最终输出完整的解包文件。
逐函数详细解释
1. function Native($DllFile) - 动态封装Windows差分压缩API
这是整个工具的核心底层函数,作用是动态创建.NET类型并封装P/Invoke调用,绕开直接声明C#/PowerShell DllImport的限制,调用系统差分压缩库的API。
| 代码段 | 核心作用 |
|---|---|
$Lib = [IO.Path]::GetFileName($DllFile) |
获取DLL文件名(如msdelta.dll),用于后续P/Invoke声明 |
$Module = [AppDomain]::CurrentDomain.DefineDynamicAssembly(...) |
创建动态程序集/模块,作为后续定义类型的容器 |
定义DI结构体(DELTA_INPUT) |
对应Windows DELTA_INPUT结构体,用于传递差分输入数据的指针、大小、是否可编辑 |
定义DO结构体(DELTA_OUTPUT) |
对应Windows DELTA_OUTPUT结构体,用于接收还原后的输出数据指针和大小 |
$Class = $Module.DefineType('PSFE', ...) |
定义动态类PSFE,用于封装P/Invoke方法 |
DefinePInvokeMethod('LoadLibraryW', ...) |
封装kernel32.dll的LoadLibraryW:加载指定的差分压缩DLL |
DefinePInvokeMethod('ApplyDeltaB', ...) |
封装差分库的ApplyDeltaB:核心API,应用差分数据还原原始文件 |
DefinePInvokeMethod('DeltaFree', ...) |
封装差分库的DeltaFree:释放ApplyDeltaB分配的内存 |
$Win32 = $Class.CreateType() |
编译动态类,生成可调用的Win32类型 |
关键说明:
1048841/1048961是.NET类型属性的数值常量,分别代表“结构体+密封”、“类+公共”。[System.Runtime.InteropServices.Marshal]用于内存的分配/拷贝/释放(跨托管/非托管内存)。
2. function ApplyDelta($dBuffer, $dFile) - 应用差分数据还原文件
该函数是ApplyDeltaB API的封装,接收差分数据缓冲区和输出文件路径,完成“差分数据→原始文件”的还原。
核心执行流程:
- 创建
DELTA_INPUT(源/差分)和DELTA_OUTPUT(输出)结构体实例。 - 分配非托管内存,将差分数据缓冲区(
$dBuffer)拷贝到非托管内存,绑定到dlt(差分输入)结构体。 - 调用
Win32::ApplyDeltaB:传入差分数据,还原出原始文件的内存指针和大小。 - 将还原后的内存数据拷贝到托管字节数组,写入目标文件(
$dFile)。 - 释放所有分配的非托管内存(避免内存泄漏)。
关键判断:
if ($trg.lpStart -eq [IntPtr]::Zero) { return }:如果还原失败(输出指针为空),直接退出。
3. function G($DirectoryName) - 解析XML配置文件提取元数据
该函数的作用是读取express.psf.cix.xml(PSF文件的配套配置文件),解析出每个差分文件的关键元数据,并返回有序字典。
核心执行流程:
- 加载XML文件,遍历节点找到
<Files>标签(包含所有文件列表)。 - 对每个文件节点,遍历找到
<Delta>→<Source>子节点(差分数据的元数据)。 - 提取关键信息并存储到
$DeltaList:name:文件路径/名称time:文件最后修改时间stype:差分压缩类型(如PA30/PA31)offset:差分数据在PSF文件中的偏移量length:差分数据的长度
返回值:[ordered] @{} 有序字典,键为文件ID,值为上述元数据。
4. function P($CabFile, $DllFile = 'msdelta.dll') - 主解包函数
这是整个工具的入口函数,接收CAB/PSF文件路径,整合所有子函数完成最终解包。
核心执行流程:
flowchart TD
A[参数初始化] --> B[加载差分压缩DLL]
B --> C[创建解包目录+解析XML元数据]
C --> D[打开PSF文件读取流]
D --> E[遍历每个差分文件]
E --> F[处理长路径问题(UseRobo)]
F --> G[读取PSF中的差分数据]
G --> H{是否PA30/PA31压缩?}
H -- 是 --> I[调用ApplyDelta还原文件]
H -- 否 --> J[直接写入文件]
I --> K[设置文件修改时间]
J --> K
K --> L{是否长路径?}
L -- 是 --> M[调用robocopy移动文件]
L -- 否 --> E
M --> N[关闭流+清理临时目录]
关键细节解释:
-
DLL选择逻辑:
1
if ($DllFile -eq 'msdelta.dll' -and (Test-Path "$env:SystemRoot\System32\UpdateCompression.dll")) { $DllFile = "UpdateCompression.dll" }优先使用
UpdateCompression.dll(新版Windows的差分压缩库),兼容旧版的msdelta.dll。 -
长路径处理(UseRobo):
1
[bool]$UseRobo = (($cwd + '\' + $FullFileName).Length -gt 255) -or (($cwd + '\' + $ShortFold).Length -gt 248)Windows默认路径长度限制为255字符,超过时先将文件写入临时目录
000,再通过robocopy移动到目标路径(robocopy支持长路径)。 -
差分还原判断:
1
if ($DeltaFile.stype -eq "PA30" -or $DeltaFile.stype -eq "PA31") { ApplyDelta $Buffer $WhereFile }仅对PA30/PA31类型的差分数据调用
ApplyDelta还原,其他类型直接写入文件。 -
robocopy参数说明:
/MOV:移动文件(而非复制)/R:1 /W:1:重试1次,每次等待1秒/NS /NC /NFL等:关闭冗余输出,只执行操作
总结
这段代码的核心关键点如下:
- 核心能力:通过动态P/Invoke封装Windows差分压缩API,实现PSF文件的解包和差分数据还原。
- 兼容性:适配
msdelta.dll/UpdateCompression.dll,处理Windows长路径限制(通过robocopy)。 - 内存管理:手动分配/释放非托管内存,避免内存泄漏;解析XML元数据精准定位差分数据位置。
简单来说,这是一个针对Windows系统PSF差分压缩文件的专业解包工具,核心依赖系统自带的差分压缩库,通过PowerShell动态调用底层API完成文件还原。





