From e10503be7659ce5199d0161df09a764446633134 Mon Sep 17 00:00:00 2001 From: Shuery <2463253700@qq.com> Date: Sat, 26 Sep 2026 22:15:20 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E8=AE=A9=205.1=20=E7=9C=9F=E6=AD=A3?= =?UTF-8?q?=E5=8F=AF=E7=94=A8=EF=BC=88=E6=98=BE=E5=BC=8F=E7=BC=96=E7=A0=81?= =?UTF-8?q?=20+=20=E5=8E=9F=E7=94=9F=20stderr=20=E5=A4=84=E7=90=86=20+=20.?= =?UTF-8?q?psd1=20=E5=A4=B9=E5=85=B7=E5=B8=A6=20BOM=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 上一提交让 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 实验机才跑得到, 不在验收门槛内。 --- Backup.ps1 | 4 +- Restore.ps1 | 4 +- test.ps1 | 4 ++ tests/BakNRet.Formats.Tests.ps1 | 44 +++++++++---------- tests/BakNRet.Security.Tests.ps1 | 24 ++++++++--- tests/BakNRet.Tests.ps1 | 32 +++++++------- tests/Restore-Drill.ps1 | 4 +- tests/Run-E2E.ps1 | 38 ++++++++-------- tests/Run-Tests.ps1 | 60 +++++++++++++------------- tools/Rename-Archives.ps1 | 2 +- tools/lab/Lab.ps1 | 2 +- tools/lab/New-BakNRetLab.ps1 | 2 +- tools/lab/payload/run-acl-scenario.ps1 | 6 +-- 13 files changed, 122 insertions(+), 104 deletions(-) diff --git a/Backup.ps1 b/Backup.ps1 index 18884d8..e57bd8f 100644 --- a/Backup.ps1 +++ b/Backup.ps1 @@ -134,7 +134,7 @@ if (-not (Test-Path -LiteralPath $BackupListPath)) { "# 示例: Edge`n" + "# %UserProfile%\.ssh :encrypt`n" + "# 完整语法见 README 与 BackupList.txt 自身的注释。`n" - [System.IO.File]::WriteAllText($BackupListPath, $template, [System.Text.UTF8Encoding]::new($false)) + [System.IO.File]::WriteAllText($BackupListPath, $template, [System.Text.UTF8Encoding]::new($true)) Write-Log '模板 BackupList.txt 已创建,请编辑后重试。' -Level INFO Stop-BaknretLog exit 0 @@ -162,7 +162,7 @@ $encryptAll = [bool]$script:Config.Encryption.Enabled $showToolOutput = (-not $QuietTool) -and ($script:Config.ToolOutput -ne 'quiet') $toolQuietArgument = if ($showToolOutput) { @() } else { @('-bso0', '-bsp0') } -$lines = Get-Content -LiteralPath $BackupListPath +$lines = Get-Content -Encoding UTF8 -LiteralPath $BackupListPath $seenBaseNames = @{} $processed = 0; $skipped = 0; $failed = 0; $planned = 0 $securityErrorCount = 0 # 有条目"安全描述符里有读不到的对象" diff --git a/Restore.ps1 b/Restore.ps1 index 53e448b..05848c2 100644 --- a/Restore.ps1 +++ b/Restore.ps1 @@ -484,7 +484,7 @@ if (-not (Test-Path -LiteralPath $BackupListPath)) { } $content = "# BackupList.txt(自动生成,排除规则需要手工补回)`n" + (($paths -join [Environment]::NewLine) + [Environment]::NewLine) - [System.IO.File]::WriteAllText($BackupListPath, $content, [System.Text.UTF8Encoding]::new($false)) + [System.IO.File]::WriteAllText($BackupListPath, $content, [System.Text.UTF8Encoding]::new($true)) Write-Log "已生成配置,包含 $($paths.Count) 个项目,请检查后重新运行" -Level INFO Stop-BaknretLog exit 0 @@ -509,7 +509,7 @@ function Test-EntrySelected { # 主流程 # ============================================================================ -$lines = Get-Content -LiteralPath $BackupListPath -ErrorAction Stop +$lines = Get-Content -Encoding UTF8 -LiteralPath $BackupListPath -ErrorAction Stop $stats = @{ restored = 0; skipped = 0; failed = 0; verified = 0; planned = 0 } $securityApplied = 0 # 本次回放成功的安全描述符对象数 $failures = @() diff --git a/test.ps1 b/test.ps1 index 9c5ec1f..0360bcc 100644 --- a/test.ps1 +++ b/test.ps1 @@ -94,6 +94,9 @@ function Invoke-ScriptInHost { $argumentList = @('-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', $ScriptPath) + $Arguments try { + # 5.1 在 $ErrorActionPreference = 'Stop' 下会把原生命令写到 stderr 的内容升级成 + # 终止性错误;而被调脚本往 stderr 写字是正常的(7z 就这么干),不该让验收崩掉。 + $ErrorActionPreference = 'Continue' if ($Quiet) { & $exeOf[$HostName] @argumentList *> $null } else { @@ -176,6 +179,7 @@ function Invoke-ParseLayer { Set-Content -LiteralPath $probe -Value $parseProbeSource -Encoding ascii try { + $ErrorActionPreference = 'Continue' # 同上:原生命令的 stderr 不是错误 $output = & $exeOf[$HostName] -NoProfile -ExecutionPolicy Bypass -File $probe $root 2>&1 $summary = @($output | Where-Object { $_ -match '^SUMMARY=' }) $failures = @($output | Where-Object { $_ -match '^\s+\[!\]' }) diff --git a/tests/BakNRet.Formats.Tests.ps1 b/tests/BakNRet.Formats.Tests.ps1 index 47c6566..d52f3cd 100644 --- a/tests/BakNRet.Formats.Tests.ps1 +++ b/tests/BakNRet.Formats.Tests.ps1 @@ -79,7 +79,7 @@ BeforeAll { function Write-ListFile { param([Parameter(Mandatory = $true)][string]$Path, [Parameter(Mandatory = $true)][string]$Content) - [System.IO.File]::WriteAllText($Path, $Content, [System.Text.UTF8Encoding]::new($false)) + [System.IO.File]::WriteAllText($Path, $Content, [System.Text.UTF8Encoding]::new($true)) return $Path } } @@ -102,18 +102,18 @@ Describe '软件名录:Slot 形状(新契约)' { $script:DirB = Join-Path $script:FormatRoot 'dirB' foreach ($directory in $script:DirA, $script:DirB) { New-Item -ItemType Directory -Path $directory -Force | Out-Null - Set-Content -LiteralPath (Join-Path $directory 'keep.txt') "keep-$directory" + Set-Content -Encoding UTF8 -LiteralPath (Join-Path $directory 'keep.txt') "keep-$directory" } New-Item -ItemType Directory -Path (Join-Path $script:DirA 'Cache') -Force | Out-Null - Set-Content -LiteralPath (Join-Path $script:DirA 'Cache\c.bin') 'cache' + Set-Content -Encoding UTF8 -LiteralPath (Join-Path $script:DirA 'Cache\c.bin') 'cache' $script:CfgFile = Join-Path $script:FormatRoot 'settings.json' - Set-Content -LiteralPath $script:CfgFile '{"a":1}' + Set-Content -Encoding UTF8 -LiteralPath $script:CfgFile '{"a":1}' # 两个不同父目录下各有一个**同名**子目录 —— 供"归档内同名"冲突测试用 $script:CollideRoot = Join-Path $script:FormatRoot 'collide' foreach ($parent in 'p1', 'p2') { New-Item -ItemType Directory -Path (Join-Path $script:CollideRoot "$parent\dupdir") -Force | Out-Null - Set-Content -LiteralPath (Join-Path $script:CollideRoot "$parent\dupdir\x.txt") $parent + Set-Content -Encoding UTF8 -LiteralPath (Join-Path $script:CollideRoot "$parent\dupdir\x.txt") $parent } $script:MissingDir = Join-Path $script:FormatRoot 'not-here' @@ -324,12 +324,12 @@ Describe '集成:Slot 布局的打包与恢复' -Skip:(-not ($script:HasSevenZ New-Item -ItemType Directory -Path $script:SlotCfgDir -Force | Out-Null New-Item -ItemType Directory -Path $script:SlotInclude -Force | Out-Null - Set-Content -LiteralPath (Join-Path $script:SlotAppOne 'one.txt') 'one' - Set-Content -LiteralPath (Join-Path $script:SlotAppOne 'sub\deep.txt') 'deep' - Set-Content -LiteralPath (Join-Path $script:SlotAppOne 'Cache\c.bin') 'cache' - Set-Content -LiteralPath (Join-Path $script:SlotAppTwo 'two.txt') 'two' - Set-Content -LiteralPath (Join-Path $script:SlotCfgDir 'settings.json') '{"a":1}' - Set-Content -LiteralPath (Join-Path $script:SlotInclude 'mod.txt') 'mod' + Set-Content -Encoding UTF8 -LiteralPath (Join-Path $script:SlotAppOne 'one.txt') 'one' + Set-Content -Encoding UTF8 -LiteralPath (Join-Path $script:SlotAppOne 'sub\deep.txt') 'deep' + Set-Content -Encoding UTF8 -LiteralPath (Join-Path $script:SlotAppOne 'Cache\c.bin') 'cache' + Set-Content -Encoding UTF8 -LiteralPath (Join-Path $script:SlotAppTwo 'two.txt') 'two' + Set-Content -Encoding UTF8 -LiteralPath (Join-Path $script:SlotCfgDir 'settings.json') '{"a":1}' + Set-Content -Encoding UTF8 -LiteralPath (Join-Path $script:SlotInclude 'mod.txt') 'mod' $appOne = $script:SlotAppOne $appTwo = $script:SlotAppTwo @@ -388,7 +388,7 @@ Describe '集成:Slot 布局的打包与恢复' -Skip:(-not ($script:HasSevenZ Test-Path -LiteralPath (Join-Path $verify 'Extra\two.txt') | Should -BeTrue Test-Path -LiteralPath (Join-Path $verify 'Modules\mod.txt') | Should -BeTrue Test-Path -LiteralPath (Join-Path $verify 'Cfg') -PathType Leaf | Should -BeTrue - (Get-Content -LiteralPath (Join-Path $verify 'Cfg') -Raw).Trim() | Should -Be '{"a":1}' + (Get-Content -Encoding UTF8 -LiteralPath (Join-Path $verify 'Cfg') -Raw).Trim() | Should -Be '{"a":1}' } It '真实恢复:目录 Slot、文件 Slot 与 Include 都落回各自的原位' { @@ -404,10 +404,10 @@ Describe '集成:Slot 布局的打包与恢复' -Skip:(-not ($script:HasSevenZ $run.ExitCode | Should -Be 0 $run.Output | Should -Match '恢复成功: appkit' - (Get-Content -LiteralPath (Join-Path $script:SlotAppOne 'one.txt') -Raw).Trim() | Should -Be 'one' - (Get-Content -LiteralPath (Join-Path $script:SlotAppOne 'sub\deep.txt') -Raw).Trim() | Should -Be 'deep' - (Get-Content -LiteralPath (Join-Path $script:SlotAppTwo 'two.txt') -Raw).Trim() | Should -Be 'two' - (Get-Content -LiteralPath (Join-Path $script:SlotInclude 'mod.txt') -Raw).Trim() | Should -Be 'mod' + (Get-Content -Encoding UTF8 -LiteralPath (Join-Path $script:SlotAppOne 'one.txt') -Raw).Trim() | Should -Be 'one' + (Get-Content -Encoding UTF8 -LiteralPath (Join-Path $script:SlotAppOne 'sub\deep.txt') -Raw).Trim() | Should -Be 'deep' + (Get-Content -Encoding UTF8 -LiteralPath (Join-Path $script:SlotAppTwo 'two.txt') -Raw).Trim() | Should -Be 'two' + (Get-Content -Encoding UTF8 -LiteralPath (Join-Path $script:SlotInclude 'mod.txt') -Raw).Trim() | Should -Be 'mod' # 被 Slot 排除的缓存没有进过归档,自然也不会被恢复出来 Test-Path -LiteralPath (Join-Path $script:SlotAppOne 'Cache\c.bin') | Should -BeFalse @@ -417,7 +417,7 @@ Describe '集成:Slot 布局的打包与恢复' -Skip:(-not ($script:HasSevenZ # 目标文件被删掉了,名录解析只能得到 IsFile=false;判据要靠 manifest 的 layouts。 $restored = Join-Path $script:SlotCfgDir 'settings.json' (Test-Path -LiteralPath $restored -PathType Leaf) | Should -BeTrue - (Get-Content -LiteralPath $restored -Raw).Trim() | Should -Be '{"a":1}' + (Get-Content -Encoding UTF8 -LiteralPath $restored -Raw).Trim() | Should -Be '{"a":1}' } It '恢复之后 manifest 记下 lastRestoreAt' { @@ -440,7 +440,7 @@ Describe '集成:旧布局归档的回退恢复' -Skip:(-not ($script:HasSeven $script:LegacyLeaf = 'My Code Space' New-Item -ItemType Directory -Path (Join-Path $script:LegacyHolder $script:LegacyLeaf) -Force | Out-Null New-Item -ItemType Directory -Path $script:LegacyDestParent -Force | Out-Null - Set-Content -LiteralPath (Join-Path $script:LegacyHolder "$script:LegacyLeaf\legacy.txt") 'old-layout' + Set-Content -Encoding UTF8 -LiteralPath (Join-Path $script:LegacyHolder "$script:LegacyLeaf\legacy.txt") 'old-layout' $destPath = Join-Path $script:LegacyDestParent $script:LegacyLeaf $script:LegacyCatalog = Write-ListFile -Path (Join-Path $script:LegacyRoot 'cat.psd1') -Content @" @@ -480,7 +480,7 @@ Describe '集成:旧布局归档的回退恢复' -Skip:(-not ($script:HasSeven $run.ExitCode | Should -Be 0 $run.Output | Should -Match '按旧布局回退' Test-Path -LiteralPath (Join-Path $script:LegacyDestParent "$script:LegacyLeaf\legacy.txt") | Should -BeTrue - (Get-Content -LiteralPath (Join-Path $script:LegacyDestParent "$script:LegacyLeaf\legacy.txt") -Raw).Trim() | Should -Be 'old-layout' + (Get-Content -Encoding UTF8 -LiteralPath (Join-Path $script:LegacyDestParent "$script:LegacyLeaf\legacy.txt") -Raw).Trim() | Should -Be 'old-layout' } It '归档里既没有 Slot 层、也没有旧布局名字时明确失败(不再"成功地什么都没恢复")' { @@ -513,7 +513,7 @@ Describe '集成:归档内路径冲突会被拒绝执行' -Skip:(-not ($script $p2 = Join-Path $script:RejectRoot 'p2\dupdir' foreach ($directory in $p1, $p2) { New-Item -ItemType Directory -Path $directory -Force | Out-Null - Set-Content -LiteralPath (Join-Path $directory 'x.txt') 'x' + Set-Content -Encoding UTF8 -LiteralPath (Join-Path $directory 'x.txt') 'x' } # Slot 叫 Data,Include 也要放进包内的 Data -> 同一个位置,必须报错 Write-ListFile -Path $script:RejectCatalog -Content "@{`n 'collide' = @{ Data = @{ Path = '$p1' } }`n}`n" | Out-Null @@ -556,7 +556,7 @@ Describe '条目从清单里消失后,旧归档必须被点名为孤儿' -Skip $script:OrphanRoot = Join-Path $script:Sandbox 'orphan-e2e' $script:OrphanSource = Join-Path $script:OrphanRoot 'src\My App' New-Item -ItemType Directory -Path $script:OrphanSource -Force | Out-Null - Set-Content -LiteralPath (Join-Path $script:OrphanSource 'data.txt') 'hello' + Set-Content -Encoding UTF8 -LiteralPath (Join-Path $script:OrphanSource 'data.txt') 'hello' $script:OrphanCatalog = Write-ListFile -Path (Join-Path $script:OrphanRoot 'cat.psd1') ` -Content "@{ 'my-app' = @{ Default = @{ Path = '$script:OrphanSource' } } }`n" @@ -606,7 +606,7 @@ Describe 'manifest 一致性:archive 字段只在文件真的存在时才写' BeforeAll { $script:SyncRoot = Join-Path $script:Sandbox 'sync' New-Item -ItemType Directory -Path $script:SyncRoot -Force | Out-Null - Set-Content -LiteralPath (Join-Path $script:SyncRoot 'real.7z') 'x' + Set-Content -Encoding UTF8 -LiteralPath (Join-Path $script:SyncRoot 'real.7z') 'x' $script:SyncManifestPath = Join-Path $script:SyncRoot 'manifest.json' $manifest = Read-BaknretManifest -Path $script:SyncManifestPath diff --git a/tests/BakNRet.Security.Tests.ps1 b/tests/BakNRet.Security.Tests.ps1 index a6c67c5..694685e 100644 --- a/tests/BakNRet.Security.Tests.ps1 +++ b/tests/BakNRet.Security.Tests.ps1 @@ -97,8 +97,22 @@ BeforeAll { <# .SYNOPSIS 先把 ACL 复位再删:拒绝型 / protected 的 DACL 会让 Remove-Item 直接失败。 #> param([Parameter(Mandatory = $true)][string]$Path) if (-not (Test-Path -LiteralPath $Path)) { return } - & takeown.exe /F $Path /R /D Y 2>&1 | Out-Null - & icacls.exe $Path /reset /T /C /Q 2>&1 | Out-Null + + # 这两个是原生命令,失败与否都不该打断测试 —— 它们的唯一目的是"把 ACL 复位到能删"。 + # + # 必须在 Continue 下调用:Windows PowerShell 5.1 在 $ErrorActionPreference = 'Stop' + # 时,会把原生命令写到 stderr 的内容升级成终止性的 NativeCommandError 抛出来 + # (takeown / icacls 对"已经处理过"的对象就会写 stderr)。PowerShell 7 改了这条 + # 规矩,所以这段在 7 上一直是绿的,只在 5.1 上炸。 + $previousPreference = $ErrorActionPreference + $ErrorActionPreference = 'Continue' + try { + & takeown.exe /F $Path /R /D Y 2>&1 | Out-Null + & icacls.exe $Path /reset /T /C /Q 2>&1 | Out-Null + } finally { + $ErrorActionPreference = $previousPreference + } + Remove-Item -LiteralPath $Path -Recurse -Force -ErrorAction SilentlyContinue } @@ -160,7 +174,7 @@ BeforeAll { New-Item -ItemType Directory -Path $backupDir -Force | Out-Null $listPath = Join-Path $dir 'BackupList.txt' - [System.IO.File]::WriteAllText($listPath, "$sourcePath`n", [System.Text.UTF8Encoding]::new($false)) + [System.IO.File]::WriteAllText($listPath, "$sourcePath`n", [System.Text.UTF8Encoding]::new($true)) $configPath = Join-Path $dir 'BackupConfig.psd1' $configText = @" @@ -180,7 +194,7 @@ BeforeAll { DefaultExcludes = @() } "@ - [System.IO.File]::WriteAllText($configPath, $configText, [System.Text.UTF8Encoding]::new($false)) + [System.IO.File]::WriteAllText($configPath, $configText, [System.Text.UTF8Encoding]::new($true)) return [pscustomobject]@{ Dir = $dir @@ -426,7 +440,7 @@ Describe '与 Backup.ps1 / Restore.ps1 的集成' -Skip:(-not $script:HasSevenZi $sidecars.Count | Should -Be 1 $result.Output | Should -Match '安全描述符:3 个对象' - $manifest = Get-Content -LiteralPath (Join-Path $script:Harness.BackupDir 'manifest.json') -Raw | ConvertFrom-Json + $manifest = Get-Content -Encoding UTF8 -LiteralPath (Join-Path $script:Harness.BackupDir 'manifest.json') -Raw | ConvertFrom-Json $key = @($manifest.items.PSObject.Properties.Name)[0] $manifest.items.$key.security.file | Should -Be $sidecars[0].Name $manifest.items.$key.security.objects | Should -Be 3 diff --git a/tests/BakNRet.Tests.ps1 b/tests/BakNRet.Tests.ps1 index 9df11af..81d8e11 100644 --- a/tests/BakNRet.Tests.ps1 +++ b/tests/BakNRet.Tests.ps1 @@ -121,7 +121,7 @@ BeforeAll { function Write-ListFile { param([Parameter(Mandatory = $true)][string]$Path, [Parameter(Mandatory = $true)][string]$Content) - [System.IO.File]::WriteAllText($Path, $Content, [System.Text.UTF8Encoding]::new($false)) + [System.IO.File]::WriteAllText($Path, $Content, [System.Text.UTF8Encoding]::new($true)) return $Path } } @@ -348,7 +348,7 @@ Describe '归档命名' { $script:NamingRoot = Join-Path $script:Sandbox 'naming' $script:NamingApp = Join-Path $script:NamingRoot 'Real App' New-Item -ItemType Directory -Path $script:NamingApp -Force | Out-Null - Set-Content -LiteralPath (Join-Path $script:NamingApp 'keep.txt') 'k' + Set-Content -Encoding UTF8 -LiteralPath (Join-Path $script:NamingApp 'keep.txt') 'k' $script:NamingCatalog = Write-ListFile -Path (Join-Path $script:NamingRoot 'cat.psd1') -Content @" @{ @@ -415,8 +415,8 @@ Describe '7z 排除参数翻译' { $script:ExcludeTree = Join-Path $script:Sandbox 'exclude-tree' New-Item -ItemType Directory -Path (Join-Path $script:ExcludeTree 'sub\Cache') -Force | Out-Null New-Item -ItemType Directory -Path (Join-Path $script:ExcludeTree 'Keep') -Force | Out-Null - Set-Content -LiteralPath (Join-Path $script:ExcludeTree 'sub\Cache\x.bin') 'x' - Set-Content -LiteralPath (Join-Path $script:ExcludeTree 'Keep\y.bin') 'y' + Set-Content -Encoding UTF8 -LiteralPath (Join-Path $script:ExcludeTree 'sub\Cache\x.bin') 'x' + Set-Content -Encoding UTF8 -LiteralPath (Join-Path $script:ExcludeTree 'Keep\y.bin') 'y' function New-TestArchiveItem { param([string]$ArchivePath, [string]$RealPath = 'C:\nonexistent-path') @@ -573,8 +573,8 @@ Describe '归档项与暂存目录' { $script:StagingDir = Join-Path $script:StagingRoot 'real-dir' $script:StagingFile = Join-Path $script:StagingRoot 'settings.json' New-Item -ItemType Directory -Path (Join-Path $script:StagingDir 'inner') -Force | Out-Null - Set-Content -LiteralPath (Join-Path $script:StagingDir 'inner\a.txt') 'a' - Set-Content -LiteralPath $script:StagingFile 'file-content' + Set-Content -Encoding UTF8 -LiteralPath (Join-Path $script:StagingDir 'inner\a.txt') 'a' + Set-Content -Encoding UTF8 -LiteralPath $script:StagingFile 'file-content' } It 'New-BaknretArchiveItem 规范化归档内路径并算出 TopName' { @@ -611,7 +611,7 @@ Describe '归档项与暂存目录' { # 文件项:归档内的名字就叫 Cfg(没有扩展名),内容与源文件一致 $cfg = Get-Item -LiteralPath (Join-Path $stage 'Cfg') -Force $cfg.PSIsContainer | Should -BeFalse - (Get-Content -LiteralPath $cfg.FullName -Raw).Trim() | Should -Be 'file-content' + (Get-Content -Encoding UTF8 -LiteralPath $cfg.FullName -Raw).Trim() | Should -Be 'file-content' } finally { Remove-BaknretArchiveStaging -Root $stage } @@ -734,8 +734,8 @@ Describe '软件名录' { New-Item -ItemType Directory -Path (Join-Path $script:CatRoot 'real app') -Force | Out-Null New-Item -ItemType Directory -Path (Join-Path $script:CatRoot 'other') -Force | Out-Null New-Item -ItemType Directory -Path (Join-Path $script:CatRoot 'inc') -Force | Out-Null - Set-Content -LiteralPath (Join-Path $script:CatRoot 'real app\keep.txt') 'k' - Set-Content -LiteralPath (Join-Path $script:CatRoot 'file.txt') 'file-content' + Set-Content -Encoding UTF8 -LiteralPath (Join-Path $script:CatRoot 'real app\keep.txt') 'k' + Set-Content -Encoding UTF8 -LiteralPath (Join-Path $script:CatRoot 'file.txt') 'file-content' $script:CatReal = Join-Path $script:CatRoot 'real app' $script:CatOther = Join-Path $script:CatRoot 'other' @@ -1090,12 +1090,12 @@ Describe '集成:真实 7z 压缩与排除规则' -Skip:(-not $script:HasSeven foreach ($directory in 'Default\Cache', 'Default\Code Cache', 'Default\Extensions', 'component_crx_cache', 'Default\IndexedDB') { New-Item -ItemType Directory -Path (Join-Path $script:IntegrationPath $directory) -Force | Out-Null } - Set-Content -LiteralPath (Join-Path $script:IntegrationPath 'keep.txt') 'keep' - Set-Content -LiteralPath (Join-Path $script:IntegrationPath 'Default\Cache\c.bin') 'c' - Set-Content -LiteralPath (Join-Path $script:IntegrationPath 'Default\Code Cache\cc.bin') 'cc' - Set-Content -LiteralPath (Join-Path $script:IntegrationPath 'Default\Extensions\e.bin') 'e' - Set-Content -LiteralPath (Join-Path $script:IntegrationPath 'component_crx_cache\x.bin') 'x' - Set-Content -LiteralPath (Join-Path $script:IntegrationPath 'Default\IndexedDB\i.bin') 'i' + Set-Content -Encoding UTF8 -LiteralPath (Join-Path $script:IntegrationPath 'keep.txt') 'keep' + Set-Content -Encoding UTF8 -LiteralPath (Join-Path $script:IntegrationPath 'Default\Cache\c.bin') 'c' + Set-Content -Encoding UTF8 -LiteralPath (Join-Path $script:IntegrationPath 'Default\Code Cache\cc.bin') 'cc' + Set-Content -Encoding UTF8 -LiteralPath (Join-Path $script:IntegrationPath 'Default\Extensions\e.bin') 'e' + Set-Content -Encoding UTF8 -LiteralPath (Join-Path $script:IntegrationPath 'component_crx_cache\x.bin') 'x' + Set-Content -Encoding UTF8 -LiteralPath (Join-Path $script:IntegrationPath 'Default\IndexedDB\i.bin') 'i' } It '排除规则与空格处理在真实归档上生效' { @@ -1326,7 +1326,7 @@ Describe '集成:方向标记与孤儿审计' -Skip:(-not ($script:HasSevenZip $script:DirectionRoot = Join-Path $script:Sandbox 'direction' $script:DirectionSource = Join-Path $script:DirectionRoot 'src\Keep Me' New-Item -ItemType Directory -Path $script:DirectionSource -Force | Out-Null - Set-Content -LiteralPath (Join-Path $script:DirectionSource 'data.txt') 'keep' + Set-Content -Encoding UTF8 -LiteralPath (Join-Path $script:DirectionSource 'data.txt') 'keep' $script:DirectionBackupDir = Join-Path $script:DirectionRoot 'Backups' $script:DirectionList = Join-Path $script:DirectionRoot 'list.txt' diff --git a/tests/Restore-Drill.ps1 b/tests/Restore-Drill.ps1 index 309112f..a22e6aa 100644 --- a/tests/Restore-Drill.ps1 +++ b/tests/Restore-Drill.ps1 @@ -444,7 +444,7 @@ foreach ($name in $Entries) { [System.IO.File]::WriteAllText($scratchCatalog, "@{`n '$catalogKey' = @{`n$($slotLines -join "`n")`n }`n}`n", [System.Text.UTF8Encoding]::new($false)) - [System.IO.File]::WriteAllText($scratchList, "$catalogKey`n", [System.Text.UTF8Encoding]::new($false)) + [System.IO.File]::WriteAllText($scratchList, "$catalogKey`n", [System.Text.UTF8Encoding]::new($true)) [System.IO.File]::WriteAllText($scratchConfig, @" @{ BackupDir = '$scratchBackupRoot' @@ -461,7 +461,7 @@ foreach ($name in $Entries) { if ($restore.Code -ne 0) { if ($restore.Log) { - foreach ($line in @(Get-Content -LiteralPath $restore.Log.FullName -ErrorAction SilentlyContinue | Select-Object -Last 12)) { + foreach ($line in @(Get-Content -Encoding UTF8 -LiteralPath $restore.Log.FullName -ErrorAction SilentlyContinue | Select-Object -Last 12)) { Write-Host (" | {0}" -f $line) -ForegroundColor DarkGray } } diff --git a/tests/Run-E2E.ps1 b/tests/Run-E2E.ps1 index 7494923..b1204df 100644 --- a/tests/Run-E2E.ps1 +++ b/tests/Run-E2E.ps1 @@ -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 diff --git a/tests/Run-Tests.ps1 b/tests/Run-Tests.ps1 index adfc35e..224508e 100644 --- a/tests/Run-Tests.ps1 +++ b/tests/Run-Tests.ps1 @@ -385,8 +385,8 @@ Test-Case '归档项的属性集与契约一致(没有旧的 Sources / Dirs / Test-Case 'New-BaknretArchiveStaging:目录走 junction、文件走硬链接,按归档内名字挂载' { $root = Join-Path $itemSandbox 'stage-src' New-Item -ItemType Directory -Path (Join-Path $root 'App') -Force | Out-Null - Set-Content -LiteralPath (Join-Path $root 'App\f.txt') -Value 'x' - Set-Content -LiteralPath (Join-Path $root 'settings.json') -Value '{}' + Set-Content -Encoding UTF8 -LiteralPath (Join-Path $root 'App\f.txt') -Value 'x' + Set-Content -Encoding UTF8 -LiteralPath (Join-Path $root 'settings.json') -Value '{}' $items = @( (New-BaknretArchiveItem -ArchivePath 'AlphaData' -RealPath (Join-Path $root 'App') -Exists $true), @@ -412,7 +412,7 @@ Test-Case 'New-BaknretJunction / Remove-BaknretJunction:只删连接点,不 $target = Join-Path $itemSandbox 'j-target' $link = Join-Path $itemSandbox 'j-link' New-Item -ItemType Directory -Path $target -Force | Out-Null - Set-Content -LiteralPath (Join-Path $target 'keep.txt') -Value 'keep' + Set-Content -Encoding UTF8 -LiteralPath (Join-Path $target 'keep.txt') -Value 'keep' New-BaknretJunction -Path $link -Target $target | Out-Null Assert-Equal 'Junction' (Get-Item -LiteralPath $link -Force).LinkType @@ -630,7 +630,7 @@ Test-Case '配置缺失时返回默认值' { Test-Case '配置嵌套段落合并且不丢默认键' { $path = Join-Path $sandbox 'cfg.psd1' - Set-Content -LiteralPath $path -Value "@{ Encryption = @{ Enabled = `$true }; CompressionLevel = 5 }" + Set-Content -Encoding UTF8 -LiteralPath $path -Value "@{ Encryption = @{ Enabled = `$true }; CompressionLevel = 5 }" $config = Get-BaknretConfig -Path $path Assert-Equal 5 $config.CompressionLevel Assert-True $config.Encryption.Enabled @@ -663,7 +663,7 @@ $configDir = Join-Path $catalogSandbox 'Config' Test-Case '名录解析:裸键、引号键、带 - 与 . 的名字' { $catPath = Join-Path $catalogSandbox 'SoftwareCatalog.psd1' $content = "@{`n FooClolor = @{ DefaultData = @{ Path = '$appDir' } }`n 'scoop-config' = @{ DefaultConfig = @{ Path = '$configDir' } }`n '.ssh' = @{ DefaultData = @{ Path = '$appDir' } }`n}`n" - [System.IO.File]::WriteAllText($catPath, $content, [System.Text.UTF8Encoding]::new($false)) + [System.IO.File]::WriteAllText($catPath, $content, [System.Text.UTF8Encoding]::new($true)) $catalog = Get-SoftwareCatalog -Path $catPath -NoCache Assert-Equal 3 $catalog.Count @@ -675,7 +675,7 @@ Test-Case '名录解析:裸键、引号键、带 - 与 . 的名字' { Test-Case '名录:Slot 按名字排序(哈希表顺序不可依赖)' { $catPath = Join-Path $catalogSandbox 'sorted.psd1' - [System.IO.File]::WriteAllText($catPath, "@{`n FooClolor = @{ Zeta = @{ Path = '$appDir' }; Alpha = @{ Path = '$configDir' } }`n}`n", [System.Text.UTF8Encoding]::new($false)) + [System.IO.File]::WriteAllText($catPath, "@{`n FooClolor = @{ Zeta = @{ Path = '$appDir' }; Alpha = @{ Path = '$configDir' } }`n}`n", [System.Text.UTF8Encoding]::new($true)) $catalog = Get-SoftwareCatalog -Path $catPath -NoCache Assert-Equal 'Multi' $catalog['FooClolor'].Kind @@ -687,8 +687,8 @@ Test-Case '名录:Slot 按名字排序(哈希表顺序不可依赖)' { Test-Case '名录:Slot 对象字段与契约一致' { $catPath = Join-Path $catalogSandbox 'fields.psd1' $filePath = Join-Path $catalogSandbox 'App\settings.json' - Set-Content -LiteralPath $filePath -Value '{}' - [System.IO.File]::WriteAllText($catPath, "@{`n Rich = @{`n Beta = @{ Path = '$filePath'; Encrypt = `$true; Description = 'beta 说明' }`n Alpha = @{ Path = '$configDir'; Exclude = '!*Cache,sub\one'; Include = 'Modules:$appDir' }`n }`n}`n", [System.Text.UTF8Encoding]::new($false)) + Set-Content -Encoding UTF8 -LiteralPath $filePath -Value '{}' + [System.IO.File]::WriteAllText($catPath, "@{`n Rich = @{`n Beta = @{ Path = '$filePath'; Encrypt = `$true; Description = 'beta 说明' }`n Alpha = @{ Path = '$configDir'; Exclude = '!*Cache,sub\one'; Include = 'Modules:$appDir' }`n }`n}`n", [System.Text.UTF8Encoding]::new($true)) $catalog = Get-SoftwareCatalog -Path $catPath -NoCache $entry = $catalog['Rich'] @@ -721,7 +721,7 @@ Test-Case '名录解析:目录带版本后缀时按前缀补全(legendary_2. $root = Join-Path $catalogSandbox 'probe' New-Item -ItemType Directory -Path (Join-Path $root 'legendary_2.0.4') -Force | Out-Null $catPath = Join-Path $catalogSandbox 'suffix.psd1' - [System.IO.File]::WriteAllText($catPath, "@{`n legendary = @{ DefaultConfig = @{ Path = '$root\legendary' } }`n}`n", [System.Text.UTF8Encoding]::new($false)) + [System.IO.File]::WriteAllText($catPath, "@{`n legendary = @{ DefaultConfig = @{ Path = '$root\legendary' } }`n}`n", [System.Text.UTF8Encoding]::new($true)) $catalog = Get-SoftwareCatalog -Path $catPath -NoCache Assert-Equal 'Single' $catalog['legendary'].Kind @@ -733,7 +733,7 @@ Test-Case '名录解析:不会把 Legendary 误配成 LegendarySomething' { $root = Join-Path $catalogSandbox 'strict' New-Item -ItemType Directory -Path (Join-Path $root 'LegendarySomething') -Force | Out-Null $catPath = Join-Path $catalogSandbox 'strict.psd1' - [System.IO.File]::WriteAllText($catPath, "@{`n Legendary = @{ DefaultConfig = @{ Path = '$root\Legendary' } }`n}`n", [System.Text.UTF8Encoding]::new($false)) + [System.IO.File]::WriteAllText($catPath, "@{`n Legendary = @{ DefaultConfig = @{ Path = '$root\Legendary' } }`n}`n", [System.Text.UTF8Encoding]::new($true)) $catalog = Get-SoftwareCatalog -Path $catPath -NoCache Assert-Equal 'Unresolved' $catalog['Legendary'].Kind '必须以下一个字符是 _ 或 - 为界' @@ -759,7 +759,7 @@ Test-Case '名录:一个 Slot 补全出多个目录时明确报错(否则会 New-Item -ItemType Directory -Path (Join-Path $root 'Legendary_1') -Force | Out-Null New-Item -ItemType Directory -Path (Join-Path $root 'Legendary_2') -Force | Out-Null $catPath = Join-Path $catalogSandbox 'multicand.psd1' - [System.IO.File]::WriteAllText($catPath, "@{`n Legendary = @{ DefaultConfig = @{ Path = '$root\Legendary' } }`n}`n", [System.Text.UTF8Encoding]::new($false)) + [System.IO.File]::WriteAllText($catPath, "@{`n Legendary = @{ DefaultConfig = @{ Path = '$root\Legendary' } }`n}`n", [System.Text.UTF8Encoding]::new($true)) $catalog = Get-SoftwareCatalog -Path $catPath -NoCache $entry = $catalog['Legendary'] @@ -769,7 +769,7 @@ Test-Case '名录:一个 Slot 补全出多个目录时明确报错(否则会 Test-Case '名录:Slot 写法不对 / 缺少 Path 都记成 Error,而不是崩掉' { $bad = Join-Path $catalogSandbox 'bad.psd1' - [System.IO.File]::WriteAllText($bad, "@{`n A = @{ S = 'C:\x' }`n B = @{ S = @{ Description = 'x' } }`n}`n", [System.Text.UTF8Encoding]::new($false)) + [System.IO.File]::WriteAllText($bad, "@{`n A = @{ S = 'C:\x' }`n B = @{ S = @{ Description = 'x' } }`n}`n", [System.Text.UTF8Encoding]::new($true)) $catalog = Get-SoftwareCatalog -Path $bad -NoCache Assert-Equal 'Invalid' $catalog['A'].Kind Assert-True ($catalog['A'].Error -like '*Slot S*') @@ -779,12 +779,12 @@ Test-Case '名录:Slot 写法不对 / 缺少 Path 都记成 Error,而不是 Test-Case '名录:裸字符串 / 数组等旧结构会被 ERROR 跳过(不再当目录用)' { $legacy = Join-Path $catalogSandbox 'legacy.psd1' - [System.IO.File]::WriteAllText($legacy, "@{`n A = 'C:\Programs\A'`n B = @('C:\1','C:\2')`n}`n", [System.Text.UTF8Encoding]::new($false)) + [System.IO.File]::WriteAllText($legacy, "@{`n A = 'C:\Programs\A'`n B = @('C:\1','C:\2')`n}`n", [System.Text.UTF8Encoding]::new($true)) $catalog = Get-SoftwareCatalog -Path $legacy -NoCache Assert-Equal 0 $catalog.Count '旧结构必须被跳过,不能猜成目录' $dirs = Join-Path $catalogSandbox 'dirs.psd1' - [System.IO.File]::WriteAllText($dirs, "@{`n C = @{ Dirs = @('C:\1','C:\2') }`n}`n", [System.Text.UTF8Encoding]::new($false)) + [System.IO.File]::WriteAllText($dirs, "@{`n C = @{ Dirs = @('C:\1','C:\2') }`n}`n", [System.Text.UTF8Encoding]::new($true)) $catalog = Get-SoftwareCatalog -Path $dirs -NoCache Assert-Equal 1 $catalog.Count Assert-Equal 'Invalid' $catalog['C'].Kind @@ -800,8 +800,8 @@ Test-Case '名录:%VAR% 与 $( ... ) 子表达式都会被展开' { Test-Case '名录:Includes 分文件维护会被合并' { $extraPath = Join-Path $catalogSandbox 'Extra.psd1' $basePath = Join-Path $catalogSandbox 'Base.psd1' - [System.IO.File]::WriteAllText($extraPath, "@{`n 'extra-app' = @{ D = @{ Path = '$appDir' } }`n}`n", [System.Text.UTF8Encoding]::new($false)) - [System.IO.File]::WriteAllText($basePath, "@{`n Includes = @('Extra.psd1')`n 'base-app' = @{ D = @{ Path = '$configDir' } }`n}`n", [System.Text.UTF8Encoding]::new($false)) + [System.IO.File]::WriteAllText($extraPath, "@{`n 'extra-app' = @{ D = @{ Path = '$appDir' } }`n}`n", [System.Text.UTF8Encoding]::new($true)) + [System.IO.File]::WriteAllText($basePath, "@{`n Includes = @('Extra.psd1')`n 'base-app' = @{ D = @{ Path = '$configDir' } }`n}`n", [System.Text.UTF8Encoding]::new($true)) $catalog = Get-SoftwareCatalog -Path $basePath -NoCache Assert-True $catalog.ContainsKey('base-app') @@ -810,7 +810,7 @@ Test-Case '名录:Includes 分文件维护会被合并' { Test-Case '名录:结果按文件指纹缓存,-NoCache 强制重读' { $catPath = Join-Path $catalogSandbox 'cache.psd1' - [System.IO.File]::WriteAllText($catPath, "@{`n Cached = @{ D = @{ Path = '$appDir' } }`n}`n", [System.Text.UTF8Encoding]::new($false)) + [System.IO.File]::WriteAllText($catPath, "@{`n Cached = @{ D = @{ Path = '$appDir' } }`n}`n", [System.Text.UTF8Encoding]::new($true)) $first = Get-SoftwareCatalog -Path $catPath $second = Get-SoftwareCatalog -Path $catPath @@ -819,7 +819,7 @@ Test-Case '名录:结果按文件指纹缓存,-NoCache 强制重读' { $fresh = Get-SoftwareCatalog -Path $catPath -NoCache Assert-False ([object]::ReferenceEquals($first, $fresh)) '-NoCache 必须重新读' - [System.IO.File]::WriteAllText($catPath, "@{`n Cached = @{ D = @{ Path = '$configDir' } }`n}`n", [System.Text.UTF8Encoding]::new($false)) + [System.IO.File]::WriteAllText($catPath, "@{`n Cached = @{ D = @{ Path = '$configDir' } }`n}`n", [System.Text.UTF8Encoding]::new($true)) $changed = Get-SoftwareCatalog -Path $catPath Assert-Equal $configDir $changed['Cached'].Slots[0].Resolved '文件变了缓存必须失效' } @@ -918,7 +918,7 @@ Test-Case '名录里没有该名字:BaseName 退回可读目录名,并给出 Test-Case '名录里的路径不存在时仍给出归档项(恢复要靠它还原回原位)' { $catPath = Join-Path $resolveSandbox 'gone.psd1' - [System.IO.File]::WriteAllText($catPath, "@{`n 'gone-app' = @{ DefaultData = @{ Path = 'C:\definitely\not\here' } }`n}`n", [System.Text.UTF8Encoding]::new($false)) + [System.IO.File]::WriteAllText($catPath, "@{`n 'gone-app' = @{ DefaultData = @{ Path = 'C:\definitely\not\here' } }`n}`n", [System.Text.UTF8Encoding]::new($true)) $item = ConvertFrom-BackupListLine -Line 'gone-app' $resolved = Resolve-BackupEntry -Entry $item -CatalogPath $catPath Assert-Equal 1 $resolved.Items.Count '恢复端据 Items 还原' @@ -943,7 +943,7 @@ Test-Case '条目级 :+ 覆盖名录里的 Include(不是叠加)' { $incDir = Join-Path $resolveSandbox 'Inc' New-Item -ItemType Directory -Path $incDir -Force | Out-Null $catPath = Join-Path $resolveSandbox 'withinc.psd1' - [System.IO.File]::WriteAllText($catPath, "@{`n IncApp = @{ DefaultData = @{ Path = '$dirA'; Include = 'CatalogInc:$incDir' } }`n}`n", [System.Text.UTF8Encoding]::new($false)) + [System.IO.File]::WriteAllText($catPath, "@{`n IncApp = @{ DefaultData = @{ Path = '$dirA'; Include = 'CatalogInc:$incDir' } }`n}`n", [System.Text.UTF8Encoding]::new($true)) $plain = Resolve-BackupEntry -Entry (ConvertFrom-BackupListLine -Line 'IncApp') -CatalogPath $catPath Assert-Equal 2 $plain.Items.Count @@ -988,7 +988,7 @@ Test-Case '方向标记随条目一路传到 Resolve-BackupEntry' { Test-Case '条目级 :- 覆盖名录里的 Exclude(不是叠加)' { $catPath = Join-Path $resolveSandbox 'withexc.psd1' - [System.IO.File]::WriteAllText($catPath, "@{`n ExcApp = @{ DefaultData = @{ Path = '$dirA'; Exclude = '!*CatalogCache' } }`n}`n", [System.Text.UTF8Encoding]::new($false)) + [System.IO.File]::WriteAllText($catPath, "@{`n ExcApp = @{ DefaultData = @{ Path = '$dirA'; Exclude = '!*CatalogCache' } }`n}`n", [System.Text.UTF8Encoding]::new($true)) $plain = Resolve-BackupEntry -Entry (ConvertFrom-BackupListLine -Line 'ExcApp') -CatalogPath $catPath Assert-False $plain.HasExcludeOverride @@ -1018,12 +1018,12 @@ if (-not $sevenZip) { foreach ($d in 'Default\Cache', 'Default\Code Cache', 'Default\Extensions', 'component_crx_cache', 'Default\IndexedDB') { New-Item -ItemType Directory -Path (Join-Path $source $d) -Force | Out-Null } - Set-Content -LiteralPath (Join-Path $source 'keep.txt') 'keep' - Set-Content -LiteralPath (Join-Path $source 'Default\Cache\c.bin') 'c' - Set-Content -LiteralPath (Join-Path $source 'Default\Code Cache\cc.bin') 'cc' - Set-Content -LiteralPath (Join-Path $source 'Default\Extensions\e.bin') 'e' - Set-Content -LiteralPath (Join-Path $source 'component_crx_cache\x.bin') 'x' - Set-Content -LiteralPath (Join-Path $source 'Default\IndexedDB\i.bin') 'i' + Set-Content -Encoding UTF8 -LiteralPath (Join-Path $source 'keep.txt') 'keep' + Set-Content -Encoding UTF8 -LiteralPath (Join-Path $source 'Default\Cache\c.bin') 'c' + Set-Content -Encoding UTF8 -LiteralPath (Join-Path $source 'Default\Code Cache\cc.bin') 'cc' + Set-Content -Encoding UTF8 -LiteralPath (Join-Path $source 'Default\Extensions\e.bin') 'e' + Set-Content -Encoding UTF8 -LiteralPath (Join-Path $source 'component_crx_cache\x.bin') 'x' + Set-Content -Encoding UTF8 -LiteralPath (Join-Path $source 'Default\IndexedDB\i.bin') 'i' $patterns = @('!*Cache', 'component_crx_cache', 'Default\Code Cache', 'Default\Extensions', 'Default\IndexedDB') $archiveItem = New-BaknretArchiveItem -ArchivePath $itemName -RealPath $source -Kind 'path' -Exists $true @@ -1054,7 +1054,7 @@ if (-not $sevenZip) { $source = Join-Path $root $itemName foreach ($d in 'Cache1', 'Cache2', 'Keep', 'sub\Cache3') { New-Item -ItemType Directory -Path (Join-Path $source $d) -Force | Out-Null - Set-Content -LiteralPath (Join-Path $source "$d\x.bin") 'x' + Set-Content -Encoding UTF8 -LiteralPath (Join-Path $source "$d\x.bin") 'x' } $archiveItem = New-BaknretArchiveItem -ArchivePath $itemName -RealPath $source -Kind 'slot' -Exists $true @@ -1096,8 +1096,8 @@ if (-not $sevenZip) { $root = Join-Path $sandbox 'topnames' New-Item -ItemType Directory -Path (Join-Path $root 'Alpha') -Force | Out-Null New-Item -ItemType Directory -Path (Join-Path $root 'Beta') -Force | Out-Null - Set-Content -LiteralPath (Join-Path $root 'Alpha\a.txt') 'a' - Set-Content -LiteralPath (Join-Path $root 'Beta\b.txt') 'b' + Set-Content -Encoding UTF8 -LiteralPath (Join-Path $root 'Alpha\a.txt') 'a' + Set-Content -Encoding UTF8 -LiteralPath (Join-Path $root 'Beta\b.txt') 'b' $archive = Join-Path $sandbox 'topnames.7z' $code = Invoke-ExternalCommand -FilePath $sevenZip -ArgumentList @('a', '-t7z', '-mx=1', '-bso0', '-bsp0', $archive, 'Alpha', 'Beta') -WorkingDirectory $root Assert-Equal 0 $code diff --git a/tools/Rename-Archives.ps1 b/tools/Rename-Archives.ps1 index 397deba..0698435 100644 --- a/tools/Rename-Archives.ps1 +++ b/tools/Rename-Archives.ps1 @@ -117,7 +117,7 @@ $conflicts = @() $seenNew = @{} $entries = @() -foreach ($line in (Get-Content -LiteralPath $BackupListPath)) { +foreach ($line in (Get-Content -Encoding UTF8 -LiteralPath $BackupListPath)) { $item = ConvertFrom-BackupListLine -Line $line if (-not $item) { continue } diff --git a/tools/lab/Lab.ps1 b/tools/lab/Lab.ps1 index 9fd6e7c..42aa621 100644 --- a/tools/lab/Lab.ps1 +++ b/tools/lab/Lab.ps1 @@ -267,7 +267,7 @@ switch ($Verb) { while ($sw.Elapsed.TotalMinutes -lt 30) { if (Test-LabGuestReady) { Write-LabLog ("VM 已就绪(等待 {0} 分钟)" -f [math]::Round($sw.Elapsed.TotalMinutes, 1)) 'STEP' - $facts = Invoke-LabCommand -ScriptBlock { Get-Content 'C:\BakNRet-Lab\state\provisioned.json' -Raw } + $facts = Invoke-LabCommand -ScriptBlock { Get-Content -Encoding UTF8 'C:\BakNRet-Lab\state\provisioned.json' -Raw } Write-Host $facts break } diff --git a/tools/lab/New-BakNRetLab.ps1 b/tools/lab/New-BakNRetLab.ps1 index 2ebad2c..2071519 100644 --- a/tools/lab/New-BakNRetLab.ps1 +++ b/tools/lab/New-BakNRetLab.ps1 @@ -224,7 +224,7 @@ function Wait-LabProvision { while ($sw.Elapsed.TotalMinutes -lt 30) { if (Test-LabGuestReady) { Write-LabLog "供给完成,耗时 $([math]::Round($sw.Elapsed.TotalMinutes,1)) 分钟" 'STEP' - $facts = Invoke-LabCommand -ScriptBlock { Get-Content 'C:\BakNRet-Lab\state\provisioned.json' -Raw } + $facts = Invoke-LabCommand -ScriptBlock { Get-Content -Encoding UTF8 'C:\BakNRet-Lab\state\provisioned.json' -Raw } Write-Host $facts return } diff --git a/tools/lab/payload/run-acl-scenario.ps1 b/tools/lab/payload/run-acl-scenario.ps1 index 57241bf..2f7416a 100644 --- a/tools/lab/payload/run-acl-scenario.ps1 +++ b/tools/lab/payload/run-acl-scenario.ps1 @@ -340,7 +340,7 @@ $listPath = Join-Path $WorkRoot 'BackupList.txt' $entries = @() if ($vscodeReady) { $entries += $vscodeApp; $entries += $vscodePersist } $entries += $bData -[System.IO.File]::WriteAllText($listPath, (($entries -join [Environment]::NewLine) + [Environment]::NewLine), [System.Text.UTF8Encoding]::new($false)) +[System.IO.File]::WriteAllText($listPath, (($entries -join [Environment]::NewLine) + [Environment]::NewLine), [System.Text.UTF8Encoding]::new($true)) $configPath = Join-Path $WorkRoot 'BackupConfig.psd1' $configText = @" @@ -359,7 +359,7 @@ $configText = @" DefaultExcludes = @('!Thumbs.db', '!desktop.ini') } "@ -[System.IO.File]::WriteAllText($configPath, $configText, [System.Text.UTF8Encoding]::new($false)) +[System.IO.File]::WriteAllText($configPath, $configText, [System.Text.UTF8Encoding]::new($true)) Write-Host '' Write-Host '[备份] Backup.ps1' -ForegroundColor Yellow @@ -406,7 +406,7 @@ if ($vscodeReady) { $settingsOk = $false if ($settingsPath -and (Test-Path -LiteralPath $settingsPath)) { - $settingsOk = (Get-Content -LiteralPath $settingsPath -Raw) -match [regex]::Escape($probe) + $settingsOk = (Get-Content -Encoding UTF8 -LiteralPath $settingsPath -Raw) -match [regex]::Escape($probe) } Test-Scenario 'A: 改过的 settings.json 被原样恢复(读得到、内容对)' $settingsOk $settingsPath