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::error::{Error, Result};
8use crate::node::binary::{extract_tar_gz, extract_zip, ProgressReporter};
9
10const GITHUB_REPO: &str = "WithAutonomi/ant-client";
11const CLI_BINARY_NAME: &str = "ant";
12const TAG_PREFIX: &str = "ant-cli-v";
13
14const SIGNING_CONTEXT: &[u8] = b"ant-release-v1";
16
17const SIGNATURE_SIZE: usize = 3309;
19
20const RELEASE_SIGNING_KEY: &[u8] = &[
26 0xb4, 0xcf, 0x2a, 0x24, 0x31, 0xd9, 0xb2, 0x3a, 0xab, 0xe9, 0x5e, 0xfc, 0xbc, 0xf3, 0xb1, 0x1f,
27 0x4e, 0x50, 0x0a, 0x46, 0xeb, 0x83, 0xfc, 0x6f, 0x0f, 0x89, 0x41, 0x00, 0x1b, 0x52, 0xde, 0xdc,
28 0xb5, 0xc9, 0x07, 0xed, 0x72, 0x3a, 0xe1, 0xa9, 0x82, 0xe1, 0xfc, 0xff, 0xab, 0xce, 0x9f, 0x7d,
29 0xab, 0xe2, 0x57, 0x92, 0xe0, 0xf2, 0xac, 0xa1, 0x41, 0xd3, 0x74, 0x95, 0x41, 0xd1, 0xac, 0x22,
30 0xb1, 0xbb, 0x5c, 0xf4, 0x02, 0x0b, 0x73, 0x85, 0xfd, 0x56, 0x75, 0x0d, 0x5c, 0x38, 0xe1, 0x2d,
31 0xe0, 0x15, 0x4f, 0xbf, 0x40, 0xeb, 0xf1, 0x0d, 0x8f, 0x39, 0x32, 0xeb, 0x80, 0xa7, 0x2e, 0x77,
32 0x3e, 0x54, 0xe3, 0x3d, 0x62, 0xae, 0xe7, 0x09, 0x5e, 0xfb, 0xdc, 0xaa, 0x07, 0xdc, 0xe1, 0x08,
33 0x96, 0xbb, 0x4b, 0xa0, 0x2e, 0x05, 0x2d, 0xee, 0xeb, 0x9a, 0x1a, 0xae, 0xde, 0xe9, 0x2c, 0xf8,
34 0x2f, 0x43, 0x6c, 0x78, 0x4b, 0xde, 0xef, 0x91, 0x8b, 0x94, 0x9d, 0x4f, 0x81, 0x05, 0xcc, 0xf0,
35 0x98, 0xce, 0xce, 0x67, 0x54, 0xac, 0xde, 0xcd, 0x26, 0x9e, 0x84, 0xf4, 0x88, 0xb2, 0x1a, 0x3e,
36 0x93, 0x2e, 0xff, 0xa8, 0x45, 0x95, 0xd1, 0xd0, 0xb1, 0x6c, 0x3c, 0x1e, 0xef, 0x3d, 0xe3, 0xf2,
37 0x73, 0xe2, 0xf6, 0xb7, 0xf9, 0x3f, 0x10, 0x0d, 0x3c, 0xde, 0x28, 0x94, 0x07, 0xef, 0x24, 0x70,
38 0xc4, 0x5a, 0x0a, 0x67, 0xbb, 0x0f, 0x4f, 0x5c, 0x2b, 0xd8, 0x02, 0x05, 0xa5, 0x98, 0x03, 0x5d,
39 0x8f, 0xc0, 0x4a, 0x84, 0xe9, 0xea, 0xac, 0x13, 0xdf, 0x69, 0xfc, 0x1e, 0xcf, 0xb6, 0x88, 0xba,
40 0x99, 0x30, 0xbc, 0x7a, 0xb8, 0x9d, 0x3d, 0x62, 0x3b, 0x33, 0x19, 0xbb, 0x3a, 0x2c, 0x2b, 0xa0,
41 0x5a, 0xb0, 0x8f, 0x9e, 0x10, 0x81, 0xb9, 0x12, 0x54, 0x81, 0xf8, 0xe2, 0x91, 0xb2, 0xe7, 0xe6,
42 0x9c, 0x11, 0xeb, 0x49, 0x64, 0x3a, 0x25, 0xd6, 0x53, 0x2e, 0xdf, 0xfc, 0x14, 0x32, 0x65, 0xcc,
43 0x87, 0xdb, 0xfd, 0xbb, 0x81, 0xa8, 0x50, 0xcc, 0xb4, 0x31, 0x0c, 0x70, 0xf3, 0xb6, 0x15, 0x8a,
44 0x50, 0x80, 0xad, 0xb1, 0xb0, 0x10, 0x2b, 0x67, 0x33, 0xf5, 0xf6, 0x36, 0x35, 0x5f, 0xa7, 0xd2,
45 0x81, 0xd6, 0x75, 0xa1, 0x18, 0x15, 0xbe, 0x1d, 0x5e, 0x33, 0x8e, 0x98, 0xdd, 0x45, 0x0f, 0x0c,
46 0x0f, 0x0d, 0x8b, 0x3f, 0x97, 0x11, 0x21, 0x2e, 0xa0, 0x5e, 0xfe, 0x70, 0x09, 0xa7, 0x14, 0x30,
47 0xa3, 0x01, 0x2d, 0x18, 0x2b, 0x8f, 0x19, 0x75, 0x54, 0x1f, 0xd8, 0xee, 0x66, 0x06, 0x7b, 0x9d,
48 0x7d, 0xb2, 0xae, 0x14, 0xe6, 0x51, 0x19, 0xc2, 0x45, 0x2e, 0x7e, 0x11, 0xd9, 0x7b, 0x16, 0x8e,
49 0xae, 0x17, 0xdb, 0x1b, 0x24, 0x90, 0xcd, 0xed, 0x94, 0xf9, 0xf7, 0xba, 0x9f, 0x4c, 0x12, 0xae,
50 0x31, 0x7b, 0xd4, 0x7c, 0x04, 0x42, 0x2c, 0x32, 0x16, 0xc1, 0x70, 0x6d, 0x11, 0x6f, 0x3b, 0x44,
51 0x62, 0xba, 0xbd, 0xc5, 0x7a, 0xec, 0x55, 0x1b, 0xcd, 0xdb, 0xb6, 0x55, 0x08, 0x86, 0x13, 0x7f,
52 0x4e, 0xa9, 0x63, 0xe1, 0x87, 0xa0, 0x7e, 0x49, 0xfb, 0xf4, 0xa3, 0x46, 0xcf, 0x1d, 0xec, 0xf5,
53 0xc6, 0x2f, 0xe1, 0x43, 0x02, 0xd0, 0xe4, 0x5f, 0x1b, 0x20, 0x1a, 0xa7, 0x81, 0xbd, 0x31, 0x19,
54 0x6a, 0x74, 0xd7, 0x9a, 0x6d, 0x3d, 0xf8, 0xac, 0x4d, 0xbb, 0x01, 0x63, 0xa4, 0x9d, 0x3c, 0xc9,
55 0x6c, 0x8a, 0x4f, 0x61, 0xd6, 0x98, 0xf5, 0x40, 0x22, 0xa9, 0x5e, 0x93, 0x5e, 0x13, 0xd3, 0xe0,
56 0xdb, 0x54, 0xab, 0x0d, 0xe3, 0x88, 0x85, 0x80, 0x7a, 0x5e, 0x38, 0x64, 0x97, 0xc4, 0xe9, 0xb0,
57 0x5d, 0xf7, 0x40, 0x5f, 0x6e, 0x3f, 0xbe, 0x14, 0x9a, 0x7c, 0xa2, 0x7c, 0x74, 0xa5, 0x32, 0x22,
58 0x61, 0x31, 0xa5, 0x0d, 0xa5, 0xcc, 0x93, 0xe4, 0xfd, 0xed, 0xbc, 0xe7, 0xf2, 0xe5, 0xdb, 0xc6,
59 0x0c, 0xe1, 0xc8, 0x4e, 0xee, 0xe6, 0x76, 0x1c, 0x10, 0x1b, 0xd8, 0x53, 0xd4, 0xe8, 0x07, 0xed,
60 0xea, 0x91, 0xd4, 0x1b, 0x91, 0x5c, 0x28, 0x05, 0xca, 0xe2, 0x9c, 0xd1, 0x99, 0x43, 0xed, 0xd8,
61 0x6a, 0x2b, 0xd2, 0x64, 0x9b, 0xe1, 0x0c, 0x88, 0x6c, 0x0d, 0xb2, 0x6b, 0x73, 0x85, 0x9d, 0xbf,
62 0x79, 0x78, 0xaa, 0x7b, 0x5e, 0xf8, 0xa4, 0x26, 0xdf, 0xb3, 0x9b, 0x24, 0x5a, 0xc8, 0x19, 0x22,
63 0xa5, 0xc6, 0xca, 0x00, 0x59, 0x3b, 0xad, 0x45, 0xdf, 0x71, 0x1d, 0x60, 0x60, 0x24, 0x0d, 0xa4,
64 0x3d, 0x42, 0x23, 0xb8, 0xfe, 0xac, 0x86, 0x94, 0x79, 0x87, 0x05, 0xae, 0xb8, 0x4d, 0x7e, 0x11,
65 0x5b, 0x22, 0x44, 0x15, 0x3d, 0x7f, 0x82, 0x98, 0x65, 0x0a, 0x3c, 0xd3, 0xef, 0x80, 0x0d, 0x75,
66 0x03, 0x92, 0xf6, 0x3a, 0x8b, 0xa4, 0xb0, 0x61, 0x2d, 0x2c, 0xcc, 0x1f, 0x01, 0x8e, 0x7a, 0x46,
67 0x36, 0x2a, 0x83, 0x21, 0x88, 0x98, 0x13, 0x0a, 0xd5, 0xa1, 0x54, 0x4b, 0x63, 0xe0, 0xe3, 0x1c,
68 0x07, 0x5e, 0x32, 0x8c, 0xa4, 0x6b, 0x62, 0xc3, 0x28, 0x95, 0xb8, 0x0a, 0xb9, 0x4f, 0xaf, 0x7f,
69 0x49, 0xeb, 0xff, 0xd7, 0xa1, 0x41, 0x43, 0x9a, 0x92, 0x9a, 0x5f, 0xee, 0xbd, 0xb9, 0xbe, 0xb3,
70 0x4b, 0x9d, 0x0b, 0xcb, 0x9b, 0x2c, 0x26, 0x8f, 0x0f, 0xb1, 0xfa, 0xc0, 0xe3, 0x3a, 0x7f, 0x2b,
71 0x51, 0x79, 0x75, 0x25, 0x5b, 0x23, 0x22, 0xb1, 0x01, 0x27, 0x4e, 0x43, 0xdd, 0x66, 0x7a, 0x33,
72 0x4d, 0x32, 0x96, 0x83, 0x59, 0x52, 0xd7, 0x3c, 0xb0, 0xe3, 0x03, 0xd6, 0xb0, 0xc7, 0x99, 0x68,
73 0xc6, 0xa4, 0x2d, 0x35, 0x3d, 0xa4, 0x6a, 0x17, 0xd9, 0xf4, 0x0c, 0x26, 0x11, 0xe4, 0xbc, 0x03,
74 0x87, 0x25, 0x62, 0xad, 0xa9, 0x7e, 0x96, 0x4d, 0x39, 0x9b, 0x8f, 0x09, 0xdc, 0xd1, 0x28, 0x5e,
75 0xf4, 0xe3, 0x94, 0xfd, 0x94, 0x46, 0x30, 0xe2, 0x24, 0x46, 0x30, 0x7f, 0xf4, 0x4c, 0xaa, 0x51,
76 0x7e, 0x04, 0x5c, 0xa4, 0x8c, 0xba, 0x4a, 0xb8, 0x61, 0x5e, 0x75, 0x1c, 0xa8, 0x0c, 0xbc, 0x7f,
77 0x36, 0x16, 0xa1, 0x72, 0x98, 0x6a, 0x44, 0x39, 0x42, 0x67, 0xb5, 0x4a, 0xac, 0x14, 0x35, 0x8f,
78 0xcd, 0x87, 0x3f, 0x9e, 0x2e, 0xa1, 0x53, 0xf1, 0x45, 0x68, 0x26, 0xcb, 0x35, 0x96, 0x57, 0xd5,
79 0x3a, 0x24, 0x74, 0xe2, 0xff, 0xe0, 0x70, 0xb1, 0xbd, 0xec, 0x0c, 0xd2, 0x97, 0x9a, 0xe5, 0x9f,
80 0xa9, 0xfe, 0x6a, 0x63, 0x17, 0x35, 0xad, 0x64, 0x2f, 0xd9, 0x2e, 0xdb, 0x47, 0xdc, 0x62, 0xdc,
81 0xcc, 0xee, 0x7e, 0x23, 0xa6, 0x67, 0x61, 0x7c, 0xd1, 0x03, 0xbd, 0x78, 0xe9, 0x34, 0x05, 0xed,
82 0x05, 0x87, 0xef, 0x59, 0xf4, 0x16, 0xd6, 0x8d, 0x85, 0x46, 0x65, 0x2a, 0x08, 0xac, 0x4a, 0x5d,
83 0xe6, 0x27, 0x5f, 0x43, 0xdd, 0x51, 0x4e, 0x95, 0x9b, 0xf5, 0x0c, 0x81, 0x24, 0x73, 0x39, 0x77,
84 0xe9, 0xc8, 0x35, 0x4a, 0xe2, 0xb8, 0x35, 0x92, 0xde, 0x5c, 0x31, 0x12, 0x36, 0x5c, 0xc7, 0x69,
85 0xcd, 0x79, 0xa9, 0xf9, 0xcf, 0x13, 0xa9, 0x12, 0x29, 0x25, 0x5c, 0x6a, 0x34, 0xa4, 0xbf, 0xc5,
86 0xb6, 0x2a, 0xc1, 0xba, 0x6a, 0xd3, 0x98, 0x8c, 0x9b, 0x6d, 0x9f, 0xb9, 0x25, 0xa6, 0xd1, 0x97,
87 0x80, 0x38, 0x11, 0xdc, 0x73, 0x5c, 0xe7, 0x3a, 0x1f, 0xd2, 0x16, 0xcd, 0x63, 0xfb, 0x41, 0xb0,
88 0xba, 0xb0, 0x38, 0x67, 0x48, 0xd2, 0x8a, 0x94, 0x2f, 0x11, 0x81, 0xbf, 0x66, 0x38, 0x68, 0xff,
89 0xfe, 0xd1, 0x7c, 0xcd, 0xa3, 0xac, 0xe4, 0xf7, 0x58, 0x19, 0xcd, 0x2a, 0xe3, 0xfa, 0x4d, 0xb0,
90 0xbe, 0xac, 0x05, 0x1c, 0xd9, 0x8d, 0xf7, 0x5c, 0xc0, 0xfc, 0xa6, 0xb5, 0x99, 0xb8, 0x8e, 0x2b,
91 0x72, 0xf8, 0x19, 0xfc, 0x17, 0x11, 0xf6, 0x2b, 0x08, 0xe4, 0x6e, 0xb0, 0x65, 0xab, 0x78, 0x8a,
92 0xfc, 0x7c, 0x09, 0xca, 0x73, 0xcd, 0x35, 0x5d, 0x6c, 0x7a, 0x36, 0xc0, 0x24, 0xba, 0x3f, 0x08,
93 0xea, 0x17, 0x09, 0xe1, 0x9d, 0x5d, 0x18, 0x59, 0x8a, 0xd8, 0x6a, 0x6d, 0x85, 0x6a, 0x9e, 0xa9,
94 0xe5, 0x4b, 0x45, 0xb2, 0x35, 0x6e, 0x62, 0x24, 0x08, 0x00, 0x1c, 0x06, 0x73, 0x27, 0x5d, 0x11,
95 0x4a, 0xc8, 0x51, 0xbd, 0x59, 0xd6, 0x94, 0xce, 0x16, 0x15, 0x17, 0x58, 0x7f, 0x39, 0x9d, 0x4e,
96 0x69, 0x1a, 0x64, 0xbb, 0xd4, 0x51, 0xb9, 0xe4, 0x7d, 0x51, 0x3a, 0xff, 0xe5, 0x1f, 0x29, 0xea,
97 0x7e, 0xa5, 0x62, 0x63, 0xff, 0x10, 0xf7, 0x54, 0x35, 0xd1, 0xf3, 0x73, 0x1e, 0xab, 0xca, 0x52,
98 0x14, 0xc6, 0x7e, 0x51, 0xc2, 0x48, 0x13, 0xcb, 0x30, 0xb2, 0x1a, 0x84, 0x72, 0xe5, 0x44, 0x83,
99 0xc9, 0x90, 0xa5, 0x8c, 0xf9, 0xeb, 0x3c, 0x5c, 0xc6, 0xcc, 0x8a, 0x95, 0x8a, 0xfa, 0xeb, 0x37,
100 0x9c, 0xde, 0xa2, 0xb1, 0x72, 0x4d, 0xd9, 0x3d, 0xab, 0xfd, 0x0e, 0xbd, 0x32, 0x9d, 0x23, 0xe9,
101 0x6f, 0x85, 0x4e, 0xfe, 0xcd, 0x91, 0xfb, 0x82, 0x94, 0xee, 0x8b, 0xdf, 0x6a, 0xd9, 0x01, 0xa1,
102 0xc6, 0x22, 0x18, 0x01, 0x8d, 0x10, 0xd5, 0x87, 0x42, 0xd0, 0xbd, 0x23, 0x75, 0x44, 0x53, 0x46,
103 0xa5, 0xae, 0x00, 0x4c, 0x0e, 0x88, 0x4a, 0xa8, 0x3d, 0x4a, 0x30, 0xe0, 0x1a, 0xa4, 0xe5, 0x40,
104 0xb8, 0xe0, 0x12, 0x9c, 0x44, 0x03, 0xfb, 0x2e, 0x4e, 0xf5, 0x29, 0xdb, 0x09, 0x84, 0x55, 0xc7,
105 0x6c, 0xc6, 0x1f, 0xf9, 0xee, 0x0b, 0xa4, 0x91, 0x7d, 0x79, 0x27, 0x59, 0x75, 0x97, 0xec, 0x6a,
106 0xa8, 0xf8, 0x55, 0xa8, 0x45, 0xd4, 0xd7, 0xa6, 0xc1, 0xc4, 0x27, 0x35, 0xe8, 0x4f, 0x39, 0x89,
107 0x7d, 0x41, 0xf3, 0xf6, 0xd0, 0xb6, 0xf9, 0x91, 0xeb, 0x94, 0xf1, 0xbb, 0x17, 0x46, 0x9c, 0xd5,
108 0x5a, 0x53, 0x04, 0x2d, 0x12, 0x7c, 0x17, 0x6a, 0x36, 0xb5, 0xea, 0xf3, 0x5b, 0x96, 0x1b, 0xee,
109 0xce, 0xc4, 0xc0, 0x11, 0x5a, 0xbc, 0x0c, 0x29, 0xd0, 0x42, 0x1d, 0x16, 0x63, 0xea, 0x1e, 0x04,
110 0x2f, 0xe3, 0x17, 0xed, 0x33, 0xac, 0x56, 0x80, 0x34, 0x41, 0x41, 0x1e, 0x77, 0x80, 0x06, 0x9f,
111 0xbc, 0x2e, 0x78, 0xa1, 0x04, 0x00, 0x06, 0x6f, 0x36, 0x2f, 0xb7, 0xa5, 0x95, 0x37, 0x82, 0x9d,
112 0xef, 0x41, 0x08, 0x85, 0x3d, 0x53, 0xa7, 0xfb, 0xfe, 0xba, 0x8c, 0xb9, 0xae, 0xc7, 0x89, 0x11,
113 0x69, 0x4f, 0x62, 0xe6, 0xb6, 0x08, 0x6b, 0x35, 0x1c, 0x96, 0xb3, 0x7b, 0x40, 0x2d, 0xee, 0x07,
114 0x40, 0x52, 0x4f, 0x68, 0x60, 0xf4, 0xb9, 0xc3, 0x54, 0x9f, 0x22, 0x50, 0x88, 0x48, 0x6a, 0x28,
115 0x93, 0x46, 0x00, 0xe2, 0x4a, 0x85, 0x41, 0x78, 0x0e, 0x87, 0xc5, 0xeb, 0xfc, 0xd3, 0x5f, 0x4d,
116 0x24, 0xe4, 0x9d, 0xeb, 0x1d, 0x00, 0x73, 0x85, 0x25, 0x47, 0x9e, 0x8c, 0x5b, 0x88, 0xf4, 0x3b,
117 0x33, 0xf0, 0x3d, 0x3a, 0xa1, 0x28, 0xd3, 0x06, 0xb4, 0x7a, 0x4e, 0x5d, 0x31, 0x1b, 0xca, 0xf4,
118 0x3f, 0x70, 0x30, 0x49, 0x44, 0x29, 0x24, 0x14, 0x5e, 0x35, 0xc2, 0x6c, 0x92, 0x7e, 0xf8, 0x97,
119 0x0c, 0x51, 0x9d, 0x67, 0xc0, 0x10, 0xa9, 0x35, 0x48, 0x59, 0x6a, 0x33, 0xef, 0x40, 0x4e, 0x53,
120 0x10, 0x14, 0x2a, 0x12, 0x38, 0xe6, 0xc4, 0x63, 0x9c, 0x84, 0x85, 0x06, 0xaf, 0x3d, 0x3a, 0x84,
121 0x06, 0x60, 0x88, 0x32, 0xda, 0x2c, 0xe5, 0xc6, 0x59, 0xf1, 0xe0, 0x10, 0xe2, 0x3c, 0xe6, 0xbf,
122 0x32, 0x7d, 0x32, 0x39, 0x6d, 0xe4, 0xd9, 0xca, 0xe7, 0xf5, 0xf4, 0xa6, 0x5f, 0xb2, 0x33, 0x05,
123 0xb5, 0xad, 0x5f, 0xcb, 0x0b, 0x14, 0xaf, 0xeb, 0xc0, 0xec, 0x87, 0x85, 0x9b, 0x13, 0xb5, 0x8a,
124 0x98, 0xa9, 0x92, 0x13, 0x1b, 0x74, 0xec, 0xfd, 0xe1, 0xc1, 0x22, 0x06, 0x5d, 0x4f, 0x06, 0xc7,
125 0xdd, 0xc6, 0xf0, 0xc4, 0x01, 0x04, 0xad, 0x7f, 0x71, 0xbc, 0x74, 0x4d, 0xfd, 0x18, 0xa3, 0x56,
126 0x2c, 0x45, 0x28, 0x2b, 0x2f, 0xbc, 0x9b, 0xb8, 0x4b, 0xe6, 0x51, 0x75, 0x28, 0x0c, 0x27, 0x0e,
127 0xf7, 0x92, 0x8c, 0xc9, 0xde, 0x33, 0x1a, 0x65, 0x28, 0xc7, 0x01, 0x32, 0xa2, 0x36, 0x88, 0xb6,
128 0x64, 0x10, 0x03, 0xd6, 0xb7, 0x9f, 0x9d, 0x73, 0xe1, 0xa9, 0xc7, 0xdf, 0xe1, 0x0b, 0x39, 0x31,
129 0x77, 0xbc, 0x91, 0xf1, 0x45, 0x9a, 0xc5, 0x97, 0x28, 0xc0, 0x61, 0xc5, 0x23, 0x54, 0xad, 0xe3,
130 0x23, 0x18, 0x69, 0xf7, 0x27, 0xd0, 0x5b, 0xf2, 0x44, 0x62, 0xdc, 0x97, 0xce, 0x4e, 0x40, 0x76,
131 0x00, 0xde, 0xc2, 0xf9, 0x3a, 0x42, 0xfd, 0xd4, 0xd7, 0xe1, 0x85, 0xd8, 0xc9, 0x38, 0x91, 0xc1,
132 0x79, 0x87, 0x58, 0xf1, 0x26, 0x1a, 0x29, 0x02, 0xe3, 0x54, 0xde, 0x58, 0x64, 0x9d, 0xe6, 0x8e,
133 0x33, 0x70, 0x53, 0x43, 0x47, 0x90, 0xee, 0x6e, 0x0f, 0x8c, 0xb3, 0x9e, 0x47, 0x45, 0xfc, 0xa8,
134 0xe3, 0x52, 0x62, 0x74, 0x6d, 0xa2, 0xaf, 0x28, 0x9d, 0xdf, 0x1e, 0x69, 0x1f, 0x56, 0xbc, 0x49,
135 0xc1, 0xe5, 0xd6, 0xc4, 0xb5, 0x5c, 0x4d, 0x39, 0x49, 0x4b, 0xb4, 0xec, 0x56, 0x54, 0x9a, 0x15,
136 0x94, 0x0a, 0xcb, 0xa9, 0x10, 0x46, 0x03, 0x5c, 0x23, 0x2f, 0x29, 0xed, 0x72, 0xa1, 0x57, 0xfa,
137 0x58, 0xef, 0x21, 0x7e, 0xf2, 0x8b, 0xa7, 0x04, 0x51, 0xb4, 0x03, 0x5d, 0xd8, 0x48, 0xc0, 0xe5,
138 0x83, 0xb6, 0x7a, 0x6b, 0xcd, 0xfb, 0xda, 0x47, 0xe8, 0xa1, 0xae, 0x57, 0x74, 0x49, 0xc0, 0xf9,
139 0x4a, 0x6b, 0x3c, 0xb5, 0xd8, 0x27, 0x3d, 0x1d, 0x96, 0x39, 0x09, 0x65, 0x95, 0xdb, 0x01, 0xa3,
140 0x8b, 0x78, 0x8b, 0x07, 0x6d, 0x1c, 0x8b, 0x4b, 0x1d, 0x9d, 0x4a, 0x4f, 0xcb, 0xb8, 0xf6, 0x22,
141 0x73, 0x8a, 0x7b, 0xc8, 0xf2, 0x0a, 0xef, 0x03, 0x1e, 0xb7, 0x4d, 0x8f, 0xc0, 0xdf, 0x87, 0x88,
142 0x05, 0xe1, 0x0a, 0x30, 0xea, 0xde, 0xf3, 0xc2, 0xb6, 0x00, 0x3c, 0xd6, 0xff, 0x3b, 0xb5, 0x01,
143 0xfb, 0xd8, 0xb2, 0x65, 0x26, 0x5d, 0xa0, 0x5a, 0x7c, 0xef, 0x1d, 0x85, 0xbe, 0x51, 0xc2, 0x57,
144 0x0b, 0x27, 0x37, 0x71, 0x99, 0xf5, 0x87, 0x83, 0x68, 0x0b, 0x88, 0xed, 0x66, 0x9e, 0x37, 0x59,
145 0x84, 0x23, 0x72, 0xc3, 0x80, 0xac, 0xfe, 0x45, 0x5f, 0xdf, 0x31, 0xc4, 0x84, 0x07, 0x5a, 0x17,
146 0x28, 0xcd, 0x64, 0xb4, 0xe2, 0xa3, 0x0e, 0x2c, 0x15, 0x60, 0x77, 0xdc, 0x08, 0x45, 0x36, 0x37,
147 0x68, 0x50, 0xba, 0x03, 0x85, 0xb7, 0xed, 0xd0, 0x7b, 0xb2, 0xa1, 0x62, 0xbc, 0x70, 0x00, 0x9e,
148];
149
150#[derive(Debug, Clone, Serialize, Deserialize)]
152pub struct UpdateCheck {
153 pub current_version: String,
154 pub latest_version: String,
155 pub update_available: bool,
156 pub download_url: Option<String>,
157}
158
159impl UpdateCheck {
160 pub fn force(&mut self) -> Result<()> {
164 self.update_available = true;
165 self.download_url = Some(build_download_url(&self.latest_version)?);
166 Ok(())
167 }
168}
169
170#[derive(Debug, Clone, Serialize, Deserialize)]
172pub struct UpdateResult {
173 pub previous_version: String,
174 pub new_version: String,
175}
176
177pub async fn check_for_update(current_version: &str) -> Result<UpdateCheck> {
181 let latest = fetch_latest_cli_version().await?;
182 let current = parse_version(current_version)?;
183 let latest_parsed = parse_version(&latest)?;
184 let update_available = latest_parsed > current;
185
186 let download_url = if update_available {
187 Some(build_download_url(&latest)?)
188 } else {
189 None
190 };
191
192 Ok(UpdateCheck {
193 current_version: current_version.to_string(),
194 latest_version: latest,
195 update_available,
196 download_url,
197 })
198}
199
200pub async fn perform_update(
206 check: &UpdateCheck,
207 progress: &dyn ProgressReporter,
208) -> Result<UpdateResult> {
209 let download_url = check.download_url.as_deref().ok_or_else(|| {
210 Error::UpdateFailed("no download URL — are you already on the latest version?".to_string())
211 })?;
212
213 let tmp_dir = tempfile::tempdir()
214 .map_err(|e| Error::UpdateFailed(format!("failed to create temp directory: {e}")))?;
215
216 let (archive_path, archive_bytes) =
217 download_archive(download_url, tmp_dir.path(), progress).await?;
218
219 let sig_url = format!("{download_url}.sig");
220 progress.report_started("Downloading signature...");
221 let sig_bytes = download_bytes(&sig_url).await?;
222
223 progress.report_started("Verifying ML-DSA signature...");
224 verify_signature(&archive_bytes, &sig_bytes)?;
225 progress.report_complete("Signature verified");
226
227 progress.report_started("Extracting archive...");
228 let extracted = if download_url.ends_with(".zip") {
229 extract_zip(&archive_bytes, tmp_dir.path(), CLI_BINARY_NAME)?
230 } else {
231 extract_tar_gz(&archive_bytes, tmp_dir.path(), CLI_BINARY_NAME)?
232 };
233 let binary_path = extracted.binary_path;
234
235 let actual_version = extract_version(&binary_path).await;
237 if let Ok(ref v) = actual_version {
238 if v != &check.latest_version {
239 return Err(Error::UpdateFailed(format!(
240 "version mismatch: expected {}, binary reports {v}",
241 check.latest_version
242 )));
243 }
244 }
245
246 replace_binary(&binary_path)?;
247
248 let _ = std::fs::remove_file(&archive_path);
251
252 Ok(UpdateResult {
253 previous_version: check.current_version.clone(),
254 new_version: check.latest_version.clone(),
255 })
256}
257
258async fn fetch_latest_cli_version() -> Result<String> {
263 let url = format!("https://api.github.com/repos/{GITHUB_REPO}/releases?per_page=100");
265 let client = reqwest::Client::new();
266 let resp = client
267 .get(&url)
268 .header("User-Agent", "ant-cli")
269 .header("Accept", "application/vnd.github+json")
270 .send()
271 .await
272 .map_err(|e| Error::UpdateFailed(format!("failed to fetch releases: {e}")))?;
273
274 if !resp.status().is_success() {
275 return Err(Error::UpdateFailed(format!(
276 "GitHub API returned status {} when fetching releases",
277 resp.status()
278 )));
279 }
280
281 let releases: Vec<serde_json::Value> = resp
282 .json()
283 .await
284 .map_err(|e| Error::UpdateFailed(format!("failed to parse releases JSON: {e}")))?;
285
286 let mut best: Option<semver::Version> = None;
288 for release in &releases {
289 if release["draft"].as_bool().unwrap_or(false)
290 || release["prerelease"].as_bool().unwrap_or(false)
291 {
292 continue;
293 }
294 let tag = release["tag_name"].as_str().unwrap_or_default();
295 if let Some(version_str) = tag.strip_prefix(TAG_PREFIX) {
296 if let Ok(v) = semver::Version::parse(version_str) {
297 if best.as_ref().is_none_or(|b| v > *b) {
298 best = Some(v);
299 }
300 }
301 }
302 }
303
304 best.map(|v| v.to_string())
305 .ok_or_else(|| Error::UpdateFailed("no ant-cli release found on GitHub".to_string()))
306}
307
308async fn download_archive(
313 url: &str,
314 tmp_dir: &Path,
315 progress: &dyn ProgressReporter,
316) -> Result<(PathBuf, Vec<u8>)> {
317 progress.report_started(&format!("Downloading {CLI_BINARY_NAME} from {url}"));
318
319 let client = reqwest::Client::new();
320 let resp = client
321 .get(url)
322 .header("User-Agent", "ant-cli")
323 .send()
324 .await
325 .map_err(|e| Error::UpdateFailed(format!("download request failed: {e}")))?;
326
327 if !resp.status().is_success() {
328 return Err(Error::UpdateFailed(format!(
329 "download returned status {}",
330 resp.status()
331 )));
332 }
333
334 let total_size = resp.content_length().unwrap_or(0);
335 let mut downloaded: u64 = 0;
336
337 let tmp_archive = tmp_dir.join(".download.tmp");
338 let mut tmp_file = std::fs::File::create(&tmp_archive)
339 .map_err(|e| Error::UpdateFailed(format!("failed to create temp file: {e}")))?;
340
341 let mut stream = resp.bytes_stream();
342 while let Some(chunk) = stream.next().await {
343 let chunk =
344 chunk.map_err(|e| Error::UpdateFailed(format!("download stream error: {e}")))?;
345 downloaded += chunk.len() as u64;
346 std::io::Write::write_all(&mut tmp_file, &chunk)
347 .map_err(|e| Error::UpdateFailed(format!("failed to write temp file: {e}")))?;
348 progress.report_progress(downloaded, total_size);
349 }
350 drop(tmp_file);
351
352 progress.report_complete("Download complete");
353
354 let bytes = std::fs::read(&tmp_archive)
355 .map_err(|e| Error::UpdateFailed(format!("failed to read temp file: {e}")))?;
356
357 Ok((tmp_archive, bytes))
358}
359
360async fn download_bytes(url: &str) -> Result<Vec<u8>> {
362 let client = reqwest::Client::new();
363 let resp = client
364 .get(url)
365 .header("User-Agent", "ant-cli")
366 .send()
367 .await
368 .map_err(|e| Error::UpdateFailed(format!("download request failed: {e}")))?;
369
370 if !resp.status().is_success() {
371 return Err(Error::UpdateFailed(format!(
372 "download returned status {} for {url}",
373 resp.status()
374 )));
375 }
376
377 resp.bytes()
378 .await
379 .map(|b| b.to_vec())
380 .map_err(|e| Error::UpdateFailed(format!("failed to read response body: {e}")))
381}
382
383fn verify_signature(archive_bytes: &[u8], signature_bytes: &[u8]) -> Result<()> {
385 if signature_bytes.len() != SIGNATURE_SIZE {
386 return Err(Error::UpdateFailed(format!(
387 "invalid signature size: expected {SIGNATURE_SIZE}, got {}",
388 signature_bytes.len()
389 )));
390 }
391
392 let public_key = MlDsaPublicKey::from_bytes(MlDsaVariant::MlDsa65, RELEASE_SIGNING_KEY)
393 .map_err(|e| Error::UpdateFailed(format!("invalid embedded release key: {e}")))?;
394
395 let sig = MlDsaSignature::from_bytes(MlDsaVariant::MlDsa65, signature_bytes)
396 .map_err(|e| Error::UpdateFailed(format!("invalid signature format: {e}")))?;
397
398 let dsa = ml_dsa_65();
399 let valid = dsa
400 .verify_with_context(&public_key, archive_bytes, &sig, SIGNING_CONTEXT)
401 .map_err(|e| Error::UpdateFailed(format!("signature verification error: {e}")))?;
402
403 if valid {
404 Ok(())
405 } else {
406 Err(Error::UpdateFailed(
407 "signature verification failed: archive may be corrupted or tampered".to_string(),
408 ))
409 }
410}
411
412async fn extract_version(binary_path: &Path) -> Result<String> {
414 let mut cmd = tokio::process::Command::new(binary_path);
415 cmd.arg("--version");
416 #[cfg(windows)]
420 {
421 const CREATE_NO_WINDOW: u32 = 0x08000000;
422 cmd.creation_flags(CREATE_NO_WINDOW);
423 }
424 let output = cmd.output().await.map_err(|e| {
425 Error::UpdateFailed(format!(
426 "failed to run {} --version: {e}",
427 binary_path.display()
428 ))
429 })?;
430
431 if !output.status.success() {
432 return Err(Error::UpdateFailed(format!(
433 "{} --version exited with status {}",
434 binary_path.display(),
435 output.status
436 )));
437 }
438
439 let stdout = String::from_utf8_lossy(&output.stdout);
440 Ok(parse_version_from_stdout(&stdout).to_string())
441}
442
443fn parse_version_from_stdout(stdout: &str) -> &str {
451 stdout
452 .lines()
453 .next()
454 .unwrap_or_default()
455 .split_whitespace()
456 .last()
457 .unwrap_or("unknown")
458}
459
460fn replace_binary(new_binary: &Path) -> Result<()> {
465 self_replace::self_replace(new_binary)
466 .map_err(|e| Error::UpdateFailed(format!("failed to replace binary: {e}")))?;
467 Ok(())
468}
469
470pub fn build_download_url(version: &str) -> Result<String> {
475 let asset_name = cli_platform_asset_name(version)?;
476 Ok(format!(
477 "https://github.com/{GITHUB_REPO}/releases/download/{TAG_PREFIX}{version}/{asset_name}"
478 ))
479}
480
481fn cli_platform_asset_name(version: &str) -> Result<String> {
485 let target_triple = if cfg!(all(target_os = "linux", target_arch = "x86_64")) {
486 "x86_64-unknown-linux-musl"
487 } else if cfg!(all(target_os = "linux", target_arch = "aarch64")) {
488 "aarch64-unknown-linux-musl"
489 } else if cfg!(all(target_os = "macos", target_arch = "x86_64")) {
490 "x86_64-apple-darwin"
491 } else if cfg!(all(target_os = "macos", target_arch = "aarch64")) {
492 "aarch64-apple-darwin"
493 } else if cfg!(all(target_os = "windows", target_arch = "x86_64")) {
494 "x86_64-pc-windows-msvc"
495 } else {
496 return Err(Error::UpdateFailed(format!(
497 "unsupported platform: {}-{}",
498 std::env::consts::OS,
499 std::env::consts::ARCH
500 )));
501 };
502
503 let ext = if cfg!(target_os = "windows") {
504 "zip"
505 } else {
506 "tar.gz"
507 };
508
509 Ok(format!("ant-{version}-{target_triple}.{ext}"))
510}
511
512fn parse_version(version: &str) -> Result<semver::Version> {
513 let cleaned = version.strip_prefix('v').unwrap_or(version);
514 semver::Version::parse(cleaned)
515 .map_err(|e| Error::UpdateFailed(format!("invalid version '{version}': {e}")))
516}
517
518#[cfg(test)]
519mod tests {
520 use super::*;
521
522 #[test]
523 fn parse_version_from_stdout_single_line() {
524 assert_eq!(parse_version_from_stdout("ant 0.1.4"), "0.1.4");
525 assert_eq!(parse_version_from_stdout("ant 0.1.4\n"), "0.1.4");
526 }
527
528 #[test]
529 fn parse_version_from_stdout_multi_line_license_trailer() {
530 let stdout = "ant 0.1.4\n\
532 Autonomi network client\n\
533 \n\
534 Repository: https://github.com/WithAutonomi/ant-client\n\
535 License: MIT or Apache-2.0\n";
536 assert_eq!(parse_version_from_stdout(stdout), "0.1.4");
537 }
538
539 #[test]
540 fn parse_version_from_stdout_empty() {
541 assert_eq!(parse_version_from_stdout(""), "unknown");
542 }
543
544 #[test]
545 fn parse_version_from_stdout_blank_first_line() {
546 assert_eq!(parse_version_from_stdout("\nant 0.1.4\n"), "unknown");
547 }
548
549 #[test]
550 fn parse_version_from_stdout_leading_whitespace() {
551 assert_eq!(parse_version_from_stdout(" ant 0.1.4\n"), "0.1.4");
552 }
553
554 #[test]
555 fn parse_version_valid() {
556 assert!(parse_version("1.2.3").is_ok());
557 assert!(parse_version("v1.2.3").is_ok());
558 assert!(parse_version("0.1.0").is_ok());
559 }
560
561 #[test]
562 fn parse_version_invalid() {
563 assert!(parse_version("not-a-version").is_err());
564 assert!(parse_version("").is_err());
565 }
566
567 #[test]
568 fn version_comparison() {
569 let v1 = parse_version("0.1.0").unwrap();
570 let v2 = parse_version("0.2.0").unwrap();
571 assert!(v2 > v1);
572
573 let v3 = parse_version("1.0.0").unwrap();
574 assert!(v3 > v2);
575
576 let same = parse_version("0.1.0").unwrap();
577 assert_eq!(v1, same);
578 }
579
580 #[test]
581 fn check_result_no_update() {
582 let check = UpdateCheck {
583 current_version: "1.0.0".to_string(),
584 latest_version: "1.0.0".to_string(),
585 update_available: false,
586 download_url: None,
587 };
588 assert!(!check.update_available);
589 assert!(check.download_url.is_none());
590 }
591
592 #[test]
593 fn check_result_with_update() {
594 let check = UpdateCheck {
595 current_version: "0.1.0".to_string(),
596 latest_version: "0.2.0".to_string(),
597 update_available: true,
598 download_url: Some("https://example.com/ant.tar.gz".to_string()),
599 };
600 assert!(check.update_available);
601 assert!(check.download_url.is_some());
602 }
603
604 #[test]
605 fn force_populates_download_url() {
606 let mut check = UpdateCheck {
607 current_version: "1.0.0".to_string(),
608 latest_version: "1.0.0".to_string(),
609 update_available: false,
610 download_url: None,
611 };
612 check.force().unwrap();
613 assert!(check.update_available);
614 assert!(check.download_url.is_some());
615 }
616
617 #[test]
618 fn platform_asset_name_format() {
619 let name = cli_platform_asset_name("1.2.3").unwrap();
620 assert!(name.starts_with("ant-1.2.3-"));
621 assert!(
622 name.ends_with(".tar.gz") || name.ends_with(".zip"),
623 "unexpected extension: {name}"
624 );
625 }
626
627 #[test]
628 fn build_download_url_format() {
629 let url = build_download_url("1.2.3").unwrap();
630 assert!(url.starts_with(
631 "https://github.com/WithAutonomi/ant-client/releases/download/ant-cli-v1.2.3/ant-1.2.3-"
632 ));
633 assert!(url.ends_with(".tar.gz") || url.ends_with(".zip"));
634 }
635
636 #[test]
637 fn update_check_serializes() {
638 let check = UpdateCheck {
639 current_version: "0.1.0".to_string(),
640 latest_version: "0.2.0".to_string(),
641 update_available: true,
642 download_url: Some("https://example.com/ant.tar.gz".to_string()),
643 };
644 let json = serde_json::to_string(&check).unwrap();
645 let deserialized: UpdateCheck = serde_json::from_str(&json).unwrap();
646 assert_eq!(deserialized.current_version, "0.1.0");
647 assert!(deserialized.update_available);
648 }
649
650 #[test]
651 fn verify_signature_rejects_wrong_size() {
652 let result = verify_signature(b"some archive data", &[0u8; 100]);
653 assert!(result.is_err());
654 let err = result.unwrap_err().to_string();
655 assert!(err.contains("invalid signature size"), "got: {err}");
656 }
657
658 #[test]
659 fn verify_signature_rejects_invalid_signature() {
660 let invalid_sig = vec![0u8; SIGNATURE_SIZE];
661 let result = verify_signature(b"some archive data", &invalid_sig);
662 assert!(result.is_err());
663 }
664
665 #[test]
666 fn verify_signature_valid_roundtrip() {
667 let dsa = ml_dsa_65();
668 let (public_key, secret_key) = dsa.generate_keypair().unwrap();
669 let archive = b"fake archive content for testing";
670
671 let sig = dsa
672 .sign_with_context(&secret_key, archive, SIGNING_CONTEXT)
673 .unwrap();
674
675 let parsed_sig =
677 MlDsaSignature::from_bytes(MlDsaVariant::MlDsa65, &sig.to_bytes()).unwrap();
678 let valid = dsa
679 .verify_with_context(&public_key, archive, &parsed_sig, SIGNING_CONTEXT)
680 .unwrap();
681 assert!(valid);
682 }
683}