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