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