pub const LINUX_SUFFIX_X86_64: &str = "-linux-x86_64";
pub const LINUX_SUFFIX_AARCH64: &str = "-linux-aarch64";
pub const LINUX_SUFFIXES: [&str; 2] = [LINUX_SUFFIX_X86_64, LINUX_SUFFIX_AARCH64];
pub const MACOS_SUFFIX_AARCH64: &str = "-macos-aarch64";
pub const PLATFORM_SUFFIXES: [&str; 3] = [
LINUX_SUFFIX_X86_64,
LINUX_SUFFIX_AARCH64,
MACOS_SUFFIX_AARCH64,
];
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AgentPlatform {
WindowsX86_64,
WindowsAarch64,
LinuxX86_64,
LinuxAarch64,
MacOSAarch64,
}
impl AgentPlatform {
pub fn detect(bytes: &[u8]) -> Result<AgentPlatform, String> {
if bytes.starts_with(b"MZ") {
return detect_pe(bytes);
}
if bytes.starts_with(b"\x7fELF") {
return detect_elf(bytes);
}
if let Some(result) = detect_macho(bytes) {
return result;
}
Err(
"unrecognized binary format (not a Windows PE, Linux ELF, or macOS Mach-O) — is this \
a kanade-agent build?"
.to_string(),
)
}
pub fn release_key(&self, version: &str) -> String {
match self.suffix() {
Some(suffix) => format!("{version}{suffix}"),
None => version.to_string(),
}
}
pub fn suffix(&self) -> Option<&'static str> {
match self {
AgentPlatform::WindowsX86_64 | AgentPlatform::WindowsAarch64 => None,
AgentPlatform::LinuxX86_64 => Some(LINUX_SUFFIX_X86_64),
AgentPlatform::LinuxAarch64 => Some(LINUX_SUFFIX_AARCH64),
AgentPlatform::MacOSAarch64 => Some(MACOS_SUFFIX_AARCH64),
}
}
pub fn as_str(&self) -> &'static str {
match self {
AgentPlatform::WindowsX86_64 => "windows-x86_64",
AgentPlatform::WindowsAarch64 => "windows-aarch64",
AgentPlatform::LinuxX86_64 => "linux-x86_64",
AgentPlatform::LinuxAarch64 => "linux-aarch64",
AgentPlatform::MacOSAarch64 => "macos-aarch64",
}
}
}
pub fn platform_of_key(key: &str) -> &'static str {
if key.ends_with(LINUX_SUFFIX_X86_64) {
"linux-x86_64"
} else if key.ends_with(LINUX_SUFFIX_AARCH64) {
"linux-aarch64"
} else if key.ends_with(MACOS_SUFFIX_AARCH64) {
"macos-aarch64"
} else {
"windows"
}
}
pub fn base_version_of_key(key: &str) -> &str {
for suffix in PLATFORM_SUFFIXES {
if let Some(base) = key.strip_suffix(suffix) {
return base;
}
}
key
}
pub fn candidate_keys(version: &str) -> Vec<String> {
let mut keys = Vec::with_capacity(1 + PLATFORM_SUFFIXES.len());
keys.push(version.to_string());
for suffix in PLATFORM_SUFFIXES {
keys.push(format!("{version}{suffix}"));
}
keys
}
pub fn check_release_key(key: &str) -> Result<(), String> {
if key.is_empty()
|| !key
.chars()
.all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | '+' | '-'))
{
return Err(format!(
"release key must be non-empty and contain only [A-Za-z0-9._+-], got {key:?}"
));
}
Ok(())
}
fn detect_pe(bytes: &[u8]) -> Result<AgentPlatform, String> {
let truncated = || "truncated PE header".to_string();
let pe_off = u32::from_le_bytes(
bytes
.get(0x3C..0x40)
.ok_or_else(truncated)?
.try_into()
.map_err(|_| truncated())?,
) as usize;
let coff = bytes.get(pe_off..pe_off + 6).ok_or_else(truncated)?;
if coff[..4] != *b"PE\0\0" {
return Err("MZ binary without a PE signature".to_string());
}
match u16::from_le_bytes([coff[4], coff[5]]) {
0x8664 => Ok(AgentPlatform::WindowsX86_64),
0xAA64 => Ok(AgentPlatform::WindowsAarch64),
other => Err(format!(
"unsupported PE machine type 0x{other:04X} (kanade-agent ships x86_64 and aarch64 only)"
)),
}
}
fn detect_elf(bytes: &[u8]) -> Result<AgentPlatform, String> {
let truncated = || "truncated ELF header".to_string();
let ei_data = bytes.get(5).ok_or_else(truncated)?;
let em = bytes.get(18..20).ok_or_else(truncated)?;
let machine = match ei_data {
1 => u16::from_le_bytes([em[0], em[1]]),
2 => u16::from_be_bytes([em[0], em[1]]),
other => return Err(format!("unknown ELF endianness {other}")),
};
match machine {
62 => Ok(AgentPlatform::LinuxX86_64),
183 => Ok(AgentPlatform::LinuxAarch64),
other => Err(format!(
"unsupported ELF machine {other} (kanade-agent ships x86_64 (62) and aarch64 (183) \
only)"
)),
}
}
fn detect_macho(bytes: &[u8]) -> Option<Result<AgentPlatform, String>> {
const MH_MAGIC_64_BE: [u8; 4] = [0xFE, 0xED, 0xFA, 0xCF];
const MH_MAGIC_64_LE: [u8; 4] = [0xCF, 0xFA, 0xED, 0xFE];
const MH_MAGIC_32: [[u8; 4]; 2] = [
[0xFE, 0xED, 0xFA, 0xCE], [0xCE, 0xFA, 0xED, 0xFE], ];
const FAT_MAGICS: [[u8; 4]; 4] = [
[0xCA, 0xFE, 0xBA, 0xBE], [0xBE, 0xBA, 0xFE, 0xCA], [0xCA, 0xFE, 0xBA, 0xBF], [0xBF, 0xBA, 0xFE, 0xCA], ];
let little_endian = if bytes.starts_with(&MH_MAGIC_64_LE) {
true
} else if bytes.starts_with(&MH_MAGIC_64_BE) {
false
} else if MH_MAGIC_32.iter().any(|m| bytes.starts_with(m)) {
return Some(Err(
"32-bit Mach-O is not supported — kanade-agent ships 64-bit Apple Silicon \
(arm64) macOS builds only"
.to_string(),
));
} else if FAT_MAGICS.iter().any(|m| bytes.starts_with(m)) {
return Some(Err(
"universal (fat) Mach-O binaries are not supported — publish one thin binary per \
arch (e.g. `lipo -thin arm64 kanade-agent -output kanade-agent-arm64`)"
.to_string(),
));
} else {
return None;
};
let Some(cpu) = bytes.get(4..8) else {
return Some(Err("truncated Mach-O header".to_string()));
};
let cpu = [cpu[0], cpu[1], cpu[2], cpu[3]];
let cputype = if little_endian {
u32::from_le_bytes(cpu)
} else {
u32::from_be_bytes(cpu)
};
Some(match cputype {
0x0100_000C => Ok(AgentPlatform::MacOSAarch64),
0x0100_0007 => Err(
"Intel (x86_64) macOS agents are not supported — kanade supports Apple Silicon \
(arm64) Macs only"
.to_string(),
),
other => Err(format!(
"unsupported Mach-O cputype 0x{other:08X} (kanade-agent ships Apple Silicon arm64 \
macOS builds only)"
)),
})
}
#[cfg(test)]
mod tests {
use super::*;
fn fake_pe(machine: u16) -> Vec<u8> {
let mut b = vec![0u8; 0x80 + 6];
b[0] = b'M';
b[1] = b'Z';
b[0x3C..0x40].copy_from_slice(&0x80u32.to_le_bytes());
b[0x80..0x84].copy_from_slice(b"PE\0\0");
b[0x84..0x86].copy_from_slice(&machine.to_le_bytes());
b
}
fn fake_elf(machine: u16, little_endian: bool) -> Vec<u8> {
let mut b = vec![0u8; 20];
b[..4].copy_from_slice(b"\x7fELF");
b[4] = 2; b[5] = if little_endian { 1 } else { 2 };
if little_endian {
b[18..20].copy_from_slice(&machine.to_le_bytes());
} else {
b[18..20].copy_from_slice(&machine.to_be_bytes());
}
b
}
#[test]
fn detects_pe_architectures() {
assert_eq!(
AgentPlatform::detect(&fake_pe(0x8664)).unwrap(),
AgentPlatform::WindowsX86_64
);
assert_eq!(
AgentPlatform::detect(&fake_pe(0xAA64)).unwrap(),
AgentPlatform::WindowsAarch64
);
assert!(AgentPlatform::detect(&fake_pe(0x14C)).is_err()); let mut b = fake_pe(0x8664);
b[0x80] = b'X';
assert!(AgentPlatform::detect(&b).is_err());
assert!(AgentPlatform::detect(b"MZ").is_err());
}
#[test]
fn detects_elf_architectures_and_endianness() {
assert_eq!(
AgentPlatform::detect(&fake_elf(62, true)).unwrap(),
AgentPlatform::LinuxX86_64
);
assert_eq!(
AgentPlatform::detect(&fake_elf(183, true)).unwrap(),
AgentPlatform::LinuxAarch64
);
assert_eq!(
AgentPlatform::detect(&fake_elf(183, false)).unwrap(),
AgentPlatform::LinuxAarch64
);
assert!(AgentPlatform::detect(&fake_elf(40, true)).is_err()); assert!(AgentPlatform::detect(b"\x7fELF").is_err()); }
fn fake_macho(cputype: u32, little_endian: bool) -> Vec<u8> {
let mut b = vec![0u8; 8];
if little_endian {
b[..4].copy_from_slice(&[0xCF, 0xFA, 0xED, 0xFE]);
b[4..8].copy_from_slice(&cputype.to_le_bytes());
} else {
b[..4].copy_from_slice(&[0xFE, 0xED, 0xFA, 0xCF]);
b[4..8].copy_from_slice(&cputype.to_be_bytes());
}
b
}
#[test]
fn detects_macho_architectures_by_cputype() {
for le in [true, false] {
let err = AgentPlatform::detect(&fake_macho(0x0100_0007, le)).unwrap_err();
assert!(
err.contains("Intel (x86_64) macOS agents are not supported"),
"{err}"
);
assert!(err.contains("Apple Silicon"), "{err}");
}
assert_eq!(
AgentPlatform::detect(&fake_macho(0x0100_000C, true)).unwrap(),
AgentPlatform::MacOSAarch64
);
assert_eq!(
AgentPlatform::detect(&fake_macho(0x0100_000C, false)).unwrap(),
AgentPlatform::MacOSAarch64
);
assert!(AgentPlatform::detect(&fake_macho(12, true)).is_err());
assert!(AgentPlatform::detect(&[0xCF, 0xFA, 0xED, 0xFE]).is_err());
}
#[test]
fn fat_and_32bit_macho_are_clear_errors() {
for magic in [[0xCA, 0xFE, 0xBA, 0xBE], [0xBF, 0xBA, 0xFE, 0xCA]] {
let err = AgentPlatform::detect(&magic).unwrap_err();
assert!(err.contains("thin binary per arch"), "{err}");
}
for magic in [[0xFE, 0xED, 0xFA, 0xCE], [0xCE, 0xFA, 0xED, 0xFE]] {
let err = AgentPlatform::detect(&magic).unwrap_err();
assert!(err.contains("32-bit Mach-O"), "{err}");
}
}
#[test]
fn unknown_bytes_are_an_error_not_a_windows_guess() {
assert!(AgentPlatform::detect(b"#!/bin/sh\necho hi").is_err());
assert!(AgentPlatform::detect(b"").is_err());
}
#[test]
fn release_keys_follow_the_scheme() {
assert_eq!(AgentPlatform::WindowsX86_64.release_key("0.45.4"), "0.45.4");
assert_eq!(
AgentPlatform::WindowsAarch64.release_key("0.45.4"),
"0.45.4"
);
assert_eq!(
AgentPlatform::LinuxX86_64.release_key("0.45.4"),
"0.45.4-linux-x86_64"
);
assert_eq!(
AgentPlatform::LinuxAarch64.release_key("0.45.4"),
"0.45.4-linux-aarch64"
);
assert_eq!(
AgentPlatform::MacOSAarch64.release_key("0.45.4"),
"0.45.4-macos-aarch64"
);
assert_eq!(
AgentPlatform::LinuxX86_64.release_key("0.46.0-rc.1"),
"0.46.0-rc.1-linux-x86_64"
);
assert!(check_release_key(&AgentPlatform::LinuxAarch64.release_key("0.46.0-rc.1")).is_ok());
}
#[test]
fn platform_of_key_reads_the_suffix_only() {
assert_eq!(platform_of_key("0.45.4"), "windows");
assert_eq!(platform_of_key("0.45.4-linux-x86_64"), "linux-x86_64");
assert_eq!(platform_of_key("0.45.4-linux-aarch64"), "linux-aarch64");
assert_eq!(platform_of_key("0.45.4-macos-aarch64"), "macos-aarch64");
assert_eq!(platform_of_key("1.0.0-linux"), "windows");
assert_eq!(platform_of_key("1.0.0-rc-linux-x86_64"), "linux-x86_64");
}
#[test]
fn base_version_strips_only_the_platform_suffix() {
assert_eq!(base_version_of_key("0.45.4"), "0.45.4");
assert_eq!(base_version_of_key("0.45.4-linux-x86_64"), "0.45.4");
assert_eq!(base_version_of_key("0.45.4-linux-aarch64"), "0.45.4");
assert_eq!(base_version_of_key("0.45.4-macos-aarch64"), "0.45.4");
assert_eq!(base_version_of_key("0.46.0-rc.1"), "0.46.0-rc.1");
assert_eq!(
base_version_of_key("0.46.0-rc.1-linux-x86_64"),
"0.46.0-rc.1"
);
}
#[test]
fn candidate_keys_cover_bare_then_linux_then_macos() {
assert_eq!(
candidate_keys("0.46.0"),
vec![
"0.46.0".to_string(),
"0.46.0-linux-x86_64".to_string(),
"0.46.0-linux-aarch64".to_string(),
"0.46.0-macos-aarch64".to_string(),
]
);
}
#[test]
fn release_key_charset_is_restricted() {
for bad in ["", "0.43.99\n evil", "0.43.99\"x", "0.43.99'x", "a b"] {
assert!(check_release_key(bad).is_err(), "{bad:?}");
}
for good in [
"0.43.99",
"0.43.99-rc.1+build.5",
"1.0.0_alpha",
"0.45.4-linux-x86_64",
] {
check_release_key(good).unwrap();
}
}
}