Skip to main content

fidius_host/
arch.rs

1// Copyright 2026 Colliery, Inc.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Architecture detection for dylib files.
16//!
17//! Reads binary headers to determine format (ELF/Mach-O/PE) and target
18//! architecture before attempting to dlopen.
19
20use std::path::Path;
21
22use crate::error::LoadError;
23
24/// Detected binary format and architecture.
25#[derive(Debug, Clone, PartialEq, Eq)]
26pub struct BinaryInfo {
27    pub format: BinaryFormat,
28    pub arch: Arch,
29}
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub enum BinaryFormat {
33    Elf,
34    MachO,
35    Pe,
36    Unknown,
37}
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub enum Arch {
41    X86_64,
42    Aarch64,
43    Unknown,
44}
45
46impl std::fmt::Display for BinaryFormat {
47    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48        match self {
49            BinaryFormat::Elf => write!(f, "ELF"),
50            BinaryFormat::MachO => write!(f, "Mach-O"),
51            BinaryFormat::Pe => write!(f, "PE"),
52            BinaryFormat::Unknown => write!(f, "unknown"),
53        }
54    }
55}
56
57impl std::fmt::Display for Arch {
58    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
59        match self {
60            Arch::X86_64 => write!(f, "x86_64"),
61            Arch::Aarch64 => write!(f, "aarch64"),
62            Arch::Unknown => write!(f, "unknown"),
63        }
64    }
65}
66
67/// Detect the binary format and architecture of a file.
68pub fn detect_architecture(path: &Path) -> Result<BinaryInfo, LoadError> {
69    use std::io::Read;
70
71    let mut file = std::fs::File::open(path).map_err(|e| {
72        if e.kind() == std::io::ErrorKind::NotFound {
73            LoadError::LibraryNotFound {
74                path: path.display().to_string(),
75            }
76        } else {
77            LoadError::Io(e)
78        }
79    })?;
80
81    let mut bytes = [0u8; 20];
82    let n = file.read(&mut bytes).map_err(LoadError::Io)?;
83
84    if n < 16 {
85        return Ok(BinaryInfo {
86            format: BinaryFormat::Unknown,
87            arch: Arch::Unknown,
88        });
89    }
90    let bytes = &bytes[..n];
91
92    // ELF: \x7fELF
93    if bytes[0..4] == [0x7f, b'E', b'L', b'F'] {
94        let arch = if bytes.len() > 19 {
95            let e_machine = u16::from_le_bytes([bytes[18], bytes[19]]);
96            match e_machine {
97                0x3E => Arch::X86_64,
98                0xB7 => Arch::Aarch64,
99                _ => Arch::Unknown,
100            }
101        } else {
102            Arch::Unknown
103        };
104        return Ok(BinaryInfo {
105            format: BinaryFormat::Elf,
106            arch,
107        });
108    }
109
110    // Mach-O: 0xFEEDFACE (32-bit) or 0xFEEDFACF (64-bit), or reversed (big-endian)
111    let magic32 = u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
112    if matches!(magic32, 0xFEEDFACE | 0xFEEDFACF | 0xCEFAEDFE | 0xCFFAEDFE) {
113        let arch = if bytes.len() > 8 {
114            // cputype is at offset 4, 4 bytes
115            let is_le = matches!(magic32, 0xCEFAEDFE | 0xCFFAEDFE);
116            let cputype = if is_le {
117                u32::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]])
118            } else {
119                u32::from_be_bytes([bytes[4], bytes[5], bytes[6], bytes[7]])
120            };
121            match cputype {
122                0x01000007 => Arch::X86_64,  // CPU_TYPE_X86_64
123                0x0100000C => Arch::Aarch64, // CPU_TYPE_ARM64
124                _ => Arch::Unknown,
125            }
126        } else {
127            Arch::Unknown
128        };
129        return Ok(BinaryInfo {
130            format: BinaryFormat::MachO,
131            arch,
132        });
133    }
134
135    // PE: MZ
136    if bytes[0..2] == [b'M', b'Z'] {
137        return Ok(BinaryInfo {
138            format: BinaryFormat::Pe,
139            arch: Arch::Unknown, // Would need to parse PE/COFF headers for arch
140        });
141    }
142
143    Ok(BinaryInfo {
144        format: BinaryFormat::Unknown,
145        arch: Arch::Unknown,
146    })
147}
148
149/// Check that a dylib matches the current platform's expected format.
150pub fn check_architecture(path: &Path) -> Result<(), LoadError> {
151    let info = detect_architecture(path)?;
152
153    let expected_format = if cfg!(target_os = "macos") {
154        BinaryFormat::MachO
155    } else if cfg!(target_os = "windows") {
156        BinaryFormat::Pe
157    } else {
158        BinaryFormat::Elf
159    };
160
161    let expected_arch = if cfg!(target_arch = "x86_64") {
162        Arch::X86_64
163    } else if cfg!(target_arch = "aarch64") {
164        Arch::Aarch64
165    } else {
166        Arch::Unknown
167    };
168
169    // Only reject on clear mismatches — don't reject Unknown
170    if info.format != BinaryFormat::Unknown && info.format != expected_format {
171        return Err(LoadError::ArchitectureMismatch {
172            expected: format!("{} {}", expected_format, expected_arch),
173            got: format!("{} {}", info.format, info.arch),
174        });
175    }
176
177    if info.arch != Arch::Unknown && expected_arch != Arch::Unknown && info.arch != expected_arch {
178        return Err(LoadError::ArchitectureMismatch {
179            expected: format!("{} {}", expected_format, expected_arch),
180            got: format!("{} {}", info.format, info.arch),
181        });
182    }
183
184    Ok(())
185}
186
187#[cfg(test)]
188mod tests {
189    use super::*;
190
191    #[test]
192    fn detects_elf() {
193        // Minimal ELF header: magic + enough bytes for e_machine at offset 18
194        let mut bytes = vec![0u8; 20];
195        bytes[0..4].copy_from_slice(&[0x7f, b'E', b'L', b'F']);
196        bytes[18..20].copy_from_slice(&0x3Eu16.to_le_bytes()); // x86_64
197
198        let tmp = tempfile::NamedTempFile::new().unwrap();
199        std::fs::write(tmp.path(), &bytes).unwrap();
200
201        let info = detect_architecture(tmp.path()).unwrap();
202        assert_eq!(info.format, BinaryFormat::Elf);
203        assert_eq!(info.arch, Arch::X86_64);
204    }
205
206    #[test]
207    fn detects_macho_le() {
208        // Mach-O little-endian 64-bit: 0xCFFAEDFE, cputype ARM64 = 0x0100000C
209        let mut bytes = vec![0u8; 16];
210        bytes[0..4].copy_from_slice(&0xCFFAEDFEu32.to_be_bytes());
211        bytes[4..8].copy_from_slice(&0x0100000Cu32.to_le_bytes()); // ARM64
212
213        let tmp = tempfile::NamedTempFile::new().unwrap();
214        std::fs::write(tmp.path(), &bytes).unwrap();
215
216        let info = detect_architecture(tmp.path()).unwrap();
217        assert_eq!(info.format, BinaryFormat::MachO);
218        assert_eq!(info.arch, Arch::Aarch64);
219    }
220
221    #[test]
222    fn detects_pe() {
223        let mut bytes = vec![0u8; 16];
224        bytes[0..2].copy_from_slice(&[b'M', b'Z']);
225
226        let tmp = tempfile::NamedTempFile::new().unwrap();
227        std::fs::write(tmp.path(), &bytes).unwrap();
228
229        let info = detect_architecture(tmp.path()).unwrap();
230        assert_eq!(info.format, BinaryFormat::Pe);
231    }
232
233    #[test]
234    fn unknown_format() {
235        let bytes = vec![0u8; 16];
236
237        let tmp = tempfile::NamedTempFile::new().unwrap();
238        std::fs::write(tmp.path(), &bytes).unwrap();
239
240        let info = detect_architecture(tmp.path()).unwrap();
241        assert_eq!(info.format, BinaryFormat::Unknown);
242    }
243}