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 来更新系统

函数内容

方法来自:https://forums.mydigitallife.net/posts/1216064

BatUtil/W10UI/W10UI.cmd at master · abbodi1406/BatUtil

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脚本的主要功能是解析和应用增量更新文件(delta files)。具体功能如下:

  1. **Native($DllFile)**:定义了一些动态类型和P/Invoke方法,用于加载动态链接库(如msdelta.dll)并调用其中的方法。
    • 定义了动态类型DIDO,分别用于表示输入和输出结构体。
    • 定义了一个类PSFE,并在其中定义了P/Invoke方法LoadLibraryWApplyDeltaBDeltaFree,用于加载动态链接库和调用其方法。
  2. **ApplyDelta($dBuffer, $dFile)**:应用增量更新数据到目标文件。首先创建输入和输出结构体,然后调用ApplyDeltaB方法进行更新,最后释放资源。
    • 创建输入和输出结构体实例。
    • 分配内存并将增量数据复制到内存中。
    • 调用ApplyDeltaB方法应用增量更新。
    • 将结果写入目标文件,并释放分配的内存。
  3. **G($DirectoryName)**:解析XML文件中的增量更新信息,生成一个包含文件名、时间戳、源类型等信息的字典。
    • 加载XML文件并解析其中的增量更新信息。
    • 生成一个字典,包含每个文件的名称、时间戳、源类型等信息。
  4. **P($CabFile, $DllFile = ‘msdelta.dll’)**:主函数,负责解压CAB文件,解析增量更新信息,应用增量更新,并设置文件的时间戳。
    • 检查并加载动态链接库。
    • 解压CAB文件,解析增量更新信息。
    • 遍历增量更新列表,对每个文件进行处理:
      • 检查文件是否存在,如果存在则跳过。
      • 创建必要的目录。
      • 计算路径长度,如果超过限制则使用RoboCopy复制文件,否则直接写入文件。
      • 应用增量更新。
      • 设置文件的时间戳。
    • 关闭PSF文件并删除临时目录。
flowchart TD
    A[开始] --> B[解析XML文件]
    B --> C[获取增量更新信息]
    C --> D[创建目录]
    D --> E[打开PSF文件]
    E --> F[遍历增量更新列表]
    F --> G{是否已存在文件?}
    G -->|是| H[跳过当前文件]
    G -->|否| I[创建目录]
    I --> J[计算路径长度]
    J --> K{路径长度超过限制?}
    K -->|是| L[使用RoboCopy复制文件]
    K -->|否| M[写入文件]
    M --> N[应用增量更新]
    N --> O[设置文件时间戳]
    O --> P[继续下一个文件]
    P --> F
    H --> P
    L --> P
    P --> Q[关闭PSF文件]
    Q --> R[删除临时目录]
    R --> S[结束]