AskaEth
|
7f657d225c
|
fix(addon): M111 r5 —— 基线读写同源(.ull)、plan.rs 拆回 400 内、中间产物/回滚/包身份
复审 r5 两条 P1 + 三条 P2。
P1-1 基线读、写走了两条路径(ext4 上是两个文件,Windows/macOS 上假绿)
- 写侧 pipeline 用 `LAUNCHER_HOME_DIR`(`.ULL`,启动器**数据根**的名字),读侧
`PackBaseline::path()` 用 `GameLayout::ull_root()`(`.ull`,实例**游戏根**内的私有目录)。
- 现在读写都从 `ull_root()` 派生:`PackBaseline::path()`(绝对)+ 新增
`PackBaseline::relative_path()`(相对,空根求 `ull_root()`),pipeline 的
`baseline_relative()` 只做 `/` 分隔,不再出现第二份目录名字符串;`LAUNCHER_HOME_DIR`
的引用从导入链路里删掉。
- 新断言 `baseline_read_and_write_paths_are_the_same_file`:`path(root) ==
root.join(relative_path())`、`read.parent() == ull_root()`、写侧字符串不含 `.ULL`
(路径比较是大小写敏感的,所以在大小写不敏感的系统上也抓得住)。
- 选 `.ull` 的理由:基线住在**实例的游戏根**里,那是 `GameLayout` 的地盘(且
`crates/core/tests/game/layout.rs:45` 早已钉死 `.ull`);`.ULL` 是 portable 模式下
exe 旁边的启动器数据根(`config/ data/ cache/ logs/`),层级不同,不能混用。
- 同时清掉 4 个测试里手抄的 `.ULL/pack-baseline.json`,改用 `baseline_rel()`。
P1-2 `import/plan.rs` 401 行撞穿 file-size 守门
- 把 `suffix_note()`(后缀注记)连同它的单测搬进 `plan/paths.rs`(那里本来就有
`suffix_variant`),plan.rs 401 → 373;paths.rs 119 → 157。没有压注释换行数。
P2-1 引擎中间产物会被当成内容提交
- `Staging::purge_engine_artifacts()`:提交前递归剔除 `.ullpart-*` 与 `.ullmeta.json`
(判据 `is_engine_artifact`,后缀取自 `core::download::atomic`),删掉的个数进报告注记。
- 测试:`StubFetcher::with_engine_artifacts()` 像真引擎那样在每次"下载"后留下残片,
断言实例目录里没有任何 `.ullpart` / `.ullmeta`。
P2-2 回滚失败被 `let _ =` 吞掉
- 新增 `RollbackOutcome { attempted, failed }`:`rollback_merge` 逐项记账,
`CommitGuard::rollback()` 返回它,提交阶段失败时 `CommitFailure { error, rollback }`
把两者一起交回调用方;`ImportReport` 新增 `rollback` 字段,`rolled_back` 改成
"回滚完整 / 从未写过目标目录"才为 true,不完整时 `false` 并在 warnings 里列失败项。
- CLI 人读输出相应分叉(`回滚未完整:N 项没回去`),不再无条件宣称一致。
- `Drop` 那条兜底路径仍拿不到调用方(文档写明是"防漏"不是"承诺")。
- 单测:`complete_rollback_has_no_failures` 与 `rollback_failures_are_reported_not_swallowed`
(用非空目录占住目标位置,让 remove/rename 都失败,跨平台确定)。
P2-3 基线不校验包身份
- `PackBaseline::matches_pack(format, name)`:同一目录先后导入两个包时,别人的基线一律
不拿来当 base(否则"用户改过"会被误判成"整合包升级"而静默覆盖),降级为"无基线"并
记 `pack.import.note.baseline_pack_mismatch`。
- 测试 `baseline_of_another_pack_is_not_used_as_base`:断言告警 key + 冲突降级为
pre_existing + 用户内容保留。
门禁(落盘读 $?):fmt 0 / clippy(workspace,all-targets,-D warnings) 0 /
test -p unlucky-core 0(363 lib)/ test --workspace --exclude unlucky-app 0(1993 passed)。
改动文件最大 373 行。core 依赖表未动(core-no-ui 不受影响)。
|
2026-11-04 20:54:05 +08:00 |
|
AskaEth
|
4760714743
|
feat(addon): M111 整合包导入管线(CF / mrpack / MultiMC)+ ull pack import
接 M110 的 pack 模型与安全闸,做导入:三格式 → 实例目录,落 D10 §2 三条硬要求。
core(`addon::import`):
- 三方合并:`merge::decide` 判据表(磁盘 × 基线 × 包内),base 侧落在
`<实例根>/.ULL/pack-baseline.json`;无基线时磁盘同名文件算用户已有内容 ⇒ 不覆盖。
冲突逐条进报告(user_modified / user_deleted / pre_existing + 可选处置)。
- 两阶段:先 overrides 再补远端;失败分级只有一条判据(是不是"某个文件搞不到"):
缺文件 ⇒ Partial;穿越/符号链接/限额/清单坏/摘要不符/IO ⇒ Failed 且整包回滚。
- 事务:`Staging` 临时目录 + `CommitGuard`(全新导入 rename 整目录、合并态 journal 逆序
回滚、Drop 即回滚),基线作为最后一个暂存文件同生共死。
- 三格式语义:mrpack `client-overrides` 胜 `overrides`、`server-overrides` 不解压;
`.disabled`/`.old` 按路径后缀识别并原样保留;MultiMC `formatVersion != 1` 整份拒绝、
非加载器 component 逐项进报告;CF `classId` 认不出就不猜。
- 加载器只做翻译(`LoaderTarget`:kind + 原版 + 加载器版本 + 版本 id,含 CF NeoForge
`1.20.1-47.1.106` 的 MC 前缀剥离),网络全部经注入的 `PackFetcher`
(生产实现 `DownloadEngineFetcher` 只走 core::download,无第二个 reqwest client)。
- 报告:三态 + 逐项原因 + 稳定文案 key 表(`report::keys`,含 ALL 守门表)。
CLI(`ull pack import <file> [--target] [--name] [--overwrite] [--no-register]`):
- 退出码三态 0 / 3 / 1(`Outcome::exit_code` + runner 透传,其它命令仍 0);
- `--json` 恒有信封(`data.status` / `data.error.code` / `data.stages`),Failed 不抛栈。
测试:29 条 core(三格式端到端、合并 6 条、必须先失败 8 条含双检互补样本与确定性
提交失败回滚、报告契约 5 条)+ 5 条 CLI(三态退出码、JSON 契约、既有命令回归、help)。
fmt / clippy(core+cli, -D warnings) 绿;文件全部 ≤400 行。
|
2026-11-04 20:54:04 +08:00 |
|
AskaEth
|
34f3db359a
|
fix(core): M110 r2 —— duplicate_override_paths 两种冲突同时报,补 mrpack 同侧分支用例
- p2-2:`duplicate_override_paths` 改成「先按 path 分组,再在组内报 (侧集合, 条目份数)」,
判据 `侧数 > 1 || 份数 > 侧数`。r1 修好了跨侧(Both×Client)但按侧去重把「同侧两个组
撞同一路径」这条旧能力换掉了(实测返回 []);现在两种冲突同时报得出来,返回值升为
`Vec<(String, Vec<PackSide>, usize)>`(M111 注意:这是最后一次改形状的窗口)。
- p3-a:mrpack `check_overrides` 的「每侧只能有一个覆盖目录」分支补用例(此前零测试,
删掉它 54 条全绿;现在删掉即红)。
- p3-b:把 ReadBudget 的「每次调用一份额度,不是整包一份」写进 `override_files` /
`ReadBudget` 文档(mrpack 连调三次 ⇒ 理论上限 3×;要整包共用额度就走
`override_files_with`;`open` 阶段的声明总量仍是整包一次)。
- 顺带(复审点名、各一行):mrpack 非表达目录的 hint 里 21 个连续空格;
curseforge 文档里 `.minecraft/.minecraft/…` 的说法按 CF 语义订正(overrides 是 zip 内前缀,
内容铺开不丢;要不要多拷一层取决于导入器)。
|
2026-11-04 00:20:25 +08:00 |
|
AskaEth
|
6dd652aced
|
fix(core): M110 r1 复审修复:导出安全闸、跨侧重名、第二检用例、半包与失败分级
按 tower 裁定清单逐条改,不扩战场(模型与扩展点未动)。
- p2-1 跨格式导出不再静默丢内容:`PackFormat::check_overrides` 在默认 `write` 写第一个
字节前判定"这份模型的覆盖目录集合能否被本格式表达"。mrpack 要求目录名由 side 决定
(overrides / client-overrides / server-overrides)、CF 只接受单一 Both 侧组;表达不了
即 `pack_unsupported` + hint。`format.rs` 模块文档写死"默认 write 只保证同格式往返",
并列出 M112 必须补的四件事(目录映射、side 合并、加载器 id 规范化、extra 分桶)。
- p2-2 `duplicate_override_paths` 改为按 `path` 跨侧计数,返回 `(路径, 侧集合)`;
Both×Client 同路径这条 mrpack 真实冲突现在查得出来。
- p2-3 补 `#[cfg(windows)]` 第二检用例(CON / NUL / COM1.txt / `a.` / 尾空格 / 通配符),
并订正 `archive.rs` 文档里"保留设备名这里不拦"的自相矛盾(实际由 enclosed_name 在
Windows 宿主上拦,第一检放行)。
- p3-1 导出改临时文件 + rename,失败清理临时文件:目标路径不再留半个 zip。
- p3-2 三个 `detect` 不再 `.ok().flatten()` 吞错误:清单在但读不出来 = `pack_metadata`;
注册表汇总时"命中优先于失败",一个都不命中才报,最后才退化成 `pack_unsupported`。
- p3-3 `env_extra`、CF `files[].extra`、`PackLoader::extra` 各补一条读→render→写→再读用例。
- p3-4 新增 `ReadBudget`(Cell<u64>):`override_files` 持有累计预算,按实际读出字节
逐条扣减,声明大小撒谎的包不能靠"很多条各自合规"绕过 `max_uncompressed_bytes`。
- CF 读侧始终产出一个 Both 侧 overrides 组(字段缺省 `overrides`),与 render 对称。
每处修复都有变异证据(改坏 → 对应用例红):p2-1/p2-2/p2-3/p3-1/p3-2/p3-3a-c/p3-4 共 9 条。
门禁:fmt 0 / clippy --workspace --all-targets -D warnings 0 / cargo test --workspace -j 4
exit 0(58 个 test binary,passed 1930,failed 0);全部改动文件 ≤400 行(最大 389)。
|
2026-11-04 00:20:24 +08:00 |
|
AskaEth
|
b34e86bba5
|
feat(core): 整合包格式无关模型与格式扩展点(M110)
从零建 `core::addon::pack`:格式无关中间模型 + 格式扩展点 + 安全读取层,
不碰网络、不碰 UI(core-no-ui 依赖表未变)。
- `PackManifest` 无损表达 CF / mrpack / MultiMC 三方字段:包名/版本/作者/简介/
格式版本/游戏版本/加载器依赖/文件条目/覆盖目录;`files[].hashes` 保留整张摘要表
(mrpack 允许 sha1+sha256+sha512+md5 共存),`disabled` 记语义(导出按 D10 映射成
env.client=optional,CF 落 required=false)。
- 未知字段按条目用 serde flatten 接住(清单顶层 / CF `minecraft` 对象 / 文件条目 /
env / 加载器条目),导出时并回且已知字段优先;两格式各有一条「读→写→再读相等」测试。
- `trait PackFormat`(id / label / metadata_entry / detect -> Option<证据> /
read / render / 默认 write)+ `PackRegistry`:注册表顺序即优先级,写死
CurseForge → Modrinth → MultiMC;同一份包命中多格式时 `also_matched` 暴露歧义。
测试里用一个 toy 格式(单文件、一行注册)证明扩展点真的可扩展。
- `PackArchive` 把目录与 zip 统一成规整条目表:路径穿越双检(自研七条规则 +
zip crate 的 enclosed_name)、符号链接条目拒绝、条目数/解压体积/单条目上限、
规整后重名拒绝、篡改载荷不当场吞掉;每条都有恶意样本测试。
- 格式实现:CF 可读写(缺路径如实记 None)、mrpack 可读写(强制 sha512 形状、
env 三态、三个覆盖目录成三侧)、MultiMC 只读(.minecraft 当覆盖目录,
组件顺序与非加载器组件全保留)。
- 三种格式各一个手造夹具(含截断 TOML / 半截 jar / 零字节 jar),来源在
`tests/fixtures.rs` 里声明为手造,不冒充真实导出。
|
2026-11-04 00:20:24 +08:00 |
|
AskaEth
|
fa2c6560fd
|
fix(appearance): M91 r2 —— 真交叉断言 + unsafe 门禁跨行扫描 + α 上取整
P2-1「与 design 码值一致」的守门此前不存在
- 新增 crates/app-iced/src/platform/mod.rs::palette_source_codes_match_design_level_codes:
三个格子对照 PaletteSource::as_str() 与 unlucky_design::palette::Level::code(),
并覆盖反方向(from_key_value(level.code()) 必须还原同一档)与「码值在允许值表里」
- core 侧那条测试改名为 value_tables_are_self_consistent,注释改成事实:
core 不依赖 design,只能钉自己的字面量;一致性由 app-iced 的那条对照断言守
- 实测:把 design 的 Level::Wallpaper 码值改成 "desktop" → 新断言红(design 自己 178 个测试仍全绿)
P2-2 unsafe-scope 门禁:行级正则 → 跨行扫描
- 去注释 + 按方括号闭合把续行并成逻辑行,再逐个属性组判断 allow(...unsafe_code)
- 抓得住原来两种绕过:多行属性 #![allow(\n unsafe_code\n)]、同行两属性 #[inline] #[allow(unsafe_code)]
- 门禁注释补全已知边界(不再声称「依赖 fmt job 兜底」——已不依赖)
O-1 α 量化方向:round → ceil
- 0.08×255=20.4,round 得 20 → 实际下发 20/255=0.0784,低于 D36 名义下限
- ceil 得 21 → 0.0824;新增 applied_alpha_is_never_below_the_floor 断言 下发字节/255 ≥ 0.08
O-7 契约测试补三个新键
- appearance_keys_are_all_ai_writable_and_registered 的 audited 清单加 window_opacity /
titlebar_mode / palette_source
|
2026-11-02 14:28:36 +08:00 |
|
AskaEth
|
7406785325
|
feat(appearance): M91 —— 外观下层三键 + win32 整窗 α FFI + unsafe 落点门禁
core(D36/D37/D38 的键与范围)
- appearance.window_opacity(0.08..=1.0,默认 1.0,越界钳位且不报错;下限 D36 定案)
- appearance.titlebar_mode(auto|always|never,默认 auto;判定逻辑不在本 mission)
- appearance.palette_source(background-image|wallpaper|preset,与 design 的 Level::code() 同名)
- schema 1.3.0 → 1.4.0:纯增量、不加迁移步骤(新键出厂值 = 老文件缺键)
- appearance 拆成目录 mod/modes/tests(单文件 ≤400 行),非法枚举由 diagnostics() 给可读回落说明
presenter(只读语义,不破 M89 零 UI 门禁)
- AppearanceBase { window_opacity, titlebar_mode, palette_source } + from_document()
- 单测:缺键→出厂值、越界→钳位、非法枚举→回落且合法值照读
app-iced(D36 唯一 unsafe 落点)
- platform::effective_window_alpha(backend, requested) 纯函数:≤0/非有限 → None(拒绝整窗穿透),
其余夹到 0.08..=1.0;CPU 档同样允许窗口级 α(D36 对 D27 的修订),4 组真值表测试
- platform/win32/transparency.rs:SetLayeredWindowAttributes(LWA_ALPHA) 的 FFI +
alpha_byte 量化 + apply_window_alpha;非 Windows 同签名空实现返回 Unsupported
CI
- guards.yml 新增 unsafe-scope job:全仓 allow(unsafe_code) 文件集合必须恰好等于
crates/app-iced/src/platform/win32/transparency.rs(集合相等,不是黑名单命中)
版本号绊线
- isolation_keys / java_install_keys:字面量 1.3.0 → 1.4.0(绊线保留字面量)
- store / document 的两个关系夹具改成相对 launcher_version() 构造(「更高 minor 只读」/
「同 minor 更高 patch 保留 $schema」),下次 bump 不再假绿或假红
|
2026-11-02 13:58:33 +08:00 |
|
AskaEth
|
69d0025d09
|
fix(core): M73 r1 —— Windows 壁纸来源改为「当前主题优先」,通配主题包降为末档
复审 p2-1(medium):Win10+ 上 `%APPDATA%\Microsoft\Windows\Themes\` 通常只有
`TranscodedWallpaper`,一个 `.theme` 都没有;当前生效的主题记在
`HKCU\Software\Microsoft\Windows\CurrentVersion\Themes` 的 `CurrentTheme` 值里,
指向 `%LOCALAPPDATA%\Microsoft\Windows\Themes\Custom.theme`。
- 新增第二步 `windows-current-theme`:`reg.exe query ... /v CurrentTheme` →
归一化(含 `%LOCALAPPDATA%` 展开)→ 读该 `.theme` 的 `Wallpaper=`;
- `%APPDATA%\...\Themes\*.theme` 通配扫描从「第二步」降为第四步兜底,
evidence 如实标注「用户另存的主题包,可能未启用」;
- 新顺序:TranscodedWallpaper → CurrentTheme → 注册表 WallPaper → 另存主题包;
- `HostRoots` 增加 `local_app_data`(`%LOCALAPPDATA%`,退 `~/AppData/Local`)。
复审 p2-2(low):`transcoded_wallpaper_hit_comes_first` 补一条字面量路径断言,
防止夹具与生产代码用同一构造函数一起拼错。
复审 p2-3(low):`HostIo::run` 现在检查退出码(`!output.status.success()` 也返回
`None`),与四处「非零退出 → 失败」的文案对齐;`io.rs` 补一条夹具用例
(有 stdout 但退出码非零)证明旧行为会被挡掉。
行长:`windows.rs` 371 → 拆出纯解析到 `wallpaper/windows/parse.rs`(100 行),
全部 7 个文件 ≤400 行。真机复跑 `detect()`:第 1 条 transcoded、第 2 条
`windows-current-theme` → `C:\Windows\web\wallpaper\Windows\img0.jpg`(与注册表同源,
去重后只剩一条),`%APPDATA%` 下无主题包的 note 照旧。
|
2026-11-01 21:17:55 +08:00 |
|
AskaEth
|
d3e9a62b21
|
feat(core): M73 桌面壁纸路径探测(D38 第②级输入)
新增 platform::wallpaper:只做「桌面壁纸可能是哪个文件」的路径探测,
不解码图片、不取色(莫奈取色与三级降级留给 crates/design 的后续 mission)。
- 唯一入口 detect() / 注入式 detect_with(io, env, platform):返回有序候选列表
+ 各步失败原因,全部找不到时是空列表而不是错误;
- Windows 三步:TranscodedWallpaper(无扩展名,靠魔数认)→ *.theme 的
Wallpaper= → reg.exe query HKCU\Control Panel\Desktop /v WallPaper;
- Linux 四步:gsettings picture-uri(-dark) → KDE Image= → hyprpaper/sway
配置 → XFCE xfce4-desktop.xml,每步失败不影响下一步;
- macOS 两步:com.apple.wallpaper/Store/Index.plist 字符串扫描(不引 plist
依赖,兼容 bplist00)→ defaults read com.apple.desktop Background;
代码与文档明确标注「未在真机验证」;
- 零 unsafe / 零 FFI(core 是 #![forbid(unsafe_code)]),文件读取与子进程
全部经 ProbeIo 注入,环境变量表经 EnvSource 注入;三平台采集逻辑不带 cfg,
CI 的 Linux 上也能跑 Windows / macOS 夹具用例;
- 不新增第三方依赖;6 个文件均 ≤400 行,32 条单测覆盖三平台的
正常命中 / 多候选 / 全部失败。
|
2026-11-01 21:17:52 +08:00 |
|
AskaEth
|
dc4fac8391
|
fix(core/game): M49 —— Java 去重键平台无关归一化(Linux CI 三红)
`game::java::discover::path_key` 只在 `cfg!(windows)` 分支折分隔符,POSIX 上
把 `C:\a\b\java.exe` 原样留成带反斜杠的键,与 `C:/a/b/java.exe` 不同键 →
`dedupe` 不去重。三条用例(Windows 语义无 cfg 门)因此在
`test (ubuntu-latest)` 挂:`path_key_normalises_separators`、
`dedupe_keeps_the_higher_priority_source`、
`java_discover::dedupe_collapses_one_install_found_twice`。
- `path_key`:`\?\` 前缀剥离与分隔符折成 `/` 改为**与平台无关**(同一个
`bin/java[.exe]` 用哪种拼法都是同一个文件,键必须相同);大小写折叠仍只有
Windows 做——POSIX 上 `Java` 与 `java` 是两个目录,折了会把两份真实安装误并。
已知取舍写进文档:POSIX 上文件名里的字面反斜杠会一并被当分隔符。
- 三条用例:不靠 `#[cfg(windows)]` 跳过。`path_key` 单测改成平台无关断言 +
大小写折叠的双侧断言(Windows 折 / POSIX 不折,两侧都钉住);集成用例的
「同一路径第二种写法」按平台给(Windows 用大小写+分隔符,POSIX 用分隔符)。
- 回归钉子:新增 `one_install_normalises_to_one_key_whatever_the_spelling`
(普通写法 / 反斜杠写法 / `\?\` 前缀写法 / 混合写法 四种拼法同键)与
`path_key_folds_case_only_where_the_filesystem_does`。
验证:Linux(rust:1-bookworm 容器,ull-linux-target 热缓存)
`cargo test -p unlucky-core --lib --test game` → 224 + 120 passed / 0 failed
(基线是 lib 220/2 failed、game 119/1 failed);
`cargo test --workspace --all-targets --no-fail-fast` → 34 个测试块
1137 passed / 0 failed(该次跑在 M48 launch 修复叠加的临时副本上,因为 M48
尚未合入 main,见报告)。Windows 本机 `cargo fmt --check`、
`cargo clippy -p unlucky-core --all-targets -D warnings`、
`cargo test -p unlucky-core` 全绿(17 个测试块 0 failed)。
|
2026-11-01 11:00:25 +08:00 |
|
AskaEth
|
6ce825db8d
|
fix(launch): M48 —— POSIX 组杀假成功导致 stop() 空等超时
CI test (ubuntu-latest) 自 M43 起红的根因不是「组杀打到自身进程组」,
而是 procps-ng 的 kill 把裸 `-<pgid>` 当成又一个信号选项吞掉:退出码 0、
什么都没发。旧实现据此提前 return,单 pid 兜底永远不执行,于是
process_smoke::stop_kills_process_tree 与
pipeline_cases::pipeline_stop_records_killed_exit 只能干等
graceful+5s 预算后报「强制结束后进程仍在运行」。
- process.rs:Unix 分支用 CommandExt::process_group(0)(std 1.64+ 安全
API,无需 pre_exec/unsafe)让子进程自成一个进程组,与 Windows 的
CREATE_NEW_PROCESS_GROUP 对称;这也让「树」回收真正覆盖后代。
- tree_kill.rs:组杀改用 `kill -TERM -- -<pgid>`(`--` 不可省),并且
先过 group_kill_target 三重验证——目标必须是自己的进程组组长、必须
读得到我们自己的组、目标组不能就是我们所在的组;单 pid 兜底改为
「永远执行」,kill 缺失(NotFound)返回明确错误而不是静默失败。
pid==0 守卫从 kill_tree 提到 terminate/force_kill 共用入口。
- 测试钉子(Linux 门住):spawn 后子进程 pgid == 自己的 pid 且 !=
测试进程 pgid;组杀只接受验证过的独立组;组杀覆盖孙进程且不碰我们
自己的组;同组子进程靠单 pid 兜底仍能被干掉;pid 0 一律拒绝。
验证:rust:1-bookworm 容器(ull-linux-target 热缓存)cargo fmt --check、
cargo clippy --workspace --all-targets -D warnings、cargo test -p
unlucky-core --test launch(82 passed,两条原红灯转绿)、cargo test
--workspace --all-targets --no-fail-fast 不再被 SIGTERM 打断;Windows
本机 fmt/clippy/cargo test -p unlucky-core 全绿。
|
2026-11-01 10:32:18 +08:00 |
|
AskaEth
|
07fd6113a7
|
fix(core): 回调先校验 state 再处理 error,堵掉伪造取消失效登录
reviewer P2(crates/core/src/auth/microsoft/callback.rs):handle() 先处理 error 参数、
之后才校验 state,于是任意一次 `GET http://localhost:{port}/auth?error=access_denied`
(state 随便给甚至不给)都会让 wait_for_code 直接返回 Canceled,登录被静默中止、账户不入库
—— 不需要知道随机 state、不需要 code、不需要任何凭据。违反 RFC 6749 §4.1.2.1 / RFC 9207
(错误响应同样要校验 state),也与本模块「与本次登录无关的请求一律继续等待」的声明矛盾。
- state 校验提到 error 之前:state 不匹配的请求(无论带不带 error)一律 400 后继续等;
只有带**本机 state** 的 access_denied 才判 Canceled。模块文档第 4 条同步改写。
- 顺带采纳 reviewer 的两条非计数观察:
* 读头预算改为 accept 之后按 deadline 重算(原来复用 accept 前的 remaining,
总时长可超发 ≤5s);预算归零直接超时。
* state 由 64 位升到 128 位(两个独立随机种子的 SipHash 输出拼接),对齐
RFC 6749 §10.10 的熵建议;新增单测断言 32 位 hex 且每次不同。
- 新增回归用例(crates/core/tests/auth/microsoft_authcode.rs):
`foreign_state_cannot_cancel_the_login` —— 先发 `?error=access_denied`(无 state)
再发 `?error=access_denied&state=not-our-state`,两次都必须 400 且服务器继续等待,
随后真回调拿到 code;`access_denied_with_our_state_is_still_canceled` 钉住另一半语义。
既有的「陈旧 state 回 400 后继续等」用例保持通过。
门禁:cargo fmt --all --check OK;cargo clippy --workspace --all-targets -- -D warnings
exit 0 无告警;cargo test -p unlucky-core -p unlucky-app 18 个 suite 全绿(auth 59 例)。
AskaEth <i@yeij.top>
|
2026-11-01 08:33:11 +08:00 |
|
AskaEth
|
8194e38bcf
|
feat(core): 加 MSA 授权码流(本地回调服务器)并接到账户页
research/08 方案 A:GUI 的微软登录改走授权码流(HMCL 主流做法),设备码流保留给
CLI/无浏览器场景。两条流只差「怎么拿到 MS access_token」,拿到之后共用
chain::complete_chain 与 MicrosoftSession,落盘/刷新语义没有第二套。
core(crates/core/src/auth/microsoft/)
- endpoints:新增 authorize(…/consumers/oauth2/v2.0/authorize),与 token 同租户。
- authcode:RedirectUri 是 http://localhost:{port}/auth 的唯一来源(授权 URL、
token 请求体、实际监听三处共用,坑 5);AuthCodePolicy(回调超时 300s / 绑定重试 3 次 /
固定端口 / 固定 state);authorization_url(response_type=code + prompt=select_account +
scope=XboxLive.signin%20offline_access);exchange_code 复用 flow 的 oauth_error/tokens_from。
- callback:本地 HTTP/1.1 回调服务器。内核分配端口(占用按次数退避重试);非 /auth 路径
(favicon、预连)404 后继续等;state 不匹配 400 后继续等;error=access_denied 归 Canceled;
单条连接读头超时 5s,半开连接拖不垮时间预算。
- MicrosoftAuth::authcode_login(present):绑端口 → present(宿主开浏览器)→ 等回调 →
换 token → 整链;失败与设备码流一样落 M38 失败日志。
app(crates/app)
- AppCommand::StartBrowserLogin + host::accounts::start_browser_login;设备码/授权码共用
LoginJob 取消槽位、收尾入库(store_session)与面板 DTO。
- host::opener 用系统默认浏览器打开授权 URL(Windows rundll32 url.dll,FileProtocolHandler)。
- 账户页:主按钮「微软登录」走授权码流,「设备码登录」作备用;DeviceLoginView::browser_waiting
在面板里给出可复制的授权链接(user_code 留空)。
测试
- crates/core/tests/auth/microsoft_authcode.rs:授权 URL 向量(租户/编码/prompt)、
URI 一致性(授权 URL 与 token 表单同字面量)、mock 全链、favicon+陈旧 state 后仍拿到 code、
端口占用重试文案、超时、access_denied→Canceled、login_with_xbox 403 的 M38 诊断
(detail_code=login_with_xbox_403,真机白名单未批时验收止步于此)。
- 单测:authcode 编码/向量、callback query 解码与错误分流、DTO browser_waiting、conv 面板。
未做/未验证:真机完整登录需 Azure 公共客户端 id(ULL_MSA_CLIENT_ID)与 Minecraft API 许可,
本机只能到「授权页 302 到 login.live.com 且 redirect_uri/scope/prompt 原样保留」这一步。
AskaEth <i@yeij.top>
|
2026-11-01 08:33:11 +08:00 |
|
AskaEth
|
bc41dfcf7d
|
Unlucky Launcher v0.0.1-alpha
CI / fmt (rustfmt --check) (push) Canceled after 0s
CI / clippy (ubuntu-latest) (push) Canceled after 0s
CI / clippy (windows-latest) (push) Canceled after 0s
CI / test (ubuntu-latest) (push) Canceled after 0s
CI / test (windows-latest) (push) Canceled after 0s
CI / build release (ubuntu-latest) (push) Canceled after 0s
CI / build release (windows-latest) (push) Canceled after 0s
Guards / core 零 UI 守门 (push) Canceled after 0s
Guards / 文件行数 ≤400 守门 (push) Canceled after 0s
Guards / 依赖许可与来源(cargo-deny) (push) Canceled after 0s
Rust 原生单文件 Minecraft 启动器:CLI 完整可用(install/launch/accounts/java/diagnose/agent),
MCP server 接入外部 Agent 工具,GUI 开发中。
|
2026-10-31 21:54:10 +08:00 |
|