1pub const LINUX_SUFFIX_X86_64: &str = "-linux-x86_64";
25pub const LINUX_SUFFIX_AARCH64: &str = "-linux-aarch64";
27pub const LINUX_SUFFIXES: [&str; 2] = [LINUX_SUFFIX_X86_64, LINUX_SUFFIX_AARCH64];
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub enum AgentPlatform {
33 WindowsX86_64,
34 WindowsAarch64,
35 LinuxX86_64,
36 LinuxAarch64,
37}
38
39impl AgentPlatform {
40 pub fn detect(bytes: &[u8]) -> Result<AgentPlatform, String> {
46 if bytes.starts_with(b"MZ") {
47 return detect_pe(bytes);
48 }
49 if bytes.starts_with(b"\x7fELF") {
50 return detect_elf(bytes);
51 }
52 if is_macho(bytes) {
53 return Err(
54 "macOS (Mach-O) agents are not supported — kanade-agent ships for Windows and \
55 Linux only"
56 .to_string(),
57 );
58 }
59 Err(
60 "unrecognized binary format (not a Windows PE or Linux ELF) — is this a kanade-agent \
61 build?"
62 .to_string(),
63 )
64 }
65
66 pub fn release_key(&self, version: &str) -> String {
70 match self.suffix() {
71 Some(suffix) => format!("{version}{suffix}"),
72 None => version.to_string(),
73 }
74 }
75
76 pub fn suffix(&self) -> Option<&'static str> {
78 match self {
79 AgentPlatform::WindowsX86_64 | AgentPlatform::WindowsAarch64 => None,
80 AgentPlatform::LinuxX86_64 => Some(LINUX_SUFFIX_X86_64),
81 AgentPlatform::LinuxAarch64 => Some(LINUX_SUFFIX_AARCH64),
82 }
83 }
84
85 pub fn as_str(&self) -> &'static str {
87 match self {
88 AgentPlatform::WindowsX86_64 => "windows-x86_64",
89 AgentPlatform::WindowsAarch64 => "windows-aarch64",
90 AgentPlatform::LinuxX86_64 => "linux-x86_64",
91 AgentPlatform::LinuxAarch64 => "linux-aarch64",
92 }
93 }
94}
95
96pub fn platform_of_key(key: &str) -> &'static str {
101 if key.ends_with(LINUX_SUFFIX_X86_64) {
102 "linux-x86_64"
103 } else if key.ends_with(LINUX_SUFFIX_AARCH64) {
104 "linux-aarch64"
105 } else {
106 "windows"
107 }
108}
109
110pub fn base_version_of_key(key: &str) -> &str {
116 for suffix in LINUX_SUFFIXES {
117 if let Some(base) = key.strip_suffix(suffix) {
118 return base;
119 }
120 }
121 key
122}
123
124pub fn candidate_keys(version: &str) -> Vec<String> {
129 let mut keys = Vec::with_capacity(1 + LINUX_SUFFIXES.len());
130 keys.push(version.to_string());
131 for suffix in LINUX_SUFFIXES {
132 keys.push(format!("{version}{suffix}"));
133 }
134 keys
135}
136
137pub fn check_release_key(key: &str) -> Result<(), String> {
144 if key.is_empty()
145 || !key
146 .chars()
147 .all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | '+' | '-'))
148 {
149 return Err(format!(
150 "release key must be non-empty and contain only [A-Za-z0-9._+-], got {key:?}"
151 ));
152 }
153 Ok(())
154}
155
156fn detect_pe(bytes: &[u8]) -> Result<AgentPlatform, String> {
161 let truncated = || "truncated PE header".to_string();
162 let pe_off = u32::from_le_bytes(
163 bytes
164 .get(0x3C..0x40)
165 .ok_or_else(truncated)?
166 .try_into()
167 .map_err(|_| truncated())?,
168 ) as usize;
169 let coff = bytes.get(pe_off..pe_off + 6).ok_or_else(truncated)?;
170 if coff[..4] != *b"PE\0\0" {
171 return Err("MZ binary without a PE signature".to_string());
172 }
173 match u16::from_le_bytes([coff[4], coff[5]]) {
174 0x8664 => Ok(AgentPlatform::WindowsX86_64),
175 0xAA64 => Ok(AgentPlatform::WindowsAarch64),
176 other => Err(format!(
177 "unsupported PE machine type 0x{other:04X} (kanade-agent ships x86_64 and aarch64 only)"
178 )),
179 }
180}
181
182fn detect_elf(bytes: &[u8]) -> Result<AgentPlatform, String> {
186 let truncated = || "truncated ELF header".to_string();
187 let ei_data = bytes.get(5).ok_or_else(truncated)?;
188 let em = bytes.get(18..20).ok_or_else(truncated)?;
189 let machine = match ei_data {
190 1 => u16::from_le_bytes([em[0], em[1]]),
191 2 => u16::from_be_bytes([em[0], em[1]]),
192 other => return Err(format!("unknown ELF endianness {other}")),
193 };
194 match machine {
195 62 => Ok(AgentPlatform::LinuxX86_64),
196 183 => Ok(AgentPlatform::LinuxAarch64),
197 other => Err(format!(
198 "unsupported ELF machine {other} (kanade-agent ships x86_64 (62) and aarch64 (183) \
199 only)"
200 )),
201 }
202}
203
204fn is_macho(bytes: &[u8]) -> bool {
208 const MAGICS: [[u8; 4]; 8] = [
209 [0xFE, 0xED, 0xFA, 0xCE], [0xCE, 0xFA, 0xED, 0xFE], [0xFE, 0xED, 0xFA, 0xCF], [0xCF, 0xFA, 0xED, 0xFE], [0xCA, 0xFE, 0xBA, 0xBE], [0xBE, 0xBA, 0xFE, 0xCA], [0xCA, 0xFE, 0xBA, 0xBF], [0xBF, 0xBA, 0xFE, 0xCA], ];
218 MAGICS.iter().any(|m| bytes.starts_with(m))
219}
220
221#[cfg(test)]
222mod tests {
223 use super::*;
224
225 fn fake_pe(machine: u16) -> Vec<u8> {
227 let mut b = vec![0u8; 0x80 + 6];
228 b[0] = b'M';
229 b[1] = b'Z';
230 b[0x3C..0x40].copy_from_slice(&0x80u32.to_le_bytes());
231 b[0x80..0x84].copy_from_slice(b"PE\0\0");
232 b[0x84..0x86].copy_from_slice(&machine.to_le_bytes());
233 b
234 }
235
236 fn fake_elf(machine: u16, little_endian: bool) -> Vec<u8> {
238 let mut b = vec![0u8; 20];
239 b[..4].copy_from_slice(b"\x7fELF");
240 b[4] = 2; b[5] = if little_endian { 1 } else { 2 };
242 if little_endian {
243 b[18..20].copy_from_slice(&machine.to_le_bytes());
244 } else {
245 b[18..20].copy_from_slice(&machine.to_be_bytes());
246 }
247 b
248 }
249
250 #[test]
251 fn detects_pe_architectures() {
252 assert_eq!(
253 AgentPlatform::detect(&fake_pe(0x8664)).unwrap(),
254 AgentPlatform::WindowsX86_64
255 );
256 assert_eq!(
257 AgentPlatform::detect(&fake_pe(0xAA64)).unwrap(),
258 AgentPlatform::WindowsAarch64
259 );
260 assert!(AgentPlatform::detect(&fake_pe(0x14C)).is_err()); let mut b = fake_pe(0x8664);
264 b[0x80] = b'X';
265 assert!(AgentPlatform::detect(&b).is_err());
266 assert!(AgentPlatform::detect(b"MZ").is_err());
268 }
269
270 #[test]
271 fn detects_elf_architectures_and_endianness() {
272 assert_eq!(
273 AgentPlatform::detect(&fake_elf(62, true)).unwrap(),
274 AgentPlatform::LinuxX86_64
275 );
276 assert_eq!(
277 AgentPlatform::detect(&fake_elf(183, true)).unwrap(),
278 AgentPlatform::LinuxAarch64
279 );
280 assert_eq!(
283 AgentPlatform::detect(&fake_elf(183, false)).unwrap(),
284 AgentPlatform::LinuxAarch64
285 );
286 assert!(AgentPlatform::detect(&fake_elf(40, true)).is_err()); assert!(AgentPlatform::detect(b"\x7fELF").is_err()); }
289
290 #[test]
291 fn macho_is_a_clear_unsupported_error() {
292 for magic in [
293 [0xFE, 0xED, 0xFA, 0xCE],
294 [0xCF, 0xFA, 0xED, 0xFE],
295 [0xCA, 0xFE, 0xBA, 0xBE],
296 ] {
297 let err = AgentPlatform::detect(&magic).unwrap_err();
298 assert!(err.contains("macOS"), "{err}");
299 }
300 }
301
302 #[test]
303 fn unknown_bytes_are_an_error_not_a_windows_guess() {
304 assert!(AgentPlatform::detect(b"#!/bin/sh\necho hi").is_err());
305 assert!(AgentPlatform::detect(b"").is_err());
306 }
307
308 #[test]
309 fn release_keys_follow_the_scheme() {
310 assert_eq!(AgentPlatform::WindowsX86_64.release_key("0.45.4"), "0.45.4");
312 assert_eq!(
313 AgentPlatform::WindowsAarch64.release_key("0.45.4"),
314 "0.45.4"
315 );
316 assert_eq!(
317 AgentPlatform::LinuxX86_64.release_key("0.45.4"),
318 "0.45.4-linux-x86_64"
319 );
320 assert_eq!(
321 AgentPlatform::LinuxAarch64.release_key("0.45.4"),
322 "0.45.4-linux-aarch64"
323 );
324 assert_eq!(
326 AgentPlatform::LinuxX86_64.release_key("0.46.0-rc.1"),
327 "0.46.0-rc.1-linux-x86_64"
328 );
329 assert!(check_release_key(&AgentPlatform::LinuxAarch64.release_key("0.46.0-rc.1")).is_ok());
330 }
331
332 #[test]
333 fn platform_of_key_reads_the_suffix_only() {
334 assert_eq!(platform_of_key("0.45.4"), "windows");
335 assert_eq!(platform_of_key("0.45.4-linux-x86_64"), "linux-x86_64");
336 assert_eq!(platform_of_key("0.45.4-linux-aarch64"), "linux-aarch64");
337 assert_eq!(platform_of_key("1.0.0-linux"), "windows");
341 assert_eq!(platform_of_key("1.0.0-rc-linux-x86_64"), "linux-x86_64");
342 }
343
344 #[test]
345 fn base_version_strips_only_the_platform_suffix() {
346 assert_eq!(base_version_of_key("0.45.4"), "0.45.4");
347 assert_eq!(base_version_of_key("0.45.4-linux-x86_64"), "0.45.4");
348 assert_eq!(base_version_of_key("0.45.4-linux-aarch64"), "0.45.4");
349 assert_eq!(base_version_of_key("0.46.0-rc.1"), "0.46.0-rc.1");
351 assert_eq!(
352 base_version_of_key("0.46.0-rc.1-linux-x86_64"),
353 "0.46.0-rc.1"
354 );
355 }
356
357 #[test]
358 fn candidate_keys_cover_bare_then_linux() {
359 assert_eq!(
360 candidate_keys("0.46.0"),
361 vec![
362 "0.46.0".to_string(),
363 "0.46.0-linux-x86_64".to_string(),
364 "0.46.0-linux-aarch64".to_string(),
365 ]
366 );
367 }
368
369 #[test]
370 fn release_key_charset_is_restricted() {
371 for bad in ["", "0.43.99\n evil", "0.43.99\"x", "0.43.99'x", "a b"] {
372 assert!(check_release_key(bad).is_err(), "{bad:?}");
373 }
374 for good in [
375 "0.43.99",
376 "0.43.99-rc.1+build.5",
377 "1.0.0_alpha",
378 "0.45.4-linux-x86_64",
379 ] {
380 check_release_key(good).unwrap();
381 }
382 }
383}