Merge branch 'feat/m204-cleanupdoc-debt-sweep-four-small-it'
Guards / 依赖许可与来源(cargo-deny) (push) Canceled after 0s
Guards / 文件行数 ≤400 守门 (push) Canceled after 0s
Guards / unsafe 落点守门(集合必须恰好等于期望) (push) Canceled after 0s
Guards / design 零第三方依赖守门 (push) Canceled after 0s
Guards / presenter 零 UI 守门 (push) Canceled after 0s
Guards / core 零 UI 守门 (push) Canceled after 0s
Dev Guards / 架构与合规守门(同 main 口径) (push) Canceled after 0s
Dev Guards / fmt (rustfmt --check) (push) Canceled after 0s
Dev Guards / clippy (ubuntu) (push) Canceled after 0s
Guards / 依赖许可与来源(cargo-deny) (push) Canceled after 0s
Guards / 文件行数 ≤400 守门 (push) Canceled after 0s
Guards / unsafe 落点守门(集合必须恰好等于期望) (push) Canceled after 0s
Guards / design 零第三方依赖守门 (push) Canceled after 0s
Guards / presenter 零 UI 守门 (push) Canceled after 0s
Guards / core 零 UI 守门 (push) Canceled after 0s
Dev Guards / 架构与合规守门(同 main 口径) (push) Canceled after 0s
Dev Guards / fmt (rustfmt --check) (push) Canceled after 0s
Dev Guards / clippy (ubuntu) (push) Canceled after 0s
This commit is contained in:
commit
c97630d59e
3 files changed
+64
-26
No files matched your search
@@ -20,6 +20,7 @@
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use unlucky_core::download::blocking::block_on;
|
use unlucky_core::download::blocking::block_on;
|
||||||
|
use unlucky_core::download::config::DEFAULT_MAX_ATTEMPTS_PER_SOURCE;
|
||||||
use unlucky_core::download::{DownloadRequest, ResourceKind};
|
use unlucky_core::download::{DownloadRequest, ResourceKind};
|
||||||
|
|
||||||
use super::super::env::HostEnv;
|
use super::super::env::HostEnv;
|
||||||
@@ -29,7 +30,17 @@ pub(super) const USER_AGENT: &str =
|
|||||||
"Unlucky-Launcher/0.1 (+https://github.com/AskaEth/Unlucky-Launcher)";
|
"Unlucky-Launcher/0.1 (+https://github.com/AskaEth/Unlucky-Launcher)";
|
||||||
|
|
||||||
/// 最多重试次数(含首次)。
|
/// 最多重试次数(含首次)。
|
||||||
const ATTEMPTS: u32 = 3;
|
///
|
||||||
|
/// **不在这里写数字**:取值就是下载引擎的出厂默认
|
||||||
|
/// (`core::download::config::DEFAULT_MAX_ATTEMPTS_PER_SOURCE`,`download.retry_limit` 的 schema
|
||||||
|
/// 默认值同源),改工厂默认时这里跟着走。
|
||||||
|
///
|
||||||
|
/// 边界说清:这条路径**没读**宿主设置(次数是编译期常量),所以固定吃出厂默认——**不是拿不到**。
|
||||||
|
/// `env` 就在手上(`get_with_headers` 第一句 `engine(env)` 已经灌过调优),用户当前值就在
|
||||||
|
/// `engine.config().max_attempts_per_source`;要让它跟随 `download.retry_limit`,读那个字段即可,
|
||||||
|
/// 不需要新拉一条参数链。另一条无 header 的路径([`get_via_engine`])走引擎,本来就跟随设置
|
||||||
|
/// (源冷却中时引擎只给 1 次,那是恢复探测,见 `DownloadEngine::attempts_for`)。
|
||||||
|
const ATTEMPTS: u32 = DEFAULT_MAX_ATTEMPTS_PER_SOURCE;
|
||||||
|
|
||||||
/// 取一段文本(JSON 原文)。
|
/// 取一段文本(JSON 原文)。
|
||||||
pub(super) fn get(env: &HostEnv, url: &str, headers: &[(&str, &str)]) -> Result<String, String> {
|
pub(super) fn get(env: &HostEnv, url: &str, headers: &[(&str, &str)]) -> Result<String, String> {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
//! | 用例 | 守什么 |
|
//! | 用例 | 守什么 |
|
||||||
//! |---|---|
|
//! |---|---|
|
||||||
//! | [`version_matrix_decides_who_gets_prompted`] | 版本更高才提示;同版本/更低一律不提示;`v0.2.3` 这类真实 tag 形态 |
|
//! | [`version_matrix_decides_who_gets_prompted`] | 版本更高才提示;同版本/更低一律不提示;`v0.2.3` 这类真实 tag 形态 |
|
||||||
//! | [`a_broken_manifest_degrades_to_failure_not_up_to_date`] | 清单坏 JSON → 「检查失败」,**不是**「已是最新」 |
|
//! | [`a_broken_manifest_degrades_to_failure_not_up_to_date`] | 坏 JSON → `invalid_*`、签名被拒 → `signature_*`;两种都是「检查失败」,**不是**「已是最新」 |
|
||||||
//! | [`a_higher_version_without_our_platform_is_not_up_to_date`] | 有新版但没本平台产物 → 单独的变体 |
|
//! | [`a_higher_version_without_our_platform_is_not_up_to_date`] | 有新版但没本平台产物 → 单独的变体 |
|
||||||
//! | [`sha256_mismatch_refuses_the_apply_and_leaves_no_temp_file`] | 摘要不符拒绝应用、暂存文件不残留、目标没被换掉 |
|
//! | [`sha256_mismatch_refuses_the_apply_and_leaves_no_temp_file`] | 摘要不符拒绝应用、暂存文件不残留、目标没被换掉 |
|
||||||
//! | [`a_successful_apply_leaves_the_new_file_and_a_backup`] | 换上去的是新字节、旧字节留在备份里、回报是「就位」 |
|
//! | [`a_successful_apply_leaves_the_new_file_and_a_backup`] | 换上去的是新字节、旧字节留在备份里、回报是「就位」 |
|
||||||
@@ -191,11 +191,17 @@ fn version_matrix_decides_who_gets_prompted() {
|
|||||||
|
|
||||||
// ------------------------------------------------------------- 清单契约降级
|
// ------------------------------------------------------------- 清单契约降级
|
||||||
|
|
||||||
/// 坏 JSON / 缺字段 ⇒ **检查失败**,绝不是「已是最新」。
|
/// 坏 JSON / 缺字段 / 签名被拒 ⇒ **检查失败**,绝不是「已是最新」;而且两种坏法**分类不同**。
|
||||||
///
|
///
|
||||||
/// 两个变体的文案与后果完全不同:说成「已是最新」会让用户以为网络通、版本新,实际一个字节都没取到。
|
/// M199 复审 P3-2:旧版只断言「结果是 Failed」,而它喂的清单都**签对了**——验签一旦 fail-open,
|
||||||
|
/// 坏签名清单会一路走到解析,坏 JSON 照样解析失败,断言全绿。现在内容坏必须落 `invalid_*`,
|
||||||
|
/// 签名坏必须落 `signature_*` 且不可重试。
|
||||||
|
/// 抓得住:验签被跳过/放行(② 会拿到 `Available 0.9.0`)、签名与解析互相误分类、失败被折叠成「已是最新」;
|
||||||
|
/// 抓不住:签名信封字段与钥表(在 core 的 `signature_cases.rs`),以及 `manifest_sha256` 那条冗余校验
|
||||||
|
/// (需「钥已钉住 + 方程验过 + 声明 sha 不同」,**全仓无用例**,已立 finding)。
|
||||||
#[test]
|
#[test]
|
||||||
fn a_broken_manifest_degrades_to_failure_not_up_to_date() {
|
fn a_broken_manifest_degrades_to_failure_not_up_to_date() {
|
||||||
|
// ① 签名是对的(`write_manifest` 成对落盘),内容坏:分类只能是「清单结构 / 版本号不合法」。
|
||||||
for (label, manifest) in [
|
for (label, manifest) in [
|
||||||
("not json", "{ this is not json".to_string()),
|
("not json", "{ this is not json".to_string()),
|
||||||
(
|
(
|
||||||
@@ -221,16 +227,32 @@ fn a_broken_manifest_degrades_to_failure_not_up_to_date() {
|
|||||||
let tmp = TempDir::new("badmanifest");
|
let tmp = TempDir::new("badmanifest");
|
||||||
write_manifest(&tmp, &manifest);
|
write_manifest(&tmp, &manifest);
|
||||||
let outcome = outcome_of(job(&tmp).check(&version("0.2.2"), Channel::Stable));
|
let outcome = outcome_of(job(&tmp).check(&version("0.2.2"), Channel::Stable));
|
||||||
|
let Some(failure) = outcome.failure() else {
|
||||||
|
panic!("{label}:坏清单必须降级成「检查失败」,实际 {outcome:?}");
|
||||||
|
};
|
||||||
|
let kind = failure.kind.as_str();
|
||||||
assert!(
|
assert!(
|
||||||
matches!(outcome, UpdateOutcome::Failed(_)),
|
matches!(kind, "invalid_manifest" | "invalid_version"),
|
||||||
"{label}:坏清单必须降级成「检查失败」,实际 {outcome:?}"
|
"{label}:坏内容分类是 {kind}"
|
||||||
);
|
|
||||||
let failure = outcome.failure().expect("失败带原因");
|
|
||||||
assert!(
|
|
||||||
!failure.message.trim().is_empty(),
|
|
||||||
"{label}:失败要有人话原因"
|
|
||||||
);
|
);
|
||||||
|
assert!(!failure.message.trim().is_empty(), "{label}:失败没原因");
|
||||||
}
|
}
|
||||||
|
// ② 清单字节是好的(放行就会报「有更新 0.9.0」),只有签名对不上:必须落 `signature_invalid`。
|
||||||
|
let tmp = TempDir::new("badsignature");
|
||||||
|
tmp.write(
|
||||||
|
"dist/stable.json",
|
||||||
|
manifest_for("0.9.0", b"new", "").as_bytes(),
|
||||||
|
);
|
||||||
|
tmp.write(
|
||||||
|
"dist/stable.json.sig",
|
||||||
|
signature_envelope("another").as_bytes(),
|
||||||
|
);
|
||||||
|
let outcome = outcome_of(job(&tmp).check(&version("0.2.2"), Channel::Stable));
|
||||||
|
let Some(failure) = outcome.failure() else {
|
||||||
|
panic!("签名验不过必须拒装(放行就会报「有更新 0.9.0」),实际 {outcome:?}");
|
||||||
|
};
|
||||||
|
assert_eq!(failure.kind, "signature_invalid", "签名被拒的分类");
|
||||||
|
assert!(!failure.retryable, "签名问题不可重试");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 清单里**一条发布都没有**(`{"channel":"stable"}` / 空 `releases`)⇒ 已是最新,不是失败。
|
/// 清单里**一条发布都没有**(`{"channel":"stable"}` / 空 `releases`)⇒ 已是最新,不是失败。
|
||||||
|
|||||||
+20
-15
@@ -59,7 +59,7 @@ Git Bash 下会静默落进 Docker VM(脚本内部用 `cygpath -m` 归一)
|
|||||||
|
|
||||||
| 文件 | 作用 | 行数上限 |
|
| 文件 | 作用 | 行数上限 |
|
||||||
|------|------|---------|
|
|------|------|---------|
|
||||||
| `packaging/README.md` | 本文:索引 + §11 / §12 / §13(契约入口;§1–§10 见 `docs/`) | ≤400 |
|
| `packaging/README.md` | 本文:索引 + §11–§16(契约入口;§1–§10 见 `docs/`) | ≤400 |
|
||||||
| `packaging/docs/artifacts-and-version.md` | §1 产物矩阵 + §2 版本来源 | ≤400 |
|
| `packaging/docs/artifacts-and-version.md` | §1 产物矩阵 + §2 版本来源 | ≤400 |
|
||||||
| `packaging/docs/build-and-ci.md` | §3 怎么构建(本地 / CI) | ≤400 |
|
| `packaging/docs/build-and-ci.md` | §3 怎么构建(本地 / CI) | ≤400 |
|
||||||
| `packaging/docs/runtime-deps.md` | §4 每平台运行时依赖 + §5 软渲染兜底 | ≤400 |
|
| `packaging/docs/runtime-deps.md` | §4 每平台运行时依赖 + §5 软渲染兜底 | ≤400 |
|
||||||
@@ -149,20 +149,6 @@ Git Bash 下会静默落进 Docker VM(脚本内部用 `cygpath -m` 归一)
|
|||||||
`portable-exe`;Linux/macOS 的真实产物是 `.tar.gz` + `portable-zip`,在本模块会被直接拒)。
|
`portable-exe`;Linux/macOS 的真实产物是 `.tar.gz` + `portable-zip`,在本模块会被直接拒)。
|
||||||
**不验**真 GitHub 下载、GUI 点击与重启替换(需人工)。
|
**不验**真 GitHub 下载、GUI 点击与重启替换(需人工)。
|
||||||
|
|
||||||
## 14. 产物署名与清单签名(M190)
|
|
||||||
|
|
||||||
这一节只放「去哪看」的三句话,手册本体在 **`packaging/docs/signing.md`**:
|
|
||||||
|
|
||||||
- **PE 版本资源**(Windows「属性 → 详细信息」里的发布者/产品/版权/四段式版本):口径唯一来源
|
|
||||||
`packaging/pe_version.rs`,被 `crates/cli/build.rs` 与 `crates/app-iced/build.rs` 各 `include!` 一次;
|
|
||||||
判据是**读产物字节**的 `packaging/check_pe_version.py`(CI:`release-build.yml` 的 Windows 槽位)。
|
|
||||||
- **清单签名**:`manifest.json.sig` 与 `manifest.json` 并列上传,是分离的 Ed25519 签名文件(签的是清单
|
|
||||||
的**原始字节**,不是某个规范化后的 JSON)。信任锚是 `crates/core/src/update/trust.rs` 里的
|
|
||||||
**编译期公钥数组**(keyring);源里没有 `.sig`、钥不在表里、签名对不上,一律 `signature_*` 分类拒装,
|
|
||||||
**不可重试**。签名脚本 `packaging/sign_manifest.py`(零第三方依赖,`--self-test` 跑 RFC 8032 向量)。
|
|
||||||
- **不得混淆**:项目自有签名解决的是「更新器怎么判断这份清单是不是我们发的」,**不解决 SmartScreen**
|
|
||||||
——用户双击 exe 时看到的「未知发布者」要等 Authenticode 证书,那是另一条线。
|
|
||||||
|
|
||||||
## 15. 产物 PE 子系统(M196:GUI 必须 `WINDOWS_GUI`=2)
|
## 15. 产物 PE 子系统(M196:GUI 必须 `WINDOWS_GUI`=2)
|
||||||
|
|
||||||
群友 2026-11-08 实测发布构建「运行时会带一个控制台窗口在底下」。根因:产物 PE 头的 `Subsystem` 仍是
|
群友 2026-11-08 实测发布构建「运行时会带一个控制台窗口在底下」。根因:产物 PE 头的 `Subsystem` 仍是
|
||||||
@@ -203,3 +189,22 @@ CUI(3),`grep -rn windows_subsystem crates/` 当时零命中。同一个修复
|
|||||||
`instances list` / `--help` 全 0(`logs list` 是 1,但重定向跑同一条也是 1 ⇒ 用户错误,不是写失败)。
|
`instances list` / `--help` 全 0(`logs list` 是 1,但重定向跑同一条也是 1 ⇒ 用户错误,不是写失败)。
|
||||||
想让这种情形有专属退出码/文案,得在 CLI 侧显式判一次能力(`platform::win32::console` 的
|
想让这种情形有专属退出码/文案,得在 CLI 侧显式判一次能力(`platform::win32::console` 的
|
||||||
`output_visible()` 就是现成判据),那在 `crates/cli/**`、不在 M196 的 scope 内——这里只如实记录形态。
|
`output_visible()` 就是现成判据),那在 `crates/cli/**`、不在 M196 的 scope 内——这里只如实记录形态。
|
||||||
|
|
||||||
|
## 16. 产物署名与清单签名(M190)
|
||||||
|
|
||||||
|
**编号来历(M203)**:本节原先也编作 §14,与 §14「更新链路的测试通道(M169)」撞号。撞号按「谁后插进来谁让位」
|
||||||
|
处理:M169 的 §14 与 M196 的 §15 都被仓库里的引用钉着(`packaging/check_test_source_off.py` 引 §14;
|
||||||
|
`.github/workflows/release-build.yml` 与 `crates/app-iced/src/platform/win32/console.rs` 引 §15),
|
||||||
|
所以本节后移编 §16 并放到文末——**内容一字未改,只改了编号和位置**。
|
||||||
|
|
||||||
|
这一节只放「去哪看」的三句话,手册本体在 **`packaging/docs/signing.md`**:
|
||||||
|
|
||||||
|
- **PE 版本资源**(Windows「属性 → 详细信息」里的发布者/产品/版权/四段式版本):口径唯一来源
|
||||||
|
`packaging/pe_version.rs`,被 `crates/cli/build.rs` 与 `crates/app-iced/build.rs` 各 `include!` 一次;
|
||||||
|
判据是**读产物字节**的 `packaging/check_pe_version.py`(CI:`release-build.yml` 的 Windows 槽位)。
|
||||||
|
- **清单签名**:`manifest.json.sig` 与 `manifest.json` 并列上传,是分离的 Ed25519 签名文件(签的是清单
|
||||||
|
的**原始字节**,不是某个规范化后的 JSON)。信任锚是 `crates/core/src/update/trust.rs` 里的
|
||||||
|
**编译期公钥数组**(keyring);源里没有 `.sig`、钥不在表里、签名对不上,一律 `signature_*` 分类拒装,
|
||||||
|
**不可重试**。签名脚本 `packaging/sign_manifest.py`(零第三方依赖,`--self-test` 跑 RFC 8032 向量)。
|
||||||
|
- **不得混淆**:项目自有签名解决的是「更新器怎么判断这份清单是不是我们发的」,**不解决 SmartScreen**
|
||||||
|
——用户双击 exe 时看到的「未知发布者」要等 Authenticode 证书,那是另一条线。
|
||||||
Reference in new issue
Block a user