DigestedSet

There's nothing actually digested.

0%

Rust 中处理异常的一种方式是 Result 作为函数的返回值,和 panic 相对的,Result 通常意味着希望你处理异常并恢复运行。

Result 的定义如下:

1
2
3
4
enum Result<T, E> {
Ok(T),
Err(E),
}

看起来有点像 go 的函数返回值,但是不同的是因为是 enum,所以 Ok 和 Err 是二选一的,而且这俩都是泛型,可以进行类型推断。

Err 比较复杂时,可以被 match 处理,例如 match 枚举值,但是这应该不是一个好的写法,看着晕晕的。而且这意味着你得知道 Result 里的 E 泛型到底实现了哪个 Error,看起来这里是返回了一个 std::io::Error, 谁知道其他地方会返回啥呢。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
use std::fs::File;
use std::io::ErrorKind;

fn main() {
let greeting_file_result = File::open("hello.txt");

let greeting_file = match greeting_file_result {
Ok(file) => file,
Err(error) => match error.kind() {
ErrorKind::NotFound => match File::create("hello.txt") {
Ok(fc) => fc,
Err(e) => panic!("Problem creating the file: {:?}", e),
},
other_error => {
panic!("Problem opening the file: {:?}", other_error);
}
},
};
}

和 Option 一样,std 也给 Result 实现了一大堆方法,比较典型的是 unwrap 和 expect,这样当遇到 Err 的时候就直接 panic 了。

例如上面这段代码还可以这么写:这里似乎用了一个 lambda 语法,就是不知道这个写法要到哪里才会介绍。另外,注意 File::create 实际上是有返回值的,最后没有用分号,把这个作为新的返回值给到 greeting_file 了,不知道 rust 为什么要用这么隐晦的方式来写返回值,可能这就是对 IDE 的信任把。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
use std::fs::File;
use std::io::ErrorKind;

fn main() {
let greeting_file = File::open("hello.txt").unwrap_or_else(|error| {
if error.kind() == ErrorKind::NotFound {
File::create("hello.txt").unwrap_or_else(|error| {
panic!("Problem creating the file: {:?}", error);
})
} else {
panic!("Problem opening the file: {:?}", error);
}
});
}

另一个重要的语法糖就是 Error 的传播,适用于一连串操作,多次返回 Error 的情况,这在进行 IO 或是一步操作时非常常见:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
use std::fs::File;
use std::io::{self, Read};

fn read_username_from_file() -> Result<String, io::Error> {
let username_file_result = File::open("hello.txt");

let mut username_file = match username_file_result {
Ok(file) => file,
Err(e) => return Err(e),
};

let mut username = String::new();

match username_file.read_to_string(&mut username) {
Ok(_) => Ok(username),
Err(e) => Err(e),
}
}

这种常见的操作可以用 ? operator 进行简化:

1
2
3
4
5
6
7
8
9
use std::fs::File;
use std::io::{self, Read};

fn read_username_from_file() -> Result<String, io::Error> {
let mut username_file = File::open("hello.txt")?;
let mut username = String::new();
username_file.read_to_string(&mut username)?;
Ok(username)
}

对上面这个操作进行进一步简化以后就是我们常见的 optional chaining:

1
2
3
4
5
6
7
8
9
10
11
use std::fs::File;
use std::io::{self, Read};

fn read_username_from_file() -> Result<String, io::Error> {
let mut username = String::new();

File::open("hello.txt")?.read_to_string(&mut username)?;

Ok(username)
}

? 操作符并没有那么简单,它还可以被用于返回 Option 类型的函数:

1
2
3
4
fn last_char_of_first_line(text: &str) -> Option<char> {
text.lines().next()?.chars().last()
}

上面这个 next()返回了 Option 类型,但仍然可以使用?操作符来获取 Option 中的值, 这里如果?操作符终端,None 会被提前返回,而不是继续执行后续的语句,因此?只能被用在最后一个 expression 或是 return 语句里面。

?到底是啥意思取决于当前函数的返回值,而返回值必须被手动指定而不能被推导出来,所以?的行为是明确的。
不过拷贝代码的时候需要考虑到同一段代码实际上再不同的函数里 有不同的含义。

