use super::bytes::{Endian, Source, cstr, subslice, to_u64};
use super::consts::{
DYLD_CHAINED_IMPORT, DYLD_CHAINED_IMPORT_ADDEND, DYLD_CHAINED_IMPORT_ADDEND64,
DYLD_CHAINED_SYMBOL_UNCOMPRESSED,
};
use crate::error::Result;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ChainedFixupsHeader {
pub fixups_version: u32,
pub starts_offset: u32,
pub imports_offset: u32,
pub symbols_offset: u32,
pub imports_count: u32,
pub imports_format: u32,
pub symbols_format: u32,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ChainedImport<'a> {
pub lib_ordinal: i32,
pub weak_import: bool,
pub name: &'a [u8],
pub addend: i64,
}
#[derive(Clone, Copy, Debug)]
pub struct ChainedFixups<'a> {
pub header: ChainedFixupsHeader,
data: &'a [u8],
file_offset: u64,
endian: Endian,
source: Source<'a>,
}
impl<'a> ChainedFixups<'a> {
pub fn parse(
data: &'a [u8],
file_offset: u64,
endian: Endian,
source: Source<'a>,
) -> Result<Self> {
let fail = |what: &str| source.malformed(file_offset, format!("chained fixups ({what})"));
let field = |at: usize| endian.u32(data, at).ok_or_else(|| fail("truncated header"));
let header = ChainedFixupsHeader {
fixups_version: field(0)?,
starts_offset: field(4)?,
imports_offset: field(8)?,
symbols_offset: field(12)?,
imports_count: field(16)?,
imports_format: field(20)?,
symbols_format: field(24)?,
};
let size = match header.imports_format {
DYLD_CHAINED_IMPORT => 4u64,
DYLD_CHAINED_IMPORT_ADDEND => 8,
DYLD_CHAINED_IMPORT_ADDEND64 => 16,
_ => return Err(fail("unknown imports format")),
};
if header.symbols_format != DYLD_CHAINED_SYMBOL_UNCOMPRESSED {
return Err(fail("compressed symbol names are not supported"));
}
if subslice(
data,
u64::from(header.imports_offset),
u64::from(header.imports_count).saturating_mul(size),
)
.is_none()
{
return Err(fail("imports table extends past the blob"));
}
if usize::try_from(header.symbols_offset)
.ok()
.is_none_or(|at| at > data.len())
{
return Err(fail("symbols offset out of range"));
}
Ok(Self {
header,
data,
file_offset,
endian,
source,
})
}
#[must_use]
pub fn len(&self) -> usize {
self.header.imports_count as usize
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.header.imports_count == 0
}
pub fn import(&self, index: usize) -> Result<ChainedImport<'a>> {
let fail = |what: &str| {
self.source.malformed(
self.file_offset
.saturating_add(u64::from(self.header.imports_offset)),
format!("chained import {index} ({what})"),
)
};
if index >= self.len() {
return Err(fail("out of range"));
}
let e = self.endian;
let base = usize::try_from(self.header.imports_offset).unwrap_or(usize::MAX);
let (lib_ordinal, weak_import, name_offset, addend) = match self.header.imports_format {
DYLD_CHAINED_IMPORT | DYLD_CHAINED_IMPORT_ADDEND => {
let size = if self.header.imports_format == DYLD_CHAINED_IMPORT {
4
} else {
8
};
let at = index
.checked_mul(size)
.and_then(|o| o.checked_add(base))
.ok_or_else(|| fail("offset"))?;
let raw = e.u32(self.data, at).ok_or_else(|| fail("truncated"))?;
let addend = if size == 8 {
at.checked_add(4)
.and_then(|a| e.u32(self.data, a))
.map_or(0, |v| i64::from(v as i32))
} else {
0
};
let ordinal = (raw & 0xff) as u8;
let lib_ordinal = if ordinal > 0xf0 {
i32::from(ordinal as i8)
} else {
i32::from(ordinal)
};
(lib_ordinal, (raw >> 8) & 1 != 0, raw >> 9, addend)
}
_ => {
let at = index
.checked_mul(16)
.and_then(|o| o.checked_add(base))
.ok_or_else(|| fail("offset"))?;
let raw = e.u64(self.data, at).ok_or_else(|| fail("truncated"))?;
let addend = at
.checked_add(8)
.and_then(|a| e.u64(self.data, a))
.map_or(0, |v| v as i64);
let ordinal = (raw & 0xffff) as u16;
let lib_ordinal = if ordinal > 0xfff0 {
i32::from(ordinal as i16)
} else {
i32::from(ordinal)
};
(
lib_ordinal,
(raw >> 16) & 1 != 0,
(raw >> 32) as u32,
addend,
)
}
};
let name = usize::try_from(self.header.symbols_offset)
.ok()
.and_then(|s| s.checked_add(usize::try_from(name_offset).ok()?))
.and_then(|at| self.data.get(at..))
.and_then(cstr)
.ok_or_else(|| fail("name offset"))?;
Ok(ChainedImport {
lib_ordinal,
weak_import,
name,
addend,
})
}
pub fn imports(&self) -> impl Iterator<Item = Result<ChainedImport<'a>>> + '_ {
(0..self.len()).map(|index| self.import(index))
}
#[must_use]
pub fn file_offset(&self) -> u64 {
self.file_offset
}
#[must_use]
pub fn size(&self) -> u64 {
to_u64(self.data.len())
}
}
#[cfg(test)]
#[allow(clippy::arithmetic_side_effects)]
mod tests {
use super::*;
use std::path::Path;
#[test]
fn imports() {
let src = Source::new(Path::new("lib.dylib"));
let mut blob = Vec::new();
for v in [0u32, 28, 28, 36, 2, DYLD_CHAINED_IMPORT, 0] {
blob.extend_from_slice(&v.to_le_bytes());
}
blob.extend_from_slice(&1u32.to_le_bytes());
blob.extend_from_slice(&(0xfeu32 | (1 << 8) | (4 << 9)).to_le_bytes());
blob.extend_from_slice(b"_a\0\0_b\0");
let fixups = ChainedFixups::parse(&blob, 0, Endian::LITTLE, src).unwrap();
let imports: Vec<_> = fixups.imports().collect::<Result<_>>().unwrap();
assert_eq!(imports.len(), 2);
assert_eq!((imports[0].lib_ordinal, imports[0].name), (1, &b"_a"[..]));
assert_eq!(imports[1].lib_ordinal, -2);
assert!(imports[1].weak_import);
assert_eq!(imports[1].name, b"_b");
assert!(fixups.import(2).is_err());
assert!(ChainedFixups::parse(&blob[..20], 0, Endian::LITTLE, src).is_err());
}
}