fix(download): M295 r2 —— 代理地址四类原因各一句中文,不再摊 reqwest / url 的原文
r2 复审退回的形态:`代理地址无法解析(http://127.0.0.1:99999):builder error`。第一版靠 `Proxy::all` 的报错(只有这一句),第二版靠 `url::ParseError` 的 Display(英文 `invalid port number`,而且 reqwest 的宽口径会让「解析成功/失败」本身不可靠)——两种都还是把别人的原文当结论。 - `validate()` 自己判四类,各给一句中文:scheme 不支持 / 缺少主机名 / 端口(空、0、越界)/ 空地址; `manual_proxy` 先过它,再 `Proxy::all`。 - 残余情形(validate 放行但 reqwest 不认,例如 userinfo 有非法字符):中文结论在前,reqwest 原文 只作为括号里的证据。 - 判据改成「文案里命中该类中文原因,且不含 `builder error` / `invalid port number`」,四类表驱动 + 合法写法(含 `[::1]:8080` 与少写 scheme)不许被拒。 - 实测(真二进制,`ull install 1.20.1 --dry-run`,口令样本 PWMUSTNOTLEAK): `代理地址用不了(http://user:***@127.0.0.1:99999):端口 99999 不是 1–65535 之间的数字` `代理地址用不了(socks5://user:***@127.0.0.1:1080):本构建不支持 socks5 代理(只支持 http / https)` 口令出现 0 次。 - 变异(本轮重做):关掉端口判定 ⇒ 红,且红的样子正是「地址写法不被接受(builder error:invalid port number)」——证明没有自己的判定时原文就会漏出来;关掉主机名判定 ⇒ 红;scheme 白名单写 `if false` ⇒ 红;`redact_proxy_url` 返回原文 ⇒ 3 条红(口令泄漏)。见 docs §6 M23/M25/M26/M27。 - 门禁(含塔点名的整棵 `-p unlucky-core --lib`):914 passed / 0 failed;四 crate 测试 45 个 binary、3206 passed / 0 failed;clippy `-D warnings`、`fmt --all --check`、 `check --workspace --all-targets` 全 0。
This commit is contained in:
1 parent
c182d8d5c9
commit
422f198c84
4 files changed
+139
-60
No files matched your search
@@ -47,10 +47,13 @@
|
||||
//!
|
||||
//! # 非法值不得穿透(两类都走同一条路:有界错误 + 回落直连 + 告知)
|
||||
//!
|
||||
//! 1. **解析不了**(端口越界 `…:99999`、方括号不闭合…):[`ProxySetting::apply`] 返回
|
||||
//! `InvalidRequest` + 带(脱敏后)原串与**能读**的原因,**不 panic**;
|
||||
//! 2. **解析得了但本构建用不上**(`socks5://` 等:reqwest 没开 `socks` feature,而
|
||||
//! `Proxy::all` 对它们照样返回 `Ok`):同样是有界错误,文案直接点名 scheme。
|
||||
//! 1. **用不了**:由 `validate` **自己**判四类——空地址 / scheme 不支持(`socks5://` 等)/
|
||||
//! 缺少主机名 / 端口不合法(空、`0`、越界)。每类各一句中文原因,**不读** reqwest 的
|
||||
//! `builder error`、也不匹配 `url` 的英文文案(那是「摊原文」,等于没解释);
|
||||
//! 2. **reqwest 层残余的拒绝**(`validate` 放行但它不认,例如 userinfo 里有非法字符):中文结论在
|
||||
//! 前,reqwest 原文只作为括号里的证据。
|
||||
//!
|
||||
//! 两种情况都是 `InvalidRequest` + 带(脱敏后)原串的可读文案,**不 panic**。
|
||||
//!
|
||||
//! 装配点([`super::engine::DownloadEngine::new`])的处置是「回落直连 + 把原因留在引擎上」
|
||||
//! (`DownloadEngine::proxy_problem`):一个设置字符串不该让用户连文件都下不了,但也不许它悄悄变成
|
||||
@@ -58,8 +61,7 @@
|
||||
//! [`ProxySetting::problem`] 的结论画在「HTTP 代理」那一行(两个宿主都调同一个判定,不各写一份)。
|
||||
//!
|
||||
//! 一条实测(别把它当 bug 修掉):reqwest 的 `Proxy::all` 对**没有 scheme** 的串会自作主张补
|
||||
//! `http://` —— `127.0.0.1:7890` 是合法写法。所以用户少写 `http://` 不算「解析不了」;真正会失败的
|
||||
//! 是端口越界(`…:99999`)、方括号不闭合这一类。
|
||||
//! `http://` —— `127.0.0.1:7890` 是合法写法。所以「宽口径」只补到「少写 scheme」为止,判据自己算。
|
||||
//!
|
||||
//! # 不落盘
|
||||
//!
|
||||
@@ -74,7 +76,6 @@
|
||||
use std::fmt;
|
||||
|
||||
use reqwest::ClientBuilder;
|
||||
use reqwest::Url;
|
||||
|
||||
use super::error::{DownloadError, DownloadErrorKind};
|
||||
|
||||
@@ -217,44 +218,83 @@ pub fn redact_proxy_url(raw: &str) -> String {
|
||||
|
||||
/// `manual` 档的解析与判定(`apply` 与 `problem` 的**同一条**路)。
|
||||
///
|
||||
/// 顺序有讲究:先自己按 reqwest 的宽口径(少写 scheme 也认)`Url::parse` 一遍,理由是两件事——
|
||||
/// ① 拿 scheme 做白名单(`Proxy::all` 对 `socks5://` 也返回 `Ok`,见模块头「非法值不得穿透」第 2 条);
|
||||
/// ② 拿**能读**的错因(`url::ParseError` 的 Display 是 `invalid port number` 这种,
|
||||
/// 而 `Proxy::all` 的 Display 只有一句 `builder error`)。
|
||||
/// 顺序:**先自己判**([`validate`]:空 / scheme / host / 端口四类各给一句中文原因),再用
|
||||
/// `reqwest::Proxy::all` 真的造代理。为什么不靠 `Proxy::all` 的报错:它的 Display 只有一句
|
||||
/// `builder error`(原因挂在 `source()` 上,甚至可能是空的),把原文摊给用户等于没解释;
|
||||
/// 为什么不靠 `url::ParseError` 的 Display:那是英文(`invalid port number`),而且 reqwest 的
|
||||
/// 宽口径(给少写 scheme 的串补 `http://`)会让「解析成功/失败」本身就不可靠——端口越界正是
|
||||
/// 撞在这上面(r2 复审要求:四类各给一句,不许摊原文)。
|
||||
fn manual_proxy(url: &str) -> Result<reqwest::Proxy, DownloadError> {
|
||||
let shown = redact_proxy_url(url);
|
||||
// 宽口径只补到「少写 scheme」为止:整串已经有 `://` 就直接按原样解析,**不许**再套一层
|
||||
// `http://`——那会把 `http://127.0.0.1:99999` 变成 `http://http://127.0.0.1:99999`,
|
||||
// 于是真正的错因(`invalid port number`)被「解析成功了」掩盖,最后只剩 `Proxy::all` 的一句
|
||||
// `builder error`。
|
||||
let target = if url.contains("://") {
|
||||
Url::parse(url)
|
||||
} else {
|
||||
Url::parse(&format!("http://{url}"))
|
||||
}
|
||||
.map_err(|err| {
|
||||
validate(url).map_err(|why| {
|
||||
DownloadError::new(
|
||||
DownloadErrorKind::InvalidRequest,
|
||||
format!("代理地址无法解析({shown}):{}", reason(&err)),
|
||||
format!("代理地址用不了({shown}):{why}"),
|
||||
)
|
||||
})?;
|
||||
if !matches!(target.scheme(), "http" | "https") {
|
||||
return Err(DownloadError::new(
|
||||
DownloadErrorKind::InvalidRequest,
|
||||
format!(
|
||||
"本构建不支持 {} 代理(只支持 http / https):{shown}",
|
||||
target.scheme()
|
||||
),
|
||||
));
|
||||
}
|
||||
// 残余失败(validate 放行、但 reqwest 不认,例如 userinfo 里有非法字符):中文结论在前,
|
||||
// reqwest 的原文只作为括号里的**证据**,不当结论。
|
||||
reqwest::Proxy::all(url).map_err(|err| {
|
||||
DownloadError::new(
|
||||
DownloadErrorKind::InvalidRequest,
|
||||
format!("代理地址无法解析({shown}):{}", reason(&err)),
|
||||
format!(
|
||||
"代理地址用不了({shown}):地址写法不被接受({})",
|
||||
reason(&err)
|
||||
),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// 代理地址的**四类**判定:空 / scheme 不支持 / 缺少主机名 / 端口不合法。
|
||||
///
|
||||
/// 每条都**自己算**(不解析 reqwest 的错误、也不匹配英文文案),所以判定与 reqwest/url 的版本、
|
||||
/// 宽口径、错误措辞无关。这里只判「能不能拿去连」,不做归一化——通过之后原串仍会被
|
||||
/// `reqwest::Proxy::all` 拿去用(`127.0.0.1:7890` 这种少写 scheme 的写法是合法的)。
|
||||
fn validate(raw: &str) -> Result<(), String> {
|
||||
let text = raw.trim();
|
||||
if text.is_empty() {
|
||||
return Err("地址是空的(manual 档要填 `http://host:port`)".to_string());
|
||||
}
|
||||
let (scheme, rest) = match text.split_once("://") {
|
||||
Some((scheme, rest)) => (Some(scheme.to_ascii_lowercase()), rest),
|
||||
None => (None, text),
|
||||
};
|
||||
if let Some(scheme) = &scheme {
|
||||
if !matches!(scheme.as_str(), "http" | "https") {
|
||||
return Err(format!("本构建不支持 {scheme} 代理(只支持 http / https)"));
|
||||
}
|
||||
}
|
||||
let authority_end = rest.find(['/', '?', '#']).unwrap_or(rest.len());
|
||||
let authority = &rest[..authority_end];
|
||||
// 最后一个 `@` 之后才是 host[:port](userinfo 里的 `@` `:` 不参与)。
|
||||
let host_port = authority.rsplit_once('@').map_or(authority, |(_, hp)| hp);
|
||||
let (host, port) = match host_port.strip_prefix('[') {
|
||||
// IPv6 字面量:`[::1]:8080`
|
||||
Some(after_open) => match after_open.split_once(']') {
|
||||
Some((host, after)) => (host, after.strip_prefix(':')),
|
||||
None => return Err("IPv6 地址的方括号没有闭合".to_string()),
|
||||
},
|
||||
None => match host_port.rsplit_once(':') {
|
||||
Some((host, port)) => (host, Some(port)),
|
||||
None => (host_port, None),
|
||||
},
|
||||
};
|
||||
if host.is_empty() {
|
||||
return Err("缺少主机名(`host` 是空的)".to_string());
|
||||
}
|
||||
if let Some(port) = port {
|
||||
if port.is_empty() {
|
||||
return Err("端口是空的(`host:` 后面没有数字)".to_string());
|
||||
}
|
||||
match port.parse::<u16>() {
|
||||
Ok(0) => return Err("端口 0 不是有效端口(要 1–65535)".to_string()),
|
||||
Ok(_) => {}
|
||||
Err(_) => return Err(format!("端口 {port} 不是 1–65535 之间的数字")),
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 把错误的 `source` 链拼成**一句能读的原因**(r1 复审 p3-3)。
|
||||
///
|
||||
/// 为什么不能只用 `Display`:`reqwest::Error` 的 Display 是 `builder error`(kind 的名字),真正的
|
||||
|
||||
@@ -149,14 +149,16 @@ fn a_proxy_url_without_a_password_is_left_byte_identical() {
|
||||
);
|
||||
}
|
||||
|
||||
/// **两类「用不了」都走有界错误,且原因能读**(r1 复审 p3-1 / p3-3)。
|
||||
/// **四类「用不了」都走有界错误,而且每类各有一句中文原因**(r1 p3-1/p3-3 + r2 复审必修)。
|
||||
///
|
||||
/// * `socks5://`:`Proxy::all` 会返回 `Ok`,但本构建(reqwest 没开 `socks` feature)根本用不了它;
|
||||
/// 不拦的话用户只看到「所有候选源都失败了」,把原因推给源。现在点名 scheme。
|
||||
/// * 端口越界:原因串必须是 `invalid port number` 这种,不许只有一句 `builder error`。
|
||||
/// 判据落在**最终文案**上:`problem()` 里出现该类的中文原因,且**没有**把 reqwest/url 的原文
|
||||
/// 当结论(`builder error`、`invalid port number` 都不许出现在文案里)。
|
||||
///
|
||||
/// 变异:删掉 scheme 白名单 ⇒ 第一条红(`problem()` 变 `None`);把 `reason()` 换成只印 `Display`
|
||||
/// ⇒ 第二条红(文案里没有 `invalid port number`)。
|
||||
/// 为什么强调「自己算」:reqwest 的 `Proxy::all` 对端口越界只会回一句 `builder error`,而
|
||||
/// `url::ParseError` 的 Display 是英文——两种「摊原文」都不算解释(r2 复审就是这么退回的)。
|
||||
///
|
||||
/// 变异:① 删掉 `validate` 里的端口判定 ⇒ 表格第一行红(落到 reqwest 那句 `builder error`);
|
||||
/// ② 删掉 scheme 白名单 ⇒ 第二行红(`problem()` 变 `None`);③ 删掉主机名为空的判定 ⇒ 第三行红。
|
||||
#[test]
|
||||
fn unsupported_schemes_and_bad_ports_explain_themselves() {
|
||||
let socks = ProxySetting::Manual {
|
||||
@@ -171,24 +173,52 @@ fn unsupported_schemes_and_bad_ports_explain_themselves() {
|
||||
assert!(!problem.contains(SECRET), "{problem}");
|
||||
assert!(socks.apply(reqwest::Client::builder()).is_err());
|
||||
|
||||
let bad_port = ProxySetting::Manual {
|
||||
url: "http://127.0.0.1:99999".to_string(),
|
||||
};
|
||||
let problem = bad_port.problem().expect("端口越界必须报错");
|
||||
// 四类各一句(表驱动):每行都断「命中该类的中文原因」,并断**没有**摊英文原文。
|
||||
let cases: [(&str, &str); 5] = [
|
||||
("http://127.0.0.1:99999", "端口 99999 不是 1–65535"),
|
||||
("http://127.0.0.1:0", "端口 0 不是有效端口"),
|
||||
("http://127.0.0.1:", "端口是空的"),
|
||||
("http://:7890", "缺少主机名"),
|
||||
("http://", "缺少主机名"),
|
||||
];
|
||||
for (url, expected) in cases {
|
||||
let problem = ProxySetting::Manual {
|
||||
url: url.to_string(),
|
||||
}
|
||||
.problem()
|
||||
.unwrap_or_else(|| panic!("{url} 必须被判为用不了"));
|
||||
assert!(problem.contains(expected), "{url} ⇒ {problem}");
|
||||
assert!(
|
||||
problem.contains("invalid port number") || problem.contains("端口"),
|
||||
"原因要能读(`builder error` 等于没说):{problem}"
|
||||
!problem.contains("builder error") && !problem.contains("invalid port number"),
|
||||
"{url}:不许把 reqwest / url 的原文当结论:{problem}"
|
||||
);
|
||||
assert!(!problem.contains("builder error"), "{problem}");
|
||||
}
|
||||
// 空地址:`from_raw` 会把它翻成 `System`(manual + 留空 = 用系统代理),所以这种 `Manual` 只能
|
||||
// 手拼出来;手拼出来时同样有一句原因,不会静默通过。
|
||||
assert!(ProxySetting::Manual {
|
||||
url: " ".to_string()
|
||||
}
|
||||
.problem()
|
||||
.is_some_and(|problem| problem.contains("地址是空的")));
|
||||
|
||||
// 正常档与合法地址没有问题(否则 GUI 会天天挂一条假警告)。
|
||||
assert!(ProxySetting::Direct.problem().is_none());
|
||||
assert!(ProxySetting::System.problem().is_none());
|
||||
assert!(ProxySetting::Manual {
|
||||
url: "http://127.0.0.1:7890".to_string()
|
||||
for ok in [
|
||||
"http://127.0.0.1:7890",
|
||||
"127.0.0.1:7890",
|
||||
"https://proxy.example:8443/v1",
|
||||
"http://[::1]:8080",
|
||||
] {
|
||||
assert!(
|
||||
ProxySetting::Manual {
|
||||
url: ok.to_string()
|
||||
}
|
||||
.problem()
|
||||
.is_none());
|
||||
.is_none(),
|
||||
"{ok} 是合法写法,不该被拒"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// **两个默认值故意不同**(模块头那段的守卫):没读设置的子系统保持 M295 之前的行为,
|
||||
|
||||
@@ -253,7 +253,9 @@ async fn an_unparseable_proxy_url_falls_back_to_direct_and_says_why() {
|
||||
.proxy_problem()
|
||||
.expect("地址解析不了必须留下原因(宿主靠它告诉用户)");
|
||||
assert!(problem.contains("http://127.0.0.1:99999"), "{problem}");
|
||||
assert!(problem.contains("代理地址无法解析"), "{problem}");
|
||||
assert!(problem.contains("代理地址用不了"), "{problem}");
|
||||
assert!(problem.contains("端口 99999"), "原因要能读:{problem}");
|
||||
assert!(!problem.contains("builder error"), "{problem}");
|
||||
assert_eq!(
|
||||
harness.engine.config().proxy,
|
||||
broken,
|
||||
|
||||
+18
-11
@@ -239,15 +239,21 @@ config/launcher.json
|
||||
代理」,所以配一条硬要求:**回落必须有可见提示**,两处都要有(GUI 常驻、CLI 每次装配一行)。
|
||||
要改成 fail-closed 只需让 `build_client` 的错误直接冒泡,但那是产品决定,不在 M295 的范围。
|
||||
|
||||
**p3-1 / p3-3:`Proxy::all` 会骗人,原因串也不好读。** 两处补丁:
|
||||
**p3-1 / p3-3:四类原因自己算,不摊 reqwest / url 的原文。** 两轮补丁的教训:第一版靠
|
||||
`Proxy::all` 的报错(只有一句 `builder error`),第二版靠 `url::ParseError` 的 Display(英文
|
||||
`invalid port number`,而且 reqwest 的宽口径会让「解析成功/失败」本身不可靠)——两种都还是把别人的
|
||||
原文当结论。现在 `validate()` 自己判四类,各类一句中文:
|
||||
|
||||
* `socks5://…`(以及任何非 http/https)在 `Proxy::all` 里返回 **`Ok`**,但本构建 reqwest 没开 `socks`
|
||||
feature ⇒ 以前表现为「没有任何提示,然后所有候选源都失败了」,把原因推给源。现在 `manual_proxy`
|
||||
先用 `Url::parse` 拿 scheme 做白名单,不支持的直接给有界错误「本构建不支持 socks5 代理(只支持
|
||||
http / https):…」,于是它走已有的「回落直连 + 告知」路径;
|
||||
* 原因串不再只有 `builder error`:`reason()` 沿 `source()` 链拼成一句(`builder error:invalid port
|
||||
number` → 现在先自己解析,通常只剩 `invalid port number`);宽口径只补到「少写 scheme」为止,
|
||||
已经带 `://` 的串**不再**套一层 `http://`(那会把真错因掩盖成「解析成功」)。
|
||||
| 情形 | 文案(接在「代理地址用不了(<地址>):」之后) |
|
||||
|---|---|
|
||||
| scheme 不是 http/https(`socks5://` 等) | `本构建不支持 socks5 代理(只支持 http / https)` |
|
||||
| 缺主机名(`http://` / `http://:7890`) | `缺少主机名(host 是空的)` |
|
||||
| 端口越界(`…:99999`)/ `0` / 空 | `端口 99999 不是 1–65535 之间的数字` / `端口 0 不是有效端口(要 1–65535)` / `端口是空的(host: 后面没有数字)` |
|
||||
| 空地址 | `地址是空的(manual 档要填 http://host:port)` |
|
||||
|
||||
`validate()` 放行但 reqwest 仍拒绝的残余情形(例如 userinfo 里有非法字符):中文结论在前,reqwest
|
||||
原文只作为**括号里的证据**(`地址写法不被接受(…)`)。判据是「文案里命中该类中文原因,且不含
|
||||
`builder error` / `invalid port number`」——不是「`problem()` 返回了 Some」。
|
||||
|
||||
## 6. 守卫与变异证明(逐条只改实现侧,跑完即还原)
|
||||
|
||||
@@ -275,10 +281,11 @@ config/launcher.json
|
||||
| M20(M295) | presenter 的 `tuning_of` 把 `proxy_mode` / `proxy_url` 传 `None`(= 没读这两个键) | `host::settings::download::tests::selection_and_proxy::the_proxy_keys_travel_from_the_file_to_the_engine_config` | 红(`left: (Direct, None)` vs `right: (System, None)`;2 passed 1 failed) |
|
||||
| M21(M295) | CLI 的 `download::tuning` 同上 | `cli::download::tests::proxy::the_proxy_settings_reach_the_cli_engine_and_the_console_line` | 红(`left: Direct` vs `right: Manual { url: … }`;1 passed 1 failed) |
|
||||
| M22(M295) | `ProxySetting::apply` 的 `Proxy::all(url)?` 改成 `expect(...)` | `…::engine_mock::proxy::an_unparseable_proxy_url_falls_back_to_direct_and_says_why`(+ core 单测 `proxy::tests::unsupported_schemes_and_bad_ports_explain_themselves`) | 红(两处都 panic:`代理地址: … InvalidPort`) |
|
||||
| M23(M295 r1) | `redact_proxy_url` 开头直接 `return raw.to_string()` | `download::proxy::tests::a_password_in_the_proxy_url_never_reaches_a_user_visible_string`(另两条同族一起红) | 红(`test result: FAILED. 9 passed; 3 failed`)。红原文:`本构建不支持 socks5 代理(只支持 http / https):socks5://user:PWMUSTNOTLEAK@127.0.0.1:1080` |
|
||||
| M23(M295 r1/r2) | `redact_proxy_url` 开头直接 `return raw.to_string()` | `download::proxy::tests::a_password_in_the_proxy_url_never_reaches_a_user_visible_string`(另两条同族一起红) | 红(`FAILED. 2 passed; 3 failed`)。红原文:`生效值行泄漏口令:… 代理=manual(http://user:PWMUSTNOTLEAK@127.0.0.1:7890) …`;socks 那条也一起漏(`代理地址用不了(socks5://user:PWMUSTNOTLEAK@…)`) |
|
||||
| M24(M295 r1) | presenter `group()` 里 `proxy_note` 换成 `None`(= GUI 不再说「已回落到直连」) | `host::settings::download::tests::selection_and_proxy::a_broken_proxy_url_is_shown_as_falling_back_to_direct` | 红(`FAILED. 3 passed; 1 failed`):`用不了的代理地址必须在设置页说出来(独占一行):"只在 manual 模式生效…原值保留"` |
|
||||
| M25(M295 r1) | `manual_proxy` 的 scheme 白名单写成 `if false`(= socks5 又变成「解析成功」) | `download::proxy::tests::unsupported_schemes_and_bad_ports_explain_themselves`(+ engine_mock 的 socks 用例) | 红(`FAILED. 11 passed; 1 failed`):`本构建用不了的 scheme 必须报错` |
|
||||
| M26(M295 r1) | 错误文案的错因换回 `builder error`(= p3-3 报的那个形态) | 同上 | 红(`FAILED. 11 passed; 1 failed`):`原因要能读(「builder error」等于没说):代理地址无法解析(http://127.0.0.1:99999):builder error` |
|
||||
| M25(M295 r1/r2) | `validate` 的 scheme 白名单写成 `if false`(= socks5 又变回「能用」) | `download::proxy::tests::unsupported_schemes_and_bad_ports_explain_themselves`(+ engine_mock 的 socks 用例) | 红(`FAILED. 4 passed; 1 failed`):`本构建用不了的 scheme 必须报错` |
|
||||
| M26(M295 r2) | `validate` 的端口判定关掉(`if let Some(port) = None::<&str>`) | 同上 | 红(`FAILED. 4 passed; 1 failed`):`http://127.0.0.1:99999 ⇒ 代理地址用不了(…):地址写法不被接受(builder error:invalid port number)`——**没有自己的判定时,reqwest 的原文就会漏出来**(就是 r2 复审看到的那个形态) |
|
||||
| M27(M295 r2) | `validate` 的「主机名为空」判定关掉 | 同上 | 红(`FAILED. 4 passed; 1 failed`):`http://:7890 ⇒ …地址写法不被接受(builder error:empty host)` |
|
||||
|
||||
**M20 / M21 的一条附注**:直接把那两个字段**删掉**根本编译不过——两个宿主的 `RawTuning` 是完整字段
|
||||
字面量(没有 `..RawTuning::default()`),编译器会报 `E0063 missing fields proxy_mode and proxy_url`。
|
||||
|
||||
Reference in new issue
Block a user