From 187d2759fd5f00475e6d7f4f2b23921b72f16fba Mon Sep 17 00:00:00 2001 From: Shuery <2463253700@qq.com> Date: Sun, 27 Sep 2026 09:46:08 +0800 Subject: [PATCH] =?UTF-8?q?style:=20=E6=8C=89=E5=BE=AE=E8=BD=AF=E8=A7=84?= =?UTF-8?q?=E8=8C=83=E8=90=BD=E5=9C=B0=E9=9D=99=E6=80=81=E5=88=86=E6=9E=90?= =?UTF-8?q?=EF=BC=8C=E5=B9=B6=E5=85=A8=E4=BB=93=E6=9C=BA=E6=A2=B0=E9=87=8D?= =?UTF-8?q?=E6=8E=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 三件事: 1) tools\Install-TestDependencies.ps1 现在也把 PSScriptAnalyzer 装进仓库内的 .tools\modules (不动机器上的全局模块,与 Pester 同一策略)。 2) PSScriptAnalyzerSettings.psd1:这是必要的,不是装饰 —— 那 6 条格式规则 (括号、缩进、空格、对齐、大小写)默认全是 Disabled,所以不带 -Settings 的 `Invoke-ScriptAnalyzer -Severity Warning,Error` 会**静默漏掉全部排版问题**。本文件用 Rules 把它们打开(而不是用 IncludeRules 换一套),于是默认规则与格式规则同时生效。 三条有意的排除都写明了理由:PSAvoidUsingWriteHost(彩色控制台输出是这份工具的刻意设计)、 PSUseShouldProcessForStateChangingFunctions(WhatIf 的边界在入口脚本,给库里 27 个改状态的 函数都加上反而会"静默跳过",备份看着成功却什么都没做)、PSAvoidUsingPlainTextForPassword (7z 只接受命令行口令,这是 7z 的限制,README 里写明了取舍)。 3) tools\Invoke-Analyzer.ps1:独立门禁(不塞进 Pester 用例 —— 套件跑一次二十多秒, 混进去会让"测试红了"这句话失去分辨力),路径过滤与验收门槛的 Encode/Parse 两层一致。 全仓重排结果:706 条告警 -> 67 条。修掉的 639 条全部是格式(闭括号 168、空格 80、 对齐 68、缩进 60、行长 229)。重排后 9/9 验收全绿、100 个文件两版解析零错、 276 个断言原样通过 —— 机械重排没有改变任何可观察行为。 如实说明两件事: * 行长上限设成 160,**不是**官方默认的 120。120 在本仓库意味着 270 处改动(主要是 中文注释与测试夹具里的一行式目录),160 意味着 41 处。160 仍是"宽但可读",而理由是写在 配置文件里的:这不是悄悄放宽,想收紧到 120 时那份清单就在分析器输出里。 * 剩余 67 条里,41 条是上面那批行长,其余 26 条是分析器找出的真问题(未使用参数 6、 空 catch 6、MD5 指纹 1、覆盖内置命令 1、switch 默认值 1 等)。其中 Find-ChildDirectoryByName 的 MaxDepth 参数从未被使用 —— 也就是配置里的 CatalogMaxDepth = 5 是假的,前缀补全实际只查 1 层。这条要改行为、且影响真实名录的解析 结果,留给你拍板,不在本提交里动手。 --- Backup.ps1 | 100 ++++++++++------- BakNRet/Private/Import-BaknretDataFile.ps1 | 3 +- .../Public/Convert-BackupFileNameToPath.ps1 | 7 +- BakNRet/Public/ConvertFrom-BackupListLine.ps1 | 12 +- BakNRet/Public/Enable-BaknretPrivilege.ps1 | 6 +- BakNRet/Public/Enter-BaknretRunLock.ps1 | 3 +- BakNRet/Public/Exit-BaknretRunLock.ps1 | 3 +- BakNRet/Public/Expand-CatalogPathText.ps1 | 3 +- BakNRet/Public/Find-ChildDirectoryByName.ps1 | 9 +- BakNRet/Public/Format-CatalogName.ps1 | 4 +- BakNRet/Public/Get-ArchiveTopLevelNames.ps1 | 8 +- BakNRet/Public/Get-BackupBaseName.ps1 | 7 +- BakNRet/Public/Get-BaknretConfig.ps1 | 6 +- BakNRet/Public/Get-BaknretFreeSpaceGB.ps1 | 3 +- BakNRet/Public/Get-BaknretPassword.ps1 | 6 +- BakNRet/Public/Get-BaknretRegexExclude.ps1 | 3 +- BakNRet/Public/Get-BaknretSecurityRecord.ps1 | 34 +++--- BakNRet/Public/Get-BaknretSecurityRecords.ps1 | 3 +- .../Get-BaknretSecuritySddlWithStale.ps1 | 9 +- BakNRet/Public/Get-FolderSummary.ps1 | 3 +- BakNRet/Public/Get-Optimized7zArgument.ps1 | 14 ++- BakNRet/Public/Get-SoftwareCatalog.ps1 | 12 +- BakNRet/Public/Invoke-ExternalCommand.ps1 | 3 +- .../Public/Move-BaknretArchiveIntoPlace.ps1 | 3 +- BakNRet/Public/New-BaknretArchiveStaging.ps1 | 12 +- BakNRet/Public/Read-BaknretManifest.ps1 | 3 +- .../Public/Read-BaknretSecuritySidecar.ps1 | 3 +- BakNRet/Public/Remove-BaknretJunction.ps1 | 3 +- BakNRet/Public/Resolve-BackupEntry.ps1 | 18 ++- BakNRet/Public/Restore-BaknretSecurity.ps1 | 19 ++-- BakNRet/Public/Set-BaknretObjectSecurity.ps1 | 6 +- BakNRet/Public/Test-BaknretPathExcluded.ps1 | 3 +- BakNRet/Public/Write-Log.ps1 | 3 +- PSScriptAnalyzerSettings.psd1 | 106 ++++++++++++++++++ Restore.ps1 | 62 ++++++---- SoftwareCatalog.psd1 | 2 +- test.ps1 | 12 +- tests/BakNRet.Formats.Tests.ps1 | 21 ++-- tests/BakNRet.Security.Tests.ps1 | 25 +++-- tests/BakNRet.Tests.ps1 | 48 ++++---- tests/Restore-Drill.ps1 | 28 +++-- tests/Run-E2E.ps1 | 19 ++-- tests/Run-RealSmoke.ps1 | 9 +- tests/Run-Tests.ps1 | 27 +++-- tests/TestHelpers.psm1 | 6 +- tools/Install-TestDependencies.ps1 | 85 ++++++++------ tools/Invoke-Analyzer.ps1 | 101 +++++++++++++++++ tools/Register-BackupTask.ps1 | 6 +- tools/Rename-Archives.ps1 | 5 +- tools/Set-SourceEncoding.ps1 | 3 +- tools/lab/Lab-Common.ps1 | 45 ++++---- tools/lab/Lab.ps1 | 47 ++++---- tools/lab/New-BakNRetLab.ps1 | 39 ++++--- tools/lab/payload/lab-fixtures.ps1 | 20 ++-- tools/lab/payload/provision.ps1 | 22 ++-- tools/lab/payload/run-acl-scenario.ps1 | 50 +++++---- tools/lab/payload/run-drill.ps1 | 4 +- tools/lab/payload/run-suite-utf8.ps1 | 4 +- tools/lab/payload/sandbox/BackupConfig.psd1 | 2 +- .../lab/payload/sandbox/SoftwareCatalog.psd1 | 14 +-- 60 files changed, 769 insertions(+), 377 deletions(-) create mode 100644 PSScriptAnalyzerSettings.psd1 create mode 100644 tools/Invoke-Analyzer.ps1 diff --git a/Backup.ps1 b/Backup.ps1 index c71d58c..ca4572a 100644 --- a/Backup.ps1 +++ b/Backup.ps1 @@ -162,7 +162,8 @@ if (-not $tool) { $toolVersion = try { $info = (Get-Item -LiteralPath $tool.Command -ErrorAction Stop).VersionInfo if ($info.ProductVersion) { $info.ProductVersion } elseif ($info.FileVersion) { $info.FileVersion } else { $null } -} catch { $null } +} +catch { $null } Write-Log ("压缩工具:{0}{1}" -f $tool.Name, $(if ($toolVersion) { "($toolVersion)" } else { '' })) $manifest = Read-BaknretManifest -Path $manifestPath @@ -206,32 +207,32 @@ function Test-ItemSelected { function New-ItemRecord { param([string]$BaseName, [string]$Source, [string]$ResolvedSource, [string]$Phase) return [ordered]@{ - baseName = $BaseName - source = $Source - resolvedSource = $ResolvedSource - roots = @() - layouts = @() - catalog = $null - archive = $null - action = $null - reason = $null - phase = $Phase - attemptedAt = (Get-Date).ToString('o') - finishedAt = $null - durationSec = $null - exitCode = $null - verified = $false - warnings = $false + baseName = $BaseName + source = $Source + resolvedSource = $ResolvedSource + roots = @() + layouts = @() + catalog = $null + archive = $null + action = $null + reason = $null + phase = $Phase + attemptedAt = (Get-Date).ToString('o') + finishedAt = $null + durationSec = $null + exitCode = $null + verified = $false + warnings = $false attemptWarnings = $false - encrypted = $false - security = $null - sourceFiles = $null - sourceBytes = $null - archiveBytes = $null - sha256 = $null - lastSuccessAt = $null - successCount = 0 - failCount = 0 + encrypted = $false + security = $null + sourceFiles = $null + sourceBytes = $null + archiveBytes = $null + sha256 = $null + lastSuccessAt = $null + successCount = 0 + failCount = 0 } } @@ -250,7 +251,8 @@ function Save-ItemRecord { # 否则"因为不完整而保留旧归档"之后,下一次就失去保护了。 if ($Action -eq 'backed-up') { $Record.warnings = $ArchiveWarnings - } elseif ($previous -and ($previous.PSObject.Properties.Name -contains 'warnings')) { + } + elseif ($previous -and ($previous.PSObject.Properties.Name -contains 'warnings')) { $Record.warnings = [bool]$previous.warnings } @@ -269,7 +271,8 @@ function Save-ItemRecord { if ($Action -eq 'backed-up') { $Record.lastSuccessAt = $Record.finishedAt $Record.successCount = [int]$Record.successCount + 1 - } elseif ($Action -eq 'failed') { + } + elseif ($Action -eq 'failed') { $Record.failCount = [int]$Record.failCount + 1 } @@ -405,7 +408,8 @@ function Invoke-BackupItem { Move-BaknretArchiveIntoPlace -TempPath $tempPath -DestinationPath $FinalPath return [pscustomobject]@{ Ok = $true; ExitCode = 0; Warnings = $warnings; Reason = $null } - } catch { + } + catch { if (Test-Path -LiteralPath $tempPath) { Remove-Item -LiteralPath $tempPath -Force -ErrorAction SilentlyContinue } @@ -467,7 +471,8 @@ foreach ($planLine in $lines) { $planEstimate = if ($planExistingBytes -gt 0) { [int64][math]::Min([double]$planSourceBytes, [double]$planExistingBytes * 1.3) - } else { + } + else { # 没有历史归档可比时按"完全不压缩"的悲观值估,宁可报多不报少 $planSourceBytes } @@ -486,7 +491,8 @@ $freeNowGB = Get-BaknretFreeSpaceGB -Path $BackupDir if ($spacePlan.Count -eq 0) { Write-Log '备份前空间预估:本次没有需要重打的条目(源未更新或源不存在),不会写入新归档' -Level INFO -} else { +} +else { $spacePeak = [double]0 $spaceCumulative = [double]0 foreach ($plan in $spacePlan) { @@ -515,9 +521,11 @@ if ($spacePlan.Count -eq 0) { if ($freeNowGB -lt 0) { Write-Log ' 结论:读不到目标卷可用空间,请自行确认是否够用' -Level WARN - } elseif ($peakGB -le $freeNowGB) { + } + elseif ($peakGB -le $freeNowGB) { Write-Log (" 结论:空间足够(预计用 {0} GB / 可用 {1} GB)" -f [math]::Round($peakGB, 2), $freeNowGB) -Level INFO - } else { + } + else { Write-Log (" 结论:空间可能不够!预计需要 {0} GB,可用只有 {1} GB,差 {2} GB" -f ` [math]::Round($peakGB, 2), $freeNowGB, [math]::Round($peakGB - $freeNowGB, 2)) -Level WARN Write-Log ' 仍会继续执行:真正放不下的条目会被逐条目守卫跳过。建议先腾空间,或用 -Only / -Skip 分批备份。' -Level WARN @@ -592,7 +600,8 @@ foreach ($line in $lines) { $planCatalogExcludes = @() if ($resolved.HasExcludeOverride) { $planListExcludes = @($resolved.ExcludePatterns) - } else { + } + else { $planCatalogExcludes = @($resolved.Items | ForEach-Object { @($_.Exclude) } | Where-Object { $_ } | Select-Object -Unique) } Write-BackupEntryPlan -Resolved $resolved -DisplayPath $displayPath ` @@ -719,7 +728,8 @@ foreach ($line in $lines) { # 再逐项翻译成 7z 的 -x! / -xr!,最后去重合并成一次调用的参数。 $patternSource = if ($resolved.HasExcludeOverride) { @($resolved.ExcludePatterns) - } else { + } + else { @($resolved.Items | ForEach-Object { @($_.Exclude) } | Where-Object { $_ } | Select-Object -Unique) } $allPatterns = @($script:Config.DefaultExcludes) + $patternSource @@ -759,9 +769,11 @@ foreach ($line in $lines) { $result = Invoke-BackupItem -SourceItems $liveItems -StagingRoot $stagingRoot ` -FinalPath $finalPath -ExcludePatterns $effectiveExcludes -UseEncryption:$useEncryption ` -ProtectPrevious:$protectPrevious -AcceptWarnings:$AcceptWarnings - } catch { + } + catch { $result = [pscustomobject]@{ Ok = $false; ExitCode = $null; Warnings = $false; Reason = "准备归档内容失败:$_" } - } finally { + } + finally { Remove-BaknretArchiveStaging -Root $stagingRoot } @@ -782,7 +794,8 @@ foreach ($line in $lines) { if ($result.Warnings) { Write-Log "备份成功(压缩工具报告了警告,可能有文件被占用而没打进归档): $displayPath" -Level WARN Write-Log ' 该归档在 manifest 里标记为 warnings=true;如果以后现有归档是完整的,会拒绝被它覆盖' -Level WARN - } else { + } + else { Write-Log "备份成功: $baseName" -Level INFO } @@ -823,7 +836,8 @@ foreach ($line in $lines) { Write-Log (" {0} 个对象的安全描述符读不到(恢复后它们的属主/ACL 会是新建对象的默认值),例如:{1}" -f ` $capture.Errors, ($unreadable -join '、')) -Level WARN } - } catch { + } + catch { $securityFailed++ Write-Log "安全描述符采集/写盘失败:$displayPath —— $_" -Level WARN $record.security = [ordered]@{ file = $sidecarName; error = "$_" } @@ -862,7 +876,8 @@ foreach ($line in $lines) { if ($DryRun) { Write-Log '试运行:manifest 与归档都不会被写入' -Level INFO -} else { +} +else { # manifest 里写了 archive 的记录,磁盘上就必须真有那个文件 $clearedArchiveFields = Sync-BaknretManifestArchive -Manifest $manifest -BackupDir $BackupDir if ($clearedArchiveFields.Count -gt 0) { @@ -888,7 +903,7 @@ if (-not $DryRun -and $Only.Count -eq 0 -and $Skip.Count -eq 0) { foreach ($key in $seenBaseNames.Keys) { $known[$key] = $true } $orphanArchives = @(Get-ChildItem -LiteralPath $BackupDir -File -Force -ErrorAction SilentlyContinue | - Where-Object { $_.Extension.ToLower() -in @('.7z', '.rar', '.zip', '.tar') -and -not $known.ContainsKey($_.BaseName) }) + Where-Object { $_.Extension.ToLower() -in @('.7z', '.rar', '.zip', '.tar') -and -not $known.ContainsKey($_.BaseName) }) if ($orphanArchives.Count -gt 0) { Write-Log ("发现 {0} 个孤儿归档(当前清单里没有任何条目指向,恢复不到,注意别误删):" -f $orphanArchives.Count) -Level WARN @@ -896,7 +911,8 @@ if (-not $DryRun -and $Only.Count -eq 0 -and $Skip.Count -eq 0) { $inManifest = $manifest.items.Contains($orphan.BaseName) Write-Log (" - {0}({1:N1} MB,{2}){3}" -f $orphan.Name, ($orphan.Length / 1MB), $orphan.LastWriteTime, $(if ($inManifest) { ';manifest 里还留着它的历史记录,但清单里已经没有了' } else { '' })) -Level WARN } - } else { + } + else { Write-Log '孤儿归档审计:没有发现(所有归档都有清单条目指向)' -Level DEBUG } } diff --git a/BakNRet/Private/Import-BaknretDataFile.ps1 b/BakNRet/Private/Import-BaknretDataFile.ps1 index ee62a35..d980c25 100644 --- a/BakNRet/Private/Import-BaknretDataFile.ps1 +++ b/BakNRet/Private/Import-BaknretDataFile.ps1 @@ -20,7 +20,8 @@ try { return Import-PowerShellDataFile -LiteralPath $Path -ErrorAction Stop - } catch { + } + catch { $firstLine = ([string]$_.Exception.Message) -split "`r?`n" | Select-Object -First 1 Write-Log "psd1 里有 Import-PowerShellDataFile 不接受的表达式($firstLine),改用 PowerShell 求值:$Path" -Level DEBUG $raw = [System.IO.File]::ReadAllText($Path) diff --git a/BakNRet/Public/Convert-BackupFileNameToPath.ps1 b/BakNRet/Public/Convert-BackupFileNameToPath.ps1 index 4f5dcae..ccd41ac 100644 --- a/BakNRet/Public/Convert-BackupFileNameToPath.ps1 +++ b/BakNRet/Public/Convert-BackupFileNameToPath.ps1 @@ -18,13 +18,14 @@ $parts = @($pathPart -split '\+' | Where-Object { -not [string]::IsNullOrEmpty($_) }) $parts = @($parts | ForEach-Object { - if ($_ -match '^([A-Za-z])_$') { "$($matches[1]):" } else { $_ } - }) + if ($_ -match '^([A-Za-z])_$') { "$($matches[1]):" } else { $_ } + }) $reconstructed = ($parts -join '\') + '\' + $folderPart Write-Log "逆向解析:$FileName -> $reconstructed" -Level DEBUG return $reconstructed - } catch { + } + catch { Write-Log "无法解析备份文件名:$FileName" -Level WARN return $null } diff --git a/BakNRet/Public/ConvertFrom-BackupListLine.ps1 b/BakNRet/Public/ConvertFrom-BackupListLine.ps1 index 22ffc69..50be85c 100644 --- a/BakNRet/Public/ConvertFrom-BackupListLine.ps1 +++ b/BakNRet/Public/ConvertFrom-BackupListLine.ps1 @@ -79,13 +79,16 @@ if ($tokens[0] -eq '+') { $direction = 'backup' $tokens = @($tokens | Select-Object -Skip 1) - } elseif ($tokens[0] -eq '-') { + } + elseif ($tokens[0] -eq '-') { $direction = 'restore' $tokens = @($tokens | Select-Object -Skip 1) - } elseif ($tokens[0].Length -gt 1 -and $tokens[0][0] -eq '+') { + } + elseif ($tokens[0].Length -gt 1 -and $tokens[0][0] -eq '+') { $direction = 'backup' $tokens[0] = $tokens[0].Substring(1) - } elseif ($tokens[0].Length -gt 1 -and $tokens[0][0] -eq '-') { + } + elseif ($tokens[0].Length -gt 1 -and $tokens[0][0] -eq '-') { $direction = 'restore' $tokens[0] = $tokens[0].Substring(1) } @@ -106,7 +109,8 @@ if ($firstMarker -lt 0) { $targetText = ($tokens -join ' ') $markerTokens = @() - } else { + } + else { $targetText = (($tokens[0..($firstMarker - 1)]) -join ' ') $markerTokens = @($tokens[$firstMarker..($tokens.Count - 1)]) } diff --git a/BakNRet/Public/Enable-BaknretPrivilege.ps1 b/BakNRet/Public/Enable-BaknretPrivilege.ps1 index 148ea11..5aa3cba 100644 --- a/BakNRet/Public/Enable-BaknretPrivilege.ps1 +++ b/BakNRet/Public/Enable-BaknretPrivilege.ps1 @@ -59,7 +59,8 @@ public static int Enable(string name) { } finally { CloseHandle(token); } } '@ - } catch { + } + catch { Write-Log "特权启用代码编译失败(本次不启用任何特权):$($_.Exception.Message)" -Level WARN $result.Failed = @($Name) return $result @@ -71,7 +72,8 @@ public static int Enable(string name) { $cacheKey = $privilege if ($script:BaknretPrivilegeState.ContainsKey($cacheKey)) { $state = $script:BaknretPrivilegeState[$cacheKey] - } else { + } + else { $state = [Baknret.Privileges]::Enable($privilege) $script:BaknretPrivilegeState[$cacheKey] = $state } diff --git a/BakNRet/Public/Enter-BaknretRunLock.ps1 b/BakNRet/Public/Enter-BaknretRunLock.ps1 index f030727..4d2f44a 100644 --- a/BakNRet/Public/Enter-BaknretRunLock.ps1 +++ b/BakNRet/Public/Enter-BaknretRunLock.ps1 @@ -23,7 +23,8 @@ [System.IO.FileMode]::OpenOrCreate, [System.IO.FileAccess]::ReadWrite, [System.IO.FileShare]::None) - } catch { + } + catch { # 不能只写 `catch [System.IO.IOException]`:PowerShell 会把 .NET 方法抛出的异常包成 # MethodInvocationException,按内层类型做的 catch 接不住,于是锁被占用时会直接抛出去, # 而不是按约定返回 $null 让调用方明确失败(实测踩到,被自己的断言逮住)。 diff --git a/BakNRet/Public/Exit-BaknretRunLock.ps1 b/BakNRet/Public/Exit-BaknretRunLock.ps1 index 5654917..1214681 100644 --- a/BakNRet/Public/Exit-BaknretRunLock.ps1 +++ b/BakNRet/Public/Exit-BaknretRunLock.ps1 @@ -8,7 +8,8 @@ if (-not $Lock) { return } try { $Lock.Dispose() - } catch { + } + catch { Write-Log "释放运行锁失败(进程退出时会自动释放):$_" -Level WARN } } diff --git a/BakNRet/Public/Expand-CatalogPathText.ps1 b/BakNRet/Public/Expand-CatalogPathText.ps1 index a5b4abf..05f2e10 100644 --- a/BakNRet/Public/Expand-CatalogPathText.ps1 +++ b/BakNRet/Public/Expand-CatalogPathText.ps1 @@ -46,7 +46,8 @@ try { $evaluated = [scriptblock]::Create($expression).Invoke() if ($null -ne $evaluated) { $replacement = ([string]($evaluated)).Trim() } - } catch { + } + catch { Write-Log "名录路径里的表达式求值失败:$expression —— $($_.Exception.Message)" -Level WARN } $value = $value.Substring(0, $start) + $replacement + $value.Substring($end + 1) diff --git a/BakNRet/Public/Find-ChildDirectoryByName.ps1 b/BakNRet/Public/Find-ChildDirectoryByName.ps1 index 878ca6b..5a9648f 100644 --- a/BakNRet/Public/Find-ChildDirectoryByName.ps1 +++ b/BakNRet/Public/Find-ChildDirectoryByName.ps1 @@ -18,10 +18,11 @@ try { return @(Get-ChildItem -LiteralPath $Parent -Directory -Force -ErrorAction SilentlyContinue | - Where-Object { $_.Name -ieq $Name -or $_.Name -imatch $pattern } | - Sort-Object Name | - Select-Object -ExpandProperty FullName) - } catch { + Where-Object { $_.Name -ieq $Name -or $_.Name -imatch $pattern } | + Sort-Object Name | + Select-Object -ExpandProperty FullName) + } + catch { return @() } } diff --git a/BakNRet/Public/Format-CatalogName.ps1 b/BakNRet/Public/Format-CatalogName.ps1 index b99e8e3..9c87960 100644 --- a/BakNRet/Public/Format-CatalogName.ps1 +++ b/BakNRet/Public/Format-CatalogName.ps1 @@ -13,8 +13,8 @@ Where-Object { $_ -notin @('&', '%', '+') } $clean = -join ($Name.Trim().ToCharArray() | ForEach-Object { - if ($_ -in $invalidChars) { '_' } else { $_ } - }) + if ($_ -in $invalidChars) { '_' } else { $_ } + }) $clean = $clean -replace ':', '_' return $clean.Trim() } diff --git a/BakNRet/Public/Get-ArchiveTopLevelNames.ps1 b/BakNRet/Public/Get-ArchiveTopLevelNames.ps1 index 8375374..5a95fe1 100644 --- a/BakNRet/Public/Get-ArchiveTopLevelNames.ps1 +++ b/BakNRet/Public/Get-ArchiveTopLevelNames.ps1 @@ -33,11 +33,13 @@ # 先把名字读进变量,再在 finally 里删临时目录; # 直接在 return 里管道 Get-ChildItem 会被 finally 抢在前面,拿到空数组。 $names = @(Get-ChildItem -LiteralPath $staging -Force -ErrorAction SilentlyContinue | - Select-Object -ExpandProperty Name) - } catch { + Select-Object -ExpandProperty Name) + } + catch { Write-Log "无法清点归档内容(跳过顶层名核对):$($_.Exception.Message)" -Level DEBUG $names = @() - } finally { + } + finally { Remove-Item -LiteralPath $staging -Recurse -Force -ErrorAction SilentlyContinue } diff --git a/BakNRet/Public/Get-BackupBaseName.ps1 b/BakNRet/Public/Get-BackupBaseName.ps1 index a758589..15b347a 100644 --- a/BakNRet/Public/Get-BackupBaseName.ps1 +++ b/BakNRet/Public/Get-BackupBaseName.ps1 @@ -26,7 +26,8 @@ $pathPart = ($pathParts | ForEach-Object { $_.Trim() }) -join '+' $baseName = if ([string]::IsNullOrEmpty($pathPart)) { $folderName - } else { + } + else { "${folderName}_from_${pathPart}" } @@ -34,8 +35,8 @@ Where-Object { $_ -notin @('&', '%', '+') } $baseName = -join ($baseName.ToCharArray() | ForEach-Object { - if ($_ -in $invalidChars) { '_' } else { $_ } - }) + if ($_ -in $invalidChars) { '_' } else { $_ } + }) $baseName = $baseName -replace ':', '_' Write-Log "生成文件基础名:$baseName" -Level DEBUG diff --git a/BakNRet/Public/Get-BaknretConfig.ps1 b/BakNRet/Public/Get-BaknretConfig.ps1 index 3edc2e2..c84e5c0 100644 --- a/BakNRet/Public/Get-BaknretConfig.ps1 +++ b/BakNRet/Public/Get-BaknretConfig.ps1 @@ -44,7 +44,8 @@ try { $loaded = Import-BaknretDataFile -Path $Path - } catch { + } + catch { Write-Log "配置文件读取失败(改用默认值):$Path —— $_" -Level WARN return $defaults } @@ -55,7 +56,8 @@ foreach ($subKey in $defaults[$key].Keys) { $merged[$subKey] = $defaults[$key][$subKey] } foreach ($subKey in $loaded[$key].Keys) { $merged[$subKey] = $loaded[$key][$subKey] } $defaults[$key] = $merged - } else { + } + else { $defaults[$key] = $loaded[$key] } } diff --git a/BakNRet/Public/Get-BaknretFreeSpaceGB.ps1 b/BakNRet/Public/Get-BaknretFreeSpaceGB.ps1 index e00c58f..ba57884 100644 --- a/BakNRet/Public/Get-BaknretFreeSpaceGB.ps1 +++ b/BakNRet/Public/Get-BaknretFreeSpaceGB.ps1 @@ -22,7 +22,8 @@ $drive = Get-PSDrive -Name $qualifier.TrimEnd(':') -ErrorAction Stop if ($null -eq $drive.Free) { return -1 } return [math]::Round($drive.Free / 1GB, 2) - } catch { + } + catch { return -1 } } diff --git a/BakNRet/Public/Get-BaknretPassword.ps1 b/BakNRet/Public/Get-BaknretPassword.ps1 index aceb5fd..d3a1ec4 100644 --- a/BakNRet/Public/Get-BaknretPassword.ps1 +++ b/BakNRet/Public/Get-BaknretPassword.ps1 @@ -40,10 +40,12 @@ $bstr = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($secure) try { return [System.Runtime.InteropServices.Marshal]::PtrToStringBSTR($bstr) - } finally { + } + finally { [System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr) } - } catch { + } + catch { Write-Log "口令输入失败:$_" -Level ERROR return $null } diff --git a/BakNRet/Public/Get-BaknretRegexExclude.ps1 b/BakNRet/Public/Get-BaknretRegexExclude.ps1 index 2cd9aaf..a0a6f01 100644 --- a/BakNRet/Public/Get-BaknretRegexExclude.ps1 +++ b/BakNRet/Public/Get-BaknretRegexExclude.ps1 @@ -25,7 +25,8 @@ try { $regex = [System.Text.RegularExpressions.Regex]::new( $Pattern, [System.Text.RegularExpressions.RegexOptions]::IgnoreCase) - } catch { + } + catch { return [pscustomobject]@{ Arguments = @(); Matches = 0; Error = "排除正则非法:$Pattern —— $($_.Exception.Message)" } } diff --git a/BakNRet/Public/Get-BaknretSecurityRecord.ps1 b/BakNRet/Public/Get-BaknretSecurityRecord.ps1 index 2429439..1ce6519 100644 --- a/BakNRet/Public/Get-BaknretSecurityRecord.ps1 +++ b/BakNRet/Public/Get-BaknretSecurityRecord.ps1 @@ -28,29 +28,31 @@ ) $record = [pscustomobject]@{ - p = $Key - k = $Kind - s = $null - o = $null - g = $null - e = $null - Protected = $false - Explicit = 0 - Inherited = 0 - Inheritable = 0 + p = $Key + k = $Kind + s = $null + o = $null + g = $null + e = $null + Protected = $false + Explicit = 0 + Inherited = 0 + Inheritable = 0 InheritedSignatures = @() AllSignatures = @() - Analyzed = $false + Analyzed = $false } $acl = $null try { if ($IncludeSacl) { $acl = Get-Acl -LiteralPath $Path -Audit -ErrorAction Stop - } else { + } + else { $acl = Get-Acl -LiteralPath $Path -ErrorAction Stop } - } catch { + } + catch { $record.e = $_.Exception.Message return $record } @@ -58,7 +60,8 @@ try { # 陈旧继承 ACE 要固化成显式 ACE,否则恢复后会消失(见 Get-BaknretSecuritySddlWithStale) $record.s = Get-BaknretSecuritySddlWithStale -Acl $acl -ParentSignatures $ParentSignatures - } catch { + } + catch { $record.e = $_.Exception.Message } if (-not $record.s) { @@ -89,7 +92,8 @@ } $record.Inheritable = $inheritable $record.Analyzed = $true - } catch { + } + catch { # 分析失败时一律当成"需要保留"(Analyzed=$false 会让 keeper 直接保留) $record.Analyzed = $false } diff --git a/BakNRet/Public/Get-BaknretSecurityRecords.ps1 b/BakNRet/Public/Get-BaknretSecurityRecords.ps1 index e1fb8e2..c2f79e2 100644 --- a/BakNRet/Public/Get-BaknretSecurityRecords.ps1 +++ b/BakNRet/Public/Get-BaknretSecurityRecords.ps1 @@ -95,7 +95,8 @@ if ($Mode -eq 'Full') { $records.Add($record) - } elseif (Test-BaknretSecurityRecordNeeded -Record $record ` + } + elseif (Test-BaknretSecurityRecordNeeded -Record $record ` -ParentOwner $frame.Owner -ParentGroup $frame.Group ` -ParentInheritable $frame.Inheritable -ParentSignatures $frame.Signatures) { $records.Add($record) diff --git a/BakNRet/Public/Get-BaknretSecuritySddlWithStale.ps1 b/BakNRet/Public/Get-BaknretSecuritySddlWithStale.ps1 index a5c5fda..2b60ea2 100644 --- a/BakNRet/Public/Get-BaknretSecuritySddlWithStale.ps1 +++ b/BakNRet/Public/Get-BaknretSecuritySddlWithStale.ps1 @@ -46,7 +46,8 @@ $rebuilt = $null if ($Acl -is [System.Security.AccessControl.DirectorySecurity]) { $rebuilt = New-Object System.Security.AccessControl.DirectorySecurity - } else { + } + else { $rebuilt = New-Object System.Security.AccessControl.FileSecurity } @@ -57,11 +58,13 @@ try { $rebuilt.SetOwner($Acl.GetOwner($sid)) $sections = $sections -bor [System.Security.AccessControl.AccessControlSections]::Owner - } catch { } + } + catch { } try { $rebuilt.SetGroup($Acl.GetGroup($sid)) $sections = $sections -bor [System.Security.AccessControl.AccessControlSections]::Group - } catch { } + } + catch { } $rebuilt.SetAccessRuleProtection($true, $false) diff --git a/BakNRet/Public/Get-FolderSummary.ps1 b/BakNRet/Public/Get-FolderSummary.ps1 index 3cb8d08..5fc8600 100644 --- a/BakNRet/Public/Get-FolderSummary.ps1 +++ b/BakNRet/Public/Get-FolderSummary.ps1 @@ -24,7 +24,8 @@ TotalSize = [long]$totalSize LatestModifiedTime = ($items | Measure-Object -Property LastWriteTime -Maximum -ErrorAction SilentlyContinue).Maximum } - } catch { + } + catch { Write-Log "无法读取文件夹摘要:$FolderPath" -Level WARN return [pscustomobject]@{ FileCount = 0 diff --git a/BakNRet/Public/Get-Optimized7zArgument.ps1 b/BakNRet/Public/Get-Optimized7zArgument.ps1 index 32bbdb0..49eb12e 100644 --- a/BakNRet/Public/Get-Optimized7zArgument.ps1 +++ b/BakNRet/Public/Get-Optimized7zArgument.ps1 @@ -22,7 +22,8 @@ $files = @(Get-ChildItem -LiteralPath $path -File -Recurse -ErrorAction SilentlyContinue) $fileCount += $files.Count $totalSize += [int64](@($files | Measure-Object -Property Length -Sum).Sum) - } else { + } + else { $fileCount++ $totalSize += [int64]$item.Length } @@ -41,7 +42,8 @@ try { $cpuCores = (Get-CimInstance Win32_ComputerSystem -ErrorAction Stop).NumberOfLogicalProcessors $threads = [math]::Max(1, $cpuCores - 1) - } catch { + } + catch { $threads = 2 } @@ -51,9 +53,9 @@ # 只放压缩相关开关。输出开关(-bso0/-bsp0 或默认进度)必须由调用方 # 单独加一次:7z 对同一个开关出现两次会直接报 # "Multiple instances for switch" 并以退出码 7 失败。 - Argument = @('a', '-t7z', "-mx=$Level", "-md=$dictSize", '-ms=on', "-mmt=$threads") - FileCount = $fileCount - TotalSize = $totalSize - TotalSizeMB = $totalSizeMB + Argument = @('a', '-t7z', "-mx=$Level", "-md=$dictSize", '-ms=on', "-mmt=$threads") + FileCount = $fileCount + TotalSize = $totalSize + TotalSizeMB = $totalSizeMB } } diff --git a/BakNRet/Public/Get-SoftwareCatalog.ps1 b/BakNRet/Public/Get-SoftwareCatalog.ps1 index cb379f5..4a77b7b 100644 --- a/BakNRet/Public/Get-SoftwareCatalog.ps1 +++ b/BakNRet/Public/Get-SoftwareCatalog.ps1 @@ -49,7 +49,8 @@ if ($script:CatalogCache.ContainsKey($Path) -and $script:CatalogCache[$Path].Stamp -eq $stamp) { return $script:CatalogCache[$Path].Data } - } catch { + } + catch { $stamp = $null } } @@ -57,7 +58,8 @@ $data = $null try { $data = Import-BaknretDataFile -Path $Path - } catch { + } + catch { Write-Log "软件名录读取失败:$Path —— $_" -Level ERROR return $result } @@ -118,7 +120,8 @@ $candidates = @() if (Test-Path -LiteralPath $declared) { $candidates = @($declared) - } else { + } + else { $parent = Split-Path -Path $declared -Parent $leafName = Split-Path -Path $declared -Leaf if ($parent -and $leafName -and (Test-Path -LiteralPath $parent)) { @@ -176,7 +179,8 @@ if ($errors.Count -gt 0) { Write-Log ("名录条目 {0} 有问题:{1}" -f $name, ($errors -join ';')) -Level ERROR - } elseif ($missing.Count -gt 0) { + } + elseif ($missing.Count -gt 0) { Write-Log ("名录:{0} 有 {1} 个 Slot 的路径当前不存在:{2}" -f $name, $missing.Count, (($missing | ForEach-Object { $_.Declared }) -join ';')) -Level DEBUG } diff --git a/BakNRet/Public/Invoke-ExternalCommand.ps1 b/BakNRet/Public/Invoke-ExternalCommand.ps1 index 66afe71..4066850 100644 --- a/BakNRet/Public/Invoke-ExternalCommand.ps1 +++ b/BakNRet/Public/Invoke-ExternalCommand.ps1 @@ -43,7 +43,8 @@ try { $process.WaitForExit() return $process.ExitCode - } finally { + } + finally { $process.Dispose() } } diff --git a/BakNRet/Public/Move-BaknretArchiveIntoPlace.ps1 b/BakNRet/Public/Move-BaknretArchiveIntoPlace.ps1 index c4bef60..df02102 100644 --- a/BakNRet/Public/Move-BaknretArchiveIntoPlace.ps1 +++ b/BakNRet/Public/Move-BaknretArchiveIntoPlace.ps1 @@ -21,7 +21,8 @@ # 7.x:单次原子替换(MoveFileEx + REPLACE_EXISTING) [System.IO.File]::Move($TempPath, $DestinationPath, $true) return - } catch { + } + catch { Write-Log "File.Move(overwrite) 不可用(5.1 没有这个重载),改用 File.Replace:$_" -Level DEBUG } diff --git a/BakNRet/Public/New-BaknretArchiveStaging.ps1 b/BakNRet/Public/New-BaknretArchiveStaging.ps1 index 0f075f1..ebdf12d 100644 --- a/BakNRet/Public/New-BaknretArchiveStaging.ps1 +++ b/BakNRet/Public/New-BaknretArchiveStaging.ps1 @@ -45,11 +45,13 @@ if ($item.IsFile) { try { New-Item -ItemType HardLink -Path $linkPath -Target $item.RealPath -ErrorAction Stop | Out-Null - } catch { + } + catch { Write-Log ("暂存:硬链接不可用({0}),改为复制文件 {1}" -f $_.Exception.Message, $item.RealPath) -Level DEBUG Copy-Item -LiteralPath $item.RealPath -Destination $linkPath -Force -ErrorAction Stop } - } else { + } + else { New-Item -ItemType Junction -Path $linkPath -Target $item.RealPath -ErrorAction Stop | Out-Null } @@ -57,10 +59,12 @@ } return $Root - } catch { + } + catch { try { Remove-BaknretArchiveStaging -Root $Root - } catch { + } + catch { # 清理失败不能盖掉真正的失败原因(那才是排查需要的),所以只告警并点名残留路径 Write-Log ("暂存目录自清理失败,需要手工删除:{0} —— {1}" -f $Root, $_.Exception.Message) -Level WARN } diff --git a/BakNRet/Public/Read-BaknretManifest.ps1 b/BakNRet/Public/Read-BaknretManifest.ps1 index 78f6bbb..3c9d827 100644 --- a/BakNRet/Public/Read-BaknretManifest.ps1 +++ b/BakNRet/Public/Read-BaknretManifest.ps1 @@ -38,7 +38,8 @@ compressor = $parsed.compressor items = $items } - } catch { + } + catch { Write-Log "manifest 解析失败(将重新建立):$Path —— $_" -Level WARN return $empty } diff --git a/BakNRet/Public/Read-BaknretSecuritySidecar.ps1 b/BakNRet/Public/Read-BaknretSecuritySidecar.ps1 index 23288a9..86411b4 100644 --- a/BakNRet/Public/Read-BaknretSecuritySidecar.ps1 +++ b/BakNRet/Public/Read-BaknretSecuritySidecar.ps1 @@ -25,7 +25,8 @@ ErrorCount = $parsed.errorCount Records = @($records) } - } catch { + } + catch { Write-Log "安全描述符文件解析失败:$Path —— $($_.Exception.Message)" -Level WARN return $null } diff --git a/BakNRet/Public/Remove-BaknretJunction.ps1 b/BakNRet/Public/Remove-BaknretJunction.ps1 index 6b16b75..a895363 100644 --- a/BakNRet/Public/Remove-BaknretJunction.ps1 +++ b/BakNRet/Public/Remove-BaknretJunction.ps1 @@ -9,7 +9,8 @@ try { # Directory.Delete(path, recursive: $false) 删的只是重解析点,不碰目标内容 [System.IO.Directory]::Delete($Path, $false) - } catch { + } + catch { Remove-Item -LiteralPath $Path -Force -ErrorAction SilentlyContinue } } diff --git a/BakNRet/Public/Resolve-BackupEntry.ps1 b/BakNRet/Public/Resolve-BackupEntry.ps1 index ffdef99..ab5bfb6 100644 --- a/BakNRet/Public/Resolve-BackupEntry.ps1 +++ b/BakNRet/Public/Resolve-BackupEntry.ps1 @@ -68,7 +68,8 @@ if ([string]::IsNullOrWhiteSpace($leaf) -or [string]::IsNullOrWhiteSpace($real)) { $errorText = "无法从路径里拆出末级名:$real" - } else { + } + else { $items += New-BaknretArchiveItem -ArchivePath $leaf -RealPath $real -Kind 'path' ` -Origin 'path' -Exists $exists -IsFile $isFile } @@ -78,7 +79,8 @@ $catalog = Get-SoftwareCatalog -Path $CatalogPath -MaxDepth $MaxDepth if (-not $catalog.ContainsKey($Entry.Path)) { $errorText = "软件名录里没有 '$($Entry.Path)'" - } else { + } + else { $catalogEntry = $catalog[$Entry.Path] # 名录条目自身有问题(Slot 缺 Path、前缀补全命中多个目录……)时整条失败: # 继续跑只会"少打包一块"或"任选一个目录",那正是最该避免的静默错误。 @@ -91,7 +93,8 @@ if ($overridePath -and $slots.Count -ne 1) { $blocking = ("'{0}' 有 {1} 个 Slot,不能用一个 `::` / `@ Path=` 覆盖路径;请写清楚是哪个 Slot" -f ` $Entry.Path, $slots.Count) - } else { + } + else { foreach ($slot in $slots) { $resolvedPath = $slot.Resolved $exists = $slot.Exists @@ -122,7 +125,8 @@ $includeTexts = @() if ($hasIncludeOverride) { $includeTexts = @($entryInclude) - } elseif ($catalogEntry) { + } + elseif ($catalogEntry) { foreach ($slot in @($catalogEntry.Slots)) { $includeTexts += @($slot.Include) } } @@ -182,7 +186,8 @@ if (-not $key) { continue } if ($seen.ContainsKey($key)) { $collisions += ("'{0}'({1} 与 {2})" -f $item.ArchivePath, $seen[$key], $item.RealPath) - } else { + } + else { $seen[$key] = $item.RealPath } } @@ -209,7 +214,8 @@ $encrypt = $false if ($hasEncryptOverride) { $encrypt = [bool]$overrides['Encrypt'] - } elseif ($catalogEntry) { + } + elseif ($catalogEntry) { $slots = @($catalogEntry.Slots) $encryptedSlots = @($slots | Where-Object { $_.Encrypt }) $encrypt = $encryptedSlots.Count -gt 0 diff --git a/BakNRet/Public/Restore-BaknretSecurity.ps1 b/BakNRet/Public/Restore-BaknretSecurity.ps1 index 5bd5bc4..4e6fa97 100644 --- a/BakNRet/Public/Restore-BaknretSecurity.ps1 +++ b/BakNRet/Public/Restore-BaknretSecurity.ps1 @@ -50,9 +50,11 @@ $relative = $null if ($key -ieq $root) { $relative = '' - } elseif ($key.StartsWith($prefix, [System.StringComparison]::OrdinalIgnoreCase)) { + } + elseif ($key.StartsWith($prefix, [System.StringComparison]::OrdinalIgnoreCase)) { $relative = $key.Substring($prefix.Length) - } else { + } + else { continue } $selected += [pscustomobject]@{ Relative = $relative; Record = $record } @@ -61,8 +63,8 @@ if ($selected.Count -eq 0) { return $result } $ordered = @($selected | Sort-Object -Property ` - @{ Expression = { @(($_.Relative) -split '\\').Count } }, ` - @{ Expression = { $_.Relative } }) + @{ Expression = { @(($_.Relative) -split '\\').Count } }, ` + @{ Expression = { $_.Relative } }) foreach ($entry in $ordered) { $target = if ($entry.Relative) { Join-Path $TargetPath $entry.Relative } else { $TargetPath } @@ -82,18 +84,21 @@ try { Set-BaknretObjectSecurity -Item $item -Sddl $sddl -Scope All $result.Applied++ - } catch { + } + catch { $fullError = $_ try { Set-BaknretObjectSecurity -Item $item -Sddl $sddl -Scope OwnerAndAccess $result.OwnerFailed++ $result.Failures += ("{0}:属组未恢复,属主与 DACL 已恢复({1})" -f $target, $fullError.Exception.Message) - } catch { + } + catch { try { Set-BaknretObjectSecurity -Item $item -Sddl $sddl -Scope AccessOnly $result.OwnerFailed++ $result.Failures += ("{0}:属主/属组未恢复({1}),已只恢复 DACL" -f $target, $_.Exception.Message) - } catch { + } + catch { $result.Failed++ $result.Failures += ("{0}:{1}" -f $target, $_.Exception.Message) } diff --git a/BakNRet/Public/Set-BaknretObjectSecurity.ps1 b/BakNRet/Public/Set-BaknretObjectSecurity.ps1 index 50a889d..8f5ee3a 100644 --- a/BakNRet/Public/Set-BaknretObjectSecurity.ps1 +++ b/BakNRet/Public/Set-BaknretObjectSecurity.ps1 @@ -31,7 +31,8 @@ if ($Item.PSIsContainer) { $sd = New-Object System.Security.AccessControl.DirectorySecurity - } else { + } + else { $sd = New-Object System.Security.AccessControl.FileSecurity } @@ -43,7 +44,8 @@ if ($PSVersionTable.PSEdition -eq 'Core') { [System.IO.FileSystemAclExtensions]::SetAccessControl($Item, $sd) - } else { + } + else { $Item.SetAccessControl($sd) } } diff --git a/BakNRet/Public/Test-BaknretPathExcluded.ps1 b/BakNRet/Public/Test-BaknretPathExcluded.ps1 index e3f0807..a8c6c1e 100644 --- a/BakNRet/Public/Test-BaknretPathExcluded.ps1 +++ b/BakNRet/Public/Test-BaknretPathExcluded.ps1 @@ -36,7 +36,8 @@ foreach ($component in $components) { if ([regex]::IsMatch($component, $regexText, $options)) { return $true } } - } catch { + } + catch { Write-Log "排除正则非法,安全描述符采集按'不排除'处理:$regexText —— $($_.Exception.Message)" -Level WARN } continue diff --git a/BakNRet/Public/Write-Log.ps1 b/BakNRet/Public/Write-Log.ps1 index 0225c2d..aa3cc44 100644 --- a/BakNRet/Public/Write-Log.ps1 +++ b/BakNRet/Public/Write-Log.ps1 @@ -38,7 +38,8 @@ $script:LogConfig.FilePath, $line + [Environment]::NewLine, $script:LogEncoding) - } catch { + } + catch { # 日志落盘失败时保持沉默:不能因为写日志失败而让备份失败。 } } diff --git a/PSScriptAnalyzerSettings.psd1 b/PSScriptAnalyzerSettings.psd1 new file mode 100644 index 0000000..20d5980 --- /dev/null +++ b/PSScriptAnalyzerSettings.psd1 @@ -0,0 +1,106 @@ +<# + PSScriptAnalyzer 的配置。 + + 为什么这个文件是必要的,而不是"跑一下默认规则就算按规范了": + + 默认规则集**不含**格式规则 —— PSPlaceOpenBrace / PSPlaceCloseBrace / + PSUseConsistentWhitespace / PSUseConsistentIndentation / PSAlignAssignmentStatement / + PSUseCorrectCasing 这六条默认都是 Disabled。所以 + `Invoke-ScriptAnalyzer -Severity Warning,Error`(不带 -Settings) + 会**静默漏掉全部排版问题**。 + + 这里用 `Rules` 把格式规则**打开**,而不是用 `IncludeRules` 换一套:不带 IncludeRules 时 + 默认规则集照常生效,Rules 只是逐条改设置,于是"默认规则 + 格式规则"同时跑。 + + 逐条决策(都在本次改造里确认过,理由另见 docs/adr/): + + * PSAvoidUsingWriteHost 全局排除 —— Write-Log 的彩色控制台输出是这份工具的**刻意设计**, + 不是疏忽。这是架构性选择,所以在配置里排除掉,而不是逐处 Suppress 假装它是例外。 + * PSUseSingularNouns 放行 SecurityRecords / MapKeys / ArchiveTopLevelNames —— + 单数名 Get-BakNretSecurityRecord 已被"单对象版本"占用,为了规则把两个语义搅在一起 + 不值得。Data / Windows 是规则自带的默认放行项,显式写上以免被本条覆盖掉。 + * PSAvoidLongLines 上限 160,而**不是**官方默认的 120:本仓库的中文注释与测试夹具 + 里的一行式目录让 120 意味着 270 处改动,而 160 意味着 41 处(并且 160 仍是 + 「宽但可读」)。这是一次有意的偏离,理由记在这里而不是悄悄放宽:如果哪天要收 + 紧到 120,先看 tools\Invoke-Analyzer.ps1 的输出里还有多少条。 + + * PSUseShouldProcessForStateChangingFunctions 全局排除 —— 本模块是库,不是入口: + WhatIf 的边界在 Backup.ps1 / Restore.ps1(它们自己管 -DryRun / -WhatIf)。 + 给库里 27 个改状态的函数都加 SupportsShouldProcess 会更糟:入口把 $WhatIfPreference + 置真之后,这些函数会**静默跳过自己的工作**,备份看起来成功却什么都没做。 + + * PSAvoidUsingPlainTextForPassword 全局排除 —— 7z 只接受命令行口令(这是 7z 自身 + 的限制,README 的「加密」一节写明了取舍)。口令在这个工具里必然是明文字符串, + 规则说的"用 SecureString"在这里没有可用的落点。 + 真正要守的两条已经另有措施:口令不进日志(遮蔽 + 断言)、口令不进版本库 + (.gitignore + 出厂默认值留空)。 + * PSUseConsistentIndentation 用 space + 4,与 .editorconfig 一致。 +#> +@{ + Severity = @('Error', 'Warning') + + ExcludeRules = @( + 'PSAvoidUsingWriteHost', + 'PSUseShouldProcessForStateChangingFunctions', + 'PSAvoidUsingPlainTextForPassword' + ) + + Rules = @{ + + # ---------------------------------------------------------------- 格式规则 + PSPlaceOpenBrace = @{ + Enable = $true + OnSameLine = $true + NewLineAfter = $true + IgnoreOneLineBlock = $true + } + + PSPlaceCloseBrace = @{ + Enable = $true + NewLineAfter = $true + IgnoreOneLineBlock = $true + NoEmptyLineBefore = $false + } + + PSUseConsistentIndentation = @{ + Enable = $true + Kind = 'space' + IndentationSize = 4 + PipelineIndentation = 'IncreaseIndentationForFirstPipeline' + } + + PSUseConsistentWhitespace = @{ + Enable = $true + CheckInnerBrace = $true + CheckOpenBrace = $true + CheckOpenParen = $true + CheckOperator = $true + CheckPipe = $true + CheckPipeForRedundantWhitespace = $false + CheckSeparator = $true + CheckParameter = $false + IgnoreAssignmentOperatorInsideHashTable = $true + } + + PSAlignAssignmentStatement = @{ + Enable = $true + CheckHashtable = $true + } + + PSUseCorrectCasing = @{ + Enable = $true + } + + # ---------------------------------------------------------------- 行长 + PSAvoidLongLines = @{ + Enable = $true + MaximumLineLength = 160 + } + + # ---------------------------------------------------------------- 名词白名单 + PSUseSingularNouns = @{ + Enable = $true + NounAllowList = @('Data', 'Windows', 'SecurityRecords', 'MapKeys', 'ArchiveTopLevelNames') + } + } +} diff --git a/Restore.ps1 b/Restore.ps1 index 3f02198..4cdabb7 100644 --- a/Restore.ps1 +++ b/Restore.ps1 @@ -296,7 +296,8 @@ function Invoke-ExtractionByLayout { New-Item -ItemType Directory -Path $destParent -Force | Out-Null } Move-Item -LiteralPath $produced -Destination $destPath -Force - } finally { + } + finally { Remove-Item -LiteralPath $temp -Recurse -Force -ErrorAction SilentlyContinue } return $true @@ -316,7 +317,8 @@ function Invoke-ExtractionByLayout { New-BaknretJunction -Path $anchorPath -Target $destPath | Out-Null $junctionCreated = $true Write-Log ("落地:{0} -> {1}(经连接点 {2})" -f $archivePath, $destPath, $anchorPath) -Level DEBUG - } catch { + } + catch { Write-Log "无法建连接点($($_.Exception.Message)),改为先解到临时目录再合并" -Level WARN } } @@ -324,7 +326,8 @@ function Invoke-ExtractionByLayout { if ($junctionCreated) { try { return (Invoke-ExtractionRaw -ArchiveFile $ArchiveFile -Destination $destParent -RelativePath $archivePath -Password $Password) - } finally { + } + finally { Remove-BaknretJunction -Path $anchorPath } } @@ -343,7 +346,8 @@ function Invoke-ExtractionByLayout { foreach ($child in @(Get-ChildItem -LiteralPath $source -Force -ErrorAction SilentlyContinue)) { Copy-Item -LiteralPath $child.FullName -Destination $destPath -Recurse -Force } - } finally { + } + finally { Remove-Item -LiteralPath $temp -Recurse -Force -ErrorAction SilentlyContinue } return $true @@ -395,10 +399,12 @@ function Test-BaknretArchivePath { -NoNewWindow -Wait -PassThru $lines = @(Get-Content -LiteralPath $outFile -Encoding UTF8 -ErrorAction SilentlyContinue) - } catch { + } + catch { Write-Log "无法列出归档内容(跳过预判):$($_.Exception.Message)" -Level DEBUG return $true - } finally { + } + finally { Remove-Item -LiteralPath $outFile -Force -ErrorAction SilentlyContinue Remove-Item -LiteralPath $errFile -Force -ErrorAction SilentlyContinue } @@ -595,9 +601,11 @@ foreach ($line in $lines) { $isFile = [bool]$entryItem.IsFile if (Test-Path -LiteralPath $dest -PathType Leaf) { $isFile = $true - } elseif (Test-Path -LiteralPath $dest -PathType Container) { + } + elseif (Test-Path -LiteralPath $dest -PathType Container) { $isFile = $false - } elseif ($layouts.ContainsKey(([string]$entryItem.ArchivePath).ToLower())) { + } + elseif ($layouts.ContainsKey(([string]$entryItem.ArchivePath).ToLower())) { $isFile = ($layouts[([string]$entryItem.ArchivePath).ToLower()] -eq 'file') } @@ -674,7 +682,8 @@ foreach ($line in $lines) { if ($verifyCode -eq 0) { Write-Log "校验通过: $($archiveFile.Name)" -Level INFO $stats.verified++ - } else { + } + else { Write-Log "校验失败: $($archiveFile.Name)(退出码 $verifyCode)" -Level ERROR $stats.failed++ $failures += $displayPath @@ -693,7 +702,8 @@ foreach ($line in $lines) { $stats.skipped++ continue } - } catch { + } + catch { Write-Log "目标目录摘要读取失败,继续恢复:$_" -Level DEBUG } } @@ -717,7 +727,8 @@ foreach ($line in $lines) { $targetParent = Split-Path -Path $target.DestPath -Parent if ($targetParent) { Write-Log "提示: 目标不存在,将新建 $targetParent" -Level DEBUG - } else { + } + else { Write-Log "提示: 目标不存在,且没有可创建的父目录:$($target.DestPath)" -Level DEBUG } } @@ -754,9 +765,11 @@ foreach ($line in $lines) { # 必须在解压**之后**、对真实目标路径做(连接点在 Invoke-Extraction 里已经拆掉了)。 if ($SkipSecurity) { Write-Log '按 -SkipSecurity 跳过了安全描述符恢复' -Level DEBUG - } elseif (([string]$script:Config.Security.Mode) -eq 'Off') { + } + elseif (([string]$script:Config.Security.Mode) -eq 'Off') { Write-Log '配置里 Security.Mode = Off,跳过安全描述符恢复' -Level DEBUG - } else { + } + else { $sidecarName = $null if ($found.Record -and ($found.Record.PSObject.Properties.Name -contains 'security') -and $found.Record.security) { $sidecarName = [string]$found.Record.security.file @@ -766,7 +779,8 @@ foreach ($line in $lines) { $sidecar = Read-BaknretSecuritySidecar -Path (Join-Path $BackupDir $sidecarName) if (-not $sidecar) { Write-Log ("这个归档没有安全描述符旁挂文件({0}):恢复出来的属主/ACL 是新建对象的默认值 —— 原程序若依赖特殊权限(ProgramData 下的 CREATOR OWNER 最典型),会报无读写权限" -f $sidecarName) -Level WARN - } else { + } + else { $sidMap = @{} if ($script:Config.Security.SidMap) { $sidMap = $script:Config.Security.SidMap } @@ -803,16 +817,19 @@ foreach ($line in $lines) { $record = $manifest.items[$baseName] if ($record -is [System.Collections.IDictionary]) { $record['lastRestoreAt'] = (Get-Date).ToString('o') - } else { + } + else { $record | Add-Member -NotePropertyName lastRestoreAt -NotePropertyValue ((Get-Date).ToString('o')) -Force } $manifestDirty = $true } - } else { + } + else { $stats.failed++ $failures += $displayPath } - } catch { + } + catch { Write-Log "恢复失败: $displayPath,$_" -Level ERROR $stats.failed++ $failures += $displayPath @@ -825,7 +842,7 @@ foreach ($line in $lines) { if (-not $VerifyOnly -and $Only.Count -eq 0 -and $Skip.Count -eq 0) { $orphans = @(Get-ChildItem -LiteralPath $BackupDir -File -Force -ErrorAction SilentlyContinue | - Where-Object { $_.Extension.ToLower() -in $SupportedFormats -and $_.BaseName -notin $referencedArchives }) + Where-Object { $_.Extension.ToLower() -in $SupportedFormats -and $_.BaseName -notin $referencedArchives }) if ($orphans.Count -gt 0) { Write-Log '以下归档没有任何清单条目指向(恢复不到,注意别误删):' -Level WARN @@ -833,7 +850,8 @@ if (-not $VerifyOnly -and $Only.Count -eq 0 -and $Skip.Count -eq 0) { Write-Log (" - {0}({1} MB,{2})" -f $orphan.Name, [math]::Round($orphan.Length / 1MB, 2), $orphan.LastWriteTime) -Level WARN } } -} elseif (-not $VerifyOnly) { +} +elseif (-not $VerifyOnly) { # 带 -Only/-Skip 时只有被选中的条目会被处理,其余归档都不在 $referencedArchives 里, # 按上面的算法报出来全是假孤儿(还会吓唬人说"注意别误删"),所以整段跳过。 Write-Log '本次只恢复了部分条目,跳过孤儿归档审计(避免把未选中的归档误报成孤儿)' -Level DEBUG @@ -845,10 +863,12 @@ try { $null = Sync-BaknretManifestArchive -Manifest $manifest -BackupDir $BackupDir Write-BaknretManifest -Path $manifestPath -Manifest $manifest | Out-Null Write-Log 'manifest 已更新(记下本次恢复时间)' -Level DEBUG - } else { + } + else { Write-Log 'manifest 无需更新:本次没有实际恢复任何条目' -Level DEBUG } -} catch { +} +catch { Write-Log "manifest 写回失败(不影响本次恢复):$_" -Level WARN } diff --git a/SoftwareCatalog.psd1 b/SoftwareCatalog.psd1 index 01cf1d1..0e95ba7 100644 --- a/SoftwareCatalog.psd1 +++ b/SoftwareCatalog.psd1 @@ -61,7 +61,7 @@ } } - MicrosoftEdge = @{ + MicrosoftEdge = @{ DefaultData = @{ Path = '%LocalAppData%\Microsoft\Edge\User Data' Exclude = '!*Cache,!BrowserMetrics,!component_crx_cache,' + diff --git a/test.ps1 b/test.ps1 index 0360bcc..a74601b 100644 --- a/test.ps1 +++ b/test.ps1 @@ -99,11 +99,13 @@ function Invoke-ScriptInHost { $ErrorActionPreference = 'Continue' if ($Quiet) { & $exeOf[$HostName] @argumentList *> $null - } else { + } + else { & $exeOf[$HostName] @argumentList 2>&1 | Tee-Object -FilePath $stdout | Out-Null } return $LASTEXITCODE - } finally { + } + finally { Remove-Item -LiteralPath $stdout -Force -ErrorAction SilentlyContinue } } @@ -193,7 +195,8 @@ function Invoke-ParseLayer { $ok = ($numbers -split '/')[0] -eq ($numbers -split '/')[1] if ($failures.Count -gt 0) { $failures | ForEach-Object { Write-Host $_ -ForegroundColor DarkGray } } Add-Result -Layer 'Parse' -HostName $HostName -Ok $ok -Detail ("解析通过 {0} 个文件" -f $numbers) - } finally { + } + finally { Remove-Item -LiteralPath $probe -Force -ErrorAction SilentlyContinue } } @@ -248,7 +251,8 @@ Write-Host '' Write-Host ('=' * 64) if ($failed.Count -eq 0) { Write-Host ("验收全部通过:{0} 项(宿主 {1})" -f $script:Results.Count, ($hosts -join ' + ')) -ForegroundColor Green -} else { +} +else { Write-Host ("验收有失败:{0} 项里失败 {1} 项" -f $script:Results.Count, $failed.Count) -ForegroundColor Red $failed | ForEach-Object { Write-Host (" - {0} @ {1}:{2}" -f $_.Layer, $_.HostName, $_.Detail) -ForegroundColor Red } } diff --git a/tests/BakNRet.Formats.Tests.ps1 b/tests/BakNRet.Formats.Tests.ps1 index 2b4fd4d..99dfbcc 100644 --- a/tests/BakNRet.Formats.Tests.ps1 +++ b/tests/BakNRet.Formats.Tests.ps1 @@ -65,7 +65,8 @@ BeforeAll { try { $exitCode = Invoke-ExternalCommand -FilePath 'cmd.exe' -ArgumentList @('/c', $cmdFile) $lines = @(Get-Content -LiteralPath $outFile -Encoding UTF8 -ErrorAction SilentlyContinue) - } finally { + } + finally { Remove-Item -LiteralPath $outFile -Force -ErrorAction SilentlyContinue Remove-Item -LiteralPath $cmdFile -Force -ErrorAction SilentlyContinue } @@ -92,7 +93,7 @@ AfterAll { # ============================================================================ Describe '软件名录:Slot 形状(新契约)' { -# ============================================================================ + # ============================================================================ BeforeAll { $script:FormatRoot = Join-Path $script:Sandbox 'format' @@ -213,7 +214,7 @@ Describe '软件名录:Slot 形状(新契约)' { # ============================================================================ Describe '清单修饰符:新契约格式' { -# ============================================================================ + # ============================================================================ BeforeAll { $script:FormatRoot = Join-Path $script:Sandbox 'format' @@ -309,7 +310,7 @@ Describe '清单修饰符:新契约格式' { # ============================================================================ Describe '集成:Slot 布局的打包与恢复' -Skip:(-not ($script:HasSevenZip -and $script:HasPwsh)) { -# ============================================================================ + # ============================================================================ BeforeAll { $script:SlotRoot = Join-Path $script:Sandbox 'slots-e2e' @@ -428,7 +429,7 @@ Describe '集成:Slot 布局的打包与恢复' -Skip:(-not ($script:HasSevenZ # ============================================================================ Describe '集成:旧布局归档的回退恢复' -Skip:(-not ($script:HasSevenZip -and $script:HasPwsh)) { -# ============================================================================ + # ============================================================================ # Slot 布局是重构后才有的,Backups\ 里还躺着按旧布局(包内直接是 <源目录名>\...) # 生成的归档。恢复这类归档时必须回退到"把 <目标末级名> 解到目标父目录"的旧语义。 @@ -456,8 +457,8 @@ Describe '集成:旧布局归档的回退恢复' -Skip:(-not ($script:HasSeven # 手工造一个旧布局归档:顶层就是源目录名,不是 Slot 名。 $script:LegacyArchive = Join-Path $script:LegacyBackupDir 'oldapp.7z' (Invoke-ExternalCommand -FilePath $script:SevenZip ` - -ArgumentList @('a', '-t7z', '-mx=1', '-bso0', '-bsp0', $script:LegacyArchive, $script:LegacyLeaf) ` - -WorkingDirectory $script:LegacyHolder) | Should -Be 0 + -ArgumentList @('a', '-t7z', '-mx=1', '-bso0', '-bsp0', $script:LegacyArchive, $script:LegacyLeaf) ` + -WorkingDirectory $script:LegacyHolder) | Should -Be 0 } It '归档确实是旧布局:顶层是源目录名而不是 Slot 名' { @@ -504,7 +505,7 @@ Describe '集成:旧布局归档的回退恢复' -Skip:(-not ($script:HasSeven # ============================================================================ Describe '集成:归档内路径冲突会被拒绝执行' -Skip:(-not ($script:HasSevenZip -and $script:HasPwsh)) { -# ============================================================================ + # ============================================================================ BeforeAll { $script:RejectRoot = Join-Path $script:Sandbox 'collide-e2e' @@ -546,7 +547,7 @@ Describe '集成:归档内路径冲突会被拒绝执行' -Skip:(-not ($script # ============================================================================ Describe '条目从清单里消失后,旧归档必须被点名为孤儿' -Skip:(-not ($script:HasSevenZip -and $script:HasPwsh)) { -# ============================================================================ + # ============================================================================ # 这是"合并/改名条目"的真实后果:归档还在,manifest 里也还留着历史记录, # 但清单里已经没有任何条目指向它 —— Restore.ps1 按条目名找归档,所以它恢复不到。 @@ -601,7 +602,7 @@ Describe '条目从清单里消失后,旧归档必须被点名为孤儿' -Skip # ============================================================================ Describe 'manifest 一致性:archive 字段只在文件真的存在时才写' { -# ============================================================================ + # ============================================================================ BeforeAll { $script:SyncRoot = Join-Path $script:Sandbox 'sync' diff --git a/tests/BakNRet.Security.Tests.ps1 b/tests/BakNRet.Security.Tests.ps1 index 0a426dc..79ab128 100644 --- a/tests/BakNRet.Security.Tests.ps1 +++ b/tests/BakNRet.Security.Tests.ps1 @@ -44,7 +44,8 @@ BeforeAll { param([Parameter(Mandatory = $true)][System.IO.FileSystemInfo]$Item, [Parameter(Mandatory = $true)]$Security) if ($PSVersionTable.PSEdition -eq 'Core') { [System.IO.FileSystemAclExtensions]::SetAccessControl($Item, $Security) - } else { + } + else { $Item.SetAccessControl($Security) } } @@ -63,8 +64,8 @@ BeforeAll { $acl = Get-Acl -LiteralPath $Path $sid = [System.Security.Principal.SecurityIdentifier] $aces = @($acl.GetAccessRules($true, $true, $sid) | - ForEach-Object { '{0}|{1}|{2}' -f $_.AccessControlType, $_.IdentityReference.Value, [int]$_.FileSystemRights } | - Sort-Object) + ForEach-Object { '{0}|{1}|{2}' -f $_.AccessControlType, $_.IdentityReference.Value, [int]$_.FileSystemRights } | + Sort-Object) return ('O={0} G={1} P={2} [{3}]' -f $acl.GetOwner($sid).Value, $acl.GetGroup($sid).Value, $acl.AreAccessRulesProtected, ($aces -join ' ')) } @@ -109,7 +110,8 @@ BeforeAll { try { & takeown.exe /F $Path /R /D Y 2>&1 | Out-Null & icacls.exe $Path /reset /T /C /Q 2>&1 | Out-Null - } finally { + } + finally { $ErrorActionPreference = $previousPreference } @@ -145,7 +147,8 @@ BeforeAll { try { $exitCode = Invoke-ExternalCommand -FilePath 'cmd.exe' -ArgumentList @('/c', $cmdFile) $lines = @(Get-Content -LiteralPath $outFile -Encoding UTF8 -ErrorAction SilentlyContinue) - } finally { + } + finally { Remove-Item -LiteralPath $outFile -Force -ErrorAction SilentlyContinue Remove-Item -LiteralPath $cmdFile -Force -ErrorAction SilentlyContinue } @@ -219,7 +222,7 @@ AfterAll { # ============================================================================ Describe '排除判定与 7z 的 -x! / -xr! 语义对齐' { -# ============================================================================ + # ============================================================================ It '锚定模式只命中它自己那棵子树' { Test-BaknretPathExcluded -RelativePath 'Default\Cache' -Patterns @('Default\Cache') | Should -BeTrue Test-BaknretPathExcluded -RelativePath 'Default\Cache\sub\x.bin' -Patterns @('Default\Cache') | Should -BeFalse @@ -251,7 +254,7 @@ Describe '排除判定与 7z 的 -x! / -xr! 语义对齐' { # ============================================================================ Describe 'SID 映射(跨机恢复)' { -# ============================================================================ + # ============================================================================ It '整 SID 精确替换' { $sddl = 'O:S-1-5-21-1-2-3-1001G:S-1-5-21-1-2-3-1001D:(A;;FA;;;S-1-5-21-1-2-3-1001)' $mapped = Convert-BaknretSidMap -Sddl $sddl -SidMap @{ 'S-1-5-21-1-2-3-1001' = 'S-1-5-21-9-8-7-1001' } @@ -272,7 +275,7 @@ Describe 'SID 映射(跨机恢复)' { # ============================================================================ Describe '安全描述符采集' { -# ============================================================================ + # ============================================================================ BeforeAll { $script:CaptureRoot = Join-Path $script:Sandbox 'capture\Data' $script:CaptureItem = [pscustomobject]@{ ArchivePath = 'Data'; RealPath = $script:CaptureRoot } @@ -341,7 +344,7 @@ Describe '安全描述符采集' { # ============================================================================ Describe '安全描述符回放' { -# ============================================================================ + # ============================================================================ BeforeAll { $script:RestoreRoot = Join-Path $script:Sandbox 'restore\Data' $script:TargetRoot = Join-Path $script:Sandbox 'restore\target' @@ -398,7 +401,7 @@ Describe '安全描述符回放' { # 属组写成一个本机不存在的 SID:改主组需要特权,这一层必然失败 $sddl = 'O:' + (Get-Acl -LiteralPath $path).GetOwner([System.Security.Principal.SecurityIdentifier]).Value + - 'G:' + $script:OrphanSid + 'D:(A;;FA;;;SY)' + 'G:' + $script:OrphanSid + 'D:(A;;FA;;;SY)' $sidecar = [pscustomobject]@{ Records = @([pscustomobject]@{ p = 'Data'; k = 'd'; s = $sddl }) } @@ -422,7 +425,7 @@ Describe '安全描述符回放' { # ============================================================================ Describe '与 Backup.ps1 / Restore.ps1 的集成' -Skip:(-not $script:HasSevenZip) { -# ============================================================================ + # ============================================================================ BeforeAll { $script:Harness = New-AclEntryHarness -Name 'integration' -Root $script:Sandbox $script:IntegrationFingerprints = New-AclSourceTree -Root $script:Harness.SourcePath diff --git a/tests/BakNRet.Tests.ps1 b/tests/BakNRet.Tests.ps1 index 9d1df78..087e3a6 100644 --- a/tests/BakNRet.Tests.ps1 +++ b/tests/BakNRet.Tests.ps1 @@ -82,7 +82,8 @@ BeforeAll { try { $exitCode = Invoke-ExternalCommand -FilePath 'cmd.exe' -ArgumentList @('/c', $cmdFile) $lines = @(Get-Content -LiteralPath $outFile -Encoding UTF8 -ErrorAction SilentlyContinue) - } finally { + } + finally { Remove-Item -LiteralPath $outFile -Force -ErrorAction SilentlyContinue Remove-Item -LiteralPath $cmdFile -Force -ErrorAction SilentlyContinue } @@ -134,7 +135,7 @@ AfterAll { # ============================================================================ Describe 'BackupList.txt 解析' { -# ============================================================================ + # ============================================================================ It '注释行与空行返回 $null' { ConvertFrom-BackupListLine -Line '# 这是注释' | Should -BeNullOrEmpty @@ -364,7 +365,7 @@ Describe 'BackupList.txt 解析' { # ============================================================================ Describe '归档命名' { -# ============================================================================ + # ============================================================================ BeforeAll { $script:NamingRoot = Join-Path $script:Sandbox 'naming' @@ -431,7 +432,7 @@ Describe '归档命名' { # ============================================================================ Describe '7z 排除参数翻译' { -# ============================================================================ + # ============================================================================ BeforeAll { $script:ExcludeTree = Join-Path $script:Sandbox 'exclude-tree' @@ -588,7 +589,7 @@ Describe '7z 排除参数翻译' { # ============================================================================ Describe '归档项与暂存目录' { -# ============================================================================ + # ============================================================================ BeforeAll { $script:StagingRoot = Join-Path $script:Sandbox 'staging' @@ -660,7 +661,8 @@ Describe '归档项与暂存目录' { $cfg = Get-Item -LiteralPath (Join-Path $stage 'Cfg') -Force $cfg.PSIsContainer | Should -BeFalse (Get-Content -Encoding UTF8 -LiteralPath $cfg.FullName -Raw).Trim() | Should -Be 'file-content' - } finally { + } + finally { Remove-BaknretArchiveStaging -Root $stage } } @@ -678,7 +680,7 @@ Describe '归档项与暂存目录' { # ============================================================================ Describe '命令行参数拼接' { -# ============================================================================ + # ============================================================================ It '无空格参数原样输出' { ConvertTo-NativeArgumentString -ArgumentList @('a', '-mx=9') | Should -Be 'a -mx=9' @@ -703,7 +705,7 @@ Describe '命令行参数拼接' { # ============================================================================ Describe 'manifest 与配置' { -# ============================================================================ + # ============================================================================ It 'manifest 读写往返' { $path = Join-Path $script:Sandbox 'roundtrip\manifest.json' @@ -762,7 +764,8 @@ Describe 'manifest 与配置' { (Get-BaknretPassword) | Should -Be 'sekrit' $env:BAKNRET_PASSWORD = $null (Get-BaknretPassword) | Should -BeNullOrEmpty - } finally { + } + finally { $env:BAKNRET_PASSWORD = $saved } } @@ -770,7 +773,7 @@ Describe 'manifest 与配置' { # ============================================================================ Describe '软件名录' { -# ============================================================================ + # ============================================================================ BeforeAll { $script:CatRoot = Join-Path $script:Sandbox 'catalog' @@ -961,7 +964,7 @@ Describe '软件名录' { # ============================================================================ Describe 'Resolve-BackupEntry:条目解析' { -# ============================================================================ + # ============================================================================ BeforeAll { $script:CatRoot = Join-Path $script:Sandbox 'catalog' @@ -1114,7 +1117,7 @@ Describe 'Resolve-BackupEntry:条目解析' { # ============================================================================ Describe '外部命令退出码(旧实现的核心缺陷)' { -# ============================================================================ + # ============================================================================ It '口令不会进日志:DEBUG 下打印的命令行要遮蔽 -p 参数' { $sandbox = Join-Path $env:TEMP ("bnr-secret-" + [guid]::NewGuid().ToString('N').Substring(0, 8)) @@ -1125,7 +1128,8 @@ Describe '外部命令退出码(旧实现的核心缺陷)' { $logPath = Start-BaknretLog -Directory $sandbox -Prefix 'secret' Set-BaknretDebug $null = Invoke-ExternalCommand -FilePath 'cmd.exe' -ArgumentList @('/c', 'exit 0', "-p$sentinel") - } finally { + } + finally { Set-BaknretDebug -Enabled:$false Stop-BaknretLog } @@ -1153,7 +1157,8 @@ Describe '外部命令退出码(旧实现的核心缺陷)' { $second | Should -BeNullOrEmpty Test-Path -LiteralPath (Get-BaknretRunLockPath -Directory $dir) | Should -BeTrue - } finally { + } + finally { Exit-BaknretRunLock -Lock $second Exit-BaknretRunLock -Lock $first } @@ -1175,7 +1180,8 @@ Describe '外部命令退出码(旧实现的核心缺陷)' { $second = Enter-BaknretRunLock -Directory $dir $second | Should -Not -BeNullOrEmpty Exit-BaknretRunLock -Lock $second - } finally { + } + finally { Remove-Item -LiteralPath $dir -Recurse -Force -ErrorAction SilentlyContinue } } @@ -1190,7 +1196,8 @@ Describe '外部命令退出码(旧实现的核心缺陷)' { $summary.FileCount | Should -Be 0 $summary.TotalSize | Should -Not -BeNullOrEmpty $summary.TotalSize | Should -Be 0 - } finally { + } + finally { Remove-Item -LiteralPath $empty -Recurse -Force -ErrorAction SilentlyContinue } } @@ -1213,7 +1220,8 @@ Describe '外部命令退出码(旧实现的核心缺陷)' { (Get-Item -LiteralPath $target).IsReadOnly = $true { Write-BaknretAtomicText -Path $target -Text 'SHOULD-NOT-LAND' } | Should -Throw (Get-Content -Encoding UTF8 -LiteralPath $target -Raw) | Should -Be 'SECOND' - } finally { + } + finally { $file = Get-Item -LiteralPath $target -ErrorAction SilentlyContinue if ($file) { $file.IsReadOnly = $false } Remove-Item -LiteralPath $base -Recurse -Force -ErrorAction SilentlyContinue @@ -1231,7 +1239,7 @@ Describe '外部命令退出码(旧实现的核心缺陷)' { # ============================================================================ Describe '集成:真实 7z 压缩与排除规则' -Skip:(-not $script:HasSevenZip) { -# ============================================================================ + # ============================================================================ BeforeAll { $script:IntegrationRoot = Join-Path $script:Sandbox 'integration' @@ -1298,7 +1306,7 @@ Describe '集成:真实 7z 压缩与排除规则' -Skip:(-not $script:HasSeven # ============================================================================ Describe '集成:Backup.ps1 / Restore.ps1 端到端(字面路径条目)' -Skip:(-not ($script:HasSevenZip -and $script:HasPwsh)) { -# ============================================================================ + # ============================================================================ BeforeAll { $script:E2ERoot = Join-Path $script:Sandbox 'e2e' @@ -1468,7 +1476,7 @@ Describe '集成:Backup.ps1 / Restore.ps1 端到端(字面路径条目)' - # ============================================================================ Describe '集成:方向标记与孤儿审计' -Skip:(-not ($script:HasSevenZip -and $script:HasPwsh)) { -# ============================================================================ + # ============================================================================ # `+` / `-` 的归档名必须在方向过滤**之前**登记:这些条目自己不产生归档, # 但它们对应的归档是有主的,不能被孤儿审计当成没人要的孤儿。 diff --git a/tests/Restore-Drill.ps1 b/tests/Restore-Drill.ps1 index e2f6099..abc54ff 100644 --- a/tests/Restore-Drill.ps1 +++ b/tests/Restore-Drill.ps1 @@ -102,7 +102,7 @@ New-Item -ItemType Directory -Path $WorkRoot -Force | Out-Null $manifest = Read-BaknretManifest -Path (Join-Path $BackupDir 'manifest.json') $archiveFiles = @(Get-ChildItem -LiteralPath $BackupDir -File -Force -ErrorAction SilentlyContinue | - Where-Object { $_.Extension.ToLower() -in @('.7z', '.rar', '.zip', '.tar') }) + Where-Object { $_.Extension.ToLower() -in @('.7z', '.rar', '.zip', '.tar') }) # Restore.ps1 恢复成功后会**写回 manifest.json**(记 lastRestoreAt)。真实 Backups\ 只能读, # 所以给子进程一个临时 BackupDir:里面放一份 manifest 副本 + 指向真实归档的符号链接 @@ -193,9 +193,11 @@ function Compare-RestoredTree { if ((Get-FileHash -LiteralPath $restoredItem.FullName -Algorithm SHA256).Hash -eq (Get-FileHash -LiteralPath $liveItem.FullName -Algorithm SHA256).Hash) { $report.Matched = 1 - } elseif ($liveItem.LastWriteTime -gt $ArchiveTime) { + } + elseif ($liveItem.LastWriteTime -gt $ArchiveTime) { $report.Stale = @($restoredItem.Name) - } else { + } + else { $report.Changed = @($restoredItem.Name) } return $report @@ -214,7 +216,8 @@ function Compare-RestoredTree { if (-not (Test-Path -LiteralPath $liveFile)) { if (Test-RemovedFromLiveAfterBackup -LiveRoot $liveRoot -Relative $relative -ArchiveTime $ArchiveTime) { $report.Removed += $relative - } else { + } + else { $report.Extra += $relative } continue @@ -229,7 +232,8 @@ function Compare-RestoredTree { # 内容不一样:先看是不是"源在归档之后动过" if ((Get-Item -LiteralPath $liveFile -Force).LastWriteTime -gt $ArchiveTime) { $report.Stale += $relative - } else { + } + else { $report.Changed += $relative } } @@ -287,7 +291,7 @@ function Invoke-ScratchRestore { New-Item -ItemType Directory -Path $ScratchLogDir -Force | Out-Null $before = @(Get-ChildItem -LiteralPath $ScratchLogDir -File -Filter 'restore-*.log' -ErrorAction SilentlyContinue | - Select-Object -ExpandProperty FullName) + Select-Object -ExpandProperty FullName) $code = Invoke-ExternalCommand -FilePath $pwshExe -ArgumentList @( '-NoProfile', '-NonInteractive', '-File', $restoreScript, @@ -372,7 +376,8 @@ foreach ($name in $Entries) { if (-not (Test-Path -LiteralPath $scratchArchive)) { try { New-Item -ItemType SymbolicLink -Path $scratchArchive -Target $archiveFile.FullName -ErrorAction Stop | Out-Null - } catch { + } + catch { Copy-Item -LiteralPath $archiveFile.FullName -Destination $scratchArchive -Force } } @@ -416,7 +421,8 @@ foreach ($name in $Entries) { $target = Join-Path $entryRoot ([string]$index) if ($liveItem.PSIsContainer) { New-Item -ItemType Directory -Path $target -Force | Out-Null - } else { + } + else { [System.IO.File]::WriteAllText($target, '') } @@ -509,7 +515,8 @@ foreach ($name in $Entries) { if ($changed.Count -gt 0) { if ($AllowChanged) { $detail += ";与活源不一致 $($changed.Count) 个(-AllowChanged,已容忍)" - } else { + } + else { $status = 'FAIL' $detail += ";与活源不一致 $($changed.Count) 个(首例 $($changed[0]))" $failures += "$name :与活源不一致(首例 $($changed[0]))" @@ -552,7 +559,8 @@ Write-Host ("恢复演练:通过 {0},跳过 {1},失败 {2}(真正对拍 if ($KeepWorkRoot) { Write-Host "临时工作目录保留在:$WorkRoot" -ForegroundColor Yellow -} else { +} +else { Remove-Item -LiteralPath $WorkRoot -Recurse -Force -ErrorAction SilentlyContinue } diff --git a/tests/Run-E2E.ps1 b/tests/Run-E2E.ps1 index 76f273c..794ba9d 100644 --- a/tests/Run-E2E.ps1 +++ b/tests/Run-E2E.ps1 @@ -87,7 +87,7 @@ function Get-NewestLog { function Get-ArchiveNames { param([Parameter(Mandatory = $true)][string]$Dir) return @(Get-ChildItem -LiteralPath $Dir -File -Filter *.7z -ErrorAction SilentlyContinue | - Select-Object -ExpandProperty BaseName) + Select-Object -ExpandProperty BaseName) } function Get-OrphanNames { @@ -101,7 +101,8 @@ function Get-OrphanNames { if (-not $inSection) { continue } if ($line -match '-\s+([^\s()]+?)(') { $names += $Matches[1] - } else { + } + else { $inSection = $false } } @@ -288,7 +289,8 @@ try { Assert-Equal 'backed-up' $acceptedRecord.action Assert-Equal $true $acceptedRecord.warnings } -} finally { +} +finally { $lockStream.Close() $lockStream.Dispose() } @@ -339,10 +341,10 @@ Set-Content -LiteralPath (Join-Path $incDir 'i.txt') -Value 'included' -Encoding New-E2EConfig -Path $catConfig -LogDir $catLogDir -SoftwareCatalog $catFile $catHashes = @{ - (Join-Path $dirA 'keep.txt') = Get-Sha256 (Join-Path $dirA 'keep.txt') - (Join-Path $dirA 'sub\keep2.txt') = Get-Sha256 (Join-Path $dirA 'sub\keep2.txt') - $settingsFile = Get-Sha256 $settingsFile - (Join-Path $incDir 'i.txt') = Get-Sha256 (Join-Path $incDir 'i.txt') + (Join-Path $dirA 'keep.txt') = Get-Sha256 (Join-Path $dirA 'keep.txt') + (Join-Path $dirA 'sub\keep2.txt') = Get-Sha256 (Join-Path $dirA 'sub\keep2.txt') + $settingsFile = Get-Sha256 $settingsFile + (Join-Path $incDir 'i.txt') = Get-Sha256 (Join-Path $incDir 'i.txt') } & $backupScript -BackupListPath $catList -BackupDir $catBackupDir -ConfigPath $catConfig -Force -QuietTool @@ -664,7 +666,8 @@ Test-Case '源路径不存在被记为 missing-source,退出码仍为 0(跳 if ($KeepWorkRoot) { Write-Host "`n工作目录保留在:$WorkRoot" -ForegroundColor Yellow -} else { +} +else { Remove-Item -LiteralPath $WorkRoot -Recurse -Force -ErrorAction SilentlyContinue } diff --git a/tests/Run-RealSmoke.ps1 b/tests/Run-RealSmoke.ps1 index f607536..7ff0d5c 100644 --- a/tests/Run-RealSmoke.ps1 +++ b/tests/Run-RealSmoke.ps1 @@ -84,14 +84,17 @@ function Invoke-BaknretCaptured { try { & cmd.exe /c $cmdFile | Out-Null $code = $LASTEXITCODE - } finally { + } + finally { $ErrorActionPreference = $previous } $text = if (Test-Path -LiteralPath $outFile) { Get-Content -Encoding UTF8 -LiteralPath $outFile -Raw - } else { '' } + } + else { '' } return [pscustomobject]@{ ExitCode = $code; Output = $text } - } finally { + } + finally { Remove-Item -LiteralPath $outFile, $cmdFile -Force -ErrorAction SilentlyContinue } } diff --git a/tests/Run-Tests.ps1 b/tests/Run-Tests.ps1 index 61c0550..40339ed 100644 --- a/tests/Run-Tests.ps1 +++ b/tests/Run-Tests.ps1 @@ -463,7 +463,8 @@ Test-Case 'New-BaknretArchiveStaging:目录走 junction、文件走硬链接 Assert-Equal 'Junction' $dirLink.LinkType Assert-Equal 'HardLink' $fileLink.LinkType Assert-True (Test-Path -LiteralPath (Join-Path $staging 'AlphaData\f.txt')) '应能穿过 junction 看到内容' - } finally { + } + finally { Remove-BaknretArchiveStaging -Root $staging } @@ -488,7 +489,8 @@ Test-Case 'New-BaknretJunction / Remove-BaknretJunction:只删连接点,不 Remove-BaknretJunction -Path $link New-BaknretJunction -Path $link -Target $target | Out-Null Assert-True $true - } finally { + } + finally { Remove-BaknretJunction -Path $link } } @@ -707,7 +709,8 @@ Test-Case '口令:环境变量可读取,取不到返回 $null' { Assert-Null (Get-BaknretPassword -PasswordFile (Join-Path $sandbox 'nope')) $env:BAKNRET_PASSWORD = 'from-env' Assert-Equal 'from-env' (Get-BaknretPassword) - } finally { + } + finally { Remove-Item Env:BAKNRET_PASSWORD -ErrorAction SilentlyContinue if ($old) { $env:BAKNRET_PASSWORD = $old } } @@ -1071,7 +1074,8 @@ $sevenZip = Get-Command 7z -ErrorAction SilentlyContinue | Select-Object -First if (-not $sevenZip) { Write-Host ' 跳过:未找到 7z' -ForegroundColor Yellow -} else { +} +else { Test-Case '排除规则与空格处理在真实归档上生效' { $root = Join-Path $sandbox 'src' $itemName = 'User Data' @@ -1202,7 +1206,8 @@ Test-Case '口令不会进日志:DEBUG 下打印的命令行要遮蔽 -p 参 $logPath = Start-BaknretLog -Directory $sandbox -Prefix 'secret' Set-BaknretDebug $null = Invoke-ExternalCommand -FilePath 'cmd.exe' -ArgumentList @('/c', 'exit 0', "-p$sentinel") - } finally { + } + finally { Set-BaknretDebug -Enabled:$false Stop-BaknretLog } @@ -1236,7 +1241,8 @@ Test-Case '空目录的摘要给 0 而不是 $null(否则空间守卫会静默 Assert-True ($null -ne $summary.TotalSize) 'TotalSize 不能是 $null' Assert-True ($summary.TotalSize -is [long]) 'TotalSize 应当是整数' Assert-Equal 0 $summary.TotalSize - } finally { + } + finally { Remove-Item -LiteralPath $empty -Recurse -Force -ErrorAction SilentlyContinue } } @@ -1261,7 +1267,8 @@ Test-Case '原子替换:成功时新内容到位且不留 .tmp;失败时旧 try { Write-BaknretAtomicText -Path $target -Text 'SHOULD-NOT-LAND' | Out-Null } catch { $threw = $true } Assert-True $threw '目标只读时替换应当抛错' Assert-Equal 'SECOND' (Get-Content -Encoding UTF8 -LiteralPath $target -Raw) - } finally { + } + finally { $file = Get-Item -LiteralPath $target -ErrorAction SilentlyContinue if ($file) { $file.IsReadOnly = $false } Remove-Item -LiteralPath $base -Recurse -Force -ErrorAction SilentlyContinue @@ -1286,7 +1293,8 @@ Test-Case '运行锁:同一份备份目录同时只能有一个持有者' { Assert-True ($null -eq $second) '同一目录的第二次不应当拿到锁' Assert-FileExists (Get-BaknretRunLockPath -Directory $dir) - } finally { + } + finally { Exit-BaknretRunLock -Lock $second Exit-BaknretRunLock -Lock $first } @@ -1309,7 +1317,8 @@ Test-Case '运行锁:释放之后可以重新取得' { $second = Enter-BaknretRunLock -Directory $dir Assert-True ($null -ne $second) '释放之后应当能重新拿到锁' Exit-BaknretRunLock -Lock $second - } finally { + } + finally { Remove-Item -LiteralPath $dir -Recurse -Force -ErrorAction SilentlyContinue } } diff --git a/tests/TestHelpers.psm1 b/tests/TestHelpers.psm1 index b0332da..924754f 100644 --- a/tests/TestHelpers.psm1 +++ b/tests/TestHelpers.psm1 @@ -32,7 +32,8 @@ function Test-Case { & $Body $script:Passed++ Write-Host " [PASS] $Name" -ForegroundColor Green - } catch { + } + catch { $script:Failed++ $script:Failures += "$Name —— $($_.Exception.Message)" Write-Host " [FAIL] $Name" -ForegroundColor Red @@ -94,7 +95,8 @@ function Write-TestSummary { Write-Host ("=" * 60) if ($script:Failed -eq 0) { Write-Host "$Title 全部通过:$($script:Passed) 项" -ForegroundColor Green - } else { + } + else { Write-Host "$Title 通过 $($script:Passed) 项,失败 $($script:Failed) 项" -ForegroundColor Red foreach ($failure in $script:Failures) { Write-Host " - $failure" -ForegroundColor Red } } diff --git a/tools/Install-TestDependencies.ps1 b/tools/Install-TestDependencies.ps1 index 31cfa33..89d0157 100644 --- a/tools/Install-TestDependencies.ps1 +++ b/tools/Install-TestDependencies.ps1 @@ -1,26 +1,27 @@ <# .SYNOPSIS - 把测试用的 Pester 5 装进仓库内的 .tools\modules(不动机器上的全局模块)。 + 把测试与静态分析用的模块装进仓库内的 .tools\modules(不动机器上的全局模块)。 .DESCRIPTION - tests\BakNRet.Tests.ps1 需要 Pester 5.0+。本机常见的坑是: - * Windows 自带的是 Pester 3.4.0,没有 `Should -Be`,套件会语法错误; - * 直接 Install-Module 会把 5.x 装到全局,可能影响机器上别的、按 3.x 语法写的脚本。 + 装两样: + * Pester —— tests\BakNRet.Tests.ps1 需要 5.0+。本机常见的坑是 Windows 自带的是 + 3.4.0,没有 `Should -Be`,套件会语法错误; + * PSScriptAnalyzer —— tools\Invoke-Analyzer.ps1 用它做"按微软规范"的门禁。 - 所以这里用 Save-Module 把指定版本下载到仓库内的 .tools\modules, - tests\Run-Pester.ps1 会自动把该目录加进 PSModulePath。 - .tools\ 已进 .gitignore,不会污染版本库。 + 两者都用 Save-Module 下载到仓库内,而不是 Install-Module:后者会装到全局, + 可能影响机器上别的、按旧语法写的脚本。.tools\ 已进 .gitignore,不污染版本库。 .EXAMPLE pwsh -File .\tools\Install-TestDependencies.ps1 .EXAMPLE - pwsh -File .\tools\Install-TestDependencies.ps1 -PesterVersion 5.9.1 -Force + pwsh -File .\tools\Install-TestDependencies.ps1 -Force #> [CmdletBinding()] param( [version]$PesterVersion = [version]'5.9.1', + [string]$PSScriptAnalyzerVersion = 'latest', [switch]$Force ) @@ -32,40 +33,60 @@ $target = Join-Path $projectRoot '.tools\modules' $installed = Join-Path $target ("Pester\{0}" -f $PesterVersion) Write-Host '' -Write-Host ("目标目录 : {0}" -f $target) -Write-Host ("Pester : {0}" -f $PesterVersion) +Write-Host ("目标目录 : {0}" -f $target) +Write-Host ("Pester : {0}" -f $PesterVersion) +Write-Host ("PSScriptAnalyzer: {0}" -f $PSScriptAnalyzerVersion) Write-Host '' -if ((Test-Path -LiteralPath $installed) -and -not $Force) { - Write-Host "已经装好了,无需重复下载(要重装加 -Force)。" -ForegroundColor Green - exit 0 -} - -if ($Force -and (Test-Path -LiteralPath (Join-Path $target 'Pester'))) { - Remove-Item -LiteralPath (Join-Path $target 'Pester') -Recurse -Force -} - -New-Item -ItemType Directory -Path $target -Force | Out-Null - if (-not (Get-Command Save-Module -ErrorAction SilentlyContinue)) { Write-Error '当前环境没有 Save-Module(需要 PowerShellGet 2.x)。请改用:Install-Module Pester -Scope CurrentUser -MinimumVersion 5.0.0' exit 1 } -try { - Save-Module -Name Pester -RequiredVersion $PesterVersion -Path $target -Force -ErrorAction Stop -} catch { - Write-Error "下载失败(多半是访问不到 PSGallery):$_" - exit 1 +New-Item -ItemType Directory -Path $target -Force | Out-Null + +$needPester = $Force -or -not (Test-Path -LiteralPath $installed) + +if ($needPester) { + if ($Force -and (Test-Path -LiteralPath (Join-Path $target 'Pester'))) { + Remove-Item -LiteralPath (Join-Path $target 'Pester') -Recurse -Force + } + try { + Save-Module -Name Pester -RequiredVersion $PesterVersion -Path $target -Force -ErrorAction Stop + } + catch { + Write-Error "Pester 下载失败(多半是访问不到 PSGallery):$_" + exit 1 + } + Write-Host ("已安装:Pester {0}" -f $PesterVersion) -ForegroundColor Green +} +else { + Write-Host "Pester {0} 已经装好了,跳过(要重装加 -Force)。" -f $PesterVersion -ForegroundColor DarkGray } -$module = Get-Module -ListAvailable Pester | Where-Object { [version]$_.Version -eq $PesterVersion } -if (-not $module) { - Write-Error "下载结束但找不到 Pester $PesterVersion,请检查 $target。" - exit 1 +# PSScriptAnalyzer:版本目录带具体版本号,所以"装没装过"按目录是否存在判断 +$analyzerInstalled = @(Test-Path -LiteralPath (Join-Path $target 'PSScriptAnalyzer')) +if ($Force -or -not $analyzerInstalled) { + if ($Force -and $analyzerInstalled) { + Remove-Item -LiteralPath (Join-Path $target 'PSScriptAnalyzer') -Recurse -Force + } + try { + Save-Module -Name PSScriptAnalyzer -Path $target -Force -ErrorAction Stop + } + catch { + Write-Error "PSScriptAnalyzer 下载失败(多半是访问不到 PSGallery):$_" + exit 1 + } + $analyzerVersion = (Get-ChildItem (Join-Path $target 'PSScriptAnalyzer') -Directory | Select-Object -First 1).Name + Write-Host ("已安装:PSScriptAnalyzer {0}" -f $analyzerVersion) -ForegroundColor Green +} +else { + Write-Host 'PSScriptAnalyzer 已经装好了,跳过(要重装加 -Force)。' -ForegroundColor DarkGray } -Write-Host ("已安装:Pester {0} -> {1}" -f $module.Version, $module.ModuleBase) -ForegroundColor Green -Write-Host '现在可以跑:.\tests\Run-Pester.ps1' -ForegroundColor Cyan +Write-Host '' +Write-Host '现在可以跑:' -ForegroundColor Cyan +Write-Host ' .\tests\Run-Pester.ps1 (单元套件)' -ForegroundColor Gray +Write-Host ' .\tools\Invoke-Analyzer.ps1 (静态分析门禁)' -ForegroundColor Gray Write-Host '' exit 0 diff --git a/tools/Invoke-Analyzer.ps1 b/tools/Invoke-Analyzer.ps1 new file mode 100644 index 0000000..91dd086 --- /dev/null +++ b/tools/Invoke-Analyzer.ps1 @@ -0,0 +1,101 @@ +<# +.SYNOPSIS + 对全仓跑 PSScriptAnalyzer(默认规则集 + 格式规则集),按规则汇总。 + +.DESCRIPTION + 为什么要有这个入口,而不是直接敲 Invoke-ScriptAnalyzer: + + * 分析器装在**仓库内**(.tools\modules),不能指望机器上全局有一份; + * 格式规则默认是 Disabled —— 不带 -Settings 的调用会静默漏掉全部排版问题, + 那会让"按微软规范检查过了"变成一句空话; + * 结果要能一眼看出"哪条规则、几个文件、几行",而不是几百行原始输出; + * 路径过滤要与验收门槛一致:.tools\ / Backups\ / logs\ / dist\ 不进分析范围。 + + 与测试的分工:这是**独立的门禁**,不塞进 Pester 用例。那个套件跑一次二十多秒, + 把静态分析混进去会让"测试红了"这句话失去分辨力。 + +.PARAMETER Severity + 只报这些级别,默认 Error 与 Warning。 + +.PARAMETER Quiet + 只打印按规则汇总的计数,不逐条列出。 + +.EXAMPLE + .\tools\Invoke-Analyzer.ps1 + +.EXAMPLE + .\tools\Invoke-Analyzer.ps1 -Quiet +#> +[CmdletBinding()] +param( + [string[]]$Severity = @('Error', 'Warning'), + + [switch]$Quiet +) + +$ErrorActionPreference = 'Stop' + +$projectRoot = Split-Path -Parent $PSScriptRoot +$localModules = Join-Path $projectRoot '.tools\modules' +if (Test-Path -LiteralPath (Join-Path $localModules 'PSScriptAnalyzer')) { + $env:PSModulePath = $localModules + [System.IO.Path]::PathSeparator + $env:PSModulePath +} + +$analyzer = Get-Module -ListAvailable PSScriptAnalyzer | + Sort-Object { [version]$_.Version } -Descending | + Select-Object -First 1 + +if (-not $analyzer) { + Write-Host '' + Write-Host '找不到 PSScriptAnalyzer。把它装进仓库(不动机器上的全局模块):' -ForegroundColor Red + Write-Host ' .\tools\Install-TestDependencies.ps1' -ForegroundColor Cyan + Write-Host '' + exit 2 +} + +Import-Module $analyzer.Path -Force + +# 分析范围与验收门槛的 Encode / Parse 两层保持一致:只分析仓库自己的源码 +$excluded = '\\(\.git|\.tools|\.scratch|Backups|logs|dist|snapshots)\\' +$targets = @(Get-ChildItem -LiteralPath $projectRoot -Recurse -File | + Where-Object { $_.Extension -in '.ps1', '.psm1', '.psd1' } | + Where-Object { $_.FullName -notmatch $excluded } | + Select-Object -ExpandProperty FullName) + +$settings = Join-Path $projectRoot 'PSScriptAnalyzerSettings.psd1' + +Write-Host '' +Write-Host ("PSScriptAnalyzer {0}({1})" -f $analyzer.Version, $analyzer.ModuleBase) -ForegroundColor DarkGray +Write-Host ("分析 {0} 个文件,配置 {1}" -f $targets.Count, $settings) -ForegroundColor DarkGray +Write-Host '' + +# 逐个文件调用:-Path 在这个版本上只接受单个路径(传数组会报 Cannot convert +# 'System.Object[]' to the type 'System.String'),而我们要的正是「只分析仓库自己的 +# 源码」这个范围 —— 自己枚举文件反而更准。 +$results = @(foreach ($file in $targets) { + Invoke-ScriptAnalyzer -Path $file -Settings $settings -Severity $Severity + }) + +if ($results.Count -eq 0) { + Write-Host ("没有 {0} 级别的告警。" -f ($Severity -join '/')) -ForegroundColor Green + Write-Host '' + exit 0 +} + +$byRule = $results | Group-Object RuleName | Sort-Object Count -Descending +Write-Host ("共 {0} 条,按规则汇总:" -f $results.Count) -ForegroundColor Yellow +foreach ($group in $byRule) { + Write-Host (" {0,4} {1}" -f $group.Count, $group.Name) +} + +if (-not $Quiet) { + Write-Host '' + Write-Host '逐条:' -ForegroundColor Yellow + foreach ($item in ($results | Sort-Object ScriptName, Line)) { + $relative = $item.ScriptName.Replace($projectRoot, '').TrimStart('\') + Write-Host (" {0}:{1} [{2}] {3}" -f $relative, $item.Line, $item.RuleName, $item.Message) + } +} + +Write-Host '' +exit 1 diff --git a/tools/Register-BackupTask.ps1 b/tools/Register-BackupTask.ps1 index 209c7e3..13232c3 100644 --- a/tools/Register-BackupTask.ps1 +++ b/tools/Register-BackupTask.ps1 @@ -78,7 +78,8 @@ if ($Remove) { try { Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false -ErrorAction Stop Write-Host "已移除计划任务:$TaskName" -ForegroundColor Green - } catch { + } + catch { Write-Error "移除失败(多半是权限不足,请用管理员终端):$_" exit 1 } @@ -116,7 +117,8 @@ try { Write-Host "已注册计划任务:$TaskName(每天 $At)" -ForegroundColor Green Write-Host "查看上次运行结果:Get-ScheduledTaskInfo -TaskName '$TaskName'" -ForegroundColor DarkGray Write-Host "手动跑一次验证 :Start-ScheduledTask -TaskName '$TaskName'" -ForegroundColor DarkGray -} catch { +} +catch { Write-Error "注册失败(多半是权限不足,请用管理员终端):$_" exit 1 } diff --git a/tools/Rename-Archives.ps1 b/tools/Rename-Archives.ps1 index 18c5370..91a4517 100644 --- a/tools/Rename-Archives.ps1 +++ b/tools/Rename-Archives.ps1 @@ -242,7 +242,8 @@ foreach ($entry in $plan) { Write-Host "已重命名: $($entry.Old.Name) -> $($entry.New)" -ForegroundColor Green $ok++ - } catch { + } + catch { Write-Host "重命名失败: $($entry.Old.Name) —— $_" -ForegroundColor Red $failed++ } @@ -323,7 +324,7 @@ Write-BaknretManifest -Path $manifestPath -Manifest $manifestNew | Out-Null $referenced = @($rebuilt.Keys) $orphans = @(Get-ChildItem -LiteralPath $BackupDir -File -Force -ErrorAction SilentlyContinue | - Where-Object { $_.Extension.ToLower() -in $supportedFormats -and $_.BaseName -notin $referenced }) + Where-Object { $_.Extension.ToLower() -in $supportedFormats -and $_.BaseName -notin $referenced }) Write-Host '' Write-Host "重命名完成:成功 $ok,失败 $failed;manifest 已重建,含 $($rebuilt.Count) 个条目" -ForegroundColor Green diff --git a/tools/Set-SourceEncoding.ps1 b/tools/Set-SourceEncoding.ps1 index 1091269..079b3dc 100644 --- a/tools/Set-SourceEncoding.ps1 +++ b/tools/Set-SourceEncoding.ps1 @@ -65,7 +65,8 @@ Write-Host ("受管文件 {0} 个" -f $targets.Count) if ($addedBom.Count -gt 0) { Write-Host ("补上 BOM {0} 个:" -f $addedBom.Count) -ForegroundColor Yellow $addedBom | ForEach-Object { Write-Host " $_" } -} else { +} +else { Write-Host '所有文件都已经带 BOM。' -ForegroundColor Green } if ($normalizedLf.Count -gt 0) { diff --git a/tools/lab/Lab-Common.ps1 b/tools/lab/Lab-Common.ps1 index 2326bbe..6b349aa 100644 --- a/tools/lab/Lab-Common.ps1 +++ b/tools/lab/Lab-Common.ps1 @@ -15,20 +15,20 @@ $script:LabConfig = [ordered]@{ - VmName = 'BakNRet-Lab' - LabRoot = 'D:\VMs\BakNRet-Lab' - VhdxPath = 'D:\VMs\BakNRet-Lab\vhdx\BakNRet-Lab.vhdx' - VhdxSizeGB = 80 - IsoPath = 'F:\Images\Windows\Win11_25H2_Chinese_Simplified_x64_v2.iso' - ImageIndex = 4 # Windows 11 专业版 - SwitchName = 'Default Switch' + VmName = 'BakNRet-Lab' + LabRoot = 'D:\VMs\BakNRet-Lab' + VhdxPath = 'D:\VMs\BakNRet-Lab\vhdx\BakNRet-Lab.vhdx' + VhdxSizeGB = 80 + IsoPath = 'F:\Images\Windows\Win11_25H2_Chinese_Simplified_x64_v2.iso' + ImageIndex = 4 # Windows 11 专业版 + SwitchName = 'Default Switch' MemoryStartupGB = 8 - CpuCount = 8 - GuestRepoPath = 'C:\BakNRet' - GuestLabPath = 'C:\BakNRet-Lab' - GuestUser = 'lab' - CheckpointName = 'clean-baseline' - RepoRoot = (Split-Path -Parent (Split-Path -Parent $PSScriptRoot)) + CpuCount = 8 + GuestRepoPath = 'C:\BakNRet' + GuestLabPath = 'C:\BakNRet-Lab' + GuestUser = 'lab' + CheckpointName = 'clean-baseline' + RepoRoot = (Split-Path -Parent (Split-Path -Parent $PSScriptRoot)) } function Get-LabConfig { return $script:LabConfig } @@ -44,11 +44,11 @@ function Get-LabPath { function Write-LabLog { <# .SYNOPSIS 统一日志:同时进控制台与 $LabRoot\logs\lab.log。 #> - param([Parameter(Mandatory)][string]$Message, [ValidateSet('INFO','WARN','ERROR','STEP')][string]$Level = 'INFO') + param([Parameter(Mandatory)][string]$Message, [ValidateSet('INFO', 'WARN', 'ERROR', 'STEP')][string]$Level = 'INFO') $line = "[{0}] [{1,-5}] {2}" -f (Get-Date).ToString('HH:mm:ss'), $Level, $Message switch ($Level) { - 'STEP' { Write-Host $line -ForegroundColor Cyan } - 'WARN' { Write-Host $line -ForegroundColor Yellow } + 'STEP' { Write-Host $line -ForegroundColor Cyan } + 'WARN' { Write-Host $line -ForegroundColor Yellow } 'ERROR' { Write-Host $line -ForegroundColor Red } default { Write-Host $line } } @@ -65,8 +65,8 @@ function Assert-LabElevated { param([Parameter(Mandatory)][string]$Why) if (Test-LabElevated) { return } $gsudo = (Get-Command gsudo -ErrorAction SilentlyContinue | Select-Object -First 1).Source - $self = $MyInvocation.PSCommandPath - $hint = if ($gsudo) { "`n $gsudo pwsh -NoProfile -File `"$self`" $($MyInvocation.Line)" } else { '' } + $self = $MyInvocation.PSCommandPath + $hint = if ($gsudo) { "`n $gsudo pwsh -NoProfile -File `"$self`" $($MyInvocation.Line)" } else { '' } throw "需要管理员权限:$Why$hint" } @@ -131,7 +131,8 @@ function New-LabSession { $s = New-PSSession -VMName $script:LabConfig.VmName -Credential $cred -ErrorAction Stop Write-LabLog "PowerShell Direct 会话已建立(等待 $([math]::Round($sw.Elapsed.TotalSeconds)) 秒)" return $s - } catch { + } + catch { $lastError = $_.Exception.Message Start-Sleep -Seconds 5 } @@ -149,7 +150,8 @@ function Invoke-LabCommand { $s = New-LabSession -RetrySeconds $RetrySeconds try { return Invoke-Command -Session $s -ScriptBlock $ScriptBlock -ArgumentList $ArgumentList -ErrorAction Stop - } finally { + } + finally { Remove-PSSession -Session $s -ErrorAction SilentlyContinue } } @@ -178,5 +180,6 @@ function Test-LabGuestReady { try { $r = Invoke-LabCommand -ScriptBlock { Test-Path 'C:\BakNRet-Lab\state\provision.ok' } -RetrySeconds 60 return [bool]$r - } catch { return $false } + } + catch { return $false } } diff --git a/tools/lab/Lab.ps1 b/tools/lab/Lab.ps1 index 42aa621..d3cd581 100644 --- a/tools/lab/Lab.ps1 +++ b/tools/lab/Lab.ps1 @@ -38,10 +38,10 @@ [CmdletBinding()] param( [Parameter(Mandatory, Position = 0)] - [ValidateSet('status','start','stop','wait','sync','seed','backup','restore','acl-test','test','shell','console','checkpoint','reset','destroy')] + [ValidateSet('status', 'start', 'stop', 'wait', 'sync', 'seed', 'backup', 'restore', 'acl-test', 'test', 'shell', 'console', 'checkpoint', 'reset', 'destroy')] [string]$Verb, - [ValidateSet('all','pester','zero','e2e')][string]$Suite = 'all', + [ValidateSet('all', 'pester', 'zero', 'e2e')][string]$Suite = 'all', # 恢复演练要处理的条目(写法同 BackupList.txt 的一行) [string[]]$Entries, @@ -63,10 +63,10 @@ $ErrorActionPreference = 'Stop' . (Join-Path $PSScriptRoot 'Lab-Common.ps1') $cfg = Get-LabConfig -$guestSandbox = "$($cfg.GuestRepoPath)\tools\lab\payload\sandbox" -$guestList = "$guestSandbox\BackupList.txt" -$guestConfig = "$guestSandbox\BackupConfig.psd1" -$guestFixture = "$($cfg.GuestRepoPath)\tools\lab\payload\lab-fixtures.ps1" +$guestSandbox = "$($cfg.GuestRepoPath)\tools\lab\payload\sandbox" +$guestList = "$guestSandbox\BackupList.txt" +$guestConfig = "$guestSandbox\BackupConfig.psd1" +$guestFixture = "$($cfg.GuestRepoPath)\tools\lab\payload\lab-fixtures.ps1" $guestBackupDir = 'C:\BakNRet-Lab\Backups' Assert-LabElevated -Why "Hyper-V 操作与 PowerShell Direct 都需要管理员(动词:$Verb)" @@ -80,12 +80,12 @@ function Get-VmSummary { if (-not $vm) { return $null } $mem = Get-VMMemory -VMName $cfg.VmName return [pscustomobject]@{ - Name = $vm.Name - State = $vm.State - Uptime = [int]$vm.Uptime.TotalSeconds - Cpu = $vm.ProcessorCount - MemoryGB = [math]::Round($mem.Startup / 1GB, 1) - Gen = $vm.Generation + Name = $vm.Name + State = $vm.State + Uptime = [int]$vm.Uptime.TotalSeconds + Cpu = $vm.ProcessorCount + MemoryGB = [math]::Round($mem.Startup / 1GB, 1) + Gen = $vm.Generation UptimeText = "$([int]$vm.Uptime.TotalMinutes) 分钟" } } @@ -127,7 +127,7 @@ function Invoke-GuestScriptFile { $text = if ($badUtf8 -le $badAnsi) { $asUtf8 } else { $asAnsi } return @($text -split "`r?`n" | Where-Object { $_ -ne '' } | Select-Object -Last $lines) } - $all = @('-NoProfile','-ExecutionPolicy','Bypass','-File',$script) + $scriptArgs + $all = @('-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', $script) + $scriptArgs $out = $logPath $err = "$logPath.err" $p = Start-Process -FilePath 'pwsh.exe' -ArgumentList $all -NoNewWindow -Wait -PassThru -RedirectStandardOutput $out -RedirectStandardError $err @@ -147,7 +147,8 @@ function Invoke-LabSync { Push-Location $cfg.RepoRoot try { & $sevenZip a -tzip $zip '.\*' '-xr!Backups' '-xr!logs' '-xr!.git' '-xr!.tools' '-xr!*.tmp.7z' '-xr!*.tmp.zip' -y | Out-Null - } finally { Pop-Location } + } + finally { Pop-Location } Write-LabLog ("快照大小 {0} MB" -f [math]::Round((Get-Item -LiteralPath $zip).Length / 1MB, 2)) Write-LabLog '推送到 VM(Copy-VMFile,走 VMBus)' 'STEP' @@ -214,7 +215,7 @@ switch ($Verb) { $arch = @() if (Test-Path 'C:\BakNRet-Lab\Backups') { $arch = @(Get-ChildItem 'C:\BakNRet-Lab\Backups' -Filter *.7z -ErrorAction SilentlyContinue | - ForEach-Object { [pscustomobject]@{ Name = $_.BaseName; MB = [math]::Round($_.Length / 1MB, 2) } }) + ForEach-Object { [pscustomobject]@{ Name = $_.BaseName; MB = [math]::Round($_.Length / 1MB, 2) } }) } $src = 'C:\BakNRet-Lab\sources' [pscustomobject]@{ @@ -226,7 +227,7 @@ switch ($Verb) { SourceMB = $(if (Test-Path $src) { [math]::Round(((Get-ChildItem $src -Recurse -File -Force -ErrorAction SilentlyContinue | Measure-Object Length -Sum).Sum) / 1MB, 1) } else { 0 }) Archives = $arch LastLog = (Get-ChildItem 'C:\BakNRet-Lab\logs' -Filter 'backup-*.log' -ErrorAction SilentlyContinue | - Sort-Object LastWriteTime | Select-Object -Last 1 -ExpandProperty Name) + Sort-Object LastWriteTime | Select-Object -Last 1 -ExpandProperty Name) } } Write-Host ("VM 内 : 供给={0} {1} (build {2}) PS={3}" -f $g.Provisioned, $g.OsCaption, $g.OsBuild, $g.GuestPS) @@ -235,9 +236,11 @@ switch ($Verb) { if ($g.Archives.Count -gt 0) { Write-Host ("沙盒归档 : {0} 个({1} MB 合计)" -f $g.Archives.Count, [math]::Round((($g.Archives | Measure-Object MB -Sum).Sum), 1)) $g.Archives | Sort-Object MB -Descending | ForEach-Object { Write-Host (" {0,-52} {1,8} MB" -f $_.Name, $_.MB) } - } else { Write-Host '沙盒归档 : (还没有,跑 Lab.ps1 backup)' } + } + else { Write-Host '沙盒归档 : (还没有,跑 Lab.ps1 backup)' } if ($g.LastLog) { Write-Host ("最近备份日志: {0}" -f $g.LastLog) } - } catch { + } + catch { Write-Host ("VM 内查询失败(可能还没起来):{0}" -f $_.Exception.Message) -ForegroundColor Yellow } } @@ -337,8 +340,8 @@ switch ($Verb) { 'test' { $map = [ordered]@{ pester = @{ Path = 'tests\Run-Pester.ps1'; Args = @(); Name = 'Pester 套件' } - zero = @{ Path = 'tests\Run-Tests.ps1'; Args = @(); Name = '零依赖套件' } - e2e = @{ Path = 'tests\Run-E2E.ps1'; Args = @(); Name = '端到端验收' } + zero = @{ Path = 'tests\Run-Tests.ps1'; Args = @(); Name = '零依赖套件' } + e2e = @{ Path = 'tests\Run-E2E.ps1'; Args = @(); Name = '端到端验收' } } $pick = if ($Suite -eq 'all') { @($map.Keys) } else { @($Suite) } @@ -367,7 +370,7 @@ switch ($Verb) { $color = if ($_.ExitCode -eq 0) { 'Green' } else { 'Red' } Write-Host (" {0,-14} 退出码 {1} 日志 {2}" -f $_.Suite, $_.ExitCode, $_.Log) -ForegroundColor $color } - $bad = @($results | Where-Object ExitCode -ne 0) + $bad = @($results | Where-Object ExitCode -NE 0) if ($bad.Count -gt 0) { throw ("有 {0} 套件失败" -f $bad.Count) } } @@ -398,7 +401,7 @@ switch ($Verb) { 'reset' { if (-not $CheckpointName) { $CheckpointName = $cfg.CheckpointName } - $snap = Get-VMSnapshot -VMName $cfg.VmName -ErrorAction SilentlyContinue | Where-Object Name -eq $CheckpointName + $snap = Get-VMSnapshot -VMName $cfg.VmName -ErrorAction SilentlyContinue | Where-Object Name -EQ $CheckpointName if (-not $snap) { throw "找不到检查点 $CheckpointName" } Write-LabLog "回到检查点 $CheckpointName" 'STEP' Restore-VMSnapshot -VMSnapshot $snap -Confirm:$false diff --git a/tools/lab/New-BakNRetLab.ps1 b/tools/lab/New-BakNRetLab.ps1 index 2071519..5870c58 100644 --- a/tools/lab/New-BakNRetLab.ps1 +++ b/tools/lab/New-BakNRetLab.ps1 @@ -29,7 +29,7 @@ [CmdletBinding()] param( - [ValidateSet('all','disk','vm','provision')][string]$Stage = 'all', + [ValidateSet('all', 'disk', 'vm', 'provision')][string]$Stage = 'all', [switch]$Recreate, [switch]$ListImages, [int]$ImageIndex = 0 @@ -54,14 +54,14 @@ function Get-IsoVolume { function Get-ImageList { param([Parameter(Mandatory)][string]$IsoLetter) - $wim = @('install.wim','install.esd') | + $wim = @('install.wim', 'install.esd') | ForEach-Object { Join-Path "$IsoLetter`:\sources" $_ } | Where-Object { Test-Path -LiteralPath $_ } | Select-Object -First 1 if (-not $wim) { throw "ISO ($IsoLetter`:) 里找不到 sources\install.wim|esd" } $info = & dism.exe /English /Get-WimInfo /WimFile:"$wim" 2>&1 $list = @(); $cur = $null foreach ($line in $info) { - if ($line -match '^Index\s*:\s*(\d+)') { if ($cur) { $list += $cur }; $cur = [ordered]@{ Index = [int]$Matches[1]; Name = ''; Size = '' } } + if ($line -match '^Index\s*:\s*(\d+)') { if ($cur) { $list += $cur }; $cur = [ordered]@{ Index = [int]$Matches[1]; Name = ''; Size = '' } } elseif ($cur -and $line -match '^Name\s*:\s*(.+?)\s*$') { $cur.Name = $Matches[1] } elseif ($cur -and $line -match '^Size\s*:\s*(.+?)\s*$') { $cur.Size = $Matches[1] } } @@ -99,7 +99,7 @@ function New-LabSystemDisk { Write-LabLog "已创建动态 VHDX($($cfg.VhdxSizeGB) GB):$($cfg.VhdxPath)" 'STEP' } - $vhd = Mount-VHD -Path $cfg.VhdxPath -Passthru + $vhd = Mount-VHD -Path $cfg.VhdxPath -Passthru $disk = $vhd | Get-Disk if ($disk.PartitionStyle -eq 'RAW') { @@ -115,7 +115,7 @@ function New-LabSystemDisk { Write-LabLog '已完成 GPT 分区(ESP 类型已按 EFI System Partition 建立)与格式化' 'STEP' } - $efiPart = Get-Partition -DiskNumber $disk.Number | Where-Object GptType -eq $espGuid + $efiPart = Get-Partition -DiskNumber $disk.Number | Where-Object GptType -EQ $espGuid $winPart = Get-Partition -DiskNumber $disk.Number | Where-Object { $_.GptType -eq '{ebd0a0a2-b9e5-4433-87c0-68b6b72699c7}' -and $_.Size -gt 1GB } if (-not $efiPart -or -not $winPart) { throw '分区布局不符合预期(ESP / Windows 分区没找到)' } $efiLetter = $efiPart.DriveLetter @@ -129,30 +129,32 @@ function New-LabSystemDisk { $di = Get-IsoVolume $isoLetter = ($di | Get-Volume).DriveLetter $il = Get-ImageList -IsoLetter $isoLetter - $pick = $il.Images | Where-Object Index -eq $cfg.ImageIndex + $pick = $il.Images | Where-Object Index -EQ $cfg.ImageIndex if (-not $pick) { throw "ISO 里没有索引 $($cfg.ImageIndex);可用:$($il.Images.Index -join ', ')" } Write-LabLog "展开映像 [$($pick.Index)] $($pick.Name) -> $winLetter`:(需要十几分钟)" 'STEP' $scratch = Get-LabPath 'scratch' $out = Get-LabPath 'logs\dism-apply.out' $err = Get-LabPath 'logs\dism-apply.err' $proc = Start-Process -FilePath 'dism.exe' -NoNewWindow -Wait -PassThru -RedirectStandardOutput $out -RedirectStandardError $err ` - -ArgumentList @('/English','/Apply-Image',"/ImageFile:$($il.WimPath)", "/Index:$($pick.Index)", "/ApplyDir:$winLetter`:\", "/ScratchDir:$scratch") + -ArgumentList @('/English', '/Apply-Image', "/ImageFile:$($il.WimPath)", "/Index:$($pick.Index)", "/ApplyDir:$winLetter`:\", "/ScratchDir:$scratch") if ($proc.ExitCode -ne 0) { throw "DISM 展开失败,退出码 $($proc.ExitCode),见 $out / $err" } Write-LabLog '映像展开完成' 'STEP' - } else { + } + else { Write-LabLog '系统盘上已有 Windows,跳过展开' 'WARN' } # ---- 注入负载与无人值守应答文件 ---- Write-LabLog '注入 7-Zip / PowerShell 7 / Pester / 供给脚本' 'STEP' $payloadSrc = Join-Path $PSScriptRoot 'payload' - $guestLab = Join-Path "$winLetter`:\" ($cfg.GuestLabPath.TrimStart('\')) - foreach ($item in '7zip','pwsh','Pester','provision.ps1') { + $guestLab = Join-Path "$winLetter`:\" ($cfg.GuestLabPath.TrimStart('\')) + foreach ($item in '7zip', 'pwsh', 'Pester', 'provision.ps1') { $src = Join-Path $payloadSrc $item $dst = Join-Path $guestLab ('payload\' + $item) if (Test-Path -LiteralPath $src) { $null = robocopy $src $dst /MIR /NFL /NDL /NJH /NJS /NP /R:1 /W:1 - } else { + } + else { Write-LabLog "负载缺失(跳过):$src" 'WARN' } } @@ -191,16 +193,17 @@ function New-LabVM { if (-not $vm) { Write-LabLog "创建虚拟机 $($cfg.VmName)(Gen2 / $($cfg.MemoryStartupGB) GB / $($cfg.CpuCount) vCPU)" 'STEP' $vm = New-VM -Name $cfg.VmName -Generation 2 -MemoryStartupBytes ($cfg.MemoryStartupGB * 1GB) ` - -VHDPath $cfg.VhdxPath -SwitchName $cfg.SwitchName + -VHDPath $cfg.VhdxPath -SwitchName $cfg.SwitchName Set-VMProcessor -VMName $cfg.VmName -Count $cfg.CpuCount Set-VMFirmware -VMName $cfg.VmName -EnableSecureBoot Off Set-VM -Name $cfg.VmName -AutomaticStopAction TurnOff -AutomaticStartAction Nothing # 集成服务名随系统语言变化(中文是「来宾服务接口」),按状态启用而不是按名字找 Get-VMIntegrationService -VMName $cfg.VmName | Where-Object { -not $_.Enabled } | ForEach-Object { Enable-VMIntegrationService -VMName $cfg.VmName -Name $_.Name } - } else { + } + else { Write-LabLog "虚拟机 $($cfg.VmName) 已存在(状态 $($vm.State))" 'WARN' - if (-not (Get-VMHardDiskDrive -VMName $cfg.VmName | Where-Object Path -eq $cfg.VhdxPath)) { + if (-not (Get-VMHardDiskDrive -VMName $cfg.VmName | Where-Object Path -EQ $cfg.VhdxPath)) { Add-VMHardDiskDrive -VMName $cfg.VmName -Path $cfg.VhdxPath } } @@ -235,7 +238,7 @@ function Wait-LabProvision { function New-LabCheckpoint { Assert-LabElevated -Why '创建 Hyper-V 检查点' - $existing = Get-VMSnapshot -VMName $cfg.VmName -ErrorAction SilentlyContinue | Where-Object Name -eq $cfg.CheckpointName + $existing = Get-VMSnapshot -VMName $cfg.VmName -ErrorAction SilentlyContinue | Where-Object Name -EQ $cfg.CheckpointName if ($existing) { Write-LabLog "检查点 $($cfg.CheckpointName) 已存在,跳过" 'WARN'; return } Checkpoint-VM -Name $cfg.VmName -SnapshotName $cfg.CheckpointName Write-LabLog "已创建检查点 $($cfg.CheckpointName)(Lab.ps1 -Verb reset 可回到此状态)" 'STEP' @@ -245,8 +248,8 @@ function New-LabCheckpoint { # 主流程 # --------------------------------------------------------------------------- -if ($Stage -in @('all','disk')) { New-LabSystemDisk } -if ($Stage -in @('all','vm')) { New-LabVM } -if ($Stage -in @('all','provision')) { Wait-LabProvision; New-LabCheckpoint } +if ($Stage -in @('all', 'disk')) { New-LabSystemDisk } +if ($Stage -in @('all', 'vm')) { New-LabVM } +if ($Stage -in @('all', 'provision')) { Wait-LabProvision; New-LabCheckpoint } Write-LabLog '搭建流程结束' 'STEP' \ No newline at end of file diff --git a/tools/lab/payload/lab-fixtures.ps1 b/tools/lab/payload/lab-fixtures.ps1 index f082cc7..8a6c8ba 100644 --- a/tools/lab/payload/lab-fixtures.ps1 +++ b/tools/lab/payload/lab-fixtures.ps1 @@ -34,7 +34,8 @@ function New-TextFile { if (-not (Test-Path -LiteralPath $dir)) { New-Item -ItemType Directory -Force -Path $dir | Out-Null } if ($Count -le 1) { Set-Content -LiteralPath $Path -Value $Content -Encoding UTF8 - } else { + } + else { Set-Content -LiteralPath $Path -Value (1..$Count | ForEach-Object { "$Content #$_" }) -Encoding UTF8 } } @@ -73,7 +74,7 @@ New-TextFile (Join-Path $appC '数据 备份\记录.txt') '记录内容' -Count # --- 4. 真 NTFS 连接点 ------------------------------------------------------- $realTarget = Join-Path $Root 'AppMultiSlot\Data' -$junction = Join-Path $Root 'JunctionToData' +$junction = Join-Path $Root 'JunctionToData' if (-not (Test-Path -LiteralPath $junction)) { $null = New-Item -ItemType Junction -Path $junction -Target $realTarget -ErrorAction SilentlyContinue } @@ -86,21 +87,22 @@ New-TextFile (Join-Path $cursor 'bottom.txt') 'deep content' Write-Host ("最长路径长度:{0} 字符" -f (Join-Path $cursor 'bottom.txt').Length) # --- 6. 50 MB 大文件 --------------------------------------------------------- -$bigDir = Join-Path $Root 'AppBig' +$bigDir = Join-Path $Root 'AppBig' $bigFile = Join-Path $bigDir 'blob-50mb.bin' if (-not (Test-Path -LiteralPath $bigFile)) { New-Item -ItemType Directory -Force -Path $bigDir | Out-Null $fs = [IO.File]::Create($bigFile) try { - $rng = [Random]::new(20260926) + $rng = [Random]::new(20260926) $chunk = [byte[]]::new(1MB) for ($i = 0; $i -lt 50; $i++) { $rng.NextBytes($chunk); $fs.Write($chunk, 0, $chunk.Length) } - } finally { $fs.Dispose() } + } + finally { $fs.Dispose() } } -Write-Host ("大文件:{0} MB" -f [math]::Round((Get-Item $bigFile).Length/1MB,1)) +Write-Host ("大文件:{0} MB" -f [math]::Round((Get-Item $bigFile).Length / 1MB, 1)) # --- 7. 被占用文件(后台进程持句柄 90 秒后释放)----------------------------- -$lockDir = Join-Path $Root 'AppLocked' +$lockDir = Join-Path $Root 'AppLocked' $lockFile = Join-Path $lockDir 'locked.bin' New-Item -ItemType Directory -Force -Path $lockDir | Out-Null New-TextFile $lockFile 'this file is held open by another process' @@ -111,7 +113,7 @@ $holderLines = @( ) $holderPath = 'C:\BakNRet-Lab\state\hold-lock.ps1' Set-Content -LiteralPath $holderPath -Value $holderLines -Encoding UTF8 -Start-Process -FilePath 'powershell.exe' -ArgumentList @('-NoProfile','-ExecutionPolicy','Bypass','-File',$holderPath,$lockFile) -WindowStyle Hidden +Start-Process -FilePath 'powershell.exe' -ArgumentList @('-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', $holderPath, $lockFile) -WindowStyle Hidden Write-Host "已启动占用者进程(持句柄 90 秒):$lockFile" # --- 8. 「源不存在」条目对应的目录:故意不建 --------------------------------- @@ -121,6 +123,6 @@ Write-Host '' Write-Host '--- 沙盒源清单 ---' Get-ChildItem -LiteralPath $Root -Force | ForEach-Object { $files = @(Get-ChildItem -LiteralPath $_.FullName -Recurse -File -Force -ErrorAction SilentlyContinue) - $mb = [math]::Round((($files | Measure-Object Length -Sum).Sum) / 1MB, 2) + $mb = [math]::Round((($files | Measure-Object Length -Sum).Sum) / 1MB, 2) " {0,-24} {1,4} 个文件 {2,8} MB 连接点={3}" -f $_.Name, $files.Count, $mb, [bool]($_.Attributes -band [IO.FileAttributes]::ReparsePoint) } diff --git a/tools/lab/payload/provision.ps1 b/tools/lab/payload/provision.ps1 index 7a772ab..fdb811a 100644 --- a/tools/lab/payload/provision.ps1 +++ b/tools/lab/payload/provision.ps1 @@ -19,10 +19,10 @@ #> $ErrorActionPreference = 'Continue' -$ProgressPreference = 'SilentlyContinue' +$ProgressPreference = 'SilentlyContinue' -$lab = 'C:\BakNRet-Lab' -$logDir = Join-Path $lab 'logs' +$lab = 'C:\BakNRet-Lab' +$logDir = Join-Path $lab 'logs' $stateDir = Join-Path $lab 'state' New-Item -ItemType Directory -Force -Path $logDir, $stateDir | Out-Null @@ -40,24 +40,24 @@ try { Set-ExecutionPolicy -Scope LocalMachine -ExecutionPolicy Bypass -Force Step '3/7 工具上机器 PATH:7-Zip 与 PowerShell 7' - $zipSrc = Join-Path $lab 'payload\7zip' - $zipDst = 'C:\Program Files\7-Zip' + $zipSrc = Join-Path $lab 'payload\7zip' + $zipDst = 'C:\Program Files\7-Zip' $pwshSrc = Join-Path $lab 'payload\pwsh' $pwshDst = 'C:\Program Files\PowerShell\7' - if (-not (Test-Path $zipDst)) { robocopy $zipSrc $zipDst /MIR /NFL /NDL /NJH /NJS /NP | Out-Null } + if (-not (Test-Path $zipDst)) { robocopy $zipSrc $zipDst /MIR /NFL /NDL /NJH /NJS /NP | Out-Null } if (-not (Test-Path $pwshDst)) { robocopy $pwshSrc $pwshDst /MIR /NFL /NDL /NJH /NJS /NP | Out-Null } $machinePath = [Environment]::GetEnvironmentVariable('Path', 'Machine') foreach ($p in @($zipDst, $pwshDst)) { if ($machinePath -notlike "*$p*") { $machinePath = $machinePath.TrimEnd(';') + ';' + $p } - if ($env:Path -notlike "*$p*") { $env:Path = $env:Path.TrimEnd(';') + ';' + $p } + if ($env:Path -notlike "*$p*") { $env:Path = $env:Path.TrimEnd(';') + ';' + $p } } [Environment]::SetEnvironmentVariable('Path', $machinePath, 'Machine') Step '4/7 安装 Pester 5.9.1(Windows PowerShell 与 pwsh 各一份)' $pesterSrc = Join-Path $lab 'payload\Pester\5.9.1' foreach ($dst in @("$env:ProgramFiles\WindowsPowerShell\Modules\Pester\5.9.1", - "$env:ProgramFiles\PowerShell\Modules\Pester\5.9.1")) { + "$env:ProgramFiles\PowerShell\Modules\Pester\5.9.1")) { if (-not (Test-Path $dst)) { robocopy $pesterSrc $dst /MIR /NFL /NDL /NJH /NJS /NP | Out-Null } } @@ -75,7 +75,7 @@ try { New-ItemProperty -Path $scoobe -Name 'ScoobeSystemSettingEnabled' -Value 0 -PropertyType DWord -Force | Out-Null Step '7/7 采集真机事实并落盘' - $zipExe = Join-Path $zipDst '7z.exe' + $zipExe = Join-Path $zipDst '7z.exe' $pwshExe = Join-Path $pwshDst 'pwsh.exe' $pwshVer = '缺失' if (Test-Path $pwshExe) { $pwshVer = (& $pwshExe -NoProfile -Command '$PSVersionTable.PSVersion.ToString()' 2>&1) -join ' ' } @@ -100,8 +100,8 @@ try { CpuCount = (Get-CimInstance Win32_ComputerSystem).NumberOfLogicalProcessors RamGB = [math]::Round((Get-CimInstance Win32_ComputerSystem).TotalPhysicalMemory / 1GB, 1) Drives = @(Get-Volume | Where-Object DriveLetter | ForEach-Object { - [ordered]@{ Letter = "$($_.DriveLetter):"; Fs = $_.FileSystemType; SizeGB = [math]::Round($_.Size / 1GB, 1) } - }) + [ordered]@{ Letter = "$($_.DriveLetter):"; Fs = $_.FileSystemType; SizeGB = [math]::Round($_.Size / 1GB, 1) } + }) } $facts | ConvertTo-Json -Depth 6 | Set-Content -LiteralPath (Join-Path $stateDir 'provisioned.json') -Encoding UTF8 $facts.GetEnumerator() | ForEach-Object { Write-Host (" {0,-16} {1}" -f $_.Key, $_.Value) } diff --git a/tools/lab/payload/run-acl-scenario.ps1 b/tools/lab/payload/run-acl-scenario.ps1 index ae92d5a..0e3118a 100644 --- a/tools/lab/payload/run-acl-scenario.ps1 +++ b/tools/lab/payload/run-acl-scenario.ps1 @@ -66,7 +66,8 @@ function Invoke-NativeTolerant { $ErrorActionPreference = 'Continue' try { return @(& $FilePath @ArgumentList 2>&1) - } finally { + } + finally { $ErrorActionPreference = $previous } } @@ -75,7 +76,8 @@ function Test-Scenario { if ($Ok) { $script:Passed++ Write-Host (" [PASS] {0}" -f $Name) -ForegroundColor Green - } else { + } + else { $script:Failures += $Name Write-Host (" [FAIL] {0}{1}" -f $Name, $(if ($Detail) { ' —— ' + $Detail } else { '' })) -ForegroundColor Red } @@ -94,8 +96,8 @@ function Get-SecurityFingerprint { $acl = Get-Acl -LiteralPath $Path $sid = [System.Security.Principal.SecurityIdentifier] $aces = @($acl.GetAccessRules($true, $true, $sid) | - ForEach-Object { '{0}|{1}|{2}' -f $_.AccessControlType, $_.IdentityReference.Value, [int]$_.FileSystemRights } | - Sort-Object) + ForEach-Object { '{0}|{1}|{2}' -f $_.AccessControlType, $_.IdentityReference.Value, [int]$_.FileSystemRights } | + Sort-Object) return ('O={0} G={1} P={2} [{3}]' -f $acl.GetOwner($sid).Value, $acl.GetGroup($sid).Value, $acl.AreAccessRulesProtected, ($aces -join ' ')) } @@ -156,7 +158,8 @@ function Stop-VscodeProcesses { if ($path -and $path.StartsWith($AppRoot, [System.StringComparison]::OrdinalIgnoreCase)) { Stop-Process -Id $process.Id -Force -ErrorAction SilentlyContinue } - } catch { } + } + catch { } } } Start-Sleep -Milliseconds 700 @@ -184,7 +187,7 @@ function Remove-TreeHard { if (-not (Test-Path -LiteralPath $Path)) { return } $links = @(Get-ChildItem -LiteralPath $Path -Recurse -Force -Directory -ErrorAction SilentlyContinue | - Where-Object { $_.Attributes -band [IO.FileAttributes]::ReparsePoint }) + Where-Object { $_.Attributes -band [IO.FileAttributes]::ReparsePoint }) foreach ($link in $links) { $null = Invoke-NativeTolerant -FilePath 'cmd.exe' -ArgumentList @('/c', ('rmdir "{0}"' -f $link.FullName)) Remove-BaknretJunction -Path $link.FullName @@ -207,7 +210,8 @@ function Remove-TreeHard { if (Test-Path -LiteralPath $WorkRoot) { Get-ChildItem -LiteralPath $WorkRoot -Directory -ErrorAction SilentlyContinue | ForEach-Object { Remove-TreeHard -Path $_.FullName } -} else { +} +else { New-Item -ItemType Directory -Path $WorkRoot -Force | Out-Null } $BackupDir = Join-Path $WorkRoot 'backups' @@ -242,10 +246,12 @@ if (-not $SkipScoop) { try { Invoke-Expression "& {$(Invoke-RestMethod -Uri 'https://get.scoop.sh')} -RunAsAdmin" Write-Host ('[A] 安装器退出码:{0}' -f $LASTEXITCODE) - } catch { + } + catch { Test-Scenario 'A: 安装 scoop' $false $_.Exception.Message } - } else { + } + else { Write-Host '[A] scoop 已存在,跳过安装' } @@ -299,9 +305,11 @@ $vscodeReady = [bool]$codeCmd if ($vscodeReady) { Test-Scenario 'A: vscode 已安装(找到 CLI)' $true $codeCmd -} elseif ($SkipScoop) { +} +elseif ($SkipScoop) { Write-Host (' [SKIP] 按 -SkipScoop 跳过 vscode({0} 不存在)' -f $vscodeCli) -ForegroundColor Yellow -} else { +} +else { Test-Scenario 'A: vscode 已安装(找到 CLI)' $false $vscodeCli } @@ -357,7 +365,7 @@ $sourceOwner = (Get-Acl -LiteralPath $bData).GetOwner([System.Security.Principal Write-Host ('[B] 备份前 data 的属主:{0}' -f (Get-Acl -LiteralPath $bData).Owner) $currentSid = ([System.Security.Principal.WindowsIdentity]::GetCurrent()).User.Value Test-Scenario 'B: 现场造对了 —— 属主既不是当前账户、也不是新建对象的默认属主' ` - (($sourceOwner -ne $currentSid) -and ($sourceOwner -ne $creatorOwner)) ` +(($sourceOwner -ne $currentSid) -and ($sourceOwner -ne $creatorOwner)) ` "owner=$sourceOwner current=$currentSid creatorDefault=$creatorOwner" # --------------------------------------------------------------------------- @@ -398,7 +406,7 @@ $backup = Invoke-BaknretChild -Script (Join-Path $RepoPath 'Backup.ps1') -Parame $backup.LastLog | ForEach-Object { ' ' + $_ } Test-Scenario '备份退出码 0' ($backup.ExitCode -eq 0) ('exit=' + $backup.ExitCode) Test-Scenario '每个条目都写了 .acl.json' (@(Get-ChildItem -LiteralPath $BackupDir -Filter '*.acl.json').Count -ge $entries.Count) ` - ('acl.json=' + @(Get-ChildItem -LiteralPath $BackupDir -Filter '*.acl.json').Count + ' 条目=' + $entries.Count) +('acl.json=' + @(Get-ChildItem -LiteralPath $BackupDir -Filter '*.acl.json').Count + ' 条目=' + $entries.Count) # --------------------------------------------------------------------------- # 删源 → 恢复 @@ -445,7 +453,8 @@ if ($vscodeReady) { [System.IO.File]::WriteAllText($probeFile, 'write probe') $writeOk = (Test-Path -LiteralPath $probeFile) Remove-Item -LiteralPath $probeFile -Force -ErrorAction SilentlyContinue - } catch { + } + catch { $detail = $_.Exception.Message } Test-Scenario 'A: vscode 的数据目录可写(新建文件成功)' $writeOk $detail @@ -455,9 +464,10 @@ if ($vscodeReady) { $expectedNormalized = $expected[$pair[1]] -replace ' P=(True|False) ', ' P=' $actualNormalized = (Get-SecurityFingerprint -Path $pair[0]) -replace ' P=(True|False) ', ' P=' Test-Scenario ("A: {0} 的安全指纹与备份前一致" -f $pair[1]) ($actualNormalized -eq $expectedNormalized) ` - ("want: " + $expectedNormalized + " / got: " + $actualNormalized) + ("want: " + $expectedNormalized + " / got: " + $actualNormalized) } -} else { +} +else { Write-Host ' [SKIP] vscode 没装上,A 段的功能断言跳过(-SkipScoop 或下载失败)' -ForegroundColor Yellow } @@ -488,7 +498,7 @@ $negative = Join-Path $WorkRoot 'negative-data' & robocopy.exe $bData $negative /E /COPY:DAT /NFL /NDL /NJH /NJS /NP | Out-Null $negativeOwner = (Get-Acl -LiteralPath $negative).GetOwner([System.Security.Principal.SecurityIdentifier]).Value Test-Scenario 'B 负对照: 只搬文件时,属主变成"跑脚本的账户"而不再是原账户' ` - (($negativeOwner -ne $sourceOwner) -and ($negativeOwner -eq $creatorOwner)) ` +(($negativeOwner -ne $sourceOwner) -and ($negativeOwner -eq $creatorOwner)) ` "negative=$negativeOwner creatorDefault=$creatorOwner" Write-Host (' 原属主 = {0}' -f $sourceOwner) Write-Host (' 恢复后属主 = {0}(应与原属主相同)' -f $restoredOwner) @@ -501,14 +511,16 @@ Write-Host '' $total = $script:Passed + $script:Failures.Count if ($script:Failures.Count -eq 0) { Write-Host ('ACL 演练:全部通过 {0} 项' -f $total) -ForegroundColor Green -} else { +} +else { Write-Host ('ACL 演练:通过 {0} 项,失败 {1} 项' -f $script:Passed, $script:Failures.Count) -ForegroundColor Red foreach ($failure in $script:Failures) { Write-Host (' - ' + $failure) -ForegroundColor Red } } if ($KeepWorkRoot) { Write-Host ('临时目录保留:{0}' -f $WorkRoot) -ForegroundColor Yellow -} else { +} +else { Remove-TreeHard -Path $bRoot Remove-TreeHard -Path (Join-Path $WorkRoot 'negative-data') # 备份与日志留着,便于事后核对(归档可能有几百 MB,要腾空间就手工删这个目录) diff --git a/tools/lab/payload/run-drill.ps1 b/tools/lab/payload/run-drill.ps1 index 234fcc8..d385af1 100644 --- a/tools/lab/payload/run-drill.ps1 +++ b/tools/lab/payload/run-drill.ps1 @@ -38,8 +38,8 @@ $drillParams = [ordered]@{ ConfigPath = $ConfigPath } if ($entries.Count -gt 0) { $drillParams['Entries'] = $entries } -if ($KeepWorkRoot) { $drillParams['KeepWorkRoot'] = $true } -if ($AllowChanged) { $drillParams['AllowChanged'] = $true } +if ($KeepWorkRoot) { $drillParams['KeepWorkRoot'] = $true } +if ($AllowChanged) { $drillParams['AllowChanged'] = $true } Write-Host ("[lab] 恢复演练:{0} 个条目 -> {1}" -f $entries.Count, ($entries -join ' | ')) & 'C:\BakNRet\tests\Restore-Drill.ps1' @drillParams \ No newline at end of file diff --git a/tools/lab/payload/run-suite-utf8.ps1 b/tools/lab/payload/run-suite-utf8.ps1 index d7f97a7..40265d9 100644 --- a/tools/lab/payload/run-suite-utf8.ps1 +++ b/tools/lab/payload/run-suite-utf8.ps1 @@ -29,8 +29,8 @@ param( $ErrorActionPreference = 'Continue' [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 -[Console]::InputEncoding = [System.Text.Encoding]::UTF8 -$OutputEncoding = [System.Text.Encoding]::UTF8 +[Console]::InputEncoding = [System.Text.Encoding]::UTF8 +$OutputEncoding = [System.Text.Encoding]::UTF8 Write-Host ("[lab] 包装器:PS {0},[Console]::OutputEncoding = {1}" -f $PSVersionTable.PSVersion, [Console]::OutputEncoding.WebName) Write-Host ("[lab] 套件:{0} {1}" -f $Suite, ($SuiteArgs -join ' ')) diff --git a/tools/lab/payload/sandbox/BackupConfig.psd1 b/tools/lab/payload/sandbox/BackupConfig.psd1 index 9dc9acb..1de10c8 100644 --- a/tools/lab/payload/sandbox/BackupConfig.psd1 +++ b/tools/lab/payload/sandbox/BackupConfig.psd1 @@ -21,5 +21,5 @@ ToolOutput = 'quiet' Snapshot = @{ Enabled = $false; KeepCount = 3; KeepDays = 30 } Encryption = @{ Enabled = $false; PasswordFile = 'C:\BakNRet-Lab\state\baknret.key'; EncryptHeaders = $true } - DefaultExcludes = @('!Thumbs.db','!desktop.ini') + DefaultExcludes = @('!Thumbs.db', '!desktop.ini') } \ No newline at end of file diff --git a/tools/lab/payload/sandbox/SoftwareCatalog.psd1 b/tools/lab/payload/sandbox/SoftwareCatalog.psd1 index 2d61dac..df279af 100644 --- a/tools/lab/payload/sandbox/SoftwareCatalog.psd1 +++ b/tools/lab/payload/sandbox/SoftwareCatalog.psd1 @@ -5,18 +5,18 @@ 带连字符的键必须加引号(PSD1 会把它当减法);这里用中文键名,也统一加引号。 #> @{ - AppMultiSlot = @{ - DefaultData = @{ Path = 'C:\BakNRet-Lab\sources\AppMultiSlot\Data'; Description = '主数据(含 node_modules、session.log、空目录)' } + AppMultiSlot = @{ + DefaultData = @{ Path = 'C:\BakNRet-Lab\sources\AppMultiSlot\Data'; Description = '主数据(含 node_modules、session.log、空目录)' } DefaultConfig = @{ Path = 'C:\BakNRet-Lab\sources\AppMultiSlot\Config'; Description = '配置(含 .bak)' } - CacheSlot = @{ Path = 'C:\BakNRet-Lab\sources\AppMultiSlot\Cache'; Description = '缓存(清单里再排除一条 cache-01.tmp)' } + CacheSlot = @{ Path = 'C:\BakNRet-Lab\sources\AppMultiSlot\Cache'; Description = '缓存(清单里再排除一条 cache-01.tmp)' } } - AppFileSlot = @{ + AppFileSlot = @{ Profile = @{ Path = 'C:\BakNRet-Lab\sources\AppFileSlot\profile.json'; Description = '单文件 Slot:归档内是名为 Profile 的文件' } - Readme = @{ Path = 'C:\BakNRet-Lab\sources\AppFileSlot\readme.txt'; Description = '另一个单文件 Slot' } + Readme = @{ Path = 'C:\BakNRet-Lab\sources\AppFileSlot\readme.txt'; Description = '另一个单文件 Slot' } } - '软件目录甲' = @{ + '软件目录甲' = @{ DefaultData = @{ Path = 'C:\BakNRet-Lab\sources\软件 目录.甲'; Description = '中文 + 空格 + 点的路径' } } @@ -24,7 +24,7 @@ DefaultData = @{ Path = 'C:\BakNRet-Lab\sources\JunctionToData'; Description = '真 NTFS 连接点(指向 AppMultiSlot\Data)' } } - MissingApp = @{ + MissingApp = @{ DefaultData = @{ Path = 'C:\BakNRet-Lab\sources\MissingApp'; Description = '源不存在:备份跳过并记 missing-source,恢复仍知道目标位置' } }