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