fix(core,docs): M308 r1 —— 读表工具写错(又一句「文档比代码多」)+ .exe 名字判据空转 + adopt 之后的取值器文档

p2-1 文档:`--check-pinned-keyring` **要私钥**(`do_check_pinned(args.key)`),语义是「Secret 里那把
钥是否已钉住」这条发布前门禁,不是读表工具。`docs/update-trust-anchor.md` 的「当场看一眼」与
`signing-runbook.md` §0 那句「现在 --check-pinned-keyring 绿」(与同文件 §:116 自相矛盾)都换成
`check_signing_ready.py` 的 ① 行(真只读,实测打「就绪 —— 标记之间 1 条:ull-release…」)。

p2-2 判据:`is_launcher_candidate` 的 `|| name.ends_with(".exe")` 此前**删掉也不红**(唯一含 `.exe`
的用例同时给了 0o755)。补 `a_windows_exe_without_the_exec_bit_is_still_the_launcher`
(win-x64 + 归档内 `unlucky-launcher.exe` + MZ + **0644** 能装上),删那半条判据即红。

p2-3 文档:`StagedPackage::size()` / `integrity()` 的注释在 `adopt` 之后不再成立(前者是本体的字节数、
后者是**归档**的摘要),改成「这个文件写出来的字节数」/「为它背书的那份摘要」。
This commit is contained in:
AskaEth committed 2026-11-14 00:15:13 +08:00
1 parent af82658d08
commit 06712ff6e9
4 files changed
+45 -9

No files matched your search

@@ -197,6 +197,30 @@ fn a_macos_tar_gz_runs_the_whole_chain() {
assert_eq!(std::fs::read(job.dest()).expect("读目标"), MACHO);
}
/// Windows 的名字兜底:归档里的 `unlucky-launcher.exe` 带 **0644**(没有可执行位)也认。
///
/// 为什么需要这一条:`is_launcher_candidate` 是两条判据取并集(可执行位 **或** `.exe` 名字)。
/// 发布矩阵里 Windows 发裸 `.exe`,可「清单给 win-* 挂一个归档」是合法输入,而 tar 里没有 unix
/// 权限位、`make_artifacts.py` 也只给启动器打 0o755——名字这条兜底不能是空转的。
///
/// 抓得住:把 `is_launcher_candidate` 里 `|| name.to_ascii_lowercase().ends_with(".exe")` 删掉,
/// 本用例立刻红(归档里一个候选都没有 ⇒ `LauncherMissing`)。
#[test]
fn a_windows_exe_without_the_exec_bit_is_still_the_launcher() {
let stage = "unlucky-launcher_9.9.9_windows_x64";
let archive = tar_gz(&[
(&format!("{stage}/unlucky-launcher.exe"), PE, 0o644),
(&format!("{stage}/LICENSE"), b"license text", 0o644),
]);
let target = Target::new(OsFamily::Windows, Arch::X64);
let (_dir, job) = fixture(target, "portable-zip", &archive);
let applied = job
.apply(&Version::parse("0.2.2").expect("版本"), Channel::Stable)
.expect("应当装上(名字判据兜住没有可执行位的那份)");
assert!(matches!(applied, Applied::Installed { .. }));
assert_eq!(std::fs::read(job.dest()).expect("读目标"), PE);
}
/// 归档里没有本体(只有随包文本)⇒ `LauncherMissing`。抓得住「只认可执行位」这条判据被放松。
#[test]
fn an_archive_without_a_launcher_is_refused() {
+9 -2
View File
@@ -56,13 +56,20 @@ impl StagedPackage {
&self.path
}
/// 实际字节数(已与清单核对过,等于清单声明的 `size`)。
/// 实际字节数(**这个文件**写出来的字节数)。
///
/// `stage` 产出的那份就是清单声明的 `size`(双检已核对);M308 起 `unpack` 还会用
/// [`Self::adopt`] 造出「从归档里拆出来的本体」那一份,那时它是**本体**的字节数,
/// 不是归档的。跨卷复制那一步靠它核对该搬多少字节,所以这个字段必须始终是「本文件的」。
#[must_use]
pub const fn size(&self) -> u64 {
self.size
}
/// 校验用的摘要。
/// 为这个文件背书的那份摘要。
///
/// `stage` 产出的是清单里写的那条摘要(对**下载到的字节**算的);`adopt` 产出的(拆出来的本体)
/// 沿用**归档**的摘要——拆出来的字节由「归档过了长度+摘要双检」背书,本模块不为它另算一份。
#[must_use]
pub fn integrity(&self) -> &Integrity {
&self.integrity