use std::fs::OpenOptions;
use std::io::{Read, Seek, SeekFrom, Write};
use std::path::Path;
const MAGIC: [u8; 4] = [0x7f, b'E', b'L', b'F'];
const CLASS_64: u8 = 2;
const DATA_LITTLE_ENDIAN: u8 = 1;
const PT_LOAD: u32 = 1;
const PT_DYNAMIC: u32 = 2;
const DT_NULL: i64 = 0;
const DT_STRTAB: i64 = 5;
const DT_SONAME: i64 = 14;
#[derive(Clone, Copy)]
struct Segment {
kind: u32,
offset: u64,
address: u64,
file_size: u64,
}
#[derive(Clone, Copy)]
struct Dynamic {
tag: i64,
value: u64,
}
struct DynamicSection {
offset: u64,
entries: Vec<Dynamic>,
loads: Vec<Segment>,
}
pub fn soname(library: &Path) -> Result<Option<String>, String> {
let mut file = open(library, false)?;
let Some(section) = dynamic_section(&mut file, library)? else {
return Ok(None);
};
let Some(name_offset) = section
.entries
.iter()
.find(|entry| entry.tag == DT_SONAME)
.map(|entry| entry.value)
else {
return Ok(None);
};
let Some(string_table) = section
.entries
.iter()
.find(|entry| entry.tag == DT_STRTAB)
.map(|entry| entry.value)
else {
return Err(format!(
"{} records a SONAME but no string table to read it out of",
library.display()
));
};
let address = string_table.saturating_add(name_offset);
let Some(at) = file_offset(§ion.loads, address) else {
return Err(format!(
"{} places its SONAME at an address no loadable segment covers",
library.display()
));
};
Ok(Some(read_c_string(&mut file, at, library)?))
}
pub fn remove_soname(library: &Path) -> Result<bool, String> {
let mut file = open(library, true)?;
let Some(section) = dynamic_section(&mut file, library)? else {
return Ok(false);
};
let Some(index) = section.entries.iter().position(|entry| entry.tag == DT_SONAME) else {
return Ok(false);
};
let mut entries = section.entries;
entries.remove(index);
entries.push(Dynamic {
tag: DT_NULL,
value: 0,
});
let mut bytes = Vec::with_capacity(entries.len() * 16);
for entry in &entries {
bytes.extend_from_slice(&entry.tag.to_le_bytes());
bytes.extend_from_slice(&entry.value.to_le_bytes());
}
file.seek(SeekFrom::Start(section.offset))
.and_then(|_| file.write_all(&bytes))
.and_then(|()| file.flush())
.map_err(|error| {
format!(
"could not rewrite the dynamic section of {}: {error}",
library.display()
)
})?;
Ok(true)
}
fn open(library: &Path, writable: bool) -> Result<std::fs::File, String> {
OpenOptions::new()
.read(true)
.write(writable)
.open(library)
.map_err(|error| format!("could not open {}: {error}", library.display()))
}
fn dynamic_section(
file: &mut std::fs::File,
library: &Path,
) -> Result<Option<DynamicSection>, String> {
let mut header = [0u8; 64];
read_exact_at(file, 0, &mut header, library)?;
if header[..4] != MAGIC {
return Err(format!("{} is not an ELF file", library.display()));
}
if header[4] != CLASS_64 || header[5] != DATA_LITTLE_ENDIAN {
return Err(format!(
"{} is not a 64-bit little-endian ELF file, which is the only shape the \
renderer is published in",
library.display()
));
}
let table_offset = u64::from_le_bytes(header[32..40].try_into().expect("eight bytes"));
let entry_size = u16::from_le_bytes(header[54..56].try_into().expect("two bytes")) as u64;
let count = u16::from_le_bytes(header[56..58].try_into().expect("two bytes")) as u64;
if entry_size < 56 {
return Err(format!(
"{} declares {entry_size} byte program headers, which cannot hold one",
library.display()
));
}
let mut loads = Vec::new();
let mut dynamic = None;
for index in 0..count {
let mut entry = [0u8; 56];
read_exact_at(file, table_offset + index * entry_size, &mut entry, library)?;
let segment = Segment {
kind: u32::from_le_bytes(entry[0..4].try_into().expect("four bytes")),
offset: u64::from_le_bytes(entry[8..16].try_into().expect("eight bytes")),
address: u64::from_le_bytes(entry[16..24].try_into().expect("eight bytes")),
file_size: u64::from_le_bytes(entry[32..40].try_into().expect("eight bytes")),
};
match segment.kind {
PT_LOAD => loads.push(segment),
PT_DYNAMIC => dynamic = Some(segment),
_ => {}
}
}
let Some(dynamic) = dynamic else {
return Ok(None);
};
let mut entries = Vec::new();
for index in 0..dynamic.file_size / 16 {
let mut entry = [0u8; 16];
read_exact_at(file, dynamic.offset + index * 16, &mut entry, library)?;
let entry = Dynamic {
tag: i64::from_le_bytes(entry[0..8].try_into().expect("eight bytes")),
value: u64::from_le_bytes(entry[8..16].try_into().expect("eight bytes")),
};
let terminator = entry.tag == DT_NULL;
entries.push(entry);
if terminator {
break;
}
}
Ok(Some(DynamicSection {
offset: dynamic.offset,
entries,
loads,
}))
}
fn file_offset(loads: &[Segment], address: u64) -> Option<u64> {
loads.iter().find_map(|segment| {
let end = segment.address.checked_add(segment.file_size)?;
(segment.address <= address && address < end)
.then(|| segment.offset + (address - segment.address))
})
}
fn read_c_string(file: &mut std::fs::File, at: u64, library: &Path) -> Result<String, String> {
let mut window = [0u8; 1024];
file.seek(SeekFrom::Start(at))
.map_err(|error| format!("could not read {}: {error}", library.display()))?;
let read = file
.read(&mut window)
.map_err(|error| format!("could not read {}: {error}", library.display()))?;
let end = window[..read]
.iter()
.position(|byte| *byte == 0)
.ok_or_else(|| {
format!(
"the SONAME in {} is not terminated within a kilobyte",
library.display()
)
})?;
String::from_utf8(window[..end].to_vec())
.map_err(|_| format!("the SONAME in {} is not UTF-8", library.display()))
}
fn read_exact_at(
file: &mut std::fs::File,
at: u64,
buffer: &mut [u8],
library: &Path,
) -> Result<(), String> {
file.seek(SeekFrom::Start(at))
.and_then(|_| file.read_exact(buffer))
.map_err(|error| {
format!(
"could not read {} bytes at {at} of {}: {error}",
buffer.len(),
library.display()
)
})
}