fix: 让 5.1 真正可用(显式编码 + 原生 stderr 处理 + .psd1 夹具带 BOM)

上一提交让 5.1 能解析源码,但 Unit 与 Smoke 在 5.1 上仍然是红的。根因是三类彼此
无关的 5.1/7 行为差,全部实测确认:

1) 不写 -Encoding 时,5.1 的 Get-Content / Set-Content 默认是 ANSI,7 是 UTF-8。
   症状是 UTF-8 字节被按 GBK 解出「璇存槑」这类乱码。62 处补上显式 -Encoding UTF8。
   用 AST 而不是正则定位,避免把注释里的散文也改掉。

2) .psd1 夹具用无 BOM 写,而引擎的 .psd1 读取器(Import-PowerShellDataFile)只能靠
   BOM 判断编码、没有参数可传,于是 5.1 按 ANSI 解。40 处夹具改为带 BOM 写 —— 这正是
   .editorconfig 里 [*.psd1] charset = utf-8-bom 本来就要求的,是夹具违反了自己的约定。
   .cmd 批次文件刻意保持无 BOM:cmd.exe 会被 BOM 弄坏。

3) 5.1 在 $ErrorActionPreference = Stop 下会把原生命令写到 stderr 的内容升级成终止性
   NativeCommandError,7 改了这条。takeown/icacls 的 ACL 复位调用、以及 test.ps1 自己
   调子进程的地方,都需要在 Continue 下跑。

验收:test.ps1 9/9 全绿(Encode + Parse + Unit + Smoke + E2E,在 7 与 5.1 上各跑一遍)。

