use anyhow::{anyhow, bail, Context};
use camino::Utf8Path;
use fs_err as fs;
use goblin::{
archive::Archive,
elf::Elf,
mach::{segment::Section, symbols, Mach, MachO, SingleArch},
pe::PE,
Object,
};
use std::collections::HashSet;
use uniffi_meta::Metadata;
pub fn extract_from_library(path: &Utf8Path) -> anyhow::Result<Vec<Metadata>> {
extract_from_bytes(&fs::read(path)?)
}
fn extract_from_bytes(file_data: &[u8]) -> anyhow::Result<Vec<Metadata>> {
match Object::parse(file_data)? {
Object::Elf(elf) => extract_from_elf(elf, file_data),
Object::PE(pe) => extract_from_pe(pe, file_data),
Object::Mach(mach) => extract_from_mach(mach, file_data),
Object::Archive(archive) => extract_from_archive(archive, file_data),
_ => bail!("Unknown library format"),
}
}
pub fn extract_from_elf(elf: Elf<'_>, file_data: &[u8]) -> anyhow::Result<Vec<Metadata>> {
let mut extracted = ExtractedItems::new();
let symtab_shndx_section_offset = elf
.section_headers
.iter()
.find(|sh| sh.sh_type == goblin::elf::section_header::SHT_SYMTAB_SHNDX)
.map(|sh| sh.sh_offset as usize);
for (i, sym) in elf.syms.iter().enumerate() {
let name = elf
.strtab
.get_at(sym.st_name)
.context("Error getting symbol name")?;
if !is_metadata_symbol(name) {
continue;
}
let header_index = match sym.st_shndx as u32 {
goblin::elf::section_header::SHN_XINDEX => {
let section_offset = symtab_shndx_section_offset
.ok_or_else(|| anyhow!("Symbol {name} has st_shndx=SHN_XINDEX, but no SHT_SYMTAB_SHNDX section present"))?;
let offset = section_offset + (i * 4);
let slice = file_data.get(offset..offset + 4).ok_or_else(|| {
anyhow!("Index error looking up {name} in the SHT_SYMTAB_SHNDX section")
})?;
let byte_array = slice.try_into().unwrap();
if elf.little_endian {
u32::from_le_bytes(byte_array) as usize
} else {
u32::from_be_bytes(byte_array) as usize
}
}
_ => sym.st_shndx,
};
let sh = elf
.section_headers
.get(header_index)
.ok_or_else(|| anyhow!("Index error looking up section header for {name}"))?;
let section_offset = sym.st_value - sh.sh_addr;
extracted.extract_item(name, file_data, (sh.sh_offset + section_offset) as usize)?;
}
Ok(extracted.into_metadata())
}
pub fn extract_from_pe(pe: PE<'_>, file_data: &[u8]) -> anyhow::Result<Vec<Metadata>> {
let mut extracted = ExtractedItems::new();
for export in pe.exports {
if let Some(name) = export.name {
if is_metadata_symbol(name) {
extracted.extract_item(
name,
file_data,
export.offset.context("Error getting symbol offset")?,
)?;
}
}
}
Ok(extracted.into_metadata())
}
pub fn extract_from_mach(mach: Mach<'_>, file_data: &[u8]) -> anyhow::Result<Vec<Metadata>> {
match mach {
Mach::Binary(macho) => extract_from_macho(macho, file_data),
Mach::Fat(multi_arch) => match multi_arch.get(0)? {
SingleArch::MachO(macho) => extract_from_macho(macho, file_data),
SingleArch::Archive(archive) => extract_from_archive(archive, file_data),
},
}
}
pub fn extract_from_macho(macho: MachO<'_>, file_data: &[u8]) -> anyhow::Result<Vec<Metadata>> {
let mut sections: Vec<Section> = Vec::new();
for sects in macho.segments.sections() {
sections.extend(sects.map(|r| r.expect("section").0));
}
let mut extracted = ExtractedItems::new();
sections.sort_by_key(|s| s.addr);
for (name, nlist) in macho.symbols().flatten() {
if nlist.is_global()
&& nlist.get_type() == symbols::N_SECT
&& is_metadata_symbol(name)
&& nlist.n_sect != symbols::NO_SECT as usize
{
let section = §ions[nlist.n_sect - 1];
let offset = section.offset as usize + nlist.n_value as usize - section.addr as usize;
extracted.extract_item(name, file_data, offset)?;
}
}
for export in macho.exports()? {
let name = &export.name;
if is_metadata_symbol(name) {
extracted.extract_item(name, file_data, export.offset as usize)?;
}
}
Ok(extracted.into_metadata())
}
pub fn extract_from_archive(
archive: Archive<'_>,
file_data: &[u8],
) -> anyhow::Result<Vec<Metadata>> {
let mut members_to_check: HashSet<&str> = HashSet::new();
for (member_name, _, symbols) in archive.summarize() {
for name in symbols {
if is_metadata_symbol(name) {
members_to_check.insert(member_name);
}
}
}
let mut items = vec![];
for member_name in members_to_check {
items.append(
&mut extract_from_bytes(
archive
.extract(member_name, file_data)
.with_context(|| format!("Failed to extract archive member `{member_name}`"))?,
)
.with_context(|| {
format!("Failed to extract data from archive member `{member_name}`")
})?,
);
}
Ok(items)
}
#[derive(Default)]
struct ExtractedItems {
items: Vec<Metadata>,
names: HashSet<String>,
}
impl ExtractedItems {
fn new() -> Self {
Self::default()
}
fn extract_item(&mut self, name: &str, file_data: &[u8], offset: usize) -> anyhow::Result<()> {
if self.names.contains(name) {
return Ok(());
}
let data = &file_data[offset..];
self.items.push(
Metadata::read(data).with_context(|| format!("extracting metadata for '{name}'"))?,
);
self.names.insert(name.to_string());
Ok(())
}
fn into_metadata(self) -> Vec<Metadata> {
self.items
}
}
fn is_metadata_symbol(name: &str) -> bool {
let name = name.strip_prefix('_').unwrap_or(name);
name.starts_with("UNIFFI_META")
}