alien_core/
embedded_config.rs1use serde::{de::DeserializeOwned, Deserialize, Serialize};
9
10pub const MAGIC_BYTES: &[u8; 8] = b"ALIENCFG";
12
13pub const FOOTER_SIZE: usize = 12;
15
16#[derive(Debug, Clone, Serialize, Deserialize)]
18#[serde(rename_all = "camelCase")]
19pub struct DeployCliConfig {
20 #[serde(skip_serializing_if = "Option::is_none")]
23 pub manager_url: Option<String>,
24 #[serde(skip_serializing_if = "Option::is_none")]
26 pub token: Option<String>,
27 #[serde(skip_serializing_if = "Option::is_none")]
29 pub deployment_group_id: Option<String>,
30 #[serde(skip_serializing_if = "Option::is_none")]
32 pub default_platform: Option<String>,
33 #[serde(skip_serializing_if = "Option::is_none")]
36 pub name: Option<String>,
37 #[serde(skip_serializing_if = "Option::is_none")]
39 pub display_name: Option<String>,
40}
41
42#[derive(Debug, Clone, Serialize, Deserialize)]
44#[serde(rename_all = "camelCase")]
45pub struct AgentConfig {
46 #[serde(skip_serializing_if = "Option::is_none")]
49 pub manager_url: Option<String>,
50 #[serde(skip_serializing_if = "Option::is_none")]
52 pub token: Option<String>,
53 #[serde(skip_serializing_if = "Option::is_none")]
55 pub deployment_id: Option<String>,
56 #[serde(default = "default_sync_interval")]
58 pub sync_interval_secs: u64,
59 #[serde(skip_serializing_if = "Option::is_none")]
62 pub name: Option<String>,
63 #[serde(skip_serializing_if = "Option::is_none")]
65 pub display_name: Option<String>,
66}
67
68fn default_sync_interval() -> u64 {
69 30
70}
71
72pub fn load_embedded_config<T: DeserializeOwned>() -> Result<Option<T>, EmbeddedConfigError> {
79 let exe_path = std::env::current_exe().map_err(EmbeddedConfigError::Io)?;
80 load_embedded_config_from_path(&exe_path)
81}
82
83pub fn load_embedded_config_from_path<T: DeserializeOwned>(
85 path: &std::path::Path,
86) -> Result<Option<T>, EmbeddedConfigError> {
87 let data = std::fs::read(path).map_err(EmbeddedConfigError::Io)?;
88
89 if data.len() < FOOTER_SIZE {
90 return Ok(None);
91 }
92
93 let magic_start = data.len() - MAGIC_BYTES.len();
95 if &data[magic_start..] != MAGIC_BYTES {
96 return Ok(None);
97 }
98
99 let len_start = magic_start - 4;
101 let len_bytes: [u8; 4] = data[len_start..magic_start]
102 .try_into()
103 .map_err(|_| EmbeddedConfigError::InvalidFormat("invalid length bytes".into()))?;
104 let json_len = u32::from_le_bytes(len_bytes) as usize;
105
106 if json_len == 0 || len_start < json_len {
107 return Err(EmbeddedConfigError::InvalidFormat(
108 "config length exceeds binary size".into(),
109 ));
110 }
111
112 let json_start = len_start - json_len;
113 let json_bytes = &data[json_start..len_start];
114
115 let config: T =
116 serde_json::from_slice(json_bytes).map_err(EmbeddedConfigError::Deserialization)?;
117
118 Ok(Some(config))
119}
120
121pub fn append_embedded_config<T: Serialize>(
125 binary_data: &[u8],
126 config: &T,
127) -> Result<Vec<u8>, EmbeddedConfigError> {
128 let json_bytes = serde_json::to_vec(config).map_err(EmbeddedConfigError::Deserialization)?;
129 let json_len = json_bytes.len() as u32;
130
131 let mut result = Vec::with_capacity(binary_data.len() + json_bytes.len() + FOOTER_SIZE);
132 result.extend_from_slice(binary_data);
133 result.extend_from_slice(&json_bytes);
134 result.extend_from_slice(&json_len.to_le_bytes());
135 result.extend_from_slice(MAGIC_BYTES);
136
137 Ok(result)
138}
139
140#[derive(Debug)]
142pub enum EmbeddedConfigError {
143 Io(std::io::Error),
144 InvalidFormat(String),
145 Deserialization(serde_json::Error),
146}
147
148impl std::fmt::Display for EmbeddedConfigError {
149 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
150 match self {
151 Self::Io(e) => write!(f, "IO error reading embedded config: {}", e),
152 Self::InvalidFormat(msg) => write!(f, "invalid embedded config format: {}", msg),
153 Self::Deserialization(e) => write!(f, "failed to deserialize embedded config: {}", e),
154 }
155 }
156}
157
158impl std::error::Error for EmbeddedConfigError {}
159
160#[cfg(test)]
161mod tests {
162 use super::*;
163
164 #[test]
165 fn test_roundtrip_deploy_cli_config() {
166 let config = DeployCliConfig {
167 manager_url: Some("https://manager.example.com".into()),
168 token: Some("tok_abc123".into()),
169 deployment_group_id: Some("dg_xyz".into()),
170 display_name: Some("Production".into()),
171 default_platform: Some("aws".into()),
172 name: Some("acme-deploy".into()),
173 };
174
175 let binary = b"fake binary content";
176 let embedded = append_embedded_config(binary, &config).unwrap();
177
178 let loaded: Option<DeployCliConfig> =
179 load_embedded_config_from_path_bytes(&embedded).unwrap();
180 let loaded = loaded.unwrap();
181
182 assert_eq!(loaded.manager_url, config.manager_url);
183 assert_eq!(loaded.token, config.token);
184 assert_eq!(loaded.deployment_group_id, config.deployment_group_id);
185 assert_eq!(loaded.display_name, config.display_name);
186 assert_eq!(loaded.name, config.name);
187 }
188
189 #[test]
190 fn test_no_embedded_config() {
191 let binary = b"just a regular binary";
192 let result: Option<DeployCliConfig> =
193 load_embedded_config_from_path_bytes(binary).unwrap();
194 assert!(result.is_none());
195 }
196
197 #[test]
198 fn test_roundtrip_agent_config() {
199 let config = AgentConfig {
200 manager_url: Some("https://manager.example.com".into()),
201 token: Some("tok_agent123".into()),
202 deployment_id: Some("dep_abc".into()),
203 sync_interval_secs: 60,
204 name: Some("acme-agent".into()),
205 display_name: Some("Acme Agent".into()),
206 };
207
208 let binary = b"agent binary";
209 let embedded = append_embedded_config(binary, &config).unwrap();
210
211 let loaded: Option<AgentConfig> =
212 load_embedded_config_from_path_bytes(&embedded).unwrap();
213 let loaded = loaded.unwrap();
214
215 assert_eq!(loaded.manager_url, config.manager_url);
216 assert_eq!(loaded.deployment_id, config.deployment_id);
217 assert_eq!(loaded.sync_interval_secs, 60);
218 assert_eq!(loaded.name, config.name);
219 }
220
221 fn load_embedded_config_from_path_bytes<T: DeserializeOwned>(
223 data: &[u8],
224 ) -> Result<Option<T>, EmbeddedConfigError> {
225 if data.len() < FOOTER_SIZE {
226 return Ok(None);
227 }
228
229 let magic_start = data.len() - MAGIC_BYTES.len();
230 if &data[magic_start..] != MAGIC_BYTES {
231 return Ok(None);
232 }
233
234 let len_start = magic_start - 4;
235 let len_bytes: [u8; 4] = data[len_start..magic_start]
236 .try_into()
237 .map_err(|_| EmbeddedConfigError::InvalidFormat("invalid length bytes".into()))?;
238 let json_len = u32::from_le_bytes(len_bytes) as usize;
239
240 if json_len == 0 || len_start < json_len {
241 return Err(EmbeddedConfigError::InvalidFormat(
242 "config length exceeds binary size".into(),
243 ));
244 }
245
246 let json_start = len_start - json_len;
247 let json_bytes = &data[json_start..len_start];
248
249 let config: T =
250 serde_json::from_slice(json_bytes).map_err(EmbeddedConfigError::Deserialization)?;
251
252 Ok(Some(config))
253 }
254}