Shuery
|
232a82cd3c
|
style: 逐条修静态分析告警(706 → 44),并把 MaxDepth 这条假承诺删掉
修掉的:空 catch 5 处;名词白名单 7 处;default-value 开关、自带 -WhatIf、lab 的明文口令与 irm|iex 各挂抑制并写明理由。
MaxDepth:它是分析器拓出来的真 bug —— 参数声明了却从未使用,也就是配置里的 CatalogMaxDepth 是假的,前缀补全实际只查 1 层,而配置注释与 README 都承诺「向下找几层」。按确认过的原则处理:**先让文档不撒谎**,所以把整条链路去掉(配置默认值、三个函数的参数、70 处实参、配置注释),而不是留一个假旋钮。零行为变化。想真的支持多层补全时,那是一个独立决定。
剩下 3 条都是分析器的误判,而且我实测确认过其中一条:$sourcePath 被报「赋值后从未使用」,我照着改成 $null = 之后,Set-StrictMode -Version 3.0 下读未定义变量直接抛错,Security 套件的 BeforeAll 挂掉、4 条用例连带失败。恢复后才绿。
这一类误判有共同成因:静态分析看不到「在传给 Test-Case / It / Where-Object 的 scriptblock 里被使用」。所以我只对能证明是误判的挂抑制并写明理由,不为了数字好看去改代码。
验收:test.ps1 9/9 全绿(7 与 5.1)、100 个文件两版解析零错、Run-RealSmoke 4/4。
|
2026-09-27 10:16:10 +08:00 |
|
Shuery
|
42f02d0eca
|
refactor: 公共面补 BakNRet 前缀,产品名大小写全仓统一
16 个没有前缀的公共函数补上 BakNRet(Write-Log → Write-BakNRetLog、Resolve-BackupEntry →
Resolve-BakNRetBackupEntry、Find-ChildDirectoryByName → Find-BakNRetChildDirectoryByName 等),
另外把全仓的 Baknret 统一成 BakNRet(47 个文件、940 处、65 个定义文件重命名)。
这不是审美问题:静态分析直接拓出一条实据 —— Write-Log 与本机某个已装模块导出的命令
**重名**(PSAvoidOverwritingBuiltInCmdlets),而重名的后果是导入两个模块时有一方的命令被
静默遮蔽。补前缀正是这条规则的解法,改名后它归零。
为什么敢做这个规模:PowerShell 的函数名解析大小写不敏感,所以 Baknret → BakNRet 在功能
上是零风险;真正要验证的是 16 个补前缀的调用点,而 276 个断言几乎覆盖了每个函数。另外
"名字与文件名一致"这条不变式有断言盯着(加载器点源的文件集合 vs 磁盘)。
踩到并记下的坑:Windows 文件系统大小写不敏感,所以**只改大小写**的重命名会被 Move-Item
当成同一个文件而静默跳过 —— 同一批里同时改了名字的那 16 个文件却成功了,于是"看起来能跑"。
最后用"先移到临时名、再移到目标名"的两步走解决,判断与替换全部改用显式大小写敏感的形式
(-creplace / -cmatch)。
顺带把名录指纹缓存从 MD5 换成 SHA256(PSAvoidUsingBrokenHashAlgorithms):它只是缓存键,
没有兼容负担。
验收:test.ps1 9/9 全绿(7 与 5.1)、100 个文件两版解析零错、276 个断言全过、
构建工具仍能合回单文件(3300 行)。
|
2026-09-27 09:56:36 +08:00 |
|
Shuery
|
187d2759fd
|
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 层。这条要改行为、且影响真实名录的解析
结果,留给你拍板,不在本提交里动手。
|
2026-09-27 09:46:08 +08:00 |
|
Shuery
|
2ad7987ffe
|
refactor: Common.psm1 拆成 BakNRet/{Public,Private},一函数一文件 + 薄加载器
3152 行、66 个函数的单文件模块拆成:
BakNRet\BakNRet.psd1 模块清单:FunctionsToExport 是显式白名单(62 个名字)
BakNRet\BakNRet.psm1 加载器:点源顺序的唯一一处声明
BakNRet\Public\*.ps1 62 个对外函数,一函数一文件,文件名 = 函数名
BakNRet\Private\*.ps1 4 个内部函数 + State.ps1(模块级状态集中一处)
为什么是一函数一文件:这是社区里脚本模块的主流形态(调研实测:winutil 79 个、
Terminal-Icons 24 个、ModuleBuilder 23 个,全部如此)。收益是改动落在小文件里、diff 按职责
可读、模块级状态有唯一去处。
为什么这不算"打散":模块内 dot-source 的文件共享同一个模块作用域(实测确认),所以
"按顺序点源 67 个文件"与"点源一个大文件"在语义上等价;顺序只在加载器里出现一次,
tools\Build-BakNRetModule.ps1 从那里读出顺序就能拼回单文件 —— 本次产物 dist\BakNRet.psm1
3240 行、两个版本都解析零错。
新增一条断言把这条承诺钉住:加载器点源的文件集合必须与磁盘一致、导出名单必须与
Public\ 一一对应。漏一个文件或漏一个名字就是静默少一个函数 —— 而那种错在运行时只表现为
"找不到命令"。
引用更新:11 个文件里的 Common.psm1 改成 BakNRet\BakNRet.psd1(走清单导入,
FunctionsToExport 才真的说了算);Common.psm1 直接删除,不留转发垫片。
验收:test.ps1 9/9 全绿(7 与 5.1),98 个文件两版解析零错,276 个断言原样通过 ——
这次搬家没有改变任何可观察行为。
|
2026-09-27 09:34:10 +08:00 |
|
Shuery
|
10f97246c8
|
fix: lab 的 ACL 场景在 5.1 上会被原生命令的 stderr 中断
tools\lab\payload\run-acl-scenario.ps1 是 $ErrorActionPreference = Stop,而它要大量调用
rmdir / takeown / icacls / scoop / code —— 其中 rmdir 与 takeown 在"对象已经处理过"或
"连接点已经悬空"时就会往 stderr 写字。Windows PowerShell 5.1 会把那升级成终止性的
NativeCommandError(7 改了这条规矩),于是清理函数在设计要处理的**恰恰那个场景**里直接崩掉。
修法:新增 Invoke-NativeTolerant,在进入原生命令时把 EAP 放到 Continue、退出时还原,
把 stdout 与 stderr 合并返回;12 处调用全部收进它。判断"删掉了没有"本来就该用 Test-Path,
不需要让 stderr 变成异常。这与 tests\BakNRet.Security.Tests.ps1 里对 takeown / icacls
用的是同一招(那里已被 5.1 的失败实测逼出来过)。
范围说明:只改了 run-acl-scenario.ps1。provision.ps1 的同类写法**不需要**改 ——
它是 $ErrorActionPreference = Continue,那两处本来就不会踩。
验证边界(如实说明):这个文件要 Hyper-V + 一台 VM 才跑得到,本会话无法执行它。
所以这一步的证据是:两个版本上解析零错(Parse 层覆盖)、12 处替换逐条断言命中、以及
diff 逐行复核。它是"降低风险",不是"已验证修复"。
验收:test.ps1 9/9 全绿(7 与 5.1);Run-RealSmoke 4/4 全绿。
|
2026-09-27 09:22:45 +08:00 |
|
Shuery
|
e10503be76
|
fix: 让 5.1 真正可用(显式编码 + 原生 stderr 处理 + .psd1 夹具带 BOM)
上一提交让 5.1 能解析源码,但 Unit 与 Smoke 在 5.1 上仍然是红的。根因是三类彼此
无关的 5.1/7 行为差,全部实测确认:
1) 不写 -Encoding 时,5.1 的 Get-Content / Set-Content 默认是 ANSI,7 是 UTF-8。
症状是 UTF-8 字节被按 GBK 解出「璇存槑」这类乱码。62 处补上显式 -Encoding UTF8。
用 AST 而不是正则定位,避免把注释里的散文也改掉。
2) .psd1 夹具用无 BOM 写,而引擎的 .psd1 读取器(Import-PowerShellDataFile)只能靠
BOM 判断编码、没有参数可传,于是 5.1 按 ANSI 解。40 处夹具改为带 BOM 写 —— 这正是
.editorconfig 里 [*.psd1] charset = utf-8-bom 本来就要求的,是夹具违反了自己的约定。
.cmd 批次文件刻意保持无 BOM:cmd.exe 会被 BOM 弄坏。
3) 5.1 在 $ErrorActionPreference = Stop 下会把原生命令写到 stderr 的内容升级成终止性
NativeCommandError,7 改了这条。takeown/icacls 的 ACL 复位调用、以及 test.ps1 自己
调子进程的地方,都需要在 Continue 下跑。
验收:test.ps1 9/9 全绿(Encode + Parse + Unit + Smoke + E2E,在 7 与 5.1 上各跑一遍)。
已知未处理(留待后续提交):tools/lab/** 里还有若干「原生命令 + 2>&1 + Stop」的同类
写法(takeown / icacls / scoop / code / Get-WimInfo)。它们要 Hyper-V 实验机才跑得到,
不在验收门槛内。
|
2026-09-26 22:16:04 +08:00 |
|
Shuery
|
2d26f78d15
|
fix: 源文件改存 UTF-8 with BOM,让 Windows PowerShell 5.1 真正可用
改造前:全仓 6/6 个源文件在 5.1 上解析失败(README 却承诺支持 5.1)。原因是文件是无 BOM 的
UTF-8,而 5.1 没有 BOM 就按 ANSI 代码页解码源码,中文变乱码、全角问号吃掉引号,整块语法塌掉。
现在 28/28 个文件在 5.1 与 7 上都解析零错误,E2E 36 项在 5.1 上全绿。
顺带修掉一个被 5.1 掩盖的缺陷:带 [CmdletBinding()] 的脚本在 5.1 上,param() 默认值里
拿不到 $PSScriptRoot(实测为空串,7 上正常)。于是 Backup.ps1 / Restore.ps1 在 5.1 上不传
路径参数就报错退出 —— 而计划任务恰恰不传。E2E 之所以看不见,是因为它总是显式传路径。
8 处默认值全部移到 param() 之后的解析段,沿用本仓库对 -BackupDir 一直在用的写法。
新增三条可重放的约定,让编码不再是一次性动作:
.gitattributes 接管行尾(本机 core.autocrlf=true,会把工作区改成 CRLF 制造伪 diff)
.editorconfig 用 charset = utf-8-bom 锁住 BOM
tools\Set-SourceEncoding.ps1 是规范化脚本,tools\Invoke-* 之外的任何改动之后都能重放
test.ps1 是唯一验收入口:Encode + Parse + Unit + Smoke + E2E,在 7 与 5.1 上各跑一遍
test.ps1 的 Encode 层直接检查"必须有 BOM"这条规则。加它的原因很实际:实测本仓库用的
编辑工具在保存时会悄悄去掉 BOM,而丢了 BOM 的文件只在 5.1 上出错、在 7 上完全正常,
没有这条检查就会一直漏过去。
已知未修(下一步处理):5.1 上 Unit 有 6 项、Smoke 有 1 项失败,全部源于测试夹具写
临时文件时没指定编码(5.1 的 Set-Content 默认 ANSI),与产品代码无关。
|
2026-09-26 22:05:42 +08:00 |
|
Shuery
|
2937eb6652
|
chore: 记录改造前基线
改造开始前的完整状态,作为可回退的基点。此提交之后:Pester 175 项、零依赖套件 101 项全绿;PowerShell 5.1 尚不可用(源文件无 BOM)。
包含此前未提交的在制品:安全描述符套件、Hyper-V 实验环境(tools/lab)、agent 约定(AGENTS.md 与 docs/agents)。
.gitignore 增加 *.key / *.pfx:BackupConfig.psd1 的 PasswordFile 此前默认指向仓库内的 baknret.key,一次 git add -A 就会把口令提交进版本库。默认值在后续提交中改为空。
|
2026-09-26 21:46:55 +08:00 |
|
Shuery
|
43fa4e52dd
|
清单格式:两种写法(软件名 / 手写目录)都支持 :+ 追加与 :- 排除;名录改用对象数组并逐条介绍目录
需求
- 支持两种条目写法:1) 直接写软件名录里的软件名;2) 用户手写目录。
- 两种写法都必须支持追加(:+)与排除(:-)。
- 软件名录要改进:scoop 合并成"一个软件 + 一个目录数组"。
- 运行时要把"分别是哪些目录、每个目录是干什么的、排除/追加的理由"讲清楚。
实现
- 名录(SoftwareCatalog.psd1 / Get-SoftwareCatalog)
* 一个软件挂多个目录时写成**对象数组**:@{ Path = '...'; Description = '...' };
也接受纯字符串数组与旧的 @{ Dirs = ... } / @{ Variants = ... }。
* 目录说明(Description)一路带到运行日志里。
* "声明了但当前不存在"的目录不再被丢掉:备份跳过,恢复仍然知道它该回到哪个位置。
* scoop 合并成一个数组条目(%UserProfile%\scoop\persist + %UserProfile%\.config\scoop);
ScoopApps-persist 保持独立条目 —— 它和前者末级名同为 persist,并进同一个归档会在包里撞名。
- 解析与解析结果(ConvertFrom-BackupListLine / Resolve-BackupEntry)
* `:+` 以前只对"软件名且能解析出目录"的写法生效,**手写目录的 :+ 会被整段丢掉**;
现在统一生效,且 :+ 后面写软件名会按名录展开。
* 行尾 `# 说明` 解析成 Comment,运行时打印。
- 归档与恢复
* 同一条目里两个同名目录:打包前明确报错(退出码 1),不再静默混成一棵树。
(7z 命令行没有"入库改名"的能力,归档内顶层名只能是文件系统上的那个名字。)
* 恢复时每个源只解出**它自己那棵子树**,不会再往别的父目录里复制兄弟目录。
- 可解释性
* 新增 Write-BackupEntryPlan:打包前打印条目的目录(含来源与介绍)以及排除/追加的出处;
Restore.ps1 同样打印"哪棵子树还原到哪、会新建还是覆盖"。
- 孤儿归档审计修正:判据只看当前清单,不再把 manifest 的历史记录当成"已知"。
否则"条目被合并/改名后留下的旧归档"会被历史记录遮住,永远不会报警。
验证
- Pester 84 项、零依赖单元 49 项、端到端 23 项,全部通过。
- 真实机器:合并后的 scoop.7z 233 MB / 29478 项 / 7z t 通过,manifest.roots=[persist|scoop];
恢复演练 26982/26982 逐字节一致(.ssh、legendary、Aria 同批通过)。
- 迁移提醒:scoop-config.7z 与 scoop-persist.7z 已无清单条目指向,会出现在孤儿审计里;
确认 scoop.7z 无误后可以自行删除。
|
2026-09-22 08:18:16 +08:00 |
|
Shuery
|
e114cae8c8
|
P0-P3 全量重构:退出码 / 解析修复、manifest 与 7z t 校验、干跑、排除规则、日志、测试与计划任务
P0 正确性
- 退出码:改用 .NET Process 直接启动、让子进程继承控制台,不再用 Start-Process -PassThru
(在 7.7.0-preview.4 上 ExitCode 恒为 $null,会把成功的压缩判成失败);
7z / RAR / tar 三条解压分支统一走同一个取退出码的封装。
- BackupList 解析:先按第一个 :: 切段再处理引号(整行被一对引号包住的写法不再把排除表
吞进路径);排除表同时接受 , 与 ;(旧实现只认 ;,导致排除从未生效);支持 :- / :+ / @flag。
- 补回 .ssh 与孤儿归档:.ssh 进清单;孤儿归档在备份端也做审计并点名;
带 -Only / -Skip 时不再把未选中的归档误报成孤儿。
- Resolve-BackupEntry 里 $rootName 在赋值前被引用(会读到外层作用域残留值),已提前赋值。
P1 归档可靠性
- 每个条目写进 manifest.json:源、归档、时间、退出码、校验结果、失败原因,
并区分 warnings(在位归档)与 attemptWarnings(本次尝试)。
- 归档后做 7z t 内容校验,先写 .tmp、校验通过再原子替换(File.Move overwrite)。
- manifest.roots 记录归档内**真实**的顶层条目名(原先记的是软件名,Edge 实际是 "User Data")。
P2 可用性
- Restore 支持 -WhatIf / -DryRun / -VerifyOnly / -Only / -Skip;
这三种"只看不写"的模式一个字节都不写(原先会写回 manifest.json)。
- Edge 等高缓存条目加排除规则并实测:1781 MB / 27961 项 -> 72 MB / 2294 项;
书签、密码、Cookies、偏好、历史、IndexedDB、Local Storage 全部保留。
普通模式是相对归档根目录锚定的,嵌套的那些(如 OneAuth\WebView2 里的 Crashpad)
改用 ! 组件形式才会命中。
- 日志落盘 logs/<backup|restore>-<时间戳>.log;退出码按失败数返回。
- tools/Register-BackupTask.ps1 注册每日计划任务;tools/Rename-Archives.ps1 迁移旧归档名。
- root= 标记此前静默失效,现在明确告警(该功能尚未实现)。
P3 测试与验证
- tests/BakNRet.Tests.ps1:Pester 5 套件 62 项(含用子进程跑 Backup.ps1 / Restore.ps1
的端到端与针对上述缺陷的回归)。
- tests/Run-Pester.ps1 + tools/Install-TestDependencies.ps1:把 Pester 装到仓库内 .tools/,
不动机器上的全局模块(系统自带的 3.4.0 缺 Should -Be)。
- tests/Restore-Drill.ps1:真实归档恢复演练,明确区分"源在备份后变过"与"归档/解压有问题"。
- tests/Run-Tests.ps1(49 项,零依赖)与 tests/Run-E2E.ps1(23 项)继续可用;三套共 134 项全通过。
真实机器验证
- 生产归档 22/22 通过 7z t;-VerifyOnly 不再改动 manifest.json(SHA256 前后一致)。
- 真实恢复演练 12/12 通过,27,670 个文件与活源逐字节一致。
- 修复了生产 scoop-persist.7z:原先只有 90 字节(空归档)而源有 1.3 GB,
重打包后 233 MB,恢复演练 26981/26981 全部一致。
|
2026-09-21 23:02:47 +08:00 |
|
Shuery
|
045d51ac9c
|
引入软件名录:清单写软件名,归档名也用软件名
新功能
- 新增 SoftwareCatalog.psd1 —— "软件名 -> 目录"映射表,BackupList.txt 里
直接写软件名即可,归档名也就是软件名(FooClolor.7z),
不再是 FooClolor_from_C_+Programs.7z 这种由路径拼出来的名字。
- 三种写法可混用:软件名、字面路径(现有清单无需改写)、软件名 @pathname。
- 名录支持前缀补全(legendary -> legendary_2.0.4,只认 <名>_* / <名>-*)、
Variants(同名目录在多处)、Includes(分文件维护)。
- tools/Rename-Archives.ps1:存量归档重命名,默认试运行,逐份大小校验并重建 manifest。
- 归档名重复直接报错,不再静默互相覆盖。
两套测试全绿:单元 42 项、端到端 23 项(新增名录命名/解析/迁移用例)。
过程中修掉的缺陷
- Resolve-BackupEntry 里 @pathname 与 Unresolved 分支顺序错误,
@pathname 会被静默吃掉(改名后仍用软件名)。
- 源目录被删除时解析器丢掉 Sources,导致恢复端把软件名当路径、
报 "Cannot bind argument to parameter 'Path' because it is an empty string"。
恢复的语义恰恰是"源不存在就要还原回去",现在 Sources 照旧给出。
- 源存在性检查曾被漏掉,Get-Item 对不存在路径抛异常会中断整轮备份;
且不能用 Join-Path 探测——目标盘符不存在时它会直接抛异常。
- 计划任务脚本外的 Caller 需要 -DryRun 才能验,已实跑确认。
|
2026-09-21 20:55:19 +08:00 |
|
Shuery
|
dbc0c00554
|
重构为可核对、可恢复的备份工具(P0-P3)
修复(P0)
- 退出码:改用 .NET Process 继承控制台启动外部命令。Start-Process -PassThru 的
ExitCode 在 PowerShell 7.7.0-preview.4 上恒为 $null,会把成功的压缩判成失败,
并让 "exit 2 -> 删档重试" 的自愈分支永远不可达。
- BackupList.txt 解析:先按第一个 :: 切开再处理引号,修正整行被引号包住时
排除表被吞进路径的问题(该条目此前被静默跳过,其 2.8 GB 归档成了孤儿)。
- 排除分隔符同时接受 , 与 ;:此前解析器只认 ; 而清单里写的是 ,,
等于所有排除规则都没生效。
- 7z 排除参数不再嵌引号,含空格的模式自动转成 ?:旧写法 -x!"路径" 会让引号
成为模式的一部分,导致排除对所有条目都失效。
加固(P1)
- 先写临时归档 -> 7z t 校验 -> 原子替换,中断不再污染正式归档。
- 放弃 7z 的更新模式 u:固实压缩下收益极小,却让排除规则改动与已删文件
永远进不了归档。
- 新增 Backups/manifest.json 与 logs/*.log,跳过/失败有据可查。
- 结尾按失败数 exit;恢复支持 -WhatIf / -DryRun / -VerifyOnly / -Only。
- 恢复优先用 manifest 定位归档,并精确比较 BaseName(不再用 -Filter 通配)。
- 修正 tar 分支用 $LASTEXITCODE 判断成功与否的缺陷。
- 有警告(文件被占用)时拒绝用不完整的归档覆盖完整归档,需显式 -AcceptWarnings。
策略与安全(P2)
- Edge 条目加排除规则:解压后 4.22 GB 中 3.79 GB 是可再生的缓存/遥测/扩展本体,
保留书签、密码、偏好、历史与站点数据。
- 可选 7z 加密(@encrypt 标记或全局开关),取不到口令时明确失败,绝不写明文。
- 磁盘空间守卫:放不下就跳过该条目,低于阈值告警。
工程化(P3)
- 新增 BackupConfig.psd1、README.md、.gitignore。
- tests/Run-Tests.ps1(32 项)与 tests/Run-E2E.ps1(16 项端到端验收)。
- tools/Register-BackupTask.ps1 注册每日计划任务。
- 归档命名算法保持不变,已有归档不会失联。
|
2026-09-21 20:10:18 +08:00 |
|