已知未处理(留待后续提交):tools/lab/** 里还有若干「原生命令 + 2>&1 + Stop」的同类
写法(takeown / icacls / scoop / code / Get-WimInfo)。它们要 Hyper-V 实验机才跑得到,
不在验收门槛内。
This commit is contained in:
Shuery committed 2026-09-26 22:16:04 +08:00
1 parent 2d26f78d15
commit e10503be76
13 files changed
+122 -104

No files matched your search

+19 -19
View File
@@ -74,7 +74,7 @@ function New-E2EConfig {
if ($SoftwareCatalog) { $lines += " SoftwareCatalog = '$SoftwareCatalog'" }
$lines += " CatalogMaxDepth = $CatalogMaxDepth"
$lines += '}'
[System.IO.File]::WriteAllText($Path, ($lines -join "`r`n"), [System.Text.UTF8Encoding]::new($false))
[System.IO.File]::WriteAllText($Path, ($lines -join "`r`n"), [System.Text.UTF8Encoding]::new($true))
}
function Get-NewestLog {
@@ -141,7 +141,7 @@ $blob = New-Object byte[] 8192
(New-Object System.Random 42).NextBytes($blob)
[System.IO.File]::WriteAllBytes((Join-Path $source 'blob.bin'), $blob)
[System.IO.File]::WriteAllText($listPath, "# e2e`n$source :- logs\,!*Cache`n", [System.Text.UTF8Encoding]::new($false))
[System.IO.File]::WriteAllText($listPath, "# e2e`n$source :- logs\,!*Cache`n", [System.Text.UTF8Encoding]::new($true))
$expectedHashes = @{}
foreach ($relative in 'keep.txt', 'sub\b.txt', 'blob.bin') {
@@ -243,7 +243,7 @@ $cfg2 = Join-Path $WorkRoot 'cfg2.psd1'
New-E2EConfig -Path $cfg2 -LogDir $logDir2
New-Item -ItemType Directory -Path $lockSource -Force | Out-Null
Set-Content -LiteralPath (Join-Path $lockSource 'a.txt') -Value 'aaa' -Encoding UTF8
[System.IO.File]::WriteAllText($lockList, "$lockSource`n", [System.Text.UTF8Encoding]::new($false))
[System.IO.File]::WriteAllText($lockList, "$lockSource`n", [System.Text.UTF8Encoding]::new($true))
& $backupScript -BackupListPath $lockList -BackupDir $lockBackupDir -ConfigPath $cfg2 -Force -QuietTool
$cleanExitCode = $LASTEXITCODE
@@ -335,7 +335,7 @@ Set-Content -LiteralPath (Join-Path $incDir 'i.txt') -Value 'included' -Encoding
"@, [System.Text.UTF8Encoding]::new($false))
# 条目级排除用 Slot 前缀点名(AlphaData\plain)+ 任意层级(!*skip);:+ 把 inc 放到归档内 Modules\
[System.IO.File]::WriteAllText($catList, "my-app :- AlphaData\plain,!*skip :+ Modules:$incDir`ncat-excl`n", [System.Text.UTF8Encoding]::new($false))
[System.IO.File]::WriteAllText($catList, "my-app :- AlphaData\plain,!*skip :+ Modules:$incDir`ncat-excl`n", [System.Text.UTF8Encoding]::new($true))
New-E2EConfig -Path $catConfig -LogDir $catLogDir -SoftwareCatalog $catFile
$catHashes = @{
@@ -433,7 +433,7 @@ Test-Case '恢复不会把被排除的内容带回来' {
Test-Case '@pathname 覆盖:用名录里的真实路径跑命名算法(独立备份目录,避免污染共享 manifest)' {
$pathList = Join-Path $catRoot 'list-pathname.txt'
$pathNameDir = Join-Path $catRoot 'Backups-pathname'
[System.IO.File]::WriteAllText($pathList, "my-app @pathname :+ Modules:$settingsFile`n", [System.Text.UTF8Encoding]::new($false))
[System.IO.File]::WriteAllText($pathList, "my-app @pathname :+ Modules:$settingsFile`n", [System.Text.UTF8Encoding]::new($true))
# settings.json 刚才被删了,重建一份,让 Include 的宿主机路径存在
Set-Content -LiteralPath $settingsFile -Value '{"slot":"file"}' -Encoding UTF8
$expectedBase = Get-BackupBaseName -RawPath $dirA
@@ -455,8 +455,8 @@ $ovrConfig = Join-Path $ovrRoot 'config.psd1'
$ovrExtract = Join-Path $ovrRoot 'verify'
New-Item -ItemType Directory -Path $ovrTarget -Force | Out-Null
Set-Content -LiteralPath (Join-Path $ovrTarget 't.txt') -Value 'override-target' -Encoding UTF8
[System.IO.File]::WriteAllText($ovrCatalog, "@{`n 'ovr-app' = @{ DefaultData = @{ Path = '$ovrRoot\missing-src' } }`n}`n", [System.Text.UTF8Encoding]::new($false))
[System.IO.File]::WriteAllText($ovrList, "ovr-app :: $ovrTarget`n", [System.Text.UTF8Encoding]::new($false))
[System.IO.File]::WriteAllText($ovrCatalog, "@{`n 'ovr-app' = @{ DefaultData = @{ Path = '$ovrRoot\missing-src' } }`n}`n", [System.Text.UTF8Encoding]::new($true))
[System.IO.File]::WriteAllText($ovrList, "ovr-app :: $ovrTarget`n", [System.Text.UTF8Encoding]::new($true))
New-E2EConfig -Path $ovrConfig -LogDir (Join-Path $ovrRoot 'logs') -SoftwareCatalog $ovrCatalog
& $backupScript -BackupListPath $ovrList -BackupDir $ovrBackupDir -ConfigPath $ovrConfig -Force -QuietTool
@@ -471,7 +471,7 @@ Test-Case ':: 覆盖:备份包内容来自被覆盖的路径,且归档项名
$null = Invoke-ExternalCommand -FilePath $sevenZip -ArgumentList @('x', '-bso0', '-bsp0', '-y', "-o$ovrExtract", $ovrArchives[0].FullName)
}
Assert-FileExists (Join-Path $ovrExtract 'DefaultData\t.txt')
Assert-Equal 'override-target' (Get-Content -LiteralPath (Join-Path $ovrExtract 'DefaultData\t.txt') -Raw).Trim()
Assert-Equal 'override-target' (Get-Content -Encoding UTF8 -LiteralPath (Join-Path $ovrExtract 'DefaultData\t.txt') -Raw).Trim()
}
Test-Case ':: 覆盖:删掉被覆盖的源后仍能恢复回该路径' {
@@ -479,7 +479,7 @@ Test-Case ':: 覆盖:删掉被覆盖的源后仍能恢复回该路径' {
& $restoreScript -BackupListPath $ovrList -BackupDir $ovrBackupDir -ConfigPath $ovrConfig -Force
Assert-Equal 0 $LASTEXITCODE
Assert-FileExists (Join-Path $ovrTarget 't.txt')
Assert-Equal 'override-target' (Get-Content -LiteralPath (Join-Path $ovrTarget 't.txt') -Raw).Trim()
Assert-Equal 'override-target' (Get-Content -Encoding UTF8 -LiteralPath (Join-Path $ovrTarget 't.txt') -Raw).Trim()
}
# ============================================================================
@@ -499,10 +499,10 @@ New-Item -ItemType Directory -Path $appA -Force | Out-Null
New-Item -ItemType Directory -Path $appB -Force | Out-Null
Set-Content -LiteralPath (Join-Path $appA 'a.txt') -Value 'dir-a' -Encoding UTF8
Set-Content -LiteralPath (Join-Path $appB 'b.txt') -Value 'dir-b' -Encoding UTF8
[System.IO.File]::WriteAllText($dirCatalog, "@{`n 'app-a' = @{ DefaultData = @{ Path = '$appA' } }`n 'app-b' = @{ DefaultData = @{ Path = '$appB' } }`n}`n", [System.Text.UTF8Encoding]::new($false))
[System.IO.File]::WriteAllText($dirCatalog, "@{`n 'app-a' = @{ DefaultData = @{ Path = '$appA' } }`n 'app-b' = @{ DefaultData = @{ Path = '$appB' } }`n}`n", [System.Text.UTF8Encoding]::new($true))
New-E2EConfig -Path $dirConfig -LogDir $dirLogDir -SoftwareCatalog $dirCatalog
[System.IO.File]::WriteAllText($dirList1, "+ app-a`napp-b`n", [System.Text.UTF8Encoding]::new($false))
[System.IO.File]::WriteAllText($dirList2, "+ app-a`n- app-b`n", [System.Text.UTF8Encoding]::new($false))
[System.IO.File]::WriteAllText($dirList1, "+ app-a`napp-b`n", [System.Text.UTF8Encoding]::new($true))
[System.IO.File]::WriteAllText($dirList2, "+ app-a`n- app-b`n", [System.Text.UTF8Encoding]::new($true))
& $backupScript -BackupListPath $dirList1 -BackupDir $dirBackupDir -ConfigPath $dirConfig -Force -QuietTool
Test-Case '行首 + = 仅备份:仍然会被打包' {
@@ -539,7 +539,7 @@ Test-Case '恢复端跳过行首 + 的条目、照常恢复 - 的条目' {
Assert-Equal 0 $LASTEXITCODE
Assert-FileMissing $appA '行首 + 的条目不该被恢复'
Assert-FileExists (Join-Path $appB 'b.txt')
Assert-Equal 'dir-b' (Get-Content -LiteralPath (Join-Path $appB 'b.txt') -Raw).Trim()
Assert-Equal 'dir-b' (Get-Content -Encoding UTF8 -LiteralPath (Join-Path $appB 'b.txt') -Raw).Trim()
$restoreLog = Get-NewestLog -LogDir $dirLogDir -Prefix 'restore'
$restoreContent = Get-Content -LiteralPath $restoreLog.FullName -Raw -Encoding UTF8
@@ -572,8 +572,8 @@ if ($sevenZip) {
-WorkingDirectory $legacyScratch
}
[System.IO.File]::WriteAllText($legacyCatalog, "@{`n 'legacy-file' = @{ LegacyData = @{ Path = '$legacyLive' } }`n}`n", [System.Text.UTF8Encoding]::new($false))
[System.IO.File]::WriteAllText($legacyList, "legacy-file`n", [System.Text.UTF8Encoding]::new($false))
[System.IO.File]::WriteAllText($legacyCatalog, "@{`n 'legacy-file' = @{ LegacyData = @{ Path = '$legacyLive' } }`n}`n", [System.Text.UTF8Encoding]::new($true))
[System.IO.File]::WriteAllText($legacyList, "legacy-file`n", [System.Text.UTF8Encoding]::new($true))
New-E2EConfig -Path $legacyConfig -LogDir $legacyLogDir -SoftwareCatalog $legacyCatalog
$legacyManifest = Read-BaknretManifest -Path (Join-Path $legacyBackupDir 'manifest.json')
@@ -595,7 +595,7 @@ if ($sevenZip) {
Test-Case '旧布局归档:按 <末级名> 回退,把文件还原回原位' {
Assert-Equal 0 $legacyExitCode
Assert-Equal '{"version":"old-layout"}' (Get-Content -LiteralPath $legacyLive -Raw).Trim()
Assert-Equal '{"version":"old-layout"}' (Get-Content -Encoding UTF8 -LiteralPath $legacyLive -Raw).Trim()
$legacyLog = Get-NewestLog -LogDir $legacyLogDir -Prefix 'restore'
$legacyContent = Get-Content -LiteralPath $legacyLog.FullName -Raw -Encoding UTF8
Assert-True ($legacyContent -like '*按旧布局回退*') '回退时必须给出明确告警'
@@ -633,7 +633,7 @@ Test-Case 'Backup -DryRun:退出码 0、不写归档也不写 manifest' {
Test-Case '归档名重复时直接报失败,不静默互相覆盖' {
$dupList = Join-Path $catRoot 'dup.txt'
[System.IO.File]::WriteAllText($dupList, "my-app`nmy-app`n", [System.Text.UTF8Encoding]::new($false))
[System.IO.File]::WriteAllText($dupList, "my-app`nmy-app`n", [System.Text.UTF8Encoding]::new($true))
& $backupScript -BackupListPath $dupList -BackupDir $catBackupDir -ConfigPath $catConfig -Force -QuietTool
Assert-Equal 1 $LASTEXITCODE '重复条目应让退出码为 1'
}
@@ -641,14 +641,14 @@ Test-Case '归档名重复时直接报失败,不静默互相覆盖' {
Test-Case '字面路径不受名录影响,仍走路径命名' {
$literalList = Join-Path $catRoot 'list-literal.txt'
$literalDir = Join-Path $catRoot 'Backups-literal'
[System.IO.File]::WriteAllText($literalList, "$dirA2`n", [System.Text.UTF8Encoding]::new($false))
[System.IO.File]::WriteAllText($literalList, "$dirA2`n", [System.Text.UTF8Encoding]::new($true))
& $backupScript -BackupListPath $literalList -BackupDir $literalDir -ConfigPath $catConfig -Force -QuietTool
Assert-Equal 0 $LASTEXITCODE
Assert-FileExists (Join-Path $literalDir ((Get-BackupBaseName -RawPath $dirA2) + '.7z'))
}
$missingList = Join-Path $WorkRoot 'missing.txt'
[System.IO.File]::WriteAllText($missingList, "Z:\definitely-not-here-12345`n", [System.Text.UTF8Encoding]::new($false))
[System.IO.File]::WriteAllText($missingList, "Z:\definitely-not-here-12345`n", [System.Text.UTF8Encoding]::new($true))
& $backupScript -BackupListPath $missingList -BackupDir $backupDir -ConfigPath $cfg1 -Force -QuietTool
$missingExitCode = $LASTEXITCODE