Rust 的包管理器叫作 cargo,cargo 提供各种 cli 命令。例如cargo new my-project,一个 cargo 项目有自己唯一的 cargo.toml 文件,但是可能包含多个 crate。
从 rust 自身考虑编译的角度,可编译的最小单元叫作 crate,它可能是 binary crate 或者 library crate,其中 binary crate 意味着它可以被直接运行, 特征是有 main 函数入口。

为了把代码暴露给 crate 外部使用,除了 enum 以外的所有结构和命名空间,都必须标注 pub 才会被导出。
例如想要导出一个子模块,除了要加上pub mod submod;以外,还需要写清楚导出的方法才行: pub use submod::subfunc;

module 有两种形式,一种是写一个文件,另一种是在文件内部声明mod xxx {} 这样大括号内部会被认为是一个模块。

match 是针对 enum 类型进行判断的语法,关键字后面的表达式表示的是 , 下面的分支被称为 arm , arm 由 pattern 和 code 组成,pattern 中可以声明变量来接收 enum 类型内部的值,code 如果是 expression 则有可能被作为函数的返回值. 同时other或是_被用作表示匹配所有值的占位符

1
2
3
4
5
6
7
8
9
10
let dice_roll = 9;
match dice_roll {
3 => add_fancy_hat(),
7 => remove_fancy_hat(),
_ => (),
}

fn add_fancy_hat() {}
fn remove_fancy_hat() {}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#[derive(Debug)] // so we can inspect the state in a minute
enum UsState {
Alabama,
Alaska,
// --snip--
}

enum Coin {
Penny,
Nickel,
Dime,
Quarter(UsState),
}

fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter(state) => {
println!("State quarter from {:?}!", state);
25
}
}
}

另一种表示 enum 类型处理的方式是if let 这种方式更自由,并且不需要遍历枚举值的所有可能性,这会在很多时候方便一些。
例如针对上面的哪个 Coin 类型,可以用 if let 处理如下:

1
2
3
4
5
6
7
let mut count = 0;
if let Coin::Quarter(state) = coin {
println!("State quarter from {:?}!", state);
} else {
count += 1;
}

在 rust 中,enum 并不是单纯的值的别名,而是一种类型系统,enum 中定义的每一个项都可以去指定类型,而指定了类型以后这些项可以被当作类型来使用,典型的 case 如 IpAddr, 值得注意的是,无论这些项有没有添加类型,这些项本身都会有一个代表自己的枚举值的值存在,而不只是类型:

1
2
3
4
5
6
7
8
9
enum IpAddr {
V4(u8, u8, u8, u8),
V6(String),
}

let home = IpAddr::V4(127, 0, 0, 1);

let loopback = IpAddr::V6(String::from("::1"));

另外一个典型的 case 是 Message,enum Message 可以把不同的类型聚合成同一个类型,以为这一个类型去 impl 实现方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}

impl Message {
fn call(&self) {
// method body would be defined here
}
}

let m = Message::Write(String::from("hello"));
m.call();

更进一步地,Rust 利用这个语言特性去实现了 Option 类型,以解除对 null 的依赖,因此 Rust 里面是没有 null 的。

这有点像 TypeScript 中的 type 和 runtime 的关系,或者说 C++中的 metadata 和 runtime 的关系,类型系统减轻了运行时的负担,增加了程序的效率。

Option 定义如下,包含了两个类型 None 和 Some,这俩都是在全局空间可以直接用的,不需要 Option::None。

1
2
3
4
enum Option<T> {
None,
Some(T),
}

最终的效果就是试图使用 None 的值的时候,编译时就报错,这样运行时就显得安全了。

1
2
3
4
let x: i8 = 5;
let y: Option<i8> = Some(5);

let sum = x + y;

值得注意的是,Option 提供了一批看起来像是面试题的方法,让你可以以简单的方式改变 Option 的类型,参见这里

Rust 支持结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}

