style: 按微软规范落地静态分析,并全仓机械重排

三件事:

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 层。这条要改行为、且影响真实名录的解析
    结果,留给你拍板,不在本提交里动手。
This commit is contained in:
Shuery committed 2026-09-27 09:46:08 +08:00
1 parent 102a3e038d
commit 187d2759fd
60 files changed
+769 -377

No files matched your search

+41 -21
View File
@@ -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
}