PowerShell 解压 PSF 格式的更新包

发现 W10UI 中有一段 PowerShell 代码,能够解压 PSF 格式的更新包,支持最新 PA31,通过原生 C# + Win32 方法实现,无需 PSFExtractor.exe。提取出来发现可以直接使用,特此分享。

使用方法

  1. 准备文件:Windows11.0-KB5046732-x64.wimWindows11.0-KB5046732-x64.psf

    • 获取方式:
      • UUPDUMP 下载
      • 解压 MSU 格式更新包得到
    • 两个文件名相同,此处KB号仅供参考
    • 第一个文件之前为 CAB 格式,现在改成了 WIM 格式
  2. 打开 PowerShell。

  3. 导入函数:将下方 函数内容 中的代码复制粘贴进去,然后回车。

  4. (可选)如果不想看见满屏报错,PowerShell 中执行以下命令:

    1
    $ErrorActionPreference = 'SilentlyContinue'
  5. 解压 Windows11.0-KB5046732-x64.wim 或者 CAB 格式的补丁到 Windows11.0-KB5046732-x64 文件夹。

    • 文件夹要与更新包位于同一目录
    • 可以直接右键 Windows11.0-KB5046732-x64.wim => 7-Zip=> 解压到 "Windows11.0-KB5046732-x64\" 进行解压,报错“有效数据外包含额外数据”为正常现象
  6. PowerShell 中调用函数 P 解压 PSF(建议输入完整路径):

    1
    P "D:\TEMP\Windows11.0-KB5046732-x64.psf"
  7. 通过解压后的文件夹大小判断是否解压正确。

    • 正常 1 GB 以上,如果只有几百兆肯定有问题
  8. 此时可使用 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"

函数内容

方法来自:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
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)的文件。下面我会分模块、分函数详细解释每一部分的作用和核心逻辑。

整体功能概述

这段代码实现了以下核心流程:

  1. 解析PSF文件配套的XML配置文件(express.psf.cix.xml),获取差分文件的元数据(偏移、长度、压缩类型等)。
  2. 动态调用系统的差分压缩库(msdelta.dll/UpdateCompression.dll),通过P/Invoke方式封装底层Windows API。
  3. 从PSF文件中读取差分数据,调用API还原原始文件。
  4. 处理长路径问题(超过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.dllLoadLibraryW:加载指定的差分压缩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的封装,接收差分数据缓冲区和输出文件路径,完成“差分数据→原始文件”的还原。

核心执行流程

  1. 创建DELTA_INPUT(源/差分)和DELTA_OUTPUT(输出)结构体实例。
  2. 分配非托管内存,将差分数据缓冲区($dBuffer)拷贝到非托管内存,绑定到dlt(差分输入)结构体。
  3. 调用Win32::ApplyDeltaB:传入差分数据,还原出原始文件的内存指针和大小。
  4. 将还原后的内存数据拷贝到托管字节数组,写入目标文件($dFile)。
  5. 释放所有分配的非托管内存(避免内存泄漏)。

关键判断

  • if ($trg.lpStart -eq [IntPtr]::Zero) { return }:如果还原失败(输出指针为空),直接退出。

3. function G($DirectoryName) - 解析XML配置文件提取元数据

该函数的作用是读取express.psf.cix.xml(PSF文件的配套配置文件),解析出每个差分文件的关键元数据,并返回有序字典。

核心执行流程

  1. 加载XML文件,遍历节点找到<Files>标签(包含所有文件列表)。
  2. 对每个文件节点,遍历找到<Delta><Source>子节点(差分数据的元数据)。
  3. 提取关键信息并存储到$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[关闭流+清理临时目录]

关键细节解释

  1. DLL选择逻辑

    1
    if ($DllFile -eq 'msdelta.dll' -and (Test-Path "$env:SystemRoot\System32\UpdateCompression.dll")) { $DllFile = "UpdateCompression.dll" }

    优先使用UpdateCompression.dll(新版Windows的差分压缩库),兼容旧版的msdelta.dll

  2. 长路径处理(UseRobo)

    1
    [bool]$UseRobo = (($cwd + '\' + $FullFileName).Length -gt 255) -or (($cwd + '\' + $ShortFold).Length -gt 248)

    Windows默认路径长度限制为255字符,超过时先将文件写入临时目录000,再通过robocopy移动到目标路径(robocopy支持长路径)。

  3. 差分还原判断

    1
    if ($DeltaFile.stype -eq "PA30" -or $DeltaFile.stype -eq "PA31") { ApplyDelta $Buffer $WhereFile }

    仅对PA30/PA31类型的差分数据调用ApplyDelta还原,其他类型直接写入文件。

  4. robocopy参数说明

    • /MOV:移动文件(而非复制)
    • /R:1 /W:1:重试1次,每次等待1秒
    • /NS /NC /NFL等:关闭冗余输出,只执行操作

总结

这段代码的核心关键点如下:

  1. 核心能力:通过动态P/Invoke封装Windows差分压缩API,实现PSF文件的解包和差分数据还原。
  2. 兼容性:适配msdelta.dll/UpdateCompression.dll,处理Windows长路径限制(通过robocopy)。
  3. 内存管理:手动分配/释放非托管内存,避免内存泄漏;解析XML元数据精准定位差分数据位置。

简单来说,这是一个针对Windows系统PSF差分压缩文件的专业解包工具,核心依赖系统自带的差分压缩库,通过PowerShell动态调用底层API完成文件还原。