1use crate::release_set::dfx_root;
2use serde::{Deserialize, Serialize};
3use std::{fs, path::Path, path::PathBuf};
4
5pub(super) const INSTALL_STATE_SCHEMA_VERSION: u32 = 1;
6const CURRENT_NETWORK_FILE: &str = "current-network";
7const CURRENT_FLEET_FILE: &str = "current-fleet";
8
9#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
14pub struct InstallState {
15 pub schema_version: u32,
16 pub fleet: String,
17 pub installed_at_unix_secs: u64,
18 pub network: String,
19 pub root_target: String,
20 pub root_canister_id: String,
21 pub root_build_target: String,
22 pub workspace_root: String,
23 pub dfx_root: String,
24 pub config_path: String,
25 pub release_set_manifest_path: String,
26}
27
28#[derive(Clone, Debug, Eq, PartialEq)]
33pub struct FleetSummary {
34 pub name: String,
35 pub current: bool,
36 pub state: InstallState,
37}
38
39pub(super) fn read_install_state(
41 dfx_root: &Path,
42 network: &str,
43) -> Result<Option<InstallState>, Box<dyn std::error::Error>> {
44 if let Some(fleet) = read_selected_fleet_name(dfx_root, network)? {
45 return read_fleet_install_state(dfx_root, network, &fleet);
46 }
47
48 Ok(None)
49}
50
51pub(super) fn read_fleet_install_state(
53 dfx_root: &Path,
54 network: &str,
55 fleet: &str,
56) -> Result<Option<InstallState>, Box<dyn std::error::Error>> {
57 validate_fleet_name(fleet)?;
58 let path = fleet_install_state_path(dfx_root, network, fleet);
59 if !path.is_file() {
60 return Ok(None);
61 }
62
63 let bytes = fs::read(&path)?;
64 let state: InstallState = serde_json::from_slice(&bytes)?;
65 Ok(Some(state))
66}
67
68pub fn read_current_install_state(
70 network: &str,
71) -> Result<Option<InstallState>, Box<dyn std::error::Error>> {
72 let dfx_root = dfx_root()?;
73 read_install_state(&dfx_root, network)
74}
75
76pub fn read_current_network_name() -> Result<Option<String>, Box<dyn std::error::Error>> {
78 let dfx_root = dfx_root()?;
79 read_selected_network_name(&dfx_root)
80}
81
82pub fn select_current_network_name(network: &str) -> Result<(), Box<dyn std::error::Error>> {
84 let dfx_root = dfx_root()?;
85 write_current_network_name(&dfx_root, network)
86}
87
88pub fn read_current_fleet_name(
90 network: &str,
91) -> Result<Option<String>, Box<dyn std::error::Error>> {
92 let dfx_root = dfx_root()?;
93 read_selected_fleet_name(&dfx_root, network)
94}
95
96pub fn read_current_or_fleet_install_state(
98 network: &str,
99 fleet: Option<&str>,
100) -> Result<Option<InstallState>, Box<dyn std::error::Error>> {
101 let dfx_root = dfx_root()?;
102 match fleet {
103 Some(fleet) => read_fleet_install_state(&dfx_root, network, fleet),
104 None => read_install_state(&dfx_root, network),
105 }
106}
107
108pub fn list_current_fleets(network: &str) -> Result<Vec<FleetSummary>, Box<dyn std::error::Error>> {
110 let dfx_root = dfx_root()?;
111 list_fleets(&dfx_root, network)
112}
113
114pub(super) fn list_fleets(
116 dfx_root: &Path,
117 network: &str,
118) -> Result<Vec<FleetSummary>, Box<dyn std::error::Error>> {
119 let current = read_selected_fleet_name(dfx_root, network)?;
120 let mut fleets = Vec::new();
121 let dir = fleets_dir(dfx_root, network);
122 if dir.is_dir() {
123 for entry in fs::read_dir(&dir)? {
124 let entry = entry?;
125 let path = entry.path();
126 if path.extension().and_then(|ext| ext.to_str()) != Some("json") {
127 continue;
128 }
129 let Some(name) = path.file_stem().and_then(|stem| stem.to_str()) else {
130 continue;
131 };
132 if let Some(state) = read_fleet_install_state(dfx_root, network, name)? {
133 fleets.push(FleetSummary {
134 name: name.to_string(),
135 current: current.as_deref() == Some(name),
136 state,
137 });
138 }
139 }
140 }
141
142 fleets.sort_by(|left, right| left.name.cmp(&right.name));
143 Ok(fleets)
144}
145
146pub fn select_current_fleet(
148 network: &str,
149 fleet: &str,
150) -> Result<InstallState, Box<dyn std::error::Error>> {
151 let dfx_root = dfx_root()?;
152 select_fleet(&dfx_root, network, fleet)
153}
154
155pub fn select_current_fleet_name(
157 network: &str,
158 fleet: &str,
159) -> Result<(), Box<dyn std::error::Error>> {
160 let dfx_root = dfx_root()?;
161 write_current_fleet_name(&dfx_root, network, fleet)
162}
163
164pub fn clear_current_fleet_name_if_matches(
166 fleet: &str,
167) -> Result<Vec<String>, Box<dyn std::error::Error>> {
168 let dfx_root = dfx_root()?;
169 clear_selected_fleet_name_if_matches(&dfx_root, fleet)
170}
171
172fn select_fleet(
174 dfx_root: &Path,
175 network: &str,
176 fleet: &str,
177) -> Result<InstallState, Box<dyn std::error::Error>> {
178 let Some(state) = read_fleet_install_state(dfx_root, network, fleet)? else {
179 return Err(format!("unknown fleet {fleet} on network {network}").into());
180 };
181 write_current_fleet_name(dfx_root, network, fleet)?;
182 Ok(state)
183}
184
185#[must_use]
187pub(super) fn current_network_path(dfx_root: &Path) -> PathBuf {
188 dfx_root.join(".canic").join(CURRENT_NETWORK_FILE)
189}
190
191#[must_use]
193pub(super) fn fleet_install_state_path(dfx_root: &Path, network: &str, fleet: &str) -> PathBuf {
194 fleets_dir(dfx_root, network).join(format!("{fleet}.json"))
195}
196
197#[must_use]
199pub(super) fn current_fleet_path(dfx_root: &Path, network: &str) -> PathBuf {
200 dfx_root
201 .join(".canic")
202 .join(network)
203 .join(CURRENT_FLEET_FILE)
204}
205
206fn fleets_dir(dfx_root: &Path, network: &str) -> PathBuf {
208 dfx_root.join(".canic").join(network).join("fleets")
209}
210
211pub(super) fn write_install_state(
213 dfx_root: &Path,
214 network: &str,
215 state: &InstallState,
216) -> Result<PathBuf, Box<dyn std::error::Error>> {
217 validate_fleet_name(&state.fleet)?;
218 let path = fleet_install_state_path(dfx_root, network, &state.fleet);
219 if let Some(parent) = path.parent() {
220 fs::create_dir_all(parent)?;
221 }
222 fs::write(&path, serde_json::to_vec_pretty(state)?)?;
223 write_current_fleet_name(dfx_root, network, &state.fleet)?;
224 Ok(path)
225}
226
227fn read_selected_network_name(
229 dfx_root: &Path,
230) -> Result<Option<String>, Box<dyn std::error::Error>> {
231 let path = current_network_path(dfx_root);
232 if !path.is_file() {
233 return Ok(None);
234 }
235
236 let name = fs::read_to_string(path)?.trim().to_string();
237 validate_network_name(&name)?;
238 Ok(Some(name))
239}
240
241fn write_current_network_name(
243 dfx_root: &Path,
244 network: &str,
245) -> Result<(), Box<dyn std::error::Error>> {
246 validate_network_name(network)?;
247 let path = current_network_path(dfx_root);
248 if let Some(parent) = path.parent() {
249 fs::create_dir_all(parent)?;
250 }
251 fs::write(path, format!("{network}\n"))?;
252 Ok(())
253}
254
255pub(super) fn read_selected_fleet_name(
257 dfx_root: &Path,
258 network: &str,
259) -> Result<Option<String>, Box<dyn std::error::Error>> {
260 let path = current_fleet_path(dfx_root, network);
261 if !path.is_file() {
262 return Ok(None);
263 }
264
265 let name = fs::read_to_string(path)?.trim().to_string();
266 validate_fleet_name(&name)?;
267 Ok(Some(name))
268}
269
270fn write_current_fleet_name(
272 dfx_root: &Path,
273 network: &str,
274 fleet: &str,
275) -> Result<(), Box<dyn std::error::Error>> {
276 validate_fleet_name(fleet)?;
277 let path = current_fleet_path(dfx_root, network);
278 if let Some(parent) = path.parent() {
279 fs::create_dir_all(parent)?;
280 }
281 fs::write(path, format!("{fleet}\n"))?;
282 Ok(())
283}
284
285pub(super) fn clear_selected_fleet_name_if_matches(
287 dfx_root: &Path,
288 fleet: &str,
289) -> Result<Vec<String>, Box<dyn std::error::Error>> {
290 let canic_dir = dfx_root.join(".canic");
291 if !canic_dir.is_dir() {
292 return Ok(Vec::new());
293 }
294
295 let mut cleared = Vec::new();
296 for entry in fs::read_dir(canic_dir)? {
297 let entry = entry?;
298 if !entry.file_type()?.is_dir() {
299 continue;
300 }
301 let Some(network) = entry.file_name().to_str().map(str::to_string) else {
302 continue;
303 };
304 if validate_network_name(&network).is_err() {
305 continue;
306 }
307 let marker = current_fleet_path(dfx_root, &network);
308 if marker.is_file() && fs::read_to_string(&marker)?.trim() == fleet {
309 fs::remove_file(marker)?;
310 cleared.push(network);
311 }
312 }
313
314 cleared.sort();
315 Ok(cleared)
316}
317
318pub(super) fn validate_fleet_name(name: &str) -> Result<(), Box<dyn std::error::Error>> {
320 let valid = !name.is_empty()
321 && name
322 .bytes()
323 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_'));
324 if valid {
325 Ok(())
326 } else {
327 Err(format!("invalid fleet name {name:?}; use letters, numbers, '-' or '_'").into())
328 }
329}
330
331fn validate_network_name(name: &str) -> Result<(), Box<dyn std::error::Error>> {
333 let valid = !name.is_empty()
334 && name
335 .bytes()
336 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_'));
337 if valid {
338 Ok(())
339 } else {
340 Err(format!("invalid network name {name:?}; use letters, numbers, '-' or '_'").into())
341 }
342}