1use std::path::{Path, PathBuf};
2
3use ant_protocol::pqc::api::{ml_dsa_65, MlDsaPublicKey, MlDsaSignature, MlDsaVariant};
4use futures_util::StreamExt;
5use serde::{Deserialize, Serialize};
6
7use crate::channel::version_matches_channel;
8use crate::error::{Error, Result};
9use crate::install::{self, InstallMethod};
10use crate::node::binary::{extract_tar_gz, extract_zip, ProgressReporter};
11use crate::node::types::UpgradeChannel;
12
13const GITHUB_REPO: &str = "WithAutonomi/ant-client";
14const CLI_BINARY_NAME: &str = "ant";
15const TAG_PREFIX: &str = "ant-cli-v";
16
17const SIGNING_CONTEXT: &[u8] = b"ant-release-v1";
19
20const SIGNATURE_SIZE: usize = 3309;
22
23const RELEASE_SIGNING_KEY: &[u8] = &[
29 0xb4, 0xcf, 0x2a, 0x24, 0x31, 0xd9, 0xb2, 0x3a, 0xab, 0xe9, 0x5e, 0xfc, 0xbc, 0xf3, 0xb1, 0x1f,
30 0x4e, 0x50, 0x0a, 0x46, 0xeb, 0x83, 0xfc, 0x6f, 0x0f, 0x89, 0x41, 0x00, 0x1b, 0x52, 0xde, 0xdc,
31 0xb5, 0xc9, 0x07, 0xed, 0x72, 0x3a, 0xe1, 0xa9, 0x82, 0xe1, 0xfc, 0xff, 0xab, 0xce, 0x9f, 0x7d,
32 0xab, 0xe2, 0x57, 0x92, 0xe0, 0xf2, 0xac, 0xa1, 0x41, 0xd3, 0x74, 0x95, 0x41, 0xd1, 0xac, 0x22,
33 0xb1, 0xbb, 0x5c, 0xf4, 0x02, 0x0b, 0x73, 0x85, 0xfd, 0x56, 0x75, 0x0d, 0x5c, 0x38, 0xe1, 0x2d,
34 0xe0, 0x15, 0x4f, 0xbf, 0x40, 0xeb, 0xf1, 0x0d, 0x8f, 0x39, 0x32, 0xeb, 0x80, 0xa7, 0x2e, 0x77,
35 0x3e, 0x54, 0xe3, 0x3d, 0x62, 0xae, 0xe7, 0x09, 0x5e, 0xfb, 0xdc, 0xaa, 0x07, 0xdc, 0xe1, 0x08,
36 0x96, 0xbb, 0x4b, 0xa0, 0x2e, 0x05, 0x2d, 0xee, 0xeb, 0x9a, 0x1a, 0xae, 0xde, 0xe9, 0x2c, 0xf8,
37 0x2f, 0x43, 0x6c, 0x78, 0x4b, 0xde, 0xef, 0x91, 0x8b, 0x94, 0x9d, 0x4f, 0x81, 0x05, 0xcc, 0xf0,
38 0x98, 0xce, 0xce, 0x67, 0x54, 0xac, 0xde, 0xcd, 0x26, 0x9e, 0x84, 0xf4, 0x88, 0xb2, 0x1a, 0x3e,
39 0x93, 0x2e, 0xff, 0xa8, 0x45, 0x95, 0xd1, 0xd0, 0xb1, 0x6c, 0x3c, 0x1e, 0xef, 0x3d, 0xe3, 0xf2,
40 0x73, 0xe2, 0xf6, 0xb7, 0xf9, 0x3f, 0x10, 0x0d, 0x3c, 0xde, 0x28, 0x94, 0x07, 0xef, 0x24, 0x70,
41 0xc4, 0x5a, 0x0a, 0x67, 0xbb, 0x0f, 0x4f, 0x5c, 0x2b, 0xd8, 0x02, 0x05, 0xa5, 0x98, 0x03, 0x5d,
42 0x8f, 0xc0, 0x4a, 0x84, 0xe9, 0xea, 0xac, 0x13, 0xdf, 0x69, 0xfc, 0x1e, 0xcf, 0xb6, 0x88, 0xba,
43 0x99, 0x30, 0xbc, 0x7a, 0xb8, 0x9d, 0x3d, 0x62, 0x3b, 0x33, 0x19, 0xbb, 0x3a, 0x2c, 0x2b, 0xa0,
44 0x5a, 0xb0, 0x8f, 0x9e, 0x10, 0x81, 0xb9, 0x12, 0x54, 0x81, 0xf8, 0xe2, 0x91, 0xb2, 0xe7, 0xe6,
45 0x9c, 0x11, 0xeb, 0x49, 0x64, 0x3a, 0x25, 0xd6, 0x53, 0x2e, 0xdf, 0xfc, 0x14, 0x32, 0x65, 0xcc,
46 0x87, 0xdb, 0xfd, 0xbb, 0x81, 0xa8, 0x50, 0xcc, 0xb4, 0x31, 0x0c, 0x70, 0xf3, 0xb6, 0x15, 0x8a,
47 0x50, 0x80, 0xad, 0xb1, 0xb0, 0x10, 0x2b, 0x67, 0x33, 0xf5, 0xf6, 0x36, 0x35, 0x5f, 0xa7, 0xd2,
48 0x81, 0xd6, 0x75, 0xa1, 0x18, 0x15, 0xbe, 0x1d, 0x5e, 0x33, 0x8e, 0x98, 0xdd, 0x45, 0x0f, 0x0c,
49 0x0f, 0x0d, 0x8b, 0x3f, 0x97, 0x11, 0x21, 0x2e, 0xa0, 0x5e, 0xfe, 0x70, 0x09, 0xa7, 0x14, 0x30,
50 0xa3, 0x01, 0x2d, 0x18, 0x2b, 0x8f, 0x19, 0x75, 0x54, 0x1f, 0xd8, 0xee, 0x66, 0x06, 0x7b, 0x9d,
51 0x7d, 0xb2, 0xae, 0x14, 0xe6, 0x51, 0x19, 0xc2, 0x45, 0x2e, 0x7e, 0x11, 0xd9, 0x7b, 0x16, 0x8e,
52 0xae, 0x17, 0xdb, 0x1b, 0x24, 0x90, 0xcd, 0xed, 0x94, 0xf9, 0xf7, 0xba, 0x9f, 0x4c, 0x12, 0xae,
53 0x31, 0x7b, 0xd4, 0x7c, 0x04, 0x42, 0x2c, 0x32, 0x16, 0xc1, 0x70, 0x6d, 0x11, 0x6f, 0x3b, 0x44,
54 0x62, 0xba, 0xbd, 0xc5, 0x7a, 0xec, 0x55, 0x1b, 0xcd, 0xdb, 0xb6, 0x55, 0x08, 0x86, 0x13, 0x7f,
55 0x4e, 0xa9, 0x63, 0xe1, 0x87, 0xa0, 0x7e, 0x49, 0xfb, 0xf4, 0xa3, 0x46, 0xcf, 0x1d, 0xec, 0xf5,
56 0xc6, 0x2f, 0xe1, 0x43, 0x02, 0xd0, 0xe4, 0x5f, 0x1b, 0x20, 0x1a, 0xa7, 0x81, 0xbd, 0x31, 0x19,
57 0x6a, 0x74, 0xd7, 0x9a, 0x6d, 0x3d, 0xf8, 0xac, 0x4d, 0xbb, 0x01, 0x63, 0xa4, 0x9d, 0x3c, 0xc9,
58 0x6c, 0x8a, 0x4f, 0x61, 0xd6, 0x98, 0xf5, 0x40, 0x22, 0xa9, 0x5e, 0x93, 0x5e, 0x13, 0xd3, 0xe0,
59 0xdb, 0x54, 0xab, 0x0d, 0xe3, 0x88, 0x85, 0x80, 0x7a, 0x5e, 0x38, 0x64, 0x97, 0xc4, 0xe9, 0xb0,
60 0x5d, 0xf7, 0x40, 0x5f, 0x6e, 0x3f, 0xbe, 0x14, 0x9a, 0x7c, 0xa2, 0x7c, 0x74, 0xa5, 0x32, 0x22,
61 0x61, 0x31, 0xa5, 0x0d, 0xa5, 0xcc, 0x93, 0xe4, 0xfd, 0xed, 0xbc, 0xe7, 0xf2, 0xe5, 0xdb, 0xc6,
62 0x0c, 0xe1, 0xc8, 0x4e, 0xee, 0xe6, 0x76, 0x1c, 0x10, 0x1b, 0xd8, 0x53, 0xd4, 0xe8, 0x07, 0xed,
63 0xea, 0x91, 0xd4, 0x1b, 0x91, 0x5c, 0x28, 0x05, 0xca, 0xe2, 0x9c, 0xd1, 0x99, 0x43, 0xed, 0xd8,
64 0x6a, 0x2b, 0xd2, 0x64, 0x9b, 0xe1, 0x0c, 0x88, 0x6c, 0x0d, 0xb2, 0x6b, 0x73, 0x85, 0x9d, 0xbf,
65 0x79, 0x78, 0xaa, 0x7b, 0x5e, 0xf8, 0xa4, 0x26, 0xdf, 0xb3, 0x9b, 0x24, 0x5a, 0xc8, 0x19, 0x22,
66 0xa5, 0xc6, 0xca, 0x00, 0x59, 0x3b, 0xad, 0x45, 0xdf, 0x71, 0x1d, 0x60, 0x60, 0x24, 0x0d, 0xa4,
67 0x3d, 0x42, 0x23, 0xb8, 0xfe, 0xac, 0x86, 0x94, 0x79, 0x87, 0x05, 0xae, 0xb8, 0x4d, 0x7e, 0x11,
68 0x5b, 0x22, 0x44, 0x15, 0x3d, 0x7f, 0x82, 0x98, 0x65, 0x0a, 0x3c, 0xd3, 0xef, 0x80, 0x0d, 0x75,
69 0x03, 0x92, 0xf6, 0x3a, 0x8b, 0xa4, 0xb0, 0x61, 0x2d, 0x2c, 0xcc, 0x1f, 0x01, 0x8e, 0x7a, 0x46,
70 0x36, 0x2a, 0x83, 0x21, 0x88, 0x98, 0x13, 0x0a, 0xd5, 0xa1, 0x54, 0x4b, 0x63, 0xe0, 0xe3, 0x1c,
71 0x07, 0x5e, 0x32, 0x8c, 0xa4, 0x6b, 0x62, 0xc3, 0x28, 0x95, 0xb8, 0x0a, 0xb9, 0x4f, 0xaf, 0x7f,
72 0x49, 0xeb, 0xff, 0xd7, 0xa1, 0x41, 0x43, 0x9a, 0x92, 0x9a, 0x5f, 0xee, 0xbd, 0xb9, 0xbe, 0xb3,
73 0x4b, 0x9d, 0x0b, 0xcb, 0x9b, 0x2c, 0x26, 0x8f, 0x0f, 0xb1, 0xfa, 0xc0, 0xe3, 0x3a, 0x7f, 0x2b,
74 0x51, 0x79, 0x75, 0x25, 0x5b, 0x23, 0x22, 0xb1, 0x01, 0x27, 0x4e, 0x43, 0xdd, 0x66, 0x7a, 0x33,
75 0x4d, 0x32, 0x96, 0x83, 0x59, 0x52, 0xd7, 0x3c, 0xb0, 0xe3, 0x03, 0xd6, 0xb0, 0xc7, 0x99, 0x68,
76 0xc6, 0xa4, 0x2d, 0x35, 0x3d, 0xa4, 0x6a, 0x17, 0xd9, 0xf4, 0x0c, 0x26, 0x11, 0xe4, 0xbc, 0x03,
77 0x87, 0x25, 0x62, 0xad, 0xa9, 0x7e, 0x96, 0x4d, 0x39, 0x9b, 0x8f, 0x09, 0xdc, 0xd1, 0x28, 0x5e,
78 0xf4, 0xe3, 0x94, 0xfd, 0x94, 0x46, 0x30, 0xe2, 0x24, 0x46, 0x30, 0x7f, 0xf4, 0x4c, 0xaa, 0x51,
79 0x7e, 0x04, 0x5c, 0xa4, 0x8c, 0xba, 0x4a, 0xb8, 0x61, 0x5e, 0x75, 0x1c, 0xa8, 0x0c, 0xbc, 0x7f,
80 0x36, 0x16, 0xa1, 0x72, 0x98, 0x6a, 0x44, 0x39, 0x42, 0x67, 0xb5, 0x4a, 0xac, 0x14, 0x35, 0x8f,
81 0xcd, 0x87, 0x3f, 0x9e, 0x2e, 0xa1, 0x53, 0xf1, 0x45, 0x68, 0x26, 0xcb, 0x35, 0x96, 0x57, 0xd5,
82 0x3a, 0x24, 0x74, 0xe2, 0xff, 0xe0, 0x70, 0xb1, 0xbd, 0xec, 0x0c, 0xd2, 0x97, 0x9a, 0xe5, 0x9f,
83 0xa9, 0xfe, 0x6a, 0x63, 0x17, 0x35, 0xad, 0x64, 0x2f, 0xd9, 0x2e, 0xdb, 0x47, 0xdc, 0x62, 0xdc,
84 0xcc, 0xee, 0x7e, 0x23, 0xa6, 0x67, 0x61, 0x7c, 0xd1, 0x03, 0xbd, 0x78, 0xe9, 0x34, 0x05, 0xed,
85 0x05, 0x87, 0xef, 0x59, 0xf4, 0x16, 0xd6, 0x8d, 0x85, 0x46, 0x65, 0x2a, 0x08, 0xac, 0x4a, 0x5d,
86 0xe6, 0x27, 0x5f, 0x43, 0xdd, 0x51, 0x4e, 0x95, 0x9b, 0xf5, 0x0c, 0x81, 0x24, 0x73, 0x39, 0x77,
87 0xe9, 0xc8, 0x35, 0x4a, 0xe2, 0xb8, 0x35, 0x92, 0xde, 0x5c, 0x31, 0x12, 0x36, 0x5c, 0xc7, 0x69,
88 0xcd, 0x79, 0xa9, 0xf9, 0xcf, 0x13, 0xa9, 0x12, 0x29, 0x25, 0x5c, 0x6a, 0x34, 0xa4, 0xbf, 0xc5,
89 0xb6, 0x2a, 0xc1, 0xba, 0x6a, 0xd3, 0x98, 0x8c, 0x9b, 0x6d, 0x9f, 0xb9, 0x25, 0xa6, 0xd1, 0x97,
90 0x80, 0x38, 0x11, 0xdc, 0x73, 0x5c, 0xe7, 0x3a, 0x1f, 0xd2, 0x16, 0xcd, 0x63, 0xfb, 0x41, 0xb0,
91 0xba, 0xb0, 0x38, 0x67, 0x48, 0xd2, 0x8a, 0x94, 0x2f, 0x11, 0x81, 0xbf, 0x66, 0x38, 0x68, 0xff,
92 0xfe, 0xd1, 0x7c, 0xcd, 0xa3, 0xac, 0xe4, 0xf7, 0x58, 0x19, 0xcd, 0x2a, 0xe3, 0xfa, 0x4d, 0xb0,
93 0xbe, 0xac, 0x05, 0x1c, 0xd9, 0x8d, 0xf7, 0x5c, 0xc0, 0xfc, 0xa6, 0xb5, 0x99, 0xb8, 0x8e, 0x2b,
94 0x72, 0xf8, 0x19, 0xfc, 0x17, 0x11, 0xf6, 0x2b, 0x08, 0xe4, 0x6e, 0xb0, 0x65, 0xab, 0x78, 0x8a,
95 0xfc, 0x7c, 0x09, 0xca, 0x73, 0xcd, 0x35, 0x5d, 0x6c, 0x7a, 0x36, 0xc0, 0x24, 0xba, 0x3f, 0x08,
96 0xea, 0x17, 0x09, 0xe1, 0x9d, 0x5d, 0x18, 0x59, 0x8a, 0xd8, 0x6a, 0x6d, 0x85, 0x6a, 0x9e, 0xa9,
97 0xe5, 0x4b, 0x45, 0xb2, 0x35, 0x6e, 0x62, 0x24, 0x08, 0x00, 0x1c, 0x06, 0x73, 0x27, 0x5d, 0x11,
98 0x4a, 0xc8, 0x51, 0xbd, 0x59, 0xd6, 0x94, 0xce, 0x16, 0x15, 0x17, 0x58, 0x7f, 0x39, 0x9d, 0x4e,
99 0x69, 0x1a, 0x64, 0xbb, 0xd4, 0x51, 0xb9, 0xe4, 0x7d, 0x51, 0x3a, 0xff, 0xe5, 0x1f, 0x29, 0xea,
100 0x7e, 0xa5, 0x62, 0x63, 0xff, 0x10, 0xf7, 0x54, 0x35, 0xd1, 0xf3, 0x73, 0x1e, 0xab, 0xca, 0x52,
101 0x14, 0xc6, 0x7e, 0x51, 0xc2, 0x48, 0x13, 0xcb, 0x30, 0xb2, 0x1a, 0x84, 0x72, 0xe5, 0x44, 0x83,
102 0xc9, 0x90, 0xa5, 0x8c, 0xf9, 0xeb, 0x3c, 0x5c, 0xc6, 0xcc, 0x8a, 0x95, 0x8a, 0xfa, 0xeb, 0x37,
103 0x9c, 0xde, 0xa2, 0xb1, 0x72, 0x4d, 0xd9, 0x3d, 0xab, 0xfd, 0x0e, 0xbd, 0x32, 0x9d, 0x23, 0xe9,
104 0x6f, 0x85, 0x4e, 0xfe, 0xcd, 0x91, 0xfb, 0x82, 0x94, 0xee, 0x8b, 0xdf, 0x6a, 0xd9, 0x01, 0xa1,
105 0xc6, 0x22, 0x18, 0x01, 0x8d, 0x10, 0xd5, 0x87, 0x42, 0xd0, 0xbd, 0x23, 0x75, 0x44, 0x53, 0x46,
106 0xa5, 0xae, 0x00, 0x4c, 0x0e, 0x88, 0x4a, 0xa8, 0x3d, 0x4a, 0x30, 0xe0, 0x1a, 0xa4, 0xe5, 0x40,
107 0xb8, 0xe0, 0x12, 0x9c, 0x44, 0x03, 0xfb, 0x2e, 0x4e, 0xf5, 0x29, 0xdb, 0x09, 0x84, 0x55, 0xc7,
108 0x6c, 0xc6, 0x1f, 0xf9, 0xee, 0x0b, 0xa4, 0x91, 0x7d, 0x79, 0x27, 0x59, 0x75, 0x97, 0xec, 0x6a,
109 0xa8, 0xf8, 0x55, 0xa8, 0x45, 0xd4, 0xd7, 0xa6, 0xc1, 0xc4, 0x27, 0x35, 0xe8, 0x4f, 0x39, 0x89,
110 0x7d, 0x41, 0xf3, 0xf6, 0xd0, 0xb6, 0xf9, 0x91, 0xeb, 0x94, 0xf1, 0xbb, 0x17, 0x46, 0x9c, 0xd5,
111 0x5a, 0x53, 0x04, 0x2d, 0x12, 0x7c, 0x17, 0x6a, 0x36, 0xb5, 0xea, 0xf3, 0x5b, 0x96, 0x1b, 0xee,
112 0xce, 0xc4, 0xc0, 0x11, 0x5a, 0xbc, 0x0c, 0x29, 0xd0, 0x42, 0x1d, 0x16, 0x63, 0xea, 0x1e, 0x04,
113 0x2f, 0xe3, 0x17, 0xed, 0x33, 0xac, 0x56, 0x80, 0x34, 0x41, 0x41, 0x1e, 0x77, 0x80, 0x06, 0x9f,
114 0xbc, 0x2e, 0x78, 0xa1, 0x04, 0x00, 0x06, 0x6f, 0x36, 0x2f, 0xb7, 0xa5, 0x95, 0x37, 0x82, 0x9d,
115 0xef, 0x41, 0x08, 0x85, 0x3d, 0x53, 0xa7, 0xfb, 0xfe, 0xba, 0x8c, 0xb9, 0xae, 0xc7, 0x89, 0x11,
116 0x69, 0x4f, 0x62, 0xe6, 0xb6, 0x08, 0x6b, 0x35, 0x1c, 0x96, 0xb3, 0x7b, 0x40, 0x2d, 0xee, 0x07,
117 0x40, 0x52, 0x4f, 0x68, 0x60, 0xf4, 0xb9, 0xc3, 0x54, 0x9f, 0x22, 0x50, 0x88, 0x48, 0x6a, 0x28,
118 0x93, 0x46, 0x00, 0xe2, 0x4a, 0x85, 0x41, 0x78, 0x0e, 0x87, 0xc5, 0xeb, 0xfc, 0xd3, 0x5f, 0x4d,
119 0x24, 0xe4, 0x9d, 0xeb, 0x1d, 0x00, 0x73, 0x85, 0x25, 0x47, 0x9e, 0x8c, 0x5b, 0x88, 0xf4, 0x3b,
120 0x33, 0xf0, 0x3d, 0x3a, 0xa1, 0x28, 0xd3, 0x06, 0xb4, 0x7a, 0x4e, 0x5d, 0x31, 0x1b, 0xca, 0xf4,
121 0x3f, 0x70, 0x30, 0x49, 0x44, 0x29, 0x24, 0x14, 0x5e, 0x35, 0xc2, 0x6c, 0x92, 0x7e, 0xf8, 0x97,
122 0x0c, 0x51, 0x9d, 0x67, 0xc0, 0x10, 0xa9, 0x35, 0x48, 0x59, 0x6a, 0x33, 0xef, 0x40, 0x4e, 0x53,
123 0x10, 0x14, 0x2a, 0x12, 0x38, 0xe6, 0xc4, 0x63, 0x9c, 0x84, 0x85, 0x06, 0xaf, 0x3d, 0x3a, 0x84,
124 0x06, 0x60, 0x88, 0x32, 0xda, 0x2c, 0xe5, 0xc6, 0x59, 0xf1, 0xe0, 0x10, 0xe2, 0x3c, 0xe6, 0xbf,
125 0x32, 0x7d, 0x32, 0x39, 0x6d, 0xe4, 0xd9, 0xca, 0xe7, 0xf5, 0xf4, 0xa6, 0x5f, 0xb2, 0x33, 0x05,
126 0xb5, 0xad, 0x5f, 0xcb, 0x0b, 0x14, 0xaf, 0xeb, 0xc0, 0xec, 0x87, 0x85, 0x9b, 0x13, 0xb5, 0x8a,
127 0x98, 0xa9, 0x92, 0x13, 0x1b, 0x74, 0xec, 0xfd, 0xe1, 0xc1, 0x22, 0x06, 0x5d, 0x4f, 0x06, 0xc7,
128 0xdd, 0xc6, 0xf0, 0xc4, 0x01, 0x04, 0xad, 0x7f, 0x71, 0xbc, 0x74, 0x4d, 0xfd, 0x18, 0xa3, 0x56,
129 0x2c, 0x45, 0x28, 0x2b, 0x2f, 0xbc, 0x9b, 0xb8, 0x4b, 0xe6, 0x51, 0x75, 0x28, 0x0c, 0x27, 0x0e,
130 0xf7, 0x92, 0x8c, 0xc9, 0xde, 0x33, 0x1a, 0x65, 0x28, 0xc7, 0x01, 0x32, 0xa2, 0x36, 0x88, 0xb6,
131 0x64, 0x10, 0x03, 0xd6, 0xb7, 0x9f, 0x9d, 0x73, 0xe1, 0xa9, 0xc7, 0xdf, 0xe1, 0x0b, 0x39, 0x31,
132 0x77, 0xbc, 0x91, 0xf1, 0x45, 0x9a, 0xc5, 0x97, 0x28, 0xc0, 0x61, 0xc5, 0x23, 0x54, 0xad, 0xe3,
133 0x23, 0x18, 0x69, 0xf7, 0x27, 0xd0, 0x5b, 0xf2, 0x44, 0x62, 0xdc, 0x97, 0xce, 0x4e, 0x40, 0x76,
134 0x00, 0xde, 0xc2, 0xf9, 0x3a, 0x42, 0xfd, 0xd4, 0xd7, 0xe1, 0x85, 0xd8, 0xc9, 0x38, 0x91, 0xc1,
135 0x79, 0x87, 0x58, 0xf1, 0x26, 0x1a, 0x29, 0x02, 0xe3, 0x54, 0xde, 0x58, 0x64, 0x9d, 0xe6, 0x8e,
136 0x33, 0x70, 0x53, 0x43, 0x47, 0x90, 0xee, 0x6e, 0x0f, 0x8c, 0xb3, 0x9e, 0x47, 0x45, 0xfc, 0xa8,
137 0xe3, 0x52, 0x62, 0x74, 0x6d, 0xa2, 0xaf, 0x28, 0x9d, 0xdf, 0x1e, 0x69, 0x1f, 0x56, 0xbc, 0x49,
138 0xc1, 0xe5, 0xd6, 0xc4, 0xb5, 0x5c, 0x4d, 0x39, 0x49, 0x4b, 0xb4, 0xec, 0x56, 0x54, 0x9a, 0x15,
139 0x94, 0x0a, 0xcb, 0xa9, 0x10, 0x46, 0x03, 0x5c, 0x23, 0x2f, 0x29, 0xed, 0x72, 0xa1, 0x57, 0xfa,
140 0x58, 0xef, 0x21, 0x7e, 0xf2, 0x8b, 0xa7, 0x04, 0x51, 0xb4, 0x03, 0x5d, 0xd8, 0x48, 0xc0, 0xe5,
141 0x83, 0xb6, 0x7a, 0x6b, 0xcd, 0xfb, 0xda, 0x47, 0xe8, 0xa1, 0xae, 0x57, 0x74, 0x49, 0xc0, 0xf9,
142 0x4a, 0x6b, 0x3c, 0xb5, 0xd8, 0x27, 0x3d, 0x1d, 0x96, 0x39, 0x09, 0x65, 0x95, 0xdb, 0x01, 0xa3,
143 0x8b, 0x78, 0x8b, 0x07, 0x6d, 0x1c, 0x8b, 0x4b, 0x1d, 0x9d, 0x4a, 0x4f, 0xcb, 0xb8, 0xf6, 0x22,
144 0x73, 0x8a, 0x7b, 0xc8, 0xf2, 0x0a, 0xef, 0x03, 0x1e, 0xb7, 0x4d, 0x8f, 0xc0, 0xdf, 0x87, 0x88,
145 0x05, 0xe1, 0x0a, 0x30, 0xea, 0xde, 0xf3, 0xc2, 0xb6, 0x00, 0x3c, 0xd6, 0xff, 0x3b, 0xb5, 0x01,
146 0xfb, 0xd8, 0xb2, 0x65, 0x26, 0x5d, 0xa0, 0x5a, 0x7c, 0xef, 0x1d, 0x85, 0xbe, 0x51, 0xc2, 0x57,
147 0x0b, 0x27, 0x37, 0x71, 0x99, 0xf5, 0x87, 0x83, 0x68, 0x0b, 0x88, 0xed, 0x66, 0x9e, 0x37, 0x59,
148 0x84, 0x23, 0x72, 0xc3, 0x80, 0xac, 0xfe, 0x45, 0x5f, 0xdf, 0x31, 0xc4, 0x84, 0x07, 0x5a, 0x17,
149 0x28, 0xcd, 0x64, 0xb4, 0xe2, 0xa3, 0x0e, 0x2c, 0x15, 0x60, 0x77, 0xdc, 0x08, 0x45, 0x36, 0x37,
150 0x68, 0x50, 0xba, 0x03, 0x85, 0xb7, 0xed, 0xd0, 0x7b, 0xb2, 0xa1, 0x62, 0xbc, 0x70, 0x00, 0x9e,
151];
152
153#[derive(Debug, Clone, Serialize, Deserialize)]
155pub struct UpdateCheck {
156 pub current_version: String,
157 pub latest_version: String,
158 pub update_available: bool,
159 pub download_url: Option<String>,
160 #[serde(default = "default_channel")]
164 pub channel: UpgradeChannel,
165}
166
167fn default_channel() -> UpgradeChannel {
169 UpgradeChannel::Stable
170}
171
172#[must_use]
179pub fn channel_for_version(version: &str) -> UpgradeChannel {
180 match parse_version(version) {
181 Ok(v) if v.pre.as_str().split('.').next() == Some("beta") => UpgradeChannel::Beta,
182 _ => UpgradeChannel::Stable,
183 }
184}
185
186impl UpdateCheck {
187 pub fn force(&mut self) -> Result<()> {
191 self.update_available = true;
192 self.download_url = Some(build_download_url(&self.latest_version)?);
193 Ok(())
194 }
195}
196
197#[derive(Debug, Clone, Serialize, Deserialize)]
199pub struct UpdateResult {
200 pub previous_version: String,
201 pub new_version: String,
202}
203
204#[derive(Debug, Clone, Serialize, Deserialize)]
210pub struct DeferredUpdate {
211 pub current_version: String,
212 pub latest_version: String,
213 pub update_available: bool,
214 pub channel: UpgradeChannel,
215 pub install_method: InstallMethod,
217 pub update_command: String,
219}
220
221impl DeferredUpdate {
222 #[must_use]
226 pub fn new(check: &UpdateCheck, method: InstallMethod) -> Option<Self> {
227 Some(Self {
228 current_version: check.current_version.clone(),
229 latest_version: check.latest_version.clone(),
230 update_available: check.update_available,
231 channel: check.channel,
232 install_method: method,
233 update_command: method.update_command()?,
234 })
235 }
236}
237
238pub async fn check_for_update(
244 current_version: &str,
245 channel: UpgradeChannel,
246) -> Result<UpdateCheck> {
247 let latest = fetch_latest_cli_version(channel).await?;
248 let current = parse_version(current_version)?;
249 let latest_parsed = parse_version(&latest)?;
250 let update_available = latest_parsed > current;
251
252 let download_url = if update_available {
253 Some(build_download_url(&latest)?)
254 } else {
255 None
256 };
257
258 Ok(UpdateCheck {
259 current_version: current_version.to_string(),
260 latest_version: latest,
261 update_available,
262 download_url,
263 channel,
264 })
265}
266
267pub async fn perform_update(
273 check: &UpdateCheck,
274 progress: &dyn ProgressReporter,
275) -> Result<UpdateResult> {
276 let method = install::detect();
282 if !method.can_self_replace() {
283 let command = method
284 .update_command()
285 .unwrap_or_else(|| "your package manager".to_string());
286 return Err(Error::UpdateFailed(format!(
287 "this ant was installed by a package manager, so it cannot replace itself. \
288 Update it with: {command}"
289 )));
290 }
291
292 let download_url = check.download_url.as_deref().ok_or_else(|| {
293 Error::UpdateFailed("no download URL — are you already on the latest version?".to_string())
294 })?;
295
296 let tmp_dir = tempfile::tempdir()
297 .map_err(|e| Error::UpdateFailed(format!("failed to create temp directory: {e}")))?;
298
299 let (archive_path, archive_bytes) =
300 download_archive(download_url, tmp_dir.path(), progress).await?;
301
302 let sig_url = format!("{download_url}.sig");
303 progress.report_started("Downloading signature...");
304 let sig_bytes = download_bytes(&sig_url).await?;
305
306 progress.report_started("Verifying ML-DSA signature...");
307 verify_signature(&archive_bytes, &sig_bytes)?;
308 progress.report_complete("Signature verified");
309
310 progress.report_started("Extracting archive...");
311 let extracted = if download_url.ends_with(".zip") {
312 extract_zip(&archive_bytes, tmp_dir.path(), CLI_BINARY_NAME)?
313 } else {
314 extract_tar_gz(&archive_bytes, tmp_dir.path(), CLI_BINARY_NAME)?
315 };
316 let binary_path = extracted.binary_path;
317
318 let actual_version = extract_version(&binary_path).await;
320 if let Ok(ref v) = actual_version {
321 if v != &check.latest_version {
322 return Err(Error::UpdateFailed(format!(
323 "version mismatch: expected {}, binary reports {v}",
324 check.latest_version
325 )));
326 }
327 }
328
329 replace_binary(&binary_path)?;
330
331 let _ = std::fs::remove_file(&archive_path);
334
335 Ok(UpdateResult {
336 previous_version: check.current_version.clone(),
337 new_version: check.latest_version.clone(),
338 })
339}
340
341async fn fetch_latest_cli_version(channel: UpgradeChannel) -> Result<String> {
346 let url = format!("https://api.github.com/repos/{GITHUB_REPO}/releases?per_page=100");
348 let client = reqwest::Client::new();
349 let resp = client
350 .get(&url)
351 .header("User-Agent", "ant-cli")
352 .header("Accept", "application/vnd.github+json")
353 .send()
354 .await
355 .map_err(|e| Error::UpdateFailed(format!("failed to fetch releases: {e}")))?;
356
357 if !resp.status().is_success() {
358 return Err(Error::UpdateFailed(format!(
359 "GitHub API returned status {} when fetching releases",
360 resp.status()
361 )));
362 }
363
364 let releases: Vec<serde_json::Value> = resp
365 .json()
366 .await
367 .map_err(|e| Error::UpdateFailed(format!("failed to parse releases JSON: {e}")))?;
368
369 select_channel_version(&releases, channel).ok_or_else(|| {
370 Error::UpdateFailed(format!(
371 "no ant-cli release found on GitHub for the {channel} channel"
372 ))
373 })
374}
375
376fn select_channel_version(
384 releases: &[serde_json::Value],
385 channel: UpgradeChannel,
386) -> Option<String> {
387 let mut best: Option<semver::Version> = None;
388
389 for release in releases {
390 if release["draft"].as_bool().unwrap_or(false) {
391 continue;
392 }
393
394 let tag = release["tag_name"].as_str().unwrap_or_default();
395 let Some(version_str) = tag.strip_prefix(TAG_PREFIX) else {
396 continue;
397 };
398 let Ok(version) = semver::Version::parse(version_str) else {
399 continue;
400 };
401
402 if !version_matches_channel(&version, channel) {
403 continue;
404 }
405
406 if best.as_ref().is_none_or(|b| version > *b) {
407 best = Some(version);
408 }
409 }
410
411 best.map(|v| v.to_string())
412}
413
414async fn download_archive(
419 url: &str,
420 tmp_dir: &Path,
421 progress: &dyn ProgressReporter,
422) -> Result<(PathBuf, Vec<u8>)> {
423 progress.report_started(&format!("Downloading {CLI_BINARY_NAME} from {url}"));
424
425 let client = reqwest::Client::new();
426 let resp = client
427 .get(url)
428 .header("User-Agent", "ant-cli")
429 .send()
430 .await
431 .map_err(|e| Error::UpdateFailed(format!("download request failed: {e}")))?;
432
433 if !resp.status().is_success() {
434 return Err(Error::UpdateFailed(format!(
435 "download returned status {}",
436 resp.status()
437 )));
438 }
439
440 let total_size = resp.content_length().unwrap_or(0);
441 let mut downloaded: u64 = 0;
442
443 let tmp_archive = tmp_dir.join(".download.tmp");
444 let mut tmp_file = std::fs::File::create(&tmp_archive)
445 .map_err(|e| Error::UpdateFailed(format!("failed to create temp file: {e}")))?;
446
447 let mut stream = resp.bytes_stream();
448 while let Some(chunk) = stream.next().await {
449 let chunk =
450 chunk.map_err(|e| Error::UpdateFailed(format!("download stream error: {e}")))?;
451 downloaded += chunk.len() as u64;
452 std::io::Write::write_all(&mut tmp_file, &chunk)
453 .map_err(|e| Error::UpdateFailed(format!("failed to write temp file: {e}")))?;
454 progress.report_progress(downloaded, total_size);
455 }
456 drop(tmp_file);
457
458 progress.report_complete("Download complete");
459
460 let bytes = std::fs::read(&tmp_archive)
461 .map_err(|e| Error::UpdateFailed(format!("failed to read temp file: {e}")))?;
462
463 Ok((tmp_archive, bytes))
464}
465
466async fn download_bytes(url: &str) -> Result<Vec<u8>> {
468 let client = reqwest::Client::new();
469 let resp = client
470 .get(url)
471 .header("User-Agent", "ant-cli")
472 .send()
473 .await
474 .map_err(|e| Error::UpdateFailed(format!("download request failed: {e}")))?;
475
476 if !resp.status().is_success() {
477 return Err(Error::UpdateFailed(format!(
478 "download returned status {} for {url}",
479 resp.status()
480 )));
481 }
482
483 resp.bytes()
484 .await
485 .map(|b| b.to_vec())
486 .map_err(|e| Error::UpdateFailed(format!("failed to read response body: {e}")))
487}
488
489fn verify_signature(archive_bytes: &[u8], signature_bytes: &[u8]) -> Result<()> {
491 if signature_bytes.len() != SIGNATURE_SIZE {
492 return Err(Error::UpdateFailed(format!(
493 "invalid signature size: expected {SIGNATURE_SIZE}, got {}",
494 signature_bytes.len()
495 )));
496 }
497
498 let public_key = MlDsaPublicKey::from_bytes(MlDsaVariant::MlDsa65, RELEASE_SIGNING_KEY)
499 .map_err(|e| Error::UpdateFailed(format!("invalid embedded release key: {e}")))?;
500
501 let sig = MlDsaSignature::from_bytes(MlDsaVariant::MlDsa65, signature_bytes)
502 .map_err(|e| Error::UpdateFailed(format!("invalid signature format: {e}")))?;
503
504 let dsa = ml_dsa_65();
505 let valid = dsa
506 .verify_with_context(&public_key, archive_bytes, &sig, SIGNING_CONTEXT)
507 .map_err(|e| Error::UpdateFailed(format!("signature verification error: {e}")))?;
508
509 if valid {
510 Ok(())
511 } else {
512 Err(Error::UpdateFailed(
513 "signature verification failed: archive may be corrupted or tampered".to_string(),
514 ))
515 }
516}
517
518async fn extract_version(binary_path: &Path) -> Result<String> {
520 let mut cmd = tokio::process::Command::new(binary_path);
521 cmd.arg("--version");
522 #[cfg(windows)]
526 {
527 const CREATE_NO_WINDOW: u32 = 0x08000000;
528 cmd.creation_flags(CREATE_NO_WINDOW);
529 }
530 let output = cmd.output().await.map_err(|e| {
531 Error::UpdateFailed(format!(
532 "failed to run {} --version: {e}",
533 binary_path.display()
534 ))
535 })?;
536
537 if !output.status.success() {
538 return Err(Error::UpdateFailed(format!(
539 "{} --version exited with status {}",
540 binary_path.display(),
541 output.status
542 )));
543 }
544
545 let stdout = String::from_utf8_lossy(&output.stdout);
546 Ok(parse_version_from_stdout(&stdout).to_string())
547}
548
549fn parse_version_from_stdout(stdout: &str) -> &str {
557 stdout
558 .lines()
559 .next()
560 .unwrap_or_default()
561 .split_whitespace()
562 .last()
563 .unwrap_or("unknown")
564}
565
566fn replace_binary(new_binary: &Path) -> Result<()> {
571 self_replace::self_replace(new_binary)
572 .map_err(|e| Error::UpdateFailed(format!("failed to replace binary: {e}")))?;
573 Ok(())
574}
575
576pub fn build_download_url(version: &str) -> Result<String> {
581 let asset_name = cli_platform_asset_name(version)?;
582 Ok(format!(
583 "https://github.com/{GITHUB_REPO}/releases/download/{TAG_PREFIX}{version}/{asset_name}"
584 ))
585}
586
587fn cli_platform_asset_name(version: &str) -> Result<String> {
591 let target_triple = if cfg!(all(target_os = "linux", target_arch = "x86_64")) {
592 "x86_64-unknown-linux-musl"
593 } else if cfg!(all(target_os = "linux", target_arch = "aarch64")) {
594 "aarch64-unknown-linux-musl"
595 } else if cfg!(all(target_os = "macos", target_arch = "x86_64")) {
596 "x86_64-apple-darwin"
597 } else if cfg!(all(target_os = "macos", target_arch = "aarch64")) {
598 "aarch64-apple-darwin"
599 } else if cfg!(all(target_os = "windows", target_arch = "x86_64")) {
600 "x86_64-pc-windows-msvc"
601 } else {
602 return Err(Error::UpdateFailed(format!(
603 "unsupported platform: {}-{}",
604 std::env::consts::OS,
605 std::env::consts::ARCH
606 )));
607 };
608
609 let ext = if cfg!(target_os = "windows") {
610 "zip"
611 } else {
612 "tar.gz"
613 };
614
615 Ok(format!("ant-{version}-{target_triple}.{ext}"))
616}
617
618fn parse_version(version: &str) -> Result<semver::Version> {
619 let cleaned = version.strip_prefix('v').unwrap_or(version);
620 semver::Version::parse(cleaned)
621 .map_err(|e| Error::UpdateFailed(format!("invalid version '{version}': {e}")))
622}
623
624#[cfg(test)]
625mod tests {
626 use super::*;
627
628 #[test]
629 fn parse_version_from_stdout_single_line() {
630 assert_eq!(parse_version_from_stdout("ant 0.1.4"), "0.1.4");
631 assert_eq!(parse_version_from_stdout("ant 0.1.4\n"), "0.1.4");
632 }
633
634 #[test]
635 fn parse_version_from_stdout_multi_line_license_trailer() {
636 let stdout = "ant 0.1.4\n\
638 Autonomi network client\n\
639 \n\
640 Repository: https://github.com/WithAutonomi/ant-client\n\
641 License: MIT or Apache-2.0\n";
642 assert_eq!(parse_version_from_stdout(stdout), "0.1.4");
643 }
644
645 #[test]
646 fn parse_version_from_stdout_empty() {
647 assert_eq!(parse_version_from_stdout(""), "unknown");
648 }
649
650 #[test]
651 fn parse_version_from_stdout_blank_first_line() {
652 assert_eq!(parse_version_from_stdout("\nant 0.1.4\n"), "unknown");
653 }
654
655 #[test]
656 fn parse_version_from_stdout_leading_whitespace() {
657 assert_eq!(parse_version_from_stdout(" ant 0.1.4\n"), "0.1.4");
658 }
659
660 #[test]
661 fn parse_version_valid() {
662 assert!(parse_version("1.2.3").is_ok());
663 assert!(parse_version("v1.2.3").is_ok());
664 assert!(parse_version("0.1.0").is_ok());
665 }
666
667 #[test]
668 fn parse_version_invalid() {
669 assert!(parse_version("not-a-version").is_err());
670 assert!(parse_version("").is_err());
671 }
672
673 #[test]
674 fn version_comparison() {
675 let v1 = parse_version("0.1.0").unwrap();
676 let v2 = parse_version("0.2.0").unwrap();
677 assert!(v2 > v1);
678
679 let v3 = parse_version("1.0.0").unwrap();
680 assert!(v3 > v2);
681
682 let same = parse_version("0.1.0").unwrap();
683 assert_eq!(v1, same);
684 }
685
686 #[test]
687 fn check_result_no_update() {
688 let check = UpdateCheck {
689 current_version: "1.0.0".to_string(),
690 latest_version: "1.0.0".to_string(),
691 update_available: false,
692 download_url: None,
693 channel: UpgradeChannel::Stable,
694 };
695 assert!(!check.update_available);
696 assert!(check.download_url.is_none());
697 }
698
699 #[test]
700 fn check_result_with_update() {
701 let check = UpdateCheck {
702 current_version: "0.1.0".to_string(),
703 latest_version: "0.2.0".to_string(),
704 update_available: true,
705 download_url: Some("https://example.com/ant.tar.gz".to_string()),
706 channel: UpgradeChannel::Stable,
707 };
708 assert!(check.update_available);
709 assert!(check.download_url.is_some());
710 }
711
712 #[test]
713 fn force_populates_download_url() {
714 let mut check = UpdateCheck {
715 current_version: "1.0.0".to_string(),
716 latest_version: "1.0.0".to_string(),
717 update_available: false,
718 download_url: None,
719 channel: UpgradeChannel::Stable,
720 };
721 check.force().unwrap();
722 assert!(check.update_available);
723 assert!(check.download_url.is_some());
724 }
725
726 #[test]
727 fn platform_asset_name_format() {
728 let name = cli_platform_asset_name("1.2.3").unwrap();
729 assert!(name.starts_with("ant-1.2.3-"));
730 assert!(
731 name.ends_with(".tar.gz") || name.ends_with(".zip"),
732 "unexpected extension: {name}"
733 );
734 }
735
736 #[test]
737 fn build_download_url_format() {
738 let url = build_download_url("1.2.3").unwrap();
739 assert!(url.starts_with(
740 "https://github.com/WithAutonomi/ant-client/releases/download/ant-cli-v1.2.3/ant-1.2.3-"
741 ));
742 assert!(url.ends_with(".tar.gz") || url.ends_with(".zip"));
743 }
744
745 #[test]
746 fn update_check_serializes() {
747 let check = UpdateCheck {
748 current_version: "0.1.0".to_string(),
749 latest_version: "0.2.0".to_string(),
750 update_available: true,
751 download_url: Some("https://example.com/ant.tar.gz".to_string()),
752 channel: UpgradeChannel::Stable,
753 };
754 let json = serde_json::to_string(&check).unwrap();
755 let deserialized: UpdateCheck = serde_json::from_str(&json).unwrap();
756 assert_eq!(deserialized.current_version, "0.1.0");
757 assert!(deserialized.update_available);
758 }
759
760 #[test]
761 fn verify_signature_rejects_wrong_size() {
762 let result = verify_signature(b"some archive data", &[0u8; 100]);
763 assert!(result.is_err());
764 let err = result.unwrap_err().to_string();
765 assert!(err.contains("invalid signature size"), "got: {err}");
766 }
767
768 #[test]
769 fn verify_signature_rejects_invalid_signature() {
770 let invalid_sig = vec![0u8; SIGNATURE_SIZE];
771 let result = verify_signature(b"some archive data", &invalid_sig);
772 assert!(result.is_err());
773 }
774
775 #[test]
776 fn verify_signature_valid_roundtrip() {
777 let dsa = ml_dsa_65();
778 let (public_key, secret_key) = dsa.generate_keypair().unwrap();
779 let archive = b"fake archive content for testing";
780
781 let sig = dsa
782 .sign_with_context(&secret_key, archive, SIGNING_CONTEXT)
783 .unwrap();
784
785 let parsed_sig =
787 MlDsaSignature::from_bytes(MlDsaVariant::MlDsa65, &sig.to_bytes()).unwrap();
788 let valid = dsa
789 .verify_with_context(&public_key, archive, &parsed_sig, SIGNING_CONTEXT)
790 .unwrap();
791 assert!(valid);
792 }
793
794 fn release(tag: &str) -> serde_json::Value {
795 serde_json::json!({ "tag_name": tag, "draft": false })
796 }
797
798 #[test]
799 fn stable_selects_the_highest_final_release() {
800 let releases = [
801 release("ant-cli-v0.3.3"),
802 release("ant-cli-v0.3.4-beta.1"),
803 release("ant-cli-v0.3.4-rc.1"),
804 ];
805
806 assert_eq!(
807 select_channel_version(&releases, UpgradeChannel::Stable),
808 Some("0.3.3".to_string())
809 );
810 }
811
812 #[test]
813 fn beta_selects_the_beta_over_the_rc() {
814 let releases = [
815 release("ant-cli-v0.3.3"),
816 release("ant-cli-v0.3.4-beta.1"),
817 release("ant-cli-v0.3.4-rc.1"),
818 ];
819
820 assert_eq!(
821 select_channel_version(&releases, UpgradeChannel::Beta),
822 Some("0.3.4-beta.1".to_string())
823 );
824 }
825
826 #[test]
829 fn the_github_prerelease_flag_does_not_hide_a_beta() {
830 let mut beta = release("ant-cli-v0.3.4-beta.1");
831 beta["prerelease"] = serde_json::Value::Bool(true);
832 let releases = [release("ant-cli-v0.3.3"), beta];
833
834 assert_eq!(
835 select_channel_version(&releases, UpgradeChannel::Beta),
836 Some("0.3.4-beta.1".to_string())
837 );
838 }
839
840 #[test]
841 fn drafts_and_foreign_tags_are_skipped() {
842 let mut draft = release("ant-cli-v0.4.0");
843 draft["draft"] = serde_json::Value::Bool(true);
844 let releases = [
845 release("ant-core-v0.9.0"),
846 release("v0.5.0"),
847 draft,
848 release("ant-cli-v0.3.3"),
849 ];
850
851 assert_eq!(
852 select_channel_version(&releases, UpgradeChannel::Stable),
853 Some("0.3.3".to_string())
854 );
855 }
856
857 #[test]
858 fn no_eligible_release_yields_none() {
859 let releases = [release("ant-cli-v0.3.4-rc.1")];
860
861 assert_eq!(
862 select_channel_version(&releases, UpgradeChannel::Beta),
863 None
864 );
865 }
866
867 #[test]
868 fn channel_is_inferred_from_the_running_version() {
869 assert_eq!(channel_for_version("0.3.4-beta.1"), UpgradeChannel::Beta);
870 assert_eq!(channel_for_version("0.3.4-beta"), UpgradeChannel::Beta);
871 assert_eq!(channel_for_version("0.3.3"), UpgradeChannel::Stable);
872 assert_eq!(channel_for_version("0.3.4-rc.1"), UpgradeChannel::Stable);
873 assert_eq!(channel_for_version("0.3.4-alpha.1"), UpgradeChannel::Stable);
874 assert_eq!(
876 channel_for_version("0.3.4-betamax.1"),
877 UpgradeChannel::Stable
878 );
879 assert_eq!(channel_for_version("not-a-version"), UpgradeChannel::Stable);
880 }
881
882 #[test]
884 fn update_check_deserializes_without_a_channel_field() {
885 let json = r#"{"current_version":"0.1.0","latest_version":"0.2.0",
886 "update_available":true,"download_url":null}"#;
887 let check: UpdateCheck = serde_json::from_str(json).unwrap();
888 assert_eq!(check.channel, UpgradeChannel::Stable);
889 }
890}