use super::arch::Arch;
use super::bytes::{Endian, Source, subslice, to_u64};
use super::consts::{FAT_MAGIC, FAT_MAGIC_64};
use crate::error::Result;
const HEADER_SIZE: usize = 8;
const ARCH_SIZE: usize = 20;
const ARCH64_SIZE: usize = 32;
const JAVA_MIN_MAJOR: u32 = 45;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct FatSlice<'a> {
pub arch: Arch,
pub raw_cpu_subtype: u32,
pub offset: u64,
pub size: u64,
pub align: u32,
pub data: &'a [u8],
}
#[derive(Clone, Debug)]
pub struct FatFile<'a> {
slices: Vec<FatSlice<'a>>,
is64: bool,
source: Source<'a>,
}
impl<'a> FatFile<'a> {
#[must_use]
pub fn is_fat(data: &[u8]) -> bool {
let magic = Endian::BIG.u32(data, 0);
matches!(magic, Some(FAT_MAGIC | FAT_MAGIC_64))
&& Endian::BIG
.u32(data, 4)
.is_some_and(|count| count < JAVA_MIN_MAJOR)
}
pub fn parse(data: &'a [u8], source: Source<'a>) -> Result<Self> {
let magic = Endian::BIG
.u32(data, 0)
.ok_or_else(|| source.malformed(0, "universal header (truncated)"))?;
let is64 = match magic {
FAT_MAGIC => false,
FAT_MAGIC_64 => true,
_ => return Err(source.malformed(0, "universal header magic")),
};
let count = Endian::BIG
.u32(data, 4)
.ok_or_else(|| source.malformed(4, "universal header (truncated)"))?;
if count >= JAVA_MIN_MAJOR {
return Err(source.malformed(
4,
format!("universal header (nfat_arch {count}: a Java class file?)"),
));
}
let entry_size = if is64 { ARCH64_SIZE } else { ARCH_SIZE };
let mut slices = Vec::with_capacity(count as usize);
for i in 0..count as usize {
let at = i
.checked_mul(entry_size)
.and_then(|n| n.checked_add(HEADER_SIZE))
.ok_or_else(|| source.malformed(0, "universal header"))?;
let entry = data
.get(at..)
.and_then(|d| d.get(..entry_size))
.ok_or_else(|| source.malformed(to_u64(at), format!("fat_arch {i} (truncated)")))?;
let be32 = |off| Endian::BIG.u32(entry, off).unwrap_or(0);
let be64 = |off| Endian::BIG.u64(entry, off).unwrap_or(0);
let cpu_type = be32(0);
let raw_cpu_subtype = be32(4);
let (offset, size, align) = if is64 {
(be64(8), be64(16), be32(24))
} else {
(u64::from(be32(8)), u64::from(be32(12)), be32(16))
};
let slice_data = subslice(data, offset, size).ok_or_else(|| {
source.malformed(
to_u64(at),
format!(
"fat_arch {i} (slice at {offset:#x} size {size:#x} is outside the file)"
),
)
})?;
slices.push(FatSlice {
arch: Arch::new(cpu_type, raw_cpu_subtype),
raw_cpu_subtype,
offset,
size,
align,
data: slice_data,
});
}
Ok(Self {
slices,
is64,
source,
})
}
#[must_use]
pub fn is64(&self) -> bool {
self.is64
}
#[must_use]
pub fn slices(&self) -> &[FatSlice<'a>] {
&self.slices
}
#[must_use]
pub fn find(&self, arch: Arch) -> Option<&FatSlice<'a>> {
self.slices.iter().find(|slice| slice.arch == arch)
}
pub fn select(&self, arch: Arch) -> Result<&FatSlice<'a>> {
if let Some(slice) = self.find(arch) {
return Ok(slice);
}
if arch == Arch::X86_64 {
let mut same = self
.slices
.iter()
.filter(|slice| slice.arch.cpu_type == arch.cpu_type);
if let (Some(slice), None) = (same.next(), same.next()) {
return Ok(slice);
}
}
let available = self
.slices
.iter()
.map(|slice| slice.arch.to_string())
.collect::<Vec<_>>()
.join(", ");
Err(self.source.malformed(
0,
format!(
"universal file (no slice for architecture {arch}; available: {})",
if available.is_empty() {
"none"
} else {
&available
}
),
))
}
#[must_use]
pub fn slice_source(&self, slice: &FatSlice<'_>) -> Source<'a> {
self.source.at(slice.offset)
}
}
#[cfg(test)]
#[allow(clippy::arithmetic_side_effects)]
mod tests {
use super::*;
use crate::macho::read::consts::{CPU_TYPE_ARM64, CPU_TYPE_X86_64};
use std::path::Path;
fn fat(entries: &[(u32, u32, u32, u32)], total: usize) -> Vec<u8> {
let mut v = Vec::new();
v.extend_from_slice(&FAT_MAGIC.to_be_bytes());
v.extend_from_slice(&(entries.len() as u32).to_be_bytes());
for &(t, s, off, size) in entries {
for x in [t, s, off, size, 12] {
v.extend_from_slice(&x.to_be_bytes());
}
}
v.resize(total, 0xaa);
v
}
#[test]
fn select_slices() {
let src = Source::new(Path::new("fat"));
let data = fat(
&[
(CPU_TYPE_X86_64, 8, 0x100, 0x10),
(CPU_TYPE_ARM64, 0x8000_0002, 0x200, 0x20),
],
0x300,
);
assert!(FatFile::is_fat(&data));
let file = FatFile::parse(&data, src).unwrap();
assert_eq!(file.slices().len(), 2);
assert_eq!(file.select(Arch::ARM64E).unwrap().offset, 0x200);
assert_eq!(file.select(Arch::X86_64).unwrap().arch, Arch::X86_64H);
let error = file.select(Arch::ARM64).unwrap_err().to_string();
assert!(
error.contains("no slice for architecture arm64; available: x86_64h, arm64e"),
"{error}"
);
let bad = fat(&[(CPU_TYPE_ARM64, 0, 0x100, 0x1000)], 0x200);
assert!(FatFile::parse(&bad, src).is_err());
let mut class = FAT_MAGIC.to_be_bytes().to_vec();
class.extend_from_slice(&[0, 0, 0, 52]);
assert!(!FatFile::is_fat(&class));
assert!(FatFile::parse(&class, src).is_err());
}
}