fn build_user(email: String, username: String) -> User {
User {
active: true,
username,
email,
sign_in_count: 1,
}
}

比较神奇的是支持 spread operator:

1
2
3
4
5
6
7
8
fn main() {
// --snip--

let user2 = User {
email: String::from("another@example.com"),
..user1
};
}

以及 tuple structure:

1
2
3
4
5
6
7
struct Color(i32, i32, i32);
struct Point(i32, i32, i32);

fn main() {
let black = Color(0, 0, 0);
let origin = Point(0, 0, 0);
}

如果需要打印 structure 结构,rust 提供了方便的方式,不过打印的时候要使用 {:?} 做占位符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}

fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};

println!("rect1 is {:?}", rect1);
}

实现方法的办法有点像 go,语法有点像 python,this 指针用的是&self

1
2
3
4
5
6
7
8
9
10
11
12
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
}

impl Rectangle {
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
}

另外还有类似于 C++的静态方法的 associated functions

1
2
3
4
5
6
7
8
9
10

impl Rectangle {
fn square(size: u32) -> Self {
Self {
width: size,
height: size,
}
}
}

slices 语法允许使用切片获取 array 的一部分,例如

1
2
3
4
let s = String::from("hello world");

let hello = &s[0..5];
let world = &s[6..11];

这里 hello 和 world 相当于是 borrow 了 s,只是 borrow 了一部分,盲猜 rust 应该没有能力实现把一个 array 搞出两个 mut borrow. 实际也几乎没有必要。

1
2
3
4
5
6
7
8
9
10
11
fn first_word(s: &String) -> &str {
let bytes = s.as_bytes();

for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}

&s[..]
}

除了 String 可以切片, 数组 array 也可以切片.

1
2
3
4
5
let a = [1, 2, 3, 4, 5];

let slice = &a[1..3];

assert_eq!(slice, &[2, 3]);

很自然的语法,然后我猜这个切片语法应该和 C++的操作符一样能被重载

根据 rust book,ownership 是 rust 用来管理内存空间的生命周期的机制。

类似 C++的 RAII,rust 会在一个结构体的生命周期结束时释放内存,并调用 drop 函数

同时,String 类型的赋值会导致 ownership 转移,类似于 C++的 move, 在 rust 中这是默认的。

例如下面这段会抛异常:

1
2
3
4
let s1 = String::from("hello");
let s2 = s1;

println!("{}, world!", s1);

但是下面这段不会:

1
2
3
4
let x = 5;
let y = x;

println!("x = {}, y = {}", x, y);

这是由于部分类型实现了 Copy trait,而 String 实现了 Drop trait, 这俩是互斥的。

正因为这样的特性,当传参时,如果 String 被直接传入,那这个 String 相当于已经 move 了,不能再被试用.

因此如果只使用默认的传参方式,calculate_length函数会被写成这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
fn main() {
let s1 = String::from("hello");

let (s2, len) = calculate_length(s1);

println!("The length of '{}' is {}.", s2, len);
}

fn calculate_length(s: String) -> (String, usize) {
let length = s.len(); // len() returns the length of a String

(s, length)
}

这显然不太方便,因此后面介绍了 reference,也就是写成下面这样,这里标注引用的语法和 C++是反的,同时这里看起来是类似 C++的引用,但是因为没有更复杂的语法,所以实际上可以认为是传指针了。

1
2
3
4
5
6
7
8
9
10
11
fn main() {
let s1 = String::from("hello");

let len = calculate_length(&s1);

println!("The length of '{}' is {}.", s1, len);
}

fn calculate_length(s: &String) -> usize {
s.len()
}

上面这个行为在 rust 中被称作 borrowing。
为了安全,reference 默认也是不可变的,调用 String 的方法例如下面这个是会报错的:

1
2
3
4
5
6
7
8
9
fn main() {
let s = String::from("hello");

change(&s);
}

