fix(download): text_bounded 不再信 Content-Length 预分配,补两道内存闸;api/version 第四处裸 send 也上界(M290 r1)

复审 r1(p2-1 / p3-1 / p3-2 / p3-5)返工,三处既有结论(send_bounded 逐字节未动、三个既有调用方
零改动、阶段只用两档)原样保留。

**p2-1 `http.rs::text_bounded` 的内存**(复审判据成立):`Content-Length` 是「对面说了算」的数字,
拿它喂 `Vec::with_capacity` 等于让远端决定我们的内存,而分配失败是 abort、进不了 `Result`。
改成三道闸:
1. 头里声明 > `MAX_IN_MEMORY_BYTES`(64 MiB)⇒ 读完头立刻有界失败,**不预分配、不读正文**;
2. 容量只按真实到达增长(`Vec::new()`);
3. 读循环逐块卡同一道上限(在累加前判,不会先撑过上限一块)。
两道闸的 `message` 分别说「声明的长度」与「内存上限」,`kind` 都是 `InvalidRequest` ⇒ 与超时
(`Timeout` + 阶段)可区分。

**p3-1 第四处裸 `send()`**:`game/java/api/version.rs::fetch_release_page` 原本
`client.get(url).send()` + `response.text()`、整文件 0 处 timeout(生产路径:`source::pinned` →
`api::fetch_version`)。改走 `http::send_bounded` + `http::text_bounded`,界是参数(生产
`http::default_deadlines()`,用例压到 200ms)。

**p3-2 `download/stall.rs` 模块头**:把「`game/java/api.rs`、`game/java/net.rs`、`update/github.rs`
是裸 `send()`——未审」这句假事实换成实测清单(谁已收、谁可以留、为什么留:`auth/http.rs` 自带
`.timeout`、`preflight.rs` 由 `check_with_deadline` 套住、`region.rs` 自带 3s),并给
`http::text_bounded` 补一行「入口/上界/阶段」。

**p3-5 doc**:删掉「与 `Response::text()` 的唯一差别是 charset 解码」这条过度披露——本仓 reqwest
没开 `charset` feature,`text()` 本来就是 `from_utf8_lossy`,解码逐字节等价;改成真实差别
(读空闲 + 内存上限)。

离线用例(全在 `#[cfg(test)]`,夹具 `game::java::net::probe` 新增 `Mode::Declared{declared,sent}`
与 `Mode::Flood(n)`,外加共用的 `bounded()` 护栏助手):
* `api/version.rs::bounded_probes`:静默 ⇒ 阶段等响应头;正文停顿 ⇒ 阶段等字节;
  声明 `Content-Length: 999999999999` 且不发正文 ⇒ <2s 返回 `InvalidRequest` 且文案含
  「声明的长度」;不声明长度灌 65 MiB ⇒ 读循环在 64 MiB 处拒。
* `net.rs::tests` 的六条(4 条原有 + 2 条正常路径)改用同一个 `bounded()` 助手,断言一条未放宽。

变异实跑(贴红原文见 mission 报告):
1. 退回 `Vec::with_capacity(declared)` 且删提前拒绝 ⇒ `memory allocation of 999999999999 bytes
   failed` + 进程 abort(0xc0000409),测试二进制消失——不是一条红;
2. 只删提前拒绝(预分配仍是 `Vec::new()`)⇒ 干净红:`left: Timeout / right: InvalidRequest`(0.21s);
3. 删循环里的上限卡口 ⇒ 干净红:`expect_err` 拿到 `Ok("xxxx…")`(0.54s);
4. `fetch_release_page` 退回裸 `send()` + `text()` ⇒ 两条 version 用例各在护栏 5s 处 panic(5.01s)。

门禁(真实退出码):check=0 / lib=896 passed / game=120 passed / launch=91 passed /
engine_mock=17 passed / clippy -D warnings=0 / fmt=0。行数:http 389、stall 378、api 389、
net 390、version 397、github 363(全 ≤400)。
This commit is contained in:
AskaEth committed 2026-11-13 00:22:22 +08:00
1 parent ad99303a95
commit bde6649aab
4 files changed
+274 -101

No files matched your search

+18 -4
View File
@@ -25,6 +25,7 @@
//! | `DownloadEngine::download` 单连接 | `http::send_bounded` + `http::read_step` | 等响应头 / 等字节 |
//! | `DownloadEngine::download` 分块 | 同上 + [`StallStage::AwaitChunkJoin`](无进度窗口与总期限) | 等分块回收 |
//! | `DownloadEngine::download_bytes` | `http::send_bounded` + `read_step` 流式累加(顺带用 `MAX_IN_MEMORY_BYTES` 卡住无 `Content-Length` 的洪流) | 等响应头 / 等字节 |
//! | `http::text_bounded`(模块外几条「一锤子 GET 取文本」的正文) | `read_step` 空闲判定 + 头/循环两道 `MAX_IN_MEMORY_BYTES` | 等字节 |
//! | `DownloadEngine::acquire`(许可) | `EngineConfig::permit_timeout` | [`StallStage::AcquirePermit`] |
//! | 校验(`verify_part`) | `EngineConfig::verify_timeout` | [`StallStage::Verify`] |
//! | `font::fetch_blocking` | 自带的整作业预算(外加上面全部) | —— |
@@ -34,10 +35,23 @@
//! 与 `region.rs` 的 3s 探测(自带界)。新加下载入口时**先看这张表**,别再出现第二个
//! `download_bytes`。
//!
//! 模块外还有几处用**同一个** `reqwest::Client`(只设了 `connect_timeout`)的地方,M289 没碰
//! (scope 是 `download/**`):`auth/http.rs` 自带 `.timeout(REQUEST_TIMEOUT)`;而
//! `game/java/api.rs`、`game/java/net.rs`、`update/github.rs` 是裸 `send()`——**未审**,
//! 与这里修掉的是同族形态,已用 TowerFinding 报给塔台另立 mission。
//! # 模块外那几处 `send()` 的实测清单(M290 / M290 r1 收口)
//!
//! 口径是 `git grep -n "\.send()" crates/core/src`(M290 r1 之后的 tip)。**core 里已经没有
//! 「既无自身上界、又没有外部 deadline」的 `send()` 了**:
//!
//! | 位置 | 现状 |
//! |------|------|
//! | `game/java/api.rs`(Adoptium latest 查询) | 已收:`http::send_bounded` + `http::text_bounded`(M290) |
//! | `game/java/net.rs`(四家发行版的一锤子 GET) | 同上(M290) |
//! | `game/java/api/version.rs`(GA 清单翻页) | 同上(M290 r1,这一处是复审打穿的第四处) |
//! | `update/github.rs`(更新清单) | 同上;界取 `engine.config().deadlines()`(M290) |
//! | `auth/http.rs` | **可以留**:RequestBuilder 自己挂了 `.timeout(REQUEST_TIMEOUT)`,reqwest 的该超时覆盖到响应体读完 |
//! | `auth/microsoft/preflight.rs` | **可以留**:`probe()` 由 `check_with_deadline` 的整体 deadline 套住 |
//! | `download/region.rs` | **可以留**:自带 3s 探测界(上面那张表里也有它) |
//!
//! 这几条文本路径只用 `await_headers` / `await_bytes` 两档:它们没有分块回收、也不经引擎许可,
//! 所以 `await_chunk_join` / `acquire_permit` 在这里不适用(不是「没做到」,是「不适用」)。
//!
//! # 取舍:三段可以共存的界,**不是**「整作业必须在 N 秒内下完」
//!