docs(report): 新增 PROJECT_REPORT.md 与 CI/CD 流水线记录
PROJECT_REPORT.md:论文式项目报告(摘要 / 目录 / 7 章 / 参考文献 / 致谢 / 附录 A),
4 张 Mermaid 图同样经真实浏览器渲染验证。所有数字都与证据文件逐个核对过。
.scratch/ci-cd/:本次流水线的全部证据与可重跑脚本
* 阶段报告:依赖安全扫描 / 许可证合规 / 阶段 0 静态分析 / 阶段 3 测试与性能
* 证据:分析器原始输出(修复前 84 条、修复后 80 条)、Pester 详细输出、
性能基线 JSON、黑盒用例结果(阶段 1 的 11 例与阶段 2 回归的 12 例)
* 可重跑:blackbox-tests.ps1 / blackbox-regression.ps1 / perf-baseline.ps1
两条边界必须写在明处,不能含糊:
* **VM 内验证只取到修复前的快照**(Pester 183 项全绿)。随后会话审批策略改为 never,
gsudo 提权被自动拒绝(退出码 999),而 Hyper-V 与 PowerShell Direct 都需要管理员,
于是修复后的三套件在 VM 内**没有跑成**。报告里明确标注了范围,没有把"宿主跑绿了"
说成"VM 也跑绿了"。
* **性能基线是首次建立**,无历史可比,故无退化可判;指标只在同机同宿主下对比,
跨机比数字没有意义。
另外记一笔:静态分析的口径是"仓库自己的门禁"。剩余 80 条全是风格类(0 Error),且逐条
有依据 —— 行长 160 是配置里写明的有意偏离,PSPlaceCloseBrace 等集中在测试夹具字符串内
(改了会改变断言语义)。严格模式的"零容忍"在这里与仓库自身约定相抵,选择尊重仓库约定
并在报告里登记,而不是制造一个横跨 12 个文件的纯排版大 diff。
This commit is contained in:
1 parent
f4729baca7
commit
45bbd66815
18 files changed
+2974
No files matched your search
@@ -0,0 +1,14 @@
|
||||
|
||||
PSScriptAnalyzer 1.25.0(D:\Workspace\Temp\BakNRet\.tools\modules\PSScriptAnalyzer\1.25.0)
|
||||
分析 131 个文件,配置 D:\Workspace\Temp\BakNRet\PSScriptAnalyzerSettings.psd1
|
||||
|
||||
共 80 条,按规则汇总:
|
||||
54 PSAvoidLongLines
|
||||
7 PSPlaceCloseBrace
|
||||
4 PSAlignAssignmentStatement
|
||||
4 PSReviewUnusedParameter
|
||||
4 PSUseConsistentIndentation
|
||||
4 PSUseSupportsShouldProcess
|
||||
2 PSAvoidUsingEmptyCatchBlock
|
||||
1 PSUseDeclaredVarsMoreThanAssignments
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
|
||||
PSScriptAnalyzer 1.25.0(D:\Workspace\Temp\BakNRet\.tools\modules\PSScriptAnalyzer\1.25.0)
|
||||
分析 131 个文件,配置 D:\Workspace\Temp\BakNRet\PSScriptAnalyzerSettings.psd1
|
||||
|
||||
共 84 条,按规则汇总:
|
||||
54 PSAvoidLongLines
|
||||
7 PSPlaceCloseBrace
|
||||
4 PSAlignAssignmentStatement
|
||||
4 PSReviewUnusedParameter
|
||||
4 PSUseConsistentIndentation
|
||||
4 PSUseConsistentWhitespace
|
||||
4 PSUseSupportsShouldProcess
|
||||
2 PSAvoidUsingEmptyCatchBlock
|
||||
1 PSUseDeclaredVarsMoreThanAssignments
|
||||
|
||||
逐条:
|
||||
Backup-Data.ps1:293 [PSAvoidLongLines] Line exceeds the configured maximum length of 160 characters
|
||||
Backup-Data.ps1:299 [PSAvoidLongLines] Line exceeds the configured maximum length of 160 characters
|
||||
Backup-Data.ps1:602 [PSAvoidLongLines] Line exceeds the configured maximum length of 160 characters
|
||||
Backup-Data.ps1:822 [PSAvoidLongLines] Line exceeds the configured maximum length of 160 characters
|
||||
Backup-Data.ps1:832 [PSAvoidLongLines] Line exceeds the configured maximum length of 160 characters
|
||||
Backup.ps1:42 [PSAvoidLongLines] Line exceeds the configured maximum length of 160 characters
|
||||
Backup.ps1:42 [PSUseConsistentWhitespace] Use space before and after binary and assignment operators.
|
||||
Backup.ps1:42 [PSUseConsistentWhitespace] Use space before and after binary and assignment operators.
|
||||
BakNRet.Formats.Tests.ps1:384 [PSAvoidLongLines] Line exceeds the configured maximum length of 160 characters
|
||||
BakNRet.Security.Tests.ps1:171 [PSReviewUnusedParameter] The parameter 'Root' has been declared but not used.
|
||||
BakNRet.Security.Tests.ps1:355 [PSAvoidLongLines] Line exceeds the configured maximum length of 160 characters
|
||||
BakNRet.Security.Tests.ps1:373 [PSUseDeclaredVarsMoreThanAssignments] The variable 'sourcePath' is assigned but never used.
|
||||
BakNRet.Tests.ps1:633 [PSAvoidLongLines] Line exceeds the configured maximum length of 160 characters
|
||||
BakNRet.Tests.ps1:937 [PSAvoidLongLines] Line exceeds the configured maximum length of 160 characters
|
||||
BakNRet.Tests.ps1:980 [PSAvoidLongLines] Line exceeds the configured maximum length of 160 characters
|
||||
BakNRet.Tests.ps1:1030 [PSAvoidLongLines] Line exceeds the configured maximum length of 160 characters
|
||||
BakNRet.Tests.ps1:1285 [PSAvoidLongLines] Line exceeds the configured maximum length of 160 characters
|
||||
BakNRet.Tests.ps1:1359 [PSAvoidLongLines] Line exceeds the configured maximum length of 160 characters
|
||||
Edit-Config.ps1:63 [PSAvoidLongLines] Line exceeds the configured maximum length of 160 characters
|
||||
Edit-Config.ps1:73 [PSAvoidLongLines] Line exceeds the configured maximum length of 160 characters
|
||||
Get-BakNRetSoftwareCatalog.ps1:87 [PSAvoidLongLines] Line exceeds the configured maximum length of 160 characters
|
||||
Invoke-BakNRetBackupListEditor.ps1:24 [PSUseSupportsShouldProcess] WhatIf and/or Confirm manually defined in function Invoke-BakNRetBackupListEditor. Instead, please use SupportsShouldProcess attribute.
|
||||
Invoke-BakNRetBackupListEditor.ps1:69 [PSAvoidLongLines] Line exceeds the configured maximum length of 160 characters
|
||||
Invoke-BakNRetCatalogEditor.ps1:22 [PSUseSupportsShouldProcess] WhatIf and/or Confirm manually defined in function Invoke-BakNRetCatalogEditor. Instead, please use SupportsShouldProcess attribute.
|
||||
Invoke-BakNRetCatalogEditor.ps1:55 [PSAvoidLongLines] Line exceeds the configured maximum length of 160 characters
|
||||
Invoke-BakNRetConfigEditor.ps1:23 [PSUseSupportsShouldProcess] WhatIf and/or Confirm manually defined in function Invoke-BakNRetConfigEditor. Instead, please use SupportsShouldProcess attribute.
|
||||
Invoke-BakNRetMenu.ps1:46 [PSAvoidUsingEmptyCatchBlock] Empty catch block is used. Please use Write-Error or throw statements in catch blocks.
|
||||
Invoke-BakNRetMenu.ps1:86 [PSAlignAssignmentStatement] Assignment statements are not aligned
|
||||
Invoke-BakNRetMenu.ps1:87 [PSAlignAssignmentStatement] Assignment statements are not aligned
|
||||
Invoke-BakNRetMenu.ps1:89 [PSAlignAssignmentStatement] Assignment statements are not aligned
|
||||
Invoke-BakNRetMenu.ps1:90 [PSAlignAssignmentStatement] Assignment statements are not aligned
|
||||
Lab-Common.ps1:60 [PSAvoidLongLines] Line exceeds the configured maximum length of 160 characters
|
||||
Lab.ps1:41 [PSAvoidLongLines] Line exceeds the configured maximum length of 160 characters
|
||||
Lab.ps1:208 [PSAvoidLongLines] Line exceeds the configured maximum length of 160 characters
|
||||
Lab.ps1:226 [PSAvoidLongLines] Line exceeds the configured maximum length of 160 characters
|
||||
Lab.ps1:227 [PSAvoidLongLines] Line exceeds the configured maximum length of 160 characters
|
||||
Lab.ps1:389 [PSAvoidLongLines] Line exceeds the configured maximum length of 160 characters
|
||||
New-BakNRetLab.ps1:111 [PSAvoidLongLines] Line exceeds the configured maximum length of 160 characters
|
||||
provision.ps1:69 [PSAvoidLongLines] Line exceeds the configured maximum length of 160 characters
|
||||
provision.ps1:89 [PSAvoidLongLines] Line exceeds the configured maximum length of 160 characters
|
||||
Register-BackupTask.ps1:94 [PSAvoidLongLines] Line exceeds the configured maximum length of 160 characters
|
||||
Restore-Data.ps1:141 [PSAvoidLongLines] Line exceeds the configured maximum length of 160 characters
|
||||
Restore-Data.ps1:753 [PSAvoidLongLines] Line exceeds the configured maximum length of 160 characters
|
||||
Restore-Data.ps1:850 [PSAvoidLongLines] Line exceeds the configured maximum length of 160 characters
|
||||
Restore.ps1:42 [PSAvoidLongLines] Line exceeds the configured maximum length of 160 characters
|
||||
Restore.ps1:42 [PSUseConsistentWhitespace] Use space before and after binary and assignment operators.
|
||||
Restore.ps1:42 [PSUseConsistentWhitespace] Use space before and after binary and assignment operators.
|
||||
Run-E2E.ps1:408 [PSAvoidLongLines] Line exceeds the configured maximum length of 160 characters
|
||||
Run-E2E.ps1:458 [PSAvoidLongLines] Line exceeds the configured maximum length of 160 characters
|
||||
Run-E2E.ps1:502 [PSAvoidLongLines] Line exceeds the configured maximum length of 160 characters
|
||||
Run-E2E.ps1:575 [PSAvoidLongLines] Line exceeds the configured maximum length of 160 characters
|
||||
Run-Tests.ps1:433 [PSAvoidLongLines] Line exceeds the configured maximum length of 160 characters
|
||||
Run-Tests.ps1:731 [PSAvoidLongLines] Line exceeds the configured maximum length of 160 characters
|
||||
Run-Tests.ps1:744 [PSAvoidLongLines] Line exceeds the configured maximum length of 160 characters
|
||||
Run-Tests.ps1:757 [PSAvoidLongLines] Line exceeds the configured maximum length of 160 characters
|
||||
Run-Tests.ps1:790 [PSAvoidLongLines] Line exceeds the configured maximum length of 160 characters
|
||||
Run-Tests.ps1:802 [PSAvoidLongLines] Line exceeds the configured maximum length of 160 characters
|
||||
Run-Tests.ps1:828 [PSAvoidLongLines] Line exceeds the configured maximum length of 160 characters
|
||||
Run-Tests.ps1:870 [PSAvoidLongLines] Line exceeds the configured maximum length of 160 characters
|
||||
Run-Tests.ps1:931 [PSAvoidLongLines] Line exceeds the configured maximum length of 160 characters
|
||||
Run-Tests.ps1:987 [PSAvoidLongLines] Line exceeds the configured maximum length of 160 characters
|
||||
Run-Tests.ps1:1012 [PSAvoidLongLines] Line exceeds the configured maximum length of 160 characters
|
||||
Run-Tests.ps1:1057 [PSAvoidLongLines] Line exceeds the configured maximum length of 160 characters
|
||||
Run-Tests.ps1:1169 [PSAvoidLongLines] Line exceeds the configured maximum length of 160 characters
|
||||
Run-Tests.ps1:1417 [PSPlaceCloseBrace] Close brace does not follow a new line.
|
||||
Run-Tests.ps1:1450 [PSAvoidLongLines] Line exceeds the configured maximum length of 160 characters
|
||||
Run-Tests.ps1:1465 [PSPlaceCloseBrace] Close brace does not follow a new line.
|
||||
Run-Tests.ps1:1608 [PSReviewUnusedParameter] The parameter 't' has been declared but not used.
|
||||
Run-Tests.ps1:1615 [PSReviewUnusedParameter] The parameter 't' has been declared but not used.
|
||||
Run-Tests.ps1:1622 [PSPlaceCloseBrace] Close brace does not follow a new line.
|
||||
Run-Tests.ps1:1683 [PSPlaceCloseBrace] Close brace does not follow a new line.
|
||||
Run-Tests.ps1:1734 [PSPlaceCloseBrace] Close brace does not follow a new line.
|
||||
Run-Tests.ps1:1804 [PSAvoidLongLines] Line exceeds the configured maximum length of 160 characters
|
||||
Run-Tests.ps1:1816 [PSPlaceCloseBrace] Close brace does not follow a new line.
|
||||
Run-Tests.ps1:1858 [PSUseConsistentIndentation] Indentation not consistent
|
||||
Run-Tests.ps1:1859 [PSUseConsistentIndentation] Indentation not consistent
|
||||
Run-Tests.ps1:1860 [PSUseConsistentIndentation] Indentation not consistent
|
||||
Run-Tests.ps1:1861 [PSUseConsistentIndentation] Indentation not consistent
|
||||
Run-Tests.ps1:1883 [PSAvoidLongLines] Line exceeds the configured maximum length of 160 characters
|
||||
Run-Tests.ps1:1884 [PSAvoidLongLines] Line exceeds the configured maximum length of 160 characters
|
||||
Run-Tests.ps1:1896 [PSPlaceCloseBrace] Close brace does not follow a new line.
|
||||
Save-BakNRetConfigFile.ps1:25 [PSUseSupportsShouldProcess] WhatIf and/or Confirm manually defined in function Save-BakNRetConfigFile. Instead, please use SupportsShouldProcess attribute.
|
||||
Write-BakNRetAt.ps1:39 [PSAvoidUsingEmptyCatchBlock] Empty catch block is used. Please use Write-Error or throw statements in catch blocks.
|
||||
Write-BakNRetRunSummary.ps1:16 [PSReviewUnusedParameter] The parameter 'Mode' has been declared but not used.
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
# 依赖安全扫描报告(dependency-security-scanner)
|
||||
|
||||
- **项目**:BakNRet(Windows 备份 / 恢复工具,PowerShell)
|
||||
- **扫描时间**:2026-09-28
|
||||
- **扫描范围**:仓库根目录及全部子目录(排除 `.git`)
|
||||
- **工具**:`npm audit` / `pip-audit` / `govulncheck` 等均**不适用**(见下)
|
||||
|
||||
## 1. 项目概况
|
||||
|
||||
| 项目 | 结论 |
|
||||
| --- | --- |
|
||||
| 语言 | PowerShell(`.ps1` / `.psm1` / `.psd1`) |
|
||||
| 依赖管理文件 | **不存在** —— 全仓无 `package.json`、`package-lock.json`、`requirements.txt`、`go.mod`、`pom.xml`、`Cargo.toml` 等 |
|
||||
| 运行时依赖 | **0 个模块**。运行备份 / 恢复只需要 PowerShell 5.1 或 7.x + 7-Zip |
|
||||
| 开发期依赖 | Pester 5.9.1、PSScriptAnalyzer 1.25.0 —— 装在 `.tools/`,已 gitignore,**不随发布产物分发** |
|
||||
| 外部可执行文件 | `7z.exe`(7-Zip 26.03,经 scoop 安装) |
|
||||
|
||||
> [!NOTE]
|
||||
>
|
||||
> 依赖扫描技能面向「有包管理器的项目」。本项目**没有任何包管理器依赖**,因此
|
||||
> `npm audit` / `safety` / `govulncheck` 一类工具无对象可扫。下面改为对本项目**真实的供应链面**
|
||||
> 逐项核对,而不是输出一份空报告。
|
||||
|
||||
## 2. 供应链面核对
|
||||
|
||||
| 组件 | 来源 | 版本 | 是否随分发 | 风险 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| PowerShell 引擎 | 操作系统 / Microsoft | 5.1.26100.9502、7.7.0-preview.5 | 否(宿主环境) | 由宿主维护;预览版仅供本机验收使用 |
|
||||
| 7-Zip | scoop(`C:\Programs\Scoop\apps\7zip\26.03`) | 26.03 | 否(用户自备) | 归档/解压的执行者,**必须由用户保持更新** |
|
||||
| Pester | 仓库内 `.tools/modules/Pester/5.9.1` | 5.9.1 | 否(gitignore) | 仅测试期使用,不接触用户数据 |
|
||||
| PSScriptAnalyzer | 仓库内 `.tools/modules/PSScriptAnalyzer/1.25.0` | 1.25.0 | 否(gitignore) | 仅静态分析使用 |
|
||||
| 其余第三方 | 无 | — | — | — |
|
||||
|
||||
**已知 CVE**:无可报。本项目的依赖面里没有任何「被本项目固定版本」的第三方库
|
||||
(Pester 与 PSScriptAnalyzer 是测试工具且不进分发,7-Zip 与 PowerShell 由用户环境提供)。
|
||||
若要追查这两个工具的 CVE,应查上游公告,而不是本仓库。
|
||||
|
||||
## 3. 与安全相关的项目事实(实跑核对)
|
||||
|
||||
| 检查项 | 结果 |
|
||||
| --- | --- |
|
||||
| 仓库是否跟踪任何密钥文件 | ✅ 否(`git ls-files` 中无 `*.key` / `*.pfx`) |
|
||||
| `baknret.key` 是否被 gitignore | ✅ 是(`.gitignore:18: *.key`) |
|
||||
| 工作区是否存在口令文件 `baknret.key` | ⚠️ **是** —— 见风险 R-2 |
|
||||
| 口令是否可能落进日志 | ✅ 已处理:打印前把 `-p` 参数换成占位符(`tests/BakNRet.Tests.ps1` 有专门断言) |
|
||||
| 日志中是否出现明文口令 | ✅ 无 |
|
||||
| `.tools/` 是否可能进分发产物 | ✅ 否(已 gitignore,且构建脚本只读 `BakNRet/` 与 `tools/`) |
|
||||
|
||||
## 4. 风险清单
|
||||
|
||||
| ID | 严重程度 | 风险 | 证据 | 建议 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| R-1 | **一般** | 仓库没有 `LICENSE` 文件,许可证状态未声明 | 根目录无 `LICENSE*` | 补一份许可证(见 `license_check_report`) |
|
||||
| R-2 | **一般** | 工作区存在口令文件 `baknret.key`(未被跟踪,但确实在仓库目录里) | `Test-Path baknret.key` = True | 本项目自己的 CHANGELOG 已把「口令文件的出厂默认值指向仓库内」定为待修问题,`BackupConfig.psd1` 的注释也推荐放到仓库外。建议移到 `%USERPROFILE%\.baknret.key` 并用 `-KeyFile` 指过去。**本报告不读取、不记录其内容** |
|
||||
| R-3 | **建议** | 7-Zip 版本由用户环境决定,脚本不检查版本 | `Find-BakNRet7zExecutable` 只找路径 | 可选:在启动时打印 `7z` 版本,便于排障 |
|
||||
| R-4 | **建议** | 口令经命令行传给 7z,本机进程列表可见 | 上游限制,README 已用 CAUTION 声明 | 无技术解法,保持文档披露即可 |
|
||||
|
||||
**致命 / 严重漏洞:0 个。** 流水线可继续。
|
||||
|
||||
## 5. 结论
|
||||
|
||||
未发现已知安全漏洞。本项目**零运行时依赖**,是当前最重要的一条供应链优势;
|
||||
唯一两条「一般」级风险都属于**卫生问题**(缺许可证、口令文件放在仓库目录里),
|
||||
不影响流水线继续执行。
|
||||
@@ -0,0 +1,60 @@
|
||||
# 许可证合规检查报告(license-compliance-checker)
|
||||
|
||||
> [!WARNING]
|
||||
>
|
||||
> **免责声明**:本报告由自动化工具基于仓库内的文件生成,仅为工程参考,**不构成法律建议**。
|
||||
> 涉及对外分发、商业使用或二次许可时,请咨询法务。
|
||||
|
||||
- **项目**:BakNRet
|
||||
- **检查时间**:2026-09-28
|
||||
- **主许可证**:**未声明** —— 仓库根目录不存在 `LICENSE` / `LICENSE.md` / `LICENSE.txt`
|
||||
|
||||
## 1. 依赖与组件许可证清单
|
||||
|
||||
`license-checker` / `pip-licenses` / `go-licenses` 等工具**不适用**(本项目无包管理器依赖)。
|
||||
下列清单通过读取 `.tools/modules` 下的模块清单与许可证文件、以及七项外部组件的官方许可条款得出。
|
||||
|
||||
| 组件 | 版本 | 许可证 | 来源依据 | 与主许可证关系 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| BakNRet(本项目) | 1.0.0(模块清单) | **未声明** | 无 `LICENSE` 文件 | — |
|
||||
| Pester | 5.9.1 | **Apache-2.0** | `Pester.psd1` 的 `Copyright` + `LicenseUri` | 测试期工具,未声明主许可证时无冲突可判 |
|
||||
| PSScriptAnalyzer | 1.25.0 | **MIT** | `.tools/modules/PSScriptAnalyzer/1.25.0/LICENSE` | 同上 |
|
||||
| Newtonsoft.Json(PSScriptAnalyzer 内置依赖) | 随包 | **MIT** | 同目录 `ThirdPartyNotices.txt` | 同上 |
|
||||
| 7-Zip | 26.03 | **LGPL-2.1-or-later + BSD-3-Clause + unRAR 限制** | 上游许可(用户自备,不随本仓库分发) | 未分发,仅本地调用 |
|
||||
| PowerShell | 5.1 / 7.7 | **MIT**(宿主环境) | — | 未分发 |
|
||||
| .NET 运行时 | — | **MIT**(宿主环境) | — | 未分发 |
|
||||
|
||||
## 2. 许可证分布
|
||||
|
||||
```text
|
||||
MIT ████████████████████ 3 项(PSScriptAnalyzer、Newtonsoft.Json、PowerShell/.NET)
|
||||
Apache-2.0 ███████ 1 项(Pester)
|
||||
LGPL + BSD + unRAR ███████ 1 项(7-Zip)
|
||||
未声明 ███████ 1 项(本项目自身) <-- 需要处理
|
||||
```
|
||||
|
||||
全部第三方许可证均为**宽松型**(MIT / Apache-2.0 / BSD),无 copyleft 传染风险。
|
||||
|
||||
## 3. 冲突与注意事项
|
||||
|
||||
| ID | 级别 | 对象 | 说明 | 建议 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| L-1 | ❌ **需处理** | 本项目自身 | 没有 `LICENSE` 文件 = 默认「保留所有权利」。他人**无权**复制、修改、分发;GitHub 上也会显示为无许可证项目 | 明确选一个:想宽松就 MIT,想带专利授权就 Apache-2.0 |
|
||||
| L-2 | ⚠️ 需注意 | 7-Zip 的 unRAR 限制 | 7-Zip 许可证禁止用其 unRAR 代码**还原 RAR 压缩算法**。本项目只用 7z 解压 / 压缩,不实现 RAR 压缩 | 无需动作;当前用法不触发该限制 |
|
||||
| L-3 | ⚠️ 需注意 | Pester 5.9.1 的版权年份 | 模块清单里 `Copyright` 写的是 `(c) 2026 by Pester Team`(上游清单原文) | 无需动作;仅记录,勿在文档中改写上游声明 |
|
||||
| L-4 | ✅ 兼容 | PSScriptAnalyzer(MIT)+ Newtonsoft.Json(MIT) | MIT 与 MIT/Apache 混合无冲突 | 仅在本仓库内作为测试工具使用,未再分发 |
|
||||
|
||||
## 4. 合规建议
|
||||
|
||||
1. **先补主许可证**(L-1)。这是本仓库唯一的合规缺口,且成本最低。
|
||||
2. `.tools/` 下的模块**不要**提交进版本库(已 gitignore)——它们是第三方代码,
|
||||
提交会牵出「再分发」与版权声明保留义务。
|
||||
3. 若将来把 BakNRet 公开发布,请在 README / 发布产物里保留 7-Zip 的许可与免责声明引用;
|
||||
当前仓库并未捆绑 7-Zip 二进制,所以现在无需附带其许可证全文。
|
||||
4. 若决定以 MIT 发布,注意 MIT 要求保留版权与许可声明:README「致谢」一节已引用上游项目,
|
||||
可再补一份 `THIRD-PARTY-NOTICES.md` 收纳 Pester / PSScriptAnalyzer 的声明(可选)。
|
||||
|
||||
---
|
||||
|
||||
**免责声明(重申)**:以上判断基于文件名、模块清单字段与上游公开条款的自动比对,
|
||||
存在识别错误的可能;**不构成法律建议**。
|
||||
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"measuredAt": "2026-09-28T12:24:11",
|
||||
"machine": "STRIX-X870A",
|
||||
"os": "Microsoft Windows NT 10.0.26340.0",
|
||||
"psi": "7.7.0-preview.5",
|
||||
"cpu": "AMD Ryzen 7 9800X3D 8-Core Processor ",
|
||||
"logicalCpu": 16,
|
||||
"workdir": "D:\\Workspace\\Temp\\BakNRet",
|
||||
"metrics": {
|
||||
"dryRunFullListMs": {
|
||||
"min": 9839.9,
|
||||
"median": 9869.2,
|
||||
"max": 12900.8,
|
||||
"samples": 5
|
||||
},
|
||||
"sevenZipVersion": "7-Zip 26.03 (x64) : Copyright (c) 1999-2026 Igor Pavlov : 2026-09-03",
|
||||
"folderSummary200Files3xMs": 18.2,
|
||||
"backupListParse50xMs": {
|
||||
"min": 2535.9,
|
||||
"median": 2546.9,
|
||||
"max": 2691.5,
|
||||
"samples": 5
|
||||
},
|
||||
"sevenZipBench": [
|
||||
{
|
||||
"level": 0,
|
||||
"ms": 46.7,
|
||||
"archiveBytes": 6555514
|
||||
},
|
||||
{
|
||||
"level": 5,
|
||||
"ms": 224.2,
|
||||
"archiveBytes": 6555913
|
||||
},
|
||||
{
|
||||
"level": 9,
|
||||
"ms": 225.2,
|
||||
"archiveBytes": 6555913
|
||||
}
|
||||
],
|
||||
"moduleImportMs": {
|
||||
"min": 628.3,
|
||||
"median": 631.0,
|
||||
"max": 2052.8,
|
||||
"samples": 5
|
||||
}
|
||||
},
|
||||
"notes": [],
|
||||
"fixture": {
|
||||
"files": 200,
|
||||
"bytes": 6553600,
|
||||
"createMs": 237.6,
|
||||
"compressionInputBytes": 6553600
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,333 @@
|
||||
Pester 5.9.1 (D:\Workspace\Temp\BakNRet\.tools\modules\Pester\5.9.1)
|
||||
测试文件:D:\Workspace\Temp\BakNRet\tests
|
||||
Pester v5.9.1
|
||||
|
||||
Starting discovery in 3 files.
|
||||
Discovery found 185 tests in 226ms.
|
||||
Running tests.
|
||||
|
||||
Running tests from 'D:\Workspace\Temp\BakNRet\tests\BakNRet.Formats.Tests.ps1'
|
||||
Describing 软件名录:Slot 形状(新契约)
|
||||
[2026-09-28 12:42:05] [ERROR] 名录条目 'legacystr' 格式不对:应写成 @{ <Slot 名> = @{ Path = '...' } }
|
||||
[2026-09-28 12:42:05] [ERROR] 名录条目 'legacyarr' 格式不对:应写成 @{ <Slot 名> = @{ Path = '...' } }
|
||||
[2026-09-28 12:42:05] [ERROR] 名录条目 legacydirs 有问题:Slot Dirs 的写法不对,应写成 @{ Path = '...' }
|
||||
[+] 一个软件多个 Slot:Kind=Multi,Slot 按名排序且说明被保留 163ms (131ms|32ms)
|
||||
[2026-09-28 12:42:05] [ERROR] 名录条目 'legacystr' 格式不对:应写成 @{ <Slot 名> = @{ Path = '...' } }
|
||||
[2026-09-28 12:42:05] [ERROR] 名录条目 'legacyarr' 格式不对:应写成 @{ <Slot 名> = @{ Path = '...' } }
|
||||
[2026-09-28 12:42:05] [ERROR] 名录条目 legacydirs 有问题:Slot Dirs 的写法不对,应写成 @{ Path = '...' }
|
||||
[+] 每个 Slot 都是一个独立的归档项来源(Kind=slot / Origin=catalog) 82ms (81ms|1ms)
|
||||
[2026-09-28 12:42:05] [ERROR] 名录条目 'legacystr' 格式不对:应写成 @{ <Slot 名> = @{ Path = '...' } }
|
||||
[2026-09-28 12:42:05] [ERROR] 名录条目 'legacyarr' 格式不对:应写成 @{ <Slot 名> = @{ Path = '...' } }
|
||||
[2026-09-28 12:42:05] [ERROR] 名录条目 legacydirs 有问题:Slot Dirs 的写法不对,应写成 @{ Path = '...' }
|
||||
[+] Slot 级排除写在 Slot 自己身上(相对本 Slot 的归档根) 32ms (30ms|1ms)
|
||||
[2026-09-28 12:42:05] [ERROR] 名录条目 'legacystr' 格式不对:应写成 @{ <Slot 名> = @{ Path = '...' } }
|
||||
[2026-09-28 12:42:05] [ERROR] 名录条目 'legacyarr' 格式不对:应写成 @{ <Slot 名> = @{ Path = '...' } }
|
||||
[2026-09-28 12:42:05] [ERROR] 名录条目 legacydirs 有问题:Slot Dirs 的写法不对,应写成 @{ Path = '...' }
|
||||
[+] 数组里"当前不存在"的 Slot 仍然产出归档项(恢复要靠它还原回原位) 64ms (64ms|1ms)
|
||||
[+] 文件 Slot:归档项是文件项(归档里就是名为 Slot 的文件) 8ms (7ms|1ms)
|
||||
[2026-09-28 12:42:05] [ERROR] 名录条目 'legacystr' 格式不对:应写成 @{ <Slot 名> = @{ Path = '...' } }
|
||||
[2026-09-28 12:42:05] [ERROR] 名录条目 'legacyarr' 格式不对:应写成 @{ <Slot 名> = @{ Path = '...' } }
|
||||
[2026-09-28 12:42:05] [ERROR] 名录条目 legacydirs 有问题:Slot Dirs 的写法不对,应写成 @{ Path = '...' }
|
||||
[+] 旧的裸字符串 / 字符串数组写法被拒绝(ERROR + 跳过) 22ms (22ms|1ms)
|
||||
[2026-09-28 12:42:05] [ERROR] 名录条目 'legacystr' 格式不对:应写成 @{ <Slot 名> = @{ Path = '...' } }
|
||||
[2026-09-28 12:42:05] [ERROR] 名录条目 'legacyarr' 格式不对:应写成 @{ <Slot 名> = @{ Path = '...' } }
|
||||
[2026-09-28 12:42:05] [ERROR] 名录条目 legacydirs 有问题:Slot Dirs 的写法不对,应写成 @{ Path = '...' }
|
||||
[+] 旧的 @{ Dirs = @(...) } 写法不再展开,留下 Invalid 与原因 24ms (23ms|1ms)
|
||||
[+] 多 Slot 的名录条目在清单里仍然按软件名命名归档 3ms (3ms|0ms)
|
||||
|
||||
Describing 清单修饰符:新契约格式
|
||||
[+] :: 覆盖 Path:单 Slot 条目直接生效 13ms (12ms|2ms)
|
||||
[+] :: 覆盖遇到多 Slot 条目 -> Blocking(不知道给哪一个,绝不猜) 6ms (6ms|0ms)
|
||||
[+] :- 排除与 :+ 包含并存,顺序任意 15ms (15ms|1ms)
|
||||
[+] @ Exclude / @ Include / @ Path 与记号写法等价 7ms (6ms|0ms)
|
||||
[+] :encrypt / :!encrypt 覆盖名录里的加密默认值 11ms (11ms|0ms)
|
||||
[+] 遗留写法 @encrypt / @pathname / @root= 仍可解析 18ms (17ms|1ms)
|
||||
[+] 同一行里重复写同类记号会累积(不静默丢掉前一条规则) 12ms (11ms|1ms)
|
||||
[2026-09-28 12:42:05] [WARN] 清单行缺少目标,已忽略::- logs\
|
||||
[+] 行尾说明与缺少目标的行 6ms (6ms|1ms)
|
||||
|
||||
Describing 集成:Slot 布局的打包与恢复
|
||||
[+] 备份退出码 0,归档名就是软件名 9ms (7ms|2ms)
|
||||
[+] 归档顶层就是各个 Slot 名(目录 Slot + 文件 Slot + Include 项) 42ms (41ms|1ms)
|
||||
[+] manifest.layouts 记下每个归档项是目录还是文件 8ms (7ms|1ms)
|
||||
[+] 归档内容:\ 布局,文件 Slot 是名为 Slot 的文件,Slot 排除生效 40ms (39ms|1ms)
|
||||
[+] 真实恢复:目录 Slot、文件 Slot 与 Include 都落回各自的原位 2.37s (2.37s|1ms)
|
||||
[+] 文件 Slot 在目标不存在时靠 manifest.layouts 恢复成文件(而不是目录) 3ms (2ms|0ms)
|
||||
[+] 恢复之后 manifest 记下 lastRestoreAt 4ms (4ms|0ms)
|
||||
|
||||
Describing 集成:旧布局归档的回退恢复
|
||||
[+] 归档确实是旧布局:顶层是源目录名而不是 Slot 名 30ms (29ms|1ms)
|
||||
[+] 归档里缺 Slot 层(真实旧归档)时按旧布局回退,把内容还原回原位 1.75s (1.75s|0ms)
|
||||
[+] 归档里既没有 Slot 层、也没有旧布局名字时明确失败(不再"成功地什么都没恢复") 1.72s (1.72s|0ms)
|
||||
|
||||
Describing 集成:归档内路径冲突会被拒绝执行
|
||||
[+] 退出码 1,且给出"归档内路径冲突"的原因,不生成归档 4ms (3ms|1ms)
|
||||
[+] manifest 里记下这次是 failed,并带上原因 3ms (3ms|0ms)
|
||||
|
||||
Describing 条目从清单里消失后,旧归档必须被点名为孤儿
|
||||
[+] 归档确实还在磁盘上,manifest 里也还留着历史记录 5ms (4ms|1ms)
|
||||
[+] 第二次运行把 my-app.7z 点名成孤儿,并说明 manifest 里还有历史记录 2ms (2ms|0ms)
|
||||
|
||||
Describing manifest 一致性:archive 字段只在文件真的存在时才写
|
||||
[+] 存在的归档保留 archive,不存在的被清空 9ms (8ms|1ms)
|
||||
[+] 清空 archive 时保留条目本身的历史(source / action 不动) 3ms (3ms|0ms)
|
||||
[+] 人工删掉归档之后再同步一次,记录会被纠正过来 4ms (3ms|0ms)
|
||||
|
||||
Running tests from 'D:\Workspace\Temp\BakNRet\tests\BakNRet.Security.Tests.ps1'
|
||||
Describing 排除判定与 7z 的 -x! / -xr! 语义对齐
|
||||
[+] 锚定模式只命中它自己那棵子树 8ms (7ms|1ms)
|
||||
[+] ! 通配按任意层级的组件名匹配(* 不是正则) 2ms (1ms|0ms)
|
||||
[+] !re: 走正则,且组件名与整条相对路径都算命中 2ms (2ms|0ms)
|
||||
[+] 没有模式时一律不排除 1ms (1ms|0ms)
|
||||
[+] 模式里的空格按 7z 的规矩当 ? 处理 1ms (1ms|0ms)
|
||||
|
||||
Describing SID 映射(跨机恢复)
|
||||
[+] 整 SID 精确替换 5ms (5ms|1ms)
|
||||
[+] 不会误伤以它为前缀的更长的 SID 1ms (1ms|0ms)
|
||||
[+] 空映射表时原样返回 1ms (1ms|0ms)
|
||||
|
||||
Describing 安全描述符采集
|
||||
[2026-09-28 12:42:21] [WARN] 这些特权不在当前令牌里(需要管理员或 SYSTEM):SeBackupPrivilege —— 属主将无法改成别的账户,只能恢复 DACL
|
||||
[+] Full:每个对象一条记录,键是归档内相对路径 340ms (340ms|1ms)
|
||||
[2026-09-28 12:42:21] [WARN] 这些特权不在当前令牌里(需要管理员或 SYSTEM):SeBackupPrivilege —— 属主将无法改成别的账户,只能恢复 DACL
|
||||
[+] 根记录的 SDDL 保留了 CREATOR OWNER、IO 标志、孤儿 SID 和 protected 位 12ms (12ms|0ms)
|
||||
[2026-09-28 12:42:21] [WARN] 这些特权不在当前令牌里(需要管理员或 SYSTEM):SeBackupPrivilege —— 属主将无法改成别的账户,只能恢复 DACL
|
||||
[2026-09-28 12:42:21] [WARN] 这些特权不在当前令牌里(需要管理员或 SYSTEM):SeBackupPrivilege —— 属主将无法改成别的账户,只能恢复 DACL
|
||||
[+] Smart 比 Full 少,但根永远保留 24ms (23ms|0ms)
|
||||
[2026-09-28 12:42:21] [WARN] 这些特权不在当前令牌里(需要管理员或 SYSTEM):SeBackupPrivilege —— 属主将无法改成别的账户,只能恢复 DACL
|
||||
[+] Roots 只存归档项的根,不再往下走 4ms (4ms|0ms)
|
||||
[2026-09-28 12:42:21] [WARN] 这些特权不在当前令牌里(需要管理员或 SYSTEM):SeBackupPrivilege —— 属主将无法改成别的账户,只能恢复 DACL
|
||||
[+] sidecar 往返:条数与 SDDL 原样保留 25ms (24ms|0ms)
|
||||
[+] 旁挂文件不存在时读出 $null(调用方据此打告警,而不是静默当没事) 1ms (1ms|0ms)
|
||||
[2026-09-28 12:42:21] [WARN] 这些特权不在当前令牌里(需要管理员或 SYSTEM):SeBackupPrivilege —— 属主将无法改成别的账户,只能恢复 DACL
|
||||
[+] 排除模式在采集时同样生效(采集树 == 归档树) 9ms (9ms|0ms)
|
||||
[2026-09-28 12:42:21] [WARN] 这些特权不在当前令牌里(需要管理员或 SYSTEM):SeBackupPrivilege —— 属主将无法改成别的账户,只能恢复 DACL
|
||||
|
||||
Describing 安全描述符回放
|
||||
[2026-09-28 12:42:21] [WARN] 这些特权不在当前令牌里(需要管理员或 SYSTEM):SeRestorePrivilege、SeBackupPrivilege —— 属主将无法改成别的账户,只能恢复 DACL
|
||||
[+] 回放后根对象的安全描述符与源逐字节一致(protected / CO / 孤儿 SID 全在) 40ms (39ms|1ms)
|
||||
[+] 全部对象的安全指纹与源一致(属主/属组/ACE 集合) 5ms (5ms|0ms)
|
||||
[2026-09-28 12:42:21] [WARN] 这些特权不在当前令牌里(需要管理员或 SYSTEM):SeRestorePrivilege、SeBackupPrivilege —— 属主将无法改成别的账户,只能恢复 DACL
|
||||
[+] 目标不存在或不是普通对象时记 Skipped,不记 Failed 3ms (3ms|0ms)
|
||||
[2026-09-28 12:42:21] [WARN] 这些特权不在当前令牌里(需要管理员或 SYSTEM):SeRestorePrivilege、SeBackupPrivilege —— 属主将无法改成别的账户,只能恢复 DACL
|
||||
[+] 归档根名对不上时一条都不回放(不会把兄弟项的 ACL 倒过来) 1ms (1ms|0ms)
|
||||
[2026-09-28 12:42:21] [WARN] 这些特权不在当前令牌里(需要管理员或 SYSTEM):SeRestorePrivilege、SeBackupPrivilege —— 属主将无法改成别的账户,只能恢复 DACL
|
||||
[+] 属组写不进去时不会连累 DACL:回退到底也要把 ACL 落下去 7ms (7ms|0ms)
|
||||
[2026-09-28 12:42:21] [WARN] 这些特权不在当前令牌里(需要管理员或 SYSTEM):SeRestorePrivilege、SeBackupPrivilege —— 属主将无法改成别的账户,只能恢复 DACL
|
||||
[+] 对象的安全描述符读不到时带 e 记账,回放时跳过而不是写坏 2ms (2ms|0ms)
|
||||
|
||||
Describing 与 Backup.ps1 / Restore.ps1 的集成
|
||||
[+] 备份会写出 .acl.json,并在 manifest 里记下它 2.34s (2.34s|1ms)
|
||||
[+] 恢复会把安全描述符回放回去(删源之后仍然逐对象与备份前一致) 2.14s (2.14s|0ms)
|
||||
[+] -SkipSecurity 时不回放(目标保持新建对象的默认 ACL) 1.77s (1.77s|0ms)
|
||||
[+] 归档旁边没有 acl.json 时打告警、不算失败(旧归档照样恢复得出来) 1.76s (1.76s|0ms)
|
||||
|
||||
Running tests from 'D:\Workspace\Temp\BakNRet\tests\BakNRet.Tests.ps1'
|
||||
Describing BackupList.txt 解析
|
||||
[+] 注释行与空行返回 $null 2ms (2ms|1ms)
|
||||
[+] 裸路径:Path 原样、方向 both、不是软件名、没有任何覆盖 6ms (6ms|0ms)
|
||||
[+] 软件名:IsName 为真 2ms (1ms|0ms)
|
||||
[+] IsName 判定:含分隔符或 % 就算字面路径 3ms (2ms|0ms)
|
||||
[+] 标记必须是独立记号:C:\a:-b 仍然只是一个路径 2ms (2ms|0ms)
|
||||
[+] 行首 + 仅备份、- 仅恢复,方向标记不进入目标 4ms (4ms|0ms)
|
||||
[+] 只有方向标记、没有目标 -> 忽略该行 2ms (2ms|0ms)
|
||||
[+] 行首标记贴在目标上也算(+Name / -Path),且标记不进入目标 4ms (4ms|0ms)
|
||||
[+] 非行首的 + / - 不是方向标记 22ms (22ms|0ms)
|
||||
[+] :: 现在表示"覆盖 Path",与 :- 彻底分开 8ms (5ms|3ms)
|
||||
[+] :: 的值可以带空格(一直取到下一个标记之前) 2ms (2ms|0ms)
|
||||
[+] :- 的逗号 / 分号列表拆成多个模式 7ms (6ms|0ms)
|
||||
[+] 排除模式的引号只保护空白,不保护逗号 3ms (2ms|0ms)
|
||||
[+] :+ 的包含项是 : 2ms (2ms|0ms)
|
||||
[+] :encrypt 与 :!encrypt 控制该条目的加密开关 2ms (2ms|0ms)
|
||||
[+] @ Key='Value' 覆盖 Path / Exclude / Include / Encrypt 4ms (4ms|0ms)
|
||||
[+] @ 的键名大小写不敏感,且支持紧跟 @ 的写法 2ms (2ms|0ms)
|
||||
[+] 历史写法 @encrypt / @!encrypt 仍然被识别成加密覆盖 2ms (2ms|0ms)
|
||||
[+] 历史写法 @pathname / @root= 进入 Flags 3ms (2ms|0ms)
|
||||
[2026-09-28 12:42:30] [WARN] 清单里的 @ 字段 'UnknownKey' 不是已知字段(Path / Exclude / Include / Encrypt),已忽略:Foo @ UnknownKey='v' :- logs\
|
||||
[+] 未知的 @ 字段进入 UnknownKeys,且不影响其余字段解析 2ms (2ms|0ms)
|
||||
[+] 行尾的 # 说明会成为 Comment 2ms (2ms|0ms)
|
||||
[+] 路径里紧贴的 # 不会被当成注释 1ms (1ms|0ms)
|
||||
[+] 没有说明时 Comment 为空 1ms (1ms|0ms)
|
||||
[+] 目标可以带空格:第一个标记之前整段都是目标 3ms (2ms|0ms)
|
||||
[2026-09-28 12:42:30] [WARN] 清单行缺少目标,已忽略::- logs
|
||||
[2026-09-28 12:42:30] [WARN] 清单行缺少目标,已忽略:@ Exclude='x'
|
||||
[+] 缺少目标(标记出现在第一个位置)会被忽略并告警 2ms (2ms|0ms)
|
||||
[+] 记号切分:引号内的空白不切分,引号本身留在记号里 2ms (1ms|0ms)
|
||||
[+] 记号识别只认完整记号 2ms (2ms|0ms)
|
||||
[+] 去引号:成对才去,不成对原样返回 2ms (1ms|0ms)
|
||||
|
||||
Describing 归档命名
|
||||
[+] 基础命名规则:末级名_from_上级路径用加号连接 2ms (1ms|1ms)
|
||||
[+] / 与 \ 以及重复分隔符结果一致 2ms (1ms|0ms)
|
||||
[+] 命名与路径往返 6ms (6ms|0ms)
|
||||
[+] 归档名里不含非法文件名字符 4ms (4ms|0ms)
|
||||
[+] %变量% 写法里的 % 会保留在归档名里 1ms (1ms|0ms)
|
||||
[+] 软件名条目:默认用软件名做归档名 5ms (4ms|0ms)
|
||||
[+] 字面路径条目:仍用路径命名算法(现有清单无需改写) 2ms (2ms|0ms)
|
||||
[+] @pathname 用名录里的真实路径命名,而不是软件名 4ms (3ms|0ms)
|
||||
[2026-09-28 12:42:30] [WARN] 名录里没有 'no-such-thing',按目录名处理
|
||||
[2026-09-28 12:42:30] [WARN] 名录里没有 'no-such-thing',按目录名处理
|
||||
[+] 名录里没有该名字:归档名退回可读目录名,并给出 Error 5ms (4ms|0ms)
|
||||
|
||||
Describing 7z 排除参数翻译
|
||||
[+] 相对模式自动补上归档根目录名 5ms (5ms|1ms)
|
||||
[+] 模式已带根名前缀时,Split-BakNRetPatternScope 先摘掉前缀,结果不重复 5ms (4ms|0ms)
|
||||
[+] ! 前缀翻译成递归组件匹配(-xr!) 1ms (1ms|0ms)
|
||||
[+] 模式里的空格转成 ?(归档项自己的名字按原样保留) 2ms (1ms|0ms)
|
||||
[+] 生成的参数里绝不出现引号(旧实现 -x!"路径" 让排除全部失效) 1ms (1ms|0ms)
|
||||
[+] 空模式被忽略 2ms (1ms|0ms)
|
||||
[+] !re: 会把正则展开成精确的 -x! 参数 25ms (25ms|0ms)
|
||||
[+] 非法正则给出 Error,而不是抛异常 6ms (6ms|0ms)
|
||||
[+] 正则命中数超过上限时明确报错 3ms (3ms|0ms)
|
||||
[+] 参数总长超过安全上限时明确报错 1ms (1ms|0ms)
|
||||
[+] Split-BakNRetPatternScope:指名 Slot 的模式只分给那个 Slot 2ms (1ms|0ms)
|
||||
[+] Split-BakNRetPatternScope:没点名任何 Slot 的模式广播给每一项 1ms (1ms|0ms)
|
||||
[+] Split-BakNRetPatternScope:! 与 !re: 模式广播给每一项 1ms (1ms|0ms)
|
||||
[+] Split-BakNRetPatternScope:每个归档项都有键(没有模式时是空数组) 2ms (2ms|0ms)
|
||||
[+] Merge-BakNRetExcludeArgument 去重且保持顺序 3ms (2ms|0ms)
|
||||
[+] 多 Slot 条目:逐项分配 + 翻译 + 去重合成一份参数 3ms (3ms|0ms)
|
||||
|
||||
Describing 归档项与暂存目录
|
||||
[+] New-BakNRetArchiveItem 规范化归档内路径并算出 TopName 3ms (3ms|1ms)
|
||||
[+] Get-BakNRetArchiveTopName 取归档内相对路径的第一段 1ms (1ms|0ms)
|
||||
[+] 暂存半途失败会自己清干净(不留指向真实数据的 junction) 33ms (32ms|0ms)
|
||||
[+] 目录项用 junction 挂进暂存目录,文件项用硬链接(名字就是归档内路径) 18ms (17ms|0ms)
|
||||
[+] Remove-BakNRetArchiveStaging 只删连接点,不顺着走进真实目录 8ms (8ms|0ms)
|
||||
|
||||
Describing 命令行参数拼接
|
||||
[+] 无空格参数原样输出 2ms (1ms|1ms)
|
||||
[+] 含空格参数加引号 3ms (2ms|0ms)
|
||||
[+] 引号内的结尾反斜杠翻倍(否则会被当成转义引号) 2ms (1ms|0ms)
|
||||
[+] 内部引号被转义 1ms (1ms|0ms)
|
||||
[+] 空参数输出一对空引号 2ms (1ms|1ms)
|
||||
|
||||
Describing manifest 与配置
|
||||
[+] manifest 读写往返 10ms (9ms|1ms)
|
||||
[+] manifest 原样保存 layouts(name/kind),全新恢复时靠它判断目录还是文件 7ms (7ms|0ms)
|
||||
[2026-09-28 12:42:30] [WARN] manifest 解析失败(将重新建立):C:\Users\Shuery\AppData\Local\Temp\baknret-pester-ffa25c80\broken.json —— Conversion from JSON failed with error: Invalid character after parsing property name. Expected ':' but got: i. Path '', line 1, position 7.
|
||||
[+] manifest 损坏时不抛异常,而是重建空清单 9ms (8ms|0ms)
|
||||
[+] 配置缺失时返回默认值 4ms (4ms|0ms)
|
||||
[+] 配置嵌套段落合并且不丢默认键 6ms (6ms|0ms)
|
||||
[+] 口令:环境变量可读取,取不到时返回 $null(绝不退化成明文) 3ms (3ms|0ms)
|
||||
|
||||
Describing 软件名录
|
||||
[2026-09-28 12:42:30] [ERROR] 名录条目 nopath 有问题:Slot S 缺少 Path
|
||||
[2026-09-28 12:42:30] [ERROR] 名录条目 dupslot 有问题:Slot D 的 Path 匹配到 2 个目录:C:\Users\Shuery\AppData\Local\Temp\baknret-pester-ffa25c80\catalog\dup_1、C:\Users\Shuery\AppData\Local\Temp\baknret-pester-ffa25c80\catalog\dup_2;一个 Slot 只能对应一个目录,请拆成多个 Slot
|
||||
[2026-09-28 12:42:30] [ERROR] 名录条目 badslot 有问题:Slot S 的写法不对,应写成 @{ Path = '...' }
|
||||
[+] 名录解析:条目与 Slot 的字段集合就是新契约 54ms (53ms|1ms)
|
||||
[2026-09-28 12:42:30] [ERROR] 名录条目 nopath 有问题:Slot S 缺少 Path
|
||||
[2026-09-28 12:42:30] [ERROR] 名录条目 dupslot 有问题:Slot D 的 Path 匹配到 2 个目录:C:\Users\Shuery\AppData\Local\Temp\baknret-pester-ffa25c80\catalog\dup_1、C:\Users\Shuery\AppData\Local\Temp\baknret-pester-ffa25c80\catalog\dup_2;一个 Slot 只能对应一个目录,请拆成多个 Slot
|
||||
[2026-09-28 12:42:30] [ERROR] 名录条目 badslot 有问题:Slot S 的写法不对,应写成 @{ Path = '...' }
|
||||
[+] 一个软件多个 Slot:Kind=Multi,Slot 按名字排序 33ms (33ms|0ms)
|
||||
[2026-09-28 12:42:30] [ERROR] 名录条目 nopath 有问题:Slot S 缺少 Path
|
||||
[2026-09-28 12:42:30] [ERROR] 名录条目 dupslot 有问题:Slot D 的 Path 匹配到 2 个目录:C:\Users\Shuery\AppData\Local\Temp\baknret-pester-ffa25c80\catalog\dup_1、C:\Users\Shuery\AppData\Local\Temp\baknret-pester-ffa25c80\catalog\dup_2;一个 Slot 只能对应一个目录,请拆成多个 Slot
|
||||
[2026-09-28 12:42:30] [ERROR] 名录条目 badslot 有问题:Slot S 的写法不对,应写成 @{ Path = '...' }
|
||||
[+] Kind:Single / Multi / Partial / Unresolved / Invalid 34ms (34ms|0ms)
|
||||
[2026-09-28 12:42:30] [ERROR] 名录条目 nopath 有问题:Slot S 缺少 Path
|
||||
[2026-09-28 12:42:30] [ERROR] 名录条目 dupslot 有问题:Slot D 的 Path 匹配到 2 个目录:C:\Users\Shuery\AppData\Local\Temp\baknret-pester-ffa25c80\catalog\dup_1、C:\Users\Shuery\AppData\Local\Temp\baknret-pester-ffa25c80\catalog\dup_2;一个 Slot 只能对应一个目录,请拆成多个 Slot
|
||||
[2026-09-28 12:42:30] [ERROR] 名录条目 badslot 有问题:Slot S 的写法不对,应写成 @{ Path = '...' }
|
||||
[+] 前缀补全:_ 与 - 都会被补全 28ms (27ms|0ms)
|
||||
[2026-09-28 12:42:30] [ERROR] 名录条目 nopath 有问题:Slot S 缺少 Path
|
||||
[2026-09-28 12:42:30] [ERROR] 名录条目 dupslot 有问题:Slot D 的 Path 匹配到 2 个目录:C:\Users\Shuery\AppData\Local\Temp\baknret-pester-ffa25c80\catalog\dup_1、C:\Users\Shuery\AppData\Local\Temp\baknret-pester-ffa25c80\catalog\dup_2;一个 Slot 只能对应一个目录,请拆成多个 Slot
|
||||
[2026-09-28 12:42:30] [ERROR] 名录条目 badslot 有问题:Slot S 的写法不对,应写成 @{ Path = '...' }
|
||||
[+] 不会把 Legendary 误配成 LegendarySomething 28ms (27ms|0ms)
|
||||
[2026-09-28 12:42:30] [ERROR] 名录条目 nopath 有问题:Slot S 缺少 Path
|
||||
[2026-09-28 12:42:30] [ERROR] 名录条目 dupslot 有问题:Slot D 的 Path 匹配到 2 个目录:C:\Users\Shuery\AppData\Local\Temp\baknret-pester-ffa25c80\catalog\dup_1、C:\Users\Shuery\AppData\Local\Temp\baknret-pester-ffa25c80\catalog\dup_2;一个 Slot 只能对应一个目录,请拆成多个 Slot
|
||||
[2026-09-28 12:42:30] [ERROR] 名录条目 badslot 有问题:Slot S 的写法不对,应写成 @{ Path = '...' }
|
||||
[+] 一个 Slot 命中多个候选目录:报 Error,绝不悄悄挑一棵树 31ms (31ms|0ms)
|
||||
[2026-09-28 12:42:30] [ERROR] 名录条目 nopath 有问题:Slot S 缺少 Path
|
||||
[2026-09-28 12:42:30] [ERROR] 名录条目 dupslot 有问题:Slot D 的 Path 匹配到 2 个目录:C:\Users\Shuery\AppData\Local\Temp\baknret-pester-ffa25c80\catalog\dup_1、C:\Users\Shuery\AppData\Local\Temp\baknret-pester-ffa25c80\catalog\dup_2;一个 Slot 只能对应一个目录,请拆成多个 Slot
|
||||
[2026-09-28 12:42:30] [ERROR] 名录条目 badslot 有问题:Slot S 的写法不对,应写成 @{ Path = '...' }
|
||||
[+] 文件 Slot:Path 指向文件时 IsFile 为真,Encrypt 也带上 25ms (25ms|0ms)
|
||||
[2026-09-28 12:42:30] [ERROR] 名录条目 nopath 有问题:Slot S 缺少 Path
|
||||
[2026-09-28 12:42:30] [ERROR] 名录条目 dupslot 有问题:Slot D 的 Path 匹配到 2 个目录:C:\Users\Shuery\AppData\Local\Temp\baknret-pester-ffa25c80\catalog\dup_1、C:\Users\Shuery\AppData\Local\Temp\baknret-pester-ffa25c80\catalog\dup_2;一个 Slot 只能对应一个目录,请拆成多个 Slot
|
||||
[2026-09-28 12:42:30] [ERROR] 名录条目 badslot 有问题:Slot S 的写法不对,应写成 @{ Path = '...' }
|
||||
[+] %变量% 会在名录路径里展开 27ms (27ms|0ms)
|
||||
[+] $( ... ) 子表达式会被求值(含嵌套) 6ms (6ms|0ms)
|
||||
[2026-09-28 12:42:30] [ERROR] 名录条目 nopath 有问题:Slot S 缺少 Path
|
||||
[2026-09-28 12:42:30] [ERROR] 名录条目 dupslot 有问题:Slot D 的 Path 匹配到 2 个目录:C:\Users\Shuery\AppData\Local\Temp\baknret-pester-ffa25c80\catalog\dup_1、C:\Users\Shuery\AppData\Local\Temp\baknret-pester-ffa25c80\catalog\dup_2;一个 Slot 只能对应一个目录,请拆成多个 Slot
|
||||
[2026-09-28 12:42:30] [ERROR] 名录条目 badslot 有问题:Slot S 的写法不对,应写成 @{ Path = '...' }
|
||||
[2026-09-28 12:42:30] [ERROR] 名录条目 nopath 有问题:Slot S 缺少 Path
|
||||
[2026-09-28 12:42:30] [ERROR] 名录条目 dupslot 有问题:Slot D 的 Path 匹配到 2 个目录:C:\Users\Shuery\AppData\Local\Temp\baknret-pester-ffa25c80\catalog\dup_1、C:\Users\Shuery\AppData\Local\Temp\baknret-pester-ffa25c80\catalog\dup_2;一个 Slot 只能对应一个目录,请拆成多个 Slot
|
||||
[2026-09-28 12:42:30] [ERROR] 名录条目 badslot 有问题:Slot S 的写法不对,应写成 @{ Path = '...' }
|
||||
[+] 读取结果按"路径 + 时间戳 + 长度 + 内容 MD5"缓存 63ms (62ms|0ms)
|
||||
[+] 名录文件内容变了以后缓存自动失效 17ms (16ms|0ms)
|
||||
[+] Includes:分文件维护的名录会被合并 12ms (11ms|0ms)
|
||||
[2026-09-28 12:42:30] [ERROR] 名录条目 'arr' 格式不对:应写成 @{ <Slot 名> = @{ Path = '...' } }
|
||||
[2026-09-28 12:42:30] [ERROR] 名录条目 'objarr' 格式不对:应写成 @{ <Slot 名> = @{ Path = '...' } }
|
||||
[2026-09-28 12:42:30] [ERROR] 名录条目 'bare' 格式不对:应写成 @{ <Slot 名> = @{ Path = '...' } }
|
||||
[2026-09-28 12:42:30] [ERROR] 名录条目 dirstyle 有问题:Slot Dirs 的写法不对,应写成 @{ Path = '...' }
|
||||
[+] 旧的裸字符串 / 字符串数组 / 对象数组写法都会报 ERROR 并被跳过 3ms (3ms|0ms)
|
||||
[2026-09-28 12:42:30] [ERROR] 名录条目 'arr' 格式不对:应写成 @{ <Slot 名> = @{ Path = '...' } }
|
||||
[2026-09-28 12:42:30] [ERROR] 名录条目 'objarr' 格式不对:应写成 @{ <Slot 名> = @{ Path = '...' } }
|
||||
[2026-09-28 12:42:30] [ERROR] 名录条目 'bare' 格式不对:应写成 @{ <Slot 名> = @{ Path = '...' } }
|
||||
[2026-09-28 12:42:30] [ERROR] 名录条目 dirstyle 有问题:Slot Dirs 的写法不对,应写成 @{ Path = '...' }
|
||||
[+] 旧的 @{ Dirs = @(...) } 写法不再展开:留下 Invalid 条目和原因 4ms (3ms|0ms)
|
||||
[+] 软件名会被规范化成合法文件名 2ms (1ms|0ms)
|
||||
|
||||
Describing Resolve-BakNRetBackupEntry:条目解析
|
||||
[+] resolve 与归档项的字段集合就是新契约(旧字段已删除) 4ms (4ms|1ms)
|
||||
[+] 软件名条目:isName/CatalogEntry/BaseName/ArchiveFlavor/Source 7ms (6ms|0ms)
|
||||
[+] 软件名条目:Items 来自 Slot(ArchivePath=Slot 名,Kind=slot,Origin=catalog) 6ms (6ms|0ms)
|
||||
[+] 字面路径条目:一个 path 项(ArchivePath 是末级名) 3ms (3ms|0ms)
|
||||
[+] 字面路径条目上的 @ Path= 覆盖目标路径,但归档名仍按原路径 2ms (2ms|0ms)
|
||||
[+] 名录里的路径不存在时仍给出 Items(恢复要靠它还原回原位) 5ms (5ms|0ms)
|
||||
[+] :: 覆盖 Path:单 Slot 条目直接生效 5ms (5ms|0ms)
|
||||
[+] :: / @ Path= 覆盖遇到多 Slot 条目 -> Blocking(不猜是哪一个) 5ms (5ms|0ms)
|
||||
[+] 条目级 :- 覆盖名录里的排除:HasExcludeOverride 为真 3ms (3ms|0ms)
|
||||
[+] 名录 Slot 的 Include 会追加成 include 项 5ms (5ms|0ms)
|
||||
[+] 条目级 :+ / @ Include= 覆盖名录里的 Include 11ms (11ms|0ms)
|
||||
[2026-09-28 12:42:30] [WARN] encapp:名录里各 Slot 的 Encrypt 不一致,整个归档按加密处理
|
||||
[+] 加密:名录里各 Slot 取或;条目级 :encrypt / :!encrypt 覆盖 9ms (9ms|0ms)
|
||||
[+] 方向标记会传递到 Resolve-BakNRetBackupEntry.Direction 5ms (5ms|0ms)
|
||||
[+] 归档内路径冲突(include 与 Slot 同名)-> Blocking 5ms (4ms|0ms)
|
||||
[+] 归档内路径冲突(父子关系)-> Blocking 5ms (5ms|0ms)
|
||||
[2026-09-28 12:42:30] [WARN] 名录里没有 'no-such-thing',按目录名处理
|
||||
[+] 名录里没有该名字:Error 给出,Items 为空 3ms (2ms|0ms)
|
||||
|
||||
Describing 外部命令退出码(旧实现的核心缺陷)
|
||||
[2026-09-28 12:42:30] [DEBUG] 执行: cmd.exe /c "exit 0" -p<口令已隐藏>
|
||||
[+] 口令不会进日志:DEBUG 下打印的命令行要遮蔽 -p 参数 31ms (30ms|1ms)
|
||||
[+] 运行锁:同一份备份目录同时只能有一个持有者 27ms (27ms|0ms)
|
||||
[+] 运行锁:释放之后可以重新取得 8ms (7ms|0ms)
|
||||
[+] 空目录的摘要给 0 而不是 $null(否则空间守卫会静默失效) 9ms (9ms|0ms)
|
||||
[+] 原子替换:成功时新内容到位且不留 .tmp;失败时旧内容完好 14ms (14ms|0ms)
|
||||
[+] Invoke-ExternalCommand 能拿到真实退出码 13ms (12ms|0ms)
|
||||
[+] 成功时拿到 0 12ms (12ms|0ms)
|
||||
|
||||
Describing 集成:真实 7z 压缩与排除规则
|
||||
[+] 排除规则与空格处理在真实归档上生效 55ms (54ms|1ms)
|
||||
[+] 对照组:不加排除时被排除的文件确实在归档里(证明上一条不是空归档) 58ms (57ms|0ms)
|
||||
ERROR: C:\Users\Shuery\AppData\Local\Temp\baknret-pester-ffa25c80\integration\corrupt.7z
|
||||
C:\Users\Shuery\AppData\Local\Temp\baknret-pester-ffa25c80\integration\corrupt.7z
|
||||
Open ERROR: Cannot open the file as [7z] archive
|
||||
|
||||
|
||||
ERRORS:
|
||||
Headers Error
|
||||
[+] 7z t 对完好归档返回 0,对损坏归档返回非 0(归档后校验的依据) 52ms (52ms|0ms)
|
||||
|
||||
Describing 集成:Backup.ps1 / Restore.ps1 端到端(字面路径条目)
|
||||
[+] 备份退出码为 0(旧实现会把成功的压缩判成失败) 2ms (1ms|1ms)
|
||||
[+] 归档已生成且 manifest 记录了条目、动作、校验结果与 layouts 5ms (5ms|0ms)
|
||||
[+] 备份过程写了日志文件 3ms (2ms|0ms)
|
||||
[+] 字面路径条目沿用 布局;归档里保留应保留内容、不含被排除项 32ms (32ms|0ms)
|
||||
[+] [回归] manifest.roots 记录的是归档内真实的顶层条目名 32ms (31ms|0ms)
|
||||
[+] Restore -DryRun 退出码 0,且一个字节都不写(manifest SHA256 不变) 1.57s (1.57s|0ms)
|
||||
[+] Restore -WhatIf 同样不写盘 1.6s (1.6s|0ms)
|
||||
[+] 真实恢复:退出码 0,文件逐字节一致,被排除项没有被恢复出来 2.12s (2.12s|0ms)
|
||||
[+] 真实恢复之后 manifest 才被更新(lastRestoreAt) 2ms (2ms|0ms)
|
||||
[+] 孤儿归档会被点名报告,但不影响退出码 2.37s (2.37s|0ms)
|
||||
[+] 带 -Only 时不做孤儿审计(避免把未选中的归档误报成孤儿) 1.74s (1.74s|0ms)
|
||||
[+] 源路径不存在时记为 missing-source,退出码仍为 0(跳过不算失败) 1.76s (1.76s|0ms)
|
||||
[+] 归档名重复时直接报失败(退出码 1),不静默互相覆盖 2.37s (2.37s|0ms)
|
||||
[+] [回归] 显式指定的清单不存在时报失败(退出码 1),且不在错误位置建模板 1.43s (1.43s|0ms)
|
||||
[+] [回归] 未显式指定清单时的首次运行仍建模板并退出 0(引导不能被误伤) 1.44s (1.44s|0ms)
|
||||
|
||||
Describing 集成:方向标记与孤儿审计
|
||||
[+] 行首 - 的条目:备份跳过它,但仍把它算作"有主"(不报成孤儿) 4ms (3ms|1ms)
|
||||
[+] 行首 + 的条目:恢复跳过它、不写回目标,同时登记归档名 2ms (2ms|0ms)
|
||||
Tests completed in 50.99s
|
||||
Tests Passed: 185, Failed: 0, Skipped: 0, Inconclusive: 0, NotRun: 0
|
||||
|
||||
Pester 测试全部通过:185 项(跳过 0 项)
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 205 KiB |
@@ -0,0 +1,135 @@
|
||||
# 阶段 0 报告:静态分析与修复
|
||||
|
||||
- **项目**:BakNRet(PowerShell 5.1 / 7.x,Windows 备份恢复工具)
|
||||
- **分支**:`refactor/ms-conventions`(HEAD 之前 `d72fe63`)
|
||||
- **执行时间**:2026-09-28
|
||||
- **结论**:✅ **通过**(0 个致命 / 严重问题;2 处真实缺陷已修复并回归;1 处自伤已发现并修复)
|
||||
|
||||
---
|
||||
|
||||
## 0.1 依赖安全扫描
|
||||
|
||||
**执行器**:`/dependency-security-scanner`
|
||||
|
||||
| 项目 | 结论 |
|
||||
| --- | --- |
|
||||
| 依赖管理文件 | 不存在(无 `package.json` / `requirements.txt` / `go.mod` / `pom.xml` / `Cargo.toml`) |
|
||||
| 运行时依赖 | **0 个模块** —— 只需 PowerShell 5.1 或 7.x + 7-Zip |
|
||||
| CVE | 无可报(没有任何被本仓库固定版本的第三方库) |
|
||||
| 致命 / 严重漏洞 | **0** |
|
||||
|
||||
**风险登记**
|
||||
|
||||
| ID | 级别 | 内容 | 处置 |
|
||||
| --- | --- | --- | --- |
|
||||
| R-1 | 一般 | 无 `LICENSE`,许可证未声明 | 移交阶段 0.2;**建议补一份** |
|
||||
| R-2 | 一般 | 工作区存在口令文件 `baknret.key`(`.gitignore:18 *.key` 已忽略,`git ls-files` 确认**未被跟踪**) | 建议移到仓库外(`%USERPROFILE%\.baknret.key`)+ `-KeyFile`;**本次全程未读取其内容** |
|
||||
| R-3 | 建议 | 脚本不检查 7-Zip 版本 | 可选增强 |
|
||||
| R-4 | 建议 | 口令经命令行传给 7z,进程列表短暂可见 | 7z 上游限制,README 已 CAUTION 披露,无技术解 |
|
||||
|
||||
完整性证据:`dependency_security_report.md`。
|
||||
|
||||
---
|
||||
|
||||
## 0.2 许可证合规检查
|
||||
|
||||
**执行器**:`/license-compliance-checker`
|
||||
|
||||
| 组件 | 版本 | 许可证 | 判断 |
|
||||
| --- | --- | --- | --- |
|
||||
| 本项目 | 1.0.0 | **未声明**(无 `LICENSE`) | ❌ **L-1 需处理** |
|
||||
| Pester | 5.9.1 | Apache-2.0 | ✅ 宽松,未分发 |
|
||||
| PSScriptAnalyzer | 1.25.0 | MIT | ✅ 宽松,未分发 |
|
||||
| Newtonsoft.Json | 随包 | MIT | ✅ 宽松,未分发 |
|
||||
| 7-Zip | 26.03 | LGPL-2.1 + BSD-3 + unRAR 限制 | ⚠️ L-2:本项目只做解压/压缩,**不实现 RAR 压缩**,不触发该限制;且未捆绑分发 |
|
||||
| PowerShell / .NET | 宿主 | MIT | ✅ |
|
||||
|
||||
**冲突**:无 copyleft 传染。**唯一缺口 = 主许可证未声明**(L-1)。
|
||||
完整性证据:`license_check_report.md`(含「非法律建议」免责声明)。
|
||||
|
||||
---
|
||||
|
||||
## 0.3 语法检查与自动修复(严格模式)
|
||||
|
||||
**执行器**:`/syntax-fixer`(内含 `/syntax-checker` 语义)
|
||||
|
||||
### 检查面
|
||||
|
||||
| 检查 | 命令 | 结果 |
|
||||
| --- | --- | --- |
|
||||
| 编码门禁 | `test.ps1` Encode 层 | ✅ 受管 **133** 个文件:缺 BOM 0、CRLF 0、制表符 0 |
|
||||
| 解析(PS 7.7.0-preview.5) | `test.ps1` Parse 层 | ✅ **131/131** 解析零错 |
|
||||
| 解析(5.1.26100.9502) | 同上 | ✅ **131/131** 解析零错 |
|
||||
| 静态分析 | `tools/Invoke-Analyzer.ps1` | **80 条**,全部 `Warning` 级、**0 条 Error** |
|
||||
|
||||
### 已修复的真实缺陷
|
||||
|
||||
| # | 文件:行 | 规则 | 修复 | 语义 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 1 | `Backup.ps1:42` | `PSUseConsistentWhitespace` | `@($Rest)+ @(…)` → `@($Rest) + @(…)`(补 2 处空格) | **无行为改变**,仅排版 |
|
||||
| 2 | `Restore.ps1:42` | `PSUseConsistentWhitespace` | 同上(补 2 处空格) | **无行为改变**,仅排版 |
|
||||
|
||||
### ⚠️ 修复过程中的自伤与恢复(值得记录)
|
||||
|
||||
修改 `Backup.ps1` / `Restore.ps1` 后,两个文件的 **UTF-8 BOM 被编辑器抹掉**,导致:
|
||||
|
||||
```text
|
||||
7 : parse_bad=0 <- PowerShell 7 正常
|
||||
5.1 : ERR Backup.ps1 : The string is missing the terminator: '.
|
||||
ERR Restore.ps1 : The string is missing the terminator: '.
|
||||
```
|
||||
|
||||
根因正是本仓库 `.editorconfig` 写明的那条:**5.1 没有 BOM 就按 ANSI 解码源码**,中文注释与全角字符的字节序列吃掉了字符串引号。
|
||||
处置:用 `UTF8Encoding($true)` 重写并复核 → BOM=True、CRLF=False → 双宿主 `parse_bad=0` → `test.ps1 -Suite Parse` 恢复全绿。
|
||||
**这条恰好证明了本仓库 Encode 层的价值**,也说明「编辑 PowerShell 源文件必须保留 BOM」是一条硬约束。
|
||||
|
||||
### 未修复项:按仓库已声明的偏离登记(用户已确认此处置)
|
||||
|
||||
| 规则 | 条数 | 为什么不改 | 依据 |
|
||||
| --- | --- | --- | --- |
|
||||
| `PSAvoidLongLines` | 54 | 仓库**有意**把上限设为 160 而非官方 120(120 意味着 270 处改动) | `PSScriptAnalyzerSettings.psd1` 第 22–25 行;[ADR-0008](../../../docs/adr/0008-analyzer-deviations.md) |
|
||||
| `PSPlaceCloseBrace` | 7 | 全在 `Run-Tests.ps1` **测试夹具字符串**内,缩进/换行是被断言的文本 | 改了会改变断言语义 |
|
||||
| `PSUseConsistentIndentation` | 4 | 同上 | 同上 |
|
||||
| `PSAlignAssignmentStatement` | 4 | `Invoke-BakNRetMenu.ps1` 的表格字面量,对齐影响 TUI 列宽 | 属于刻意排版 |
|
||||
| `PSUseSupportsShouldProcess` | 4 | 该规则**已全局排除**于配置,属已知噪声 | `PSScriptAnalyzerSettings.psd1` 第 44 行 |
|
||||
| `PSAvoidUsingEmptyCatchBlock` | 2 | `Write-BakNRetAt.ps1:39` 有意空 catch(注释已写明理由:定位失败不影响写文本);加输出会破坏 TUI | 源码注释 |
|
||||
| `PSReviewUnusedParameter` | 4 | 3 条在测试 helper;1 条**可能真有价值** → 见下 | — |
|
||||
| `PSUseDeclaredVarsMoreThanAssignments` | 1 | `BakNRet.Security.Tests.ps1:373` 的 `$sourcePath` 已赋值未使用 | 疑似残留,非功能缺陷 |
|
||||
|
||||
### 🔎 待人工确认的观察项(未改动)
|
||||
|
||||
```text
|
||||
BakNRet/Public/Write-BakNRetRunSummary.ps1:16
|
||||
[Parameter(Mandatory = $true)][ValidateSet('backup','restore','verify')][string]$Mode
|
||||
```
|
||||
|
||||
- `$Mode` 声明了 `ValidateSet` 与 `Mandatory`,但函数体内**从未读取**(全文仅第 16 行出现)。
|
||||
- 该函数也**从未被仓库内任何代码调用**(`grep` 全仓仅命中定义处),只通过 `FunctionsToExport` 对外导出。
|
||||
- 判断:像是「按运行类型分组」的未完工实现,**不是**能安全自动删改的东西(删除会破坏公共 API 参数契约)。
|
||||
- 处置:登记为观察项,**留待仓库作者决定**。
|
||||
|
||||
---
|
||||
|
||||
## 0.4 回归验证
|
||||
|
||||
| 层次 | PS 7.7.0-preview.5 | 5.1.26100.9502 |
|
||||
| --- | --- | --- |
|
||||
| Encode(宿主无关,跑一次) | ✅ 133 文件 0 问题 | — |
|
||||
| Parse | ✅ 131/131 | ✅ 131/131 |
|
||||
| Unit(Pester) | ✅ 通过 | ✅ 通过 |
|
||||
| Smoke(零依赖) | ✅ 通过 | ✅ 通过 |
|
||||
| E2E | ✅ 通过 | ✅ 通过 |
|
||||
| **合计** | **9/9 PASS,退出码 0** | |
|
||||
|
||||
修复前后各跑一次,结论一致 → **无回归**。
|
||||
分析器:`84 → 80` 条(4 条 `PSUseConsistentWhitespace` 全部归零,其余不变)。
|
||||
证据:`analyzer_raw.txt`(修复前)、`analyzer_after.txt`(修复后)。
|
||||
|
||||
---
|
||||
|
||||
## 0.5 阶段结论
|
||||
|
||||
- ✅ 无致命 / 严重问题,**流水线可继续**。
|
||||
- 🔧 2 处真实排版缺陷已修复;1 处 BOM 自伤已发现并恢复(这条本身是「Encode 层有效」的实证)。
|
||||
- ⚠️ 3 项待办移交下游:补 `LICENSE`(L-1)、迁移 `baknret.key`(R-2)、确认 `$Mode` 意图(观察项)。
|
||||
- 严格模式「零容忍」与仓库**已声明偏离**冲突的部分,按用户决定:**登记而不改**,理由逐条留档(上表)。
|
||||
@@ -0,0 +1,90 @@
|
||||
# 阶段 3 报告:测试增强评估与性能基线
|
||||
|
||||
- **执行时间**:2026-09-28
|
||||
- **结论**:性能基线已建立(首次,无退化可判);单元测试增强**建议但未执行**(理由见下)
|
||||
|
||||
---
|
||||
|
||||
## 3.1 单元测试增强评估(`/unit-test-generator`,可选步骤)
|
||||
|
||||
### 现状盘点
|
||||
|
||||
| 指标 | 数值 |
|
||||
| --- | --- |
|
||||
| 对外函数总数 | **89**(`BakNRet/Public/*.ps1`,与 `FunctionsToExport` 白名单逐一核对一致) |
|
||||
| 测试中被**直接点名**的函数 | 70(78.7%) |
|
||||
| 未被直接点名 | 19(21.3%) |
|
||||
| Pester 用例 | **185** 条(`BakNRet.Tests.ps1` 127 + `Formats` 33 + `Security` 25),0 失败 0 跳过 |
|
||||
| 另有 | 零依赖套件 128 项、端到端 36 项、真实归档演练 12 项 |
|
||||
|
||||
### 未被直接点名的 19 个函数——逐条判断
|
||||
|
||||
> 「未被直接点名」≠「未被覆盖」:其中不少是通过集成路径间接执行到的(如 `Write-BakNRetLog` 被所有用例调用、`ConvertTo-BakNRetWildcardPattern` 由 `Test-BakNRetPathExcluded` 间接驱动)。
|
||||
|
||||
| 函数 | 是否值得补测试 | 理由 |
|
||||
| --- | --- | --- |
|
||||
| `Move-BakNRetArchiveIntoPlace` | **值得(最高优先)** | 「临时文件 → 校验 → 原子替换」是本仓库的核心安全承诺之一,且 CHANGELOG 记录过它在 5.1 上退化成「先删后移」的缺陷。目前只有端到端间接覆盖 |
|
||||
| `Resolve-BakNRetRootedPath` | 值得 | 「相对路径按仓库根解析,不按工作目录」是计划任务场景的关键约定(README 专门写了这一节) |
|
||||
| `Test-BakNRetItemSelected` | 值得 | `-Only` / `-Skip` 的通配匹配语义,边界(大小写、通配符)值得钉住 |
|
||||
| `ConvertFrom-BakNRetPatternList` | 值得 | `,` 与 `;` 双分隔符是历史缺陷的修复点(CHANGELOG:解析器用 `;` 而清单里写 `,`) |
|
||||
| `Get-Optimized7zArgument` | 一般 | 参数拼接优化,间接覆盖已足够 |
|
||||
| `Save-BakNRetItemRecord` | 一般 | 与 `Write-BakNRetManifest` 组合使用,集成覆盖 |
|
||||
| `Find-BakNRet7zExecutable` / `Resolve-BakNRetCompressionTool` | 一般 | 依赖机器状态,单元测试价值低(真实冒烟更合适) |
|
||||
| `Get-BakNRetFreeSpaceGB` / `Test-BakNRetAdministrator` | 低 | 环境查询,测试易受机器状态影响 |
|
||||
| `Enable-BakNRetPrivilege` / `Set-BakNRetObjectSecurity` / `Get-BakNRetAceSignatureList` / `Test-BakNRetSecurityRecordNeeded` | 低(**已由 VM 演练覆盖**) | 这几条需要真实提权与真实 NTFS,单元测试造不出可信夹具;仓库已经用 `tools/lab/Lab.ps1 acl-test`(含负对照)在真 VM 里覆盖,这是更正确的层次 |
|
||||
| `Write-BakNRetBackupEntryPlan` | 低 | 纯输出,间接覆盖 |
|
||||
| `Invoke-BakNRetEntryScript` | 低 | 子进程封装,端到端已覆盖 |
|
||||
| TUI 相关(`Write-BakNRetAt` 等) | 低 | 需要真终端;仓库已用 `-InputScript` 驱动做门禁 |
|
||||
|
||||
### 为什么本次**不执行**自动补测
|
||||
|
||||
1. 该步骤在流水线里标注为**可选**。
|
||||
2. 用户本轮选择的处置姿态是「最小侵入、不制造大 diff」;补 4~6 条新用例属于**新增工作**而非修复缺陷,应先取得确认。
|
||||
3. 上述「值得」的 4 条若要补,需要配套夹具(原子替换需要真归档、`-Only` 需要真清单),属于小规模但真实的工作量。
|
||||
|
||||
**建议**:单独一轮补 `Move-BakNRetArchiveIntoPlace` 与 `Resolve-BakNRetRootedPath` 两条(这两条对应的是历史上真出过问题的语义),其余保持现状。
|
||||
|
||||
---
|
||||
|
||||
## 3.2 性能基线(`/performance-baseline-tester`)
|
||||
|
||||
**工具适配说明**:该技能面向 HTTP 服务(k6 / RPS / P95)。BakNRet 是**本地 CLI 工具**,
|
||||
没有 HTTP 端点,因此把「核心 API」映射为**四条关键路径**:模块导入、清单解析、只读干跑(真实清单)、
|
||||
7z 压缩吞吐。指标是同一台机器上的可复现绝对耗时,用于**同环境前后对比**,不跨机器比较。
|
||||
|
||||
**环境**:宿主 `STRIX-X870A`,PowerShell 7.7.0-preview.5,7-Zip 26.03 (x64)
|
||||
|
||||
| 指标 | 采样 | min | **中位数** | max |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 模块导入(`Import-Module` 冷启) | 5 | 628.3 ms | **631.0 ms** | 2052.8 ms |
|
||||
| 清单解析 ×50 轮 | 5 | 2535.9 ms | **2546.9 ms** | 2691.5 ms |
|
||||
| **只读干跑(真实 28 条清单)** | 5 | 9839.9 ms | **9869.2 ms** | 12900.8 ms |
|
||||
| 源树扫描(200 文件 ×3 轮) | 1 | — | **18.2 ms** | — |
|
||||
|
||||
**7z 压缩基准**(固定夹具:200 × 32 KB = 6.25 MB **不可压缩随机数据**)
|
||||
|
||||
| 压缩级别 | 耗时 | 归档体积 |
|
||||
| --- | --- | --- |
|
||||
| `-mx=0` | 46.7 ms | 6,555,514 B |
|
||||
| `-mx=5` | 224.2 ms | 6,555,913 B |
|
||||
| `-mx=9` | 225.2 ms | 6,555,913 B |
|
||||
|
||||
> 夹具是随机数据,所以体积几乎不随级别变化 —— 这正好说明**该夹具测的是吞吐与 I/O,不是压缩率**。
|
||||
> 若要看压缩率,应换成真实可压缩语料。
|
||||
|
||||
### 基线判定
|
||||
|
||||
- **基线文件**:`.scratch/ci-cd/20260928-001722/perf_baseline.json`(首次建立,**无历史基线可比,故无退化可判**)。
|
||||
- **观察**:只读干跑约 9.9 秒是最大单项。它包含了**对 28 个条目的真实目录扫描与空间预估**,
|
||||
属于设计内的开销(README 承诺「动手之前先预估空间」)。`max 12.9 s` 的离群值出现在首次运行,
|
||||
与文件系统缓存冷启动一致。
|
||||
- **告警**:无退化(首次基线)。
|
||||
- **建议**:若将来要监控退化,把 `perf_baseline.json` 固定成仓库内基线,
|
||||
并在同机同宿主下对比;**不要在 VM 与宿主之间直接比数字**(两者 I/O 特性不同)。
|
||||
|
||||
---
|
||||
|
||||
## 3.3 结论
|
||||
|
||||
- ✅ 性能基线已建立并可复现,**无退化**。
|
||||
- ⏸️ 单元测试增强:已给出逐条优先级,**建议单独一轮补 2 条**,本轮不擅自新增。
|
||||
Reference in new issue
Block a user