fn change(some_string: &String) {
some_string.push_str(", world");
}
1
2
3
4
5
6
7
8
9
10
11
12
$ cargo run
Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0596]: cannot borrow `*some_string` as mutable, as it is behind a `&` reference
--> src/main.rs:8:5
|
7 | fn change(some_string: &String) {
| ------- help: consider changing this to be a mutable reference: `&mut String`
8 | some_string.push_str(", world");
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `some_string` is a `&` reference, so the data it refers to cannot be borrowed as mutable

For more information about this error, try `rustc --explain E0596`.
error: could not compile `ownership` due to previous error

所以又要传引用又要改的话,这里会有更复杂的语法

1
2
3
4
5
6
7
8
9
fn main() {
let mut s = String::from("hello");

change(&mut s);
}

fn change(some_string: &mut String) {
some_string.push_str(", world");
}

这种方式实际上有更复杂的约束条件:同一个变量不能被 mutable borrow 两次, 这样的话 Rust 就从语法层面限制了 data race.

不仅 mutable borrow 不能被重复 borrow, 如果之前被 immutalbe borrow, 那么这个变量也无法被 mutable borrow 了。

错误示范如下:

1
2
3
4
5
6
7
let mut s = String::from("hello");

let r1 = &s; // no problem
let r2 = &s; // no problem
let r3 = &mut s; // BIG PROBLEM

println!("{}, {}, and {}", r1, r2, r3);

不仅如此,reference 还有个特性,就是不允许跑出变量自身的作用域,例如下面这么用就不行:

1
2
3
4
5
6
7
8
9
fn main() {
let reference_to_nothing = dangle();
}

fn dangle() -> &String {
let s = String::from("hello");

&s
}

读后感

这应该就是 Rust 最核心的机制了,不考虑 unsafe 的情况,这种 ownership 的设计保证了内存安全。每块内存只能被一个值拥有,可以通过引用去 borrow,而内存一旦被借用,就不能再去改它,只有借用结束了才能再改它。

rust 相比 c++来说脚手架强了太多,因此只需要记住几条命令就可以开始入门。

1
2
3
4
5
6
7
8
9
# 编译单文件
rustc main.rs

# 初始化项目
cargo init

# 把项目跑起来
cargo run

对有各种语言基础的 rust 初学者来说基础的编程语法应该不是问题,因此 Rust book 给了一个非常简短的章节来介绍常见的语法概念,Common Programming Concepts

简短摘要如下 5 个部分:

  • 3.1. Variables and Mutability
  • 3.2. Data Types
  • 3.3. Functions
  • 3.4. Comments
  • 3.5. Control Flow

每个部分读后感如下:

3.1. Variables and Mutability

rust 里变量有强类型,但可以被推导. 和绝大多数语言一样, 大括号是变量的作用域. 和其他语言不同的是,rust 默认变量不可变,需要加关键字 mut。

1
2
3
4
5
6
7
8
9
10
11
12
fn main() {
let x = 5;

let x = x + 1;

{
let x = x * 2;
println!("The value of x in the inner scope is: {x}");
}

println!("The value of x is: {x}");
}

另外变量也可以被覆盖,覆盖了以后变量会是一个不同的地址,因此尽管 rust 中常有覆盖的操作,但真正使用时可能需要注意一下实际用了什么时候分配的内存:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

fn main() {
let x = 4;
println!("the first x location is {:p}", &x);
let x = 5;
println!("the second x location is {:p}", &x);
for i in 0..10 {
if x != 5 {
// 永远不会输出, 因为 x 的值不会被改变
println!("the x value is {} and not equal to 5", x);
}
let x = i;
}
}

3.2 Data Types

除了 C 基础类型以外,Rust 还额外支持 tuple 和 array.
比较神奇的是 tuple 用需要用x.0的方式去取值,而 array 和其他语言一样是用x[0]的方式去取值.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
fn main() {
let x: (i32, f64, u8) = (500, 6.4, 1);

let five_hundred = x.0;

let six_point_four = x.1;

let one = x.2;

let a: [i32; 5] = [1, 2, 3, 4, 5];

let two = a[1];
}

3.3. Functions

返回值的类型在签名最后面,最后一个 expression 表示返回值。
注意这里有一个特殊的点是 expression 是没有分号;的,加分号会把 expression 变成 statement,而函数的返回值必须是 expression

1
2
3
4
5
6
7
8
9
10
fn main() {
let x = plus_one(5);

println!("The value of x is: {x}");
}

fn plus_one(x: i32) -> i32 {
x + 1
}

3.4. Comments

1
// 真正的勇士从不写注释

3.5. Control Flow

和大部分语言一样,循环有很多种写法,rust 比较有特色的是 loop 作为关键字的循环,但是我不打算现在学,因为 for 和 while 已经够用了。for 循环只有 for…in 的用法,没有手写下标的用法, while 应该是唯一可以自定义退出条件的循环.

break 跳出循环

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
fn main() {
let a = [10, 20, 30, 40, 50];

for element in a {
println!("the value is: {element}");
}
}

fn main() {
for number in (1..4).rev() {
println!("{number}!");
}
println!("LIFTOFF!!!");
}

while 用法如下:

1
2
3
4
5
6
7
8
9
10
fn main() {
let a = [10, 20, 30, 40, 50];
let mut index = 0;

while index < 5 {
println!("the value is: {}", a[index]);

index += 1;
}
}

光猫配置

参考这篇移动光猫之桥接教程
用超级管理员账号登录
用户名 CMCCAdmin
超级密码 aDm8H%MdA

我的配置

解决 openwrt 与 ip 地址冲突

光猫侧

打开网络-宽带设置-Internet 连接,可以看到已经有 3 个连接配置,分别是

1
2
3
TR069_R_VID_4034
INTERNET_R_VID_4031
IPTV_R_VID_4033

对应 3 个不同的 VLAN,连接网络需要用到的只有第二个 vlan 为 4031 的标着 internet 的连接。
注意这个 VLAN 可能有不同,记住这个 vlan 号,后面新建的连接还需要用到。
关闭原先的 pppoe 拨号连接 INTERNET_R_VID_4031,只需要在连接里把勾去掉并保存即可!不需要删除,如果后续出现问题,回到这一步把 INTERNET_R_VID_4031 重新打开就行了。
接下来新建一个完全一样的 PPPoe 连接,这是为了确认 pppoe 拨号的账号密码,以免后续登录不上。
杭州地区的移动宽带,

宽带默认密码是 123456 或者宽带账号的后 6 位,以前老账号也有身份证后 6 位,后 8 位,联系电话前 8 位。
如果都不是,拨打 10086 或者问宽带装机小哥~
确认完成后,将刚才的那个新的可用的 PPPoe 连接改为桥接,其他选项不动:
cmcc-settings

路由器侧

先修改 LAN 口 ip 地址由于 LAN 口 ip 地址
这里将原先的 WAN 口 PPPoe 处输入宽带账号密码
openWrt side settings

Netgear R8000 刷 Openwrt

本来是配置好了 openwrt 的,结果这次一不小心改错了配置,刷成砖了,又找不到靠谱的救砖教程,只好淘宝找了一个专业救砖师傅帮我远程搞,然后发现其实有个很好的工具可以救砖:https://github.com/jclehner/nmrpflash,具体流程见另一篇。
恢复原厂 ROM 之后按照官方指导上的刷:gate io twitter
把 chk 文件上传,并等一会儿:https://downloads.openwrt.org/releases/22.03.3/targets/bcm53xx/generic/openwrt-22.03.3-bcm53xx-generic-netgear_r8000-squashfs.chk
我是等了一会儿以后毫无反应,并直接重启了路由器,然后就直接进入了 OpenWrt 系统,看起来中间可能报错了或者写入停止了,但不影响使用。

Wifi 配置问题

初始配置看起来只有 1 个可连接 wifi,2.4G 的直接需要手动开启另外两个。
第一步先给 wifi 配置密码

将 overlay 分区改到 U 盘

直接参考 OpenWrt 官方的文档ExtRoot Configureation执行命令,可以完美扩展/overlay 分区到 64G 的 U 盘上面,彻底解决空间不够用的问题。

关于 ipv6 配置

NAS 等服务需要获取独立 ipv6 地址,并开放端口到公网。其中 ipv6 的网段是动态分配给 NAS 等主机的,前缀可能会发生变化,因此需要以 ipv6 网段的形式来配置 Openwrt firewall 的规则:具体在 ip destination 处填写::a1b2:c3d4:0/::ffff:ffff:0即可,这里找了好久也没找到中文资料,简单描述一下规则:

/前面的是 ipv6 地址,/后的是掩码。ipv6 地址一共 128 位,分为 8 组 16 位的 16 进制表示,:分隔的字符。其中,双:是一种简写,表示该位置所有位都是 0,一般只出现一次。掩码也是一样的表示方式。
掩码也可以用数字简写,例如/32相当于/ffff:ffff::,在 openwrt 中,掩码还可以用负数例如/-64这样的格式表示,相当于/::ffff:ffff:ffff:ffff
之所以需要把掩码放在中间,是因为这 32 位在 ipv6 地址的分配中,无论前缀怎么变,这 32 位都不会变。具体原因可能是由于 SLAAC 协议中,ipv6 地址的后 64 位通过 EUI-64 的方法生成,相当于 MAC 地址扩展后会对应到唯一的 ipv6 子网。

配置下面这个规则即可暴露 NAS 的 ipv6 地址和高位端口到外网
expose-nas-ipv6

配置科学上网

目前最好用的插件是 OpenClash,上传 ClashX 的配置文件到 OpenClash 即可直接使用。
其中需要注意第一次启动之前,因为是在大内网环境,需要在 Overwrite Settings-General Settings-Github Address Modify 选项中,把 Github 地址修改为
https://testingcf.jsdelivr.net/
,否则 github 地址污染,会导致 openclash 检查版本或者下载 clash 内核等步骤报错:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2022-11-07 10:12:13【/tmp/clash_last_version】下载失败:【how to fix it, please visit the web page mentioned above.】
2022-11-07 10:12:13【/tmp/clash_last_version】下载失败:【establish a secure connection to it. To learn more about this situation and】
2022-11-07 10:12:13【/tmp/clash_last_version】下载失败:【curl failed to verify the legitimacy of the server and therefore could not】
2022-11-07 10:12:13【/tmp/clash_last_version】下载失败:【】
2022-11-07 10:12:13【/tmp/clash_last_version】下载失败:【More details here: https://curl.se/docs/sslcerts.html】
2022-11-07 10:12:13【/tmp/clash_last_version】下载失败:【curl: (60) Cert verify failed: BADCERT_CN_MISMATCH】
2022-11-07 10:10:57【/tmp/openclash_last_version】下载失败:【how to fix it, please visit the web page mentioned above.】
2022-11-07 10:10:57【/tmp/openclash_last_version】下载失败:【establish a secure connection to it. To learn more about this situation and】
2022-11-07 10:10:57【/tmp/openclash_last_version】下载失败:【curl failed to verify the legitimacy of the server and therefore could not】
2022-11-07 10:10:57【/tmp/openclash_last_version】下载失败:【】
2022-11-07 10:10:57【/tmp/openclash_last_version】下载失败:【More details here: https://curl.se/docs/sslcerts.html】
2022-11-07 10:10:57【/tmp/openclash_last_version】下载失败:【curl: (60) Cert verify failed: BADCERT_CN_MISMATCH】
2022-11-07 09:26:43【/tmp/clash_last_version】下载失败:【how to fix it, please visit the web page mentioned above.】
2022-11-07 09:26:43【/tmp/clash_last_version】下载失败:【establish a secure connection to it. To learn more about this situation and】
2022-11-07 09:26:43【/tmp/clash_last_version】下载失败:【curl failed to verify the legitimacy of the server and therefore could not】
2022-11-07 09:26:43【/tmp/clash_last_version】下载失败:【】
2022-11-07 09:26:43【/tmp/clash_last_version】下载失败:【More details here: https://curl.se/docs/sslcerts.html】
2022-11-07 09:26:43【/tmp/clash_last_version】下载失败:【curl: (60) Cert verify failed: BADCERT_CN_MISMATCH】

参见GitHub Issue