canic_host/install_root/
state.rs1use crate::release_set::icp_root;
2use serde::{Deserialize, Serialize};
3use std::{fs, path::Path, path::PathBuf};
4
5pub(super) const INSTALL_STATE_SCHEMA_VERSION: u32 = 1;
6
7#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
12pub struct InstallState {
13 pub schema_version: u32,
14 pub fleet: String,
15 pub installed_at_unix_secs: u64,
16 pub network: String,
17 pub root_target: String,
18 pub root_canister_id: String,
19 pub root_build_target: String,
20 pub workspace_root: String,
21 pub icp_root: String,
22 pub config_path: String,
23 pub release_set_manifest_path: String,
24}
25
26pub(super) fn read_fleet_install_state(
28 icp_root: &Path,
29 network: &str,
30 fleet: &str,
31) -> Result<Option<InstallState>, Box<dyn std::error::Error>> {
32 validate_network_name(network)?;
33 validate_fleet_name(fleet)?;
34 let path = fleet_install_state_path(icp_root, network, fleet);
35 if !path.is_file() {
36 return Ok(None);
37 }
38
39 let bytes = fs::read(&path)?;
40 let state: InstallState = serde_json::from_slice(&bytes)?;
41 Ok(Some(state))
42}
43
44pub fn read_named_fleet_install_state(
46 network: &str,
47 fleet: &str,
48) -> Result<Option<InstallState>, Box<dyn std::error::Error>> {
49 let icp_root = icp_root()?;
50 read_fleet_install_state(&icp_root, network, fleet)
51}
52
53pub fn read_named_fleet_install_state_from_root(
55 icp_root: &Path,
56 network: &str,
57 fleet: &str,
58) -> Result<Option<InstallState>, Box<dyn std::error::Error>> {
59 read_fleet_install_state(icp_root, network, fleet)
60}
61
62#[must_use]
64pub(super) fn fleet_install_state_path(icp_root: &Path, network: &str, fleet: &str) -> PathBuf {
65 fleets_dir(icp_root, network).join(format!("{fleet}.json"))
66}
67
68fn fleets_dir(icp_root: &Path, network: &str) -> PathBuf {
70 icp_root.join(".canic").join(network).join("fleets")
71}
72
73pub(super) fn write_install_state(
75 icp_root: &Path,
76 network: &str,
77 state: &InstallState,
78) -> Result<PathBuf, Box<dyn std::error::Error>> {
79 validate_network_name(network)?;
80 validate_fleet_name(&state.fleet)?;
81 let path = fleet_install_state_path(icp_root, network, &state.fleet);
82 if let Some(parent) = path.parent() {
83 fs::create_dir_all(parent)?;
84 }
85 remove_conflicting_fleet_states(icp_root, network, state)?;
86 fs::write(&path, serde_json::to_vec_pretty(state)?)?;
87 Ok(path)
88}
89
90fn remove_conflicting_fleet_states(
94 icp_root: &Path,
95 network: &str,
96 state: &InstallState,
97) -> Result<(), Box<dyn std::error::Error>> {
98 let dir = fleets_dir(icp_root, network);
99 if !dir.is_dir() {
100 return Ok(());
101 }
102
103 for entry in fs::read_dir(dir)? {
104 let entry = entry?;
105 let path = entry.path();
106 if !path.is_file() || path == fleet_install_state_path(icp_root, network, &state.fleet) {
107 continue;
108 }
109
110 let Ok(bytes) = fs::read(&path) else {
111 continue;
112 };
113 let Ok(existing) = serde_json::from_slice::<InstallState>(&bytes) else {
114 continue;
115 };
116 if existing.network == state.network
117 && (existing.root_target == state.root_target
118 || existing.root_canister_id == state.root_canister_id)
119 {
120 fs::remove_file(path)?;
121 }
122 }
123
124 Ok(())
125}
126
127pub(super) fn validate_fleet_name(name: &str) -> Result<(), Box<dyn std::error::Error>> {
129 let valid = !name.is_empty()
130 && name
131 .bytes()
132 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_'));
133 if valid {
134 Ok(())
135 } else {
136 Err(format!("invalid fleet name {name:?}; use letters, numbers, '-' or '_'").into())
137 }
138}
139
140fn validate_network_name(name: &str) -> Result<(), Box<dyn std::error::Error>> {
142 let valid = !name.is_empty()
143 && name
144 .bytes()
145 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_'));
146 if valid {
147 Ok(())
148 } else {
149 Err(format!("invalid network name {name:?}; use letters, numbers, '-' or '_'").into())
150 }
151}