use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use hashbrown::{HashMap, HashSet};
use rayon::prelude::*;
use crate::elf::read::consts::{STB_LOCAL, STV_HIDDEN, STV_INTERNAL};
use crate::elf::read::{
Elf32Be, Elf32Le, Elf64Be, Elf64Le, ElfFormat, ElfKind, SharedObject, Source,
};
use crate::input::{Archive, FileFormat, FileTable, LibraryNaming, RealFileSystem, SearchContext};
use crate::script::{CommandKind, InputName, NoIncludes, parse_script};
use super::{LinkedLibrary, SearchScope};
type FastSet<'a> = HashSet<&'a [u8], foldhash::fast::FixedState>;
const MAX_SCRIPT_DEPTH: usize = 4;
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum LibraryKind {
Shared,
Static,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Object {
pub path: PathBuf,
pub display: String,
pub kind: Option<LibraryKind>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Entry {
pub path: PathBuf,
pub dir: usize,
pub objects: Vec<usize>,
pub script: bool,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Definition {
pub object: usize,
pub version: Option<String>,
pub hidden: bool,
pub member: Option<String>,
}
#[derive(Debug, Default)]
struct Scan {
display: String,
kind: Option<LibraryKind>,
definitions: Vec<(Vec<u8>, Definition)>,
members: Vec<PathBuf>,
member_ids: Vec<usize>,
script: bool,
}
#[derive(Debug, Default)]
pub struct LibraryIndex {
entries: Vec<Entry>,
objects: Vec<Object>,
definitions: HashMap<Vec<u8>, Vec<Definition>>,
linked: BTreeMap<usize, LinkedLibrary>,
linked_static: BTreeMap<String, PathBuf>,
}
impl LibraryIndex {
#[must_use]
pub fn build(scope: &SearchScope, linked: &[LinkedLibrary], names: &[&[u8]]) -> Self {
let mut builder = Builder {
scope,
wanted: names.iter().copied().collect(),
loaded: HashMap::default(),
objects: Vec::new(),
scans: Vec::new(),
};
let listed: Vec<Vec<PathBuf>> = scope
.directories()
.par_iter()
.map(|dir| list_dir(dir))
.collect();
let mut entry_paths: Vec<(usize, PathBuf)> = Vec::new();
for (dir, files) in listed.into_iter().enumerate() {
entry_paths.extend(files.into_iter().map(|file| (dir, file)));
}
let paths: Vec<PathBuf> = entry_paths
.iter()
.map(|(_, path)| path.clone())
.chain(linked.iter().map(|l| l.path.clone()))
.collect();
let ids = builder.load_all(&paths, 0);
let (entry_ids, linked_ids) = ids.split_at(entry_paths.len().min(ids.len()));
let mut index = Self::default();
for ((dir, path), id) in entry_paths.into_iter().zip(entry_ids) {
let Some(id) = *id else {
continue;
};
let objects = builder.expand(id, 0);
if objects.is_empty() {
continue;
}
let script = builder.scans.get(id).is_some_and(|scan| scan.script);
index.entries.push(Entry {
path,
dir,
objects,
script,
});
}
for (library, id) in linked.iter().zip(linked_ids) {
if let Some(id) = *id {
for object in builder.expand(id, 0) {
index
.linked
.entry(object)
.or_insert_with(|| library.clone());
}
}
if library.static_only
&& let Some(name) = library_name(&library.path, ".a")
{
index
.linked_static
.entry(name)
.or_insert_with(|| library.path.clone());
}
}
for (id, (scan, path)) in builder.scans.into_iter().zip(builder.objects).enumerate() {
for (name, mut definition) in scan.definitions {
definition.object = id;
index.definitions.entry(name).or_default().push(definition);
}
index.objects.push(Object {
path,
display: scan.display,
kind: scan.kind,
});
}
index
}
#[must_use]
pub fn entries(&self) -> &[Entry] {
&self.entries
}
#[must_use]
pub fn objects(&self) -> &[Object] {
&self.objects
}
#[must_use]
pub fn definitions(&self, name: &[u8]) -> &[Definition] {
self.definitions.get(name).map_or(&[], Vec::as_slice)
}
#[must_use]
pub fn linked(&self, object: usize) -> Option<&LinkedLibrary> {
self.linked.get(&object)
}
#[must_use]
pub fn linked_static(&self, name: &str) -> Option<&Path> {
self.linked_static.get(name).map(PathBuf::as_path)
}
#[must_use]
pub fn best_entry(&self, object: usize) -> Option<&Entry> {
self.entries
.iter()
.filter(|entry| entry.objects.contains(&object))
.min_by(|a, b| {
let key = |e: &Entry| {
let plain = library_name(&e.path, ".so").is_some()
|| library_name(&e.path, ".a").is_some();
(e.dir, !plain)
};
key(a).cmp(&key(b)).then_with(|| a.path.cmp(&b.path))
})
}
#[must_use]
pub fn flag(scope: &SearchScope, entry: &Entry) -> String {
let fs = RealFileSystem;
let context = SearchContext {
search_paths: &scope.search_paths,
sysroot: scope.sysroot.as_deref(),
naming: LibraryNaming::Elf,
fs: &fs,
};
let finds_it = |found: Option<PathBuf>| found.is_some_and(|found| found == entry.path);
for suffix in [".so", ".a"] {
if let Some(name) = library_name(&entry.path, suffix)
&& finds_it(context.find_library(&name, false))
{
return format!("-l{name}");
}
}
if let Some(file) = entry.path.file_name().and_then(|n| n.to_str())
&& finds_it(context.find_exact(file))
{
return format!("-l:{file}");
}
entry.path.display().to_string()
}
}
pub(crate) fn library_name(path: &Path, suffix: &str) -> Option<String> {
let file = path.file_name()?.to_str()?;
let name = file.strip_prefix("lib")?.strip_suffix(suffix)?;
(!name.is_empty()).then(|| name.to_string())
}
fn list_dir(dir: &Path) -> Vec<PathBuf> {
let Ok(read) = std::fs::read_dir(dir) else {
return Vec::new();
};
let mut files: Vec<PathBuf> = read
.filter_map(Result::ok)
.filter(|entry| {
entry.file_name().to_str().is_some_and(|name| {
name.ends_with(".so") || name.ends_with(".a") || name.contains(".so.")
})
})
.map(|entry| entry.path())
.collect();
files.sort();
files
}
struct Builder<'s, 'w> {
scope: &'s SearchScope,
wanted: FastSet<'w>,
loaded: HashMap<PathBuf, usize>,
objects: Vec<PathBuf>,
scans: Vec<Scan>,
}
impl Builder<'_, '_> {
fn load_all(&mut self, paths: &[PathBuf], depth: usize) -> Vec<Option<usize>> {
let canonical: Vec<Option<PathBuf>> = paths
.par_iter()
.map(|path| std::fs::canonicalize(path).ok().filter(|p| p.is_file()))
.collect();
let mut fresh: Vec<PathBuf> = Vec::new();
{
let mut seen: HashSet<&Path> = HashSet::new();
for path in canonical.iter().flatten() {
if !self.loaded.contains_key(path) && seen.insert(path) {
fresh.push(path.clone());
}
}
}
let wanted = &self.wanted;
let scans: Vec<Scan> = fresh
.par_iter()
.map(|path| scan_file(path, wanted))
.collect();
let mut scripts = Vec::new();
for (path, scan) in fresh.into_iter().zip(scans) {
let id = self.objects.len();
if scan.script {
scripts.push(id);
}
self.loaded.insert(path.clone(), id);
self.objects.push(path);
self.scans.push(scan);
}
if depth < MAX_SCRIPT_DEPTH {
for script in scripts {
let script_path = self.objects.get(script).cloned().unwrap_or_default();
let members: Vec<PathBuf> = self
.scans
.get(script)
.map(|scan| {
scan.members
.iter()
.map(|member| resolve_member(self.scope, &script_path, member))
.collect()
})
.unwrap_or_default();
let ids: Vec<usize> = self
.load_all(&members, depth.saturating_add(1))
.into_iter()
.flatten()
.collect();
if let Some(scan) = self.scans.get_mut(script) {
scan.member_ids = ids;
}
}
}
canonical
.into_iter()
.map(|path| path.and_then(|p| self.loaded.get(&p).copied()))
.collect()
}
fn expand(&self, id: usize, depth: usize) -> Vec<usize> {
let Some(scan) = self.scans.get(id) else {
return Vec::new();
};
if !scan.script {
return if scan.kind.is_some() {
vec![id]
} else {
Vec::new()
};
}
if depth >= MAX_SCRIPT_DEPTH {
return Vec::new();
}
let mut out = Vec::new();
for &member in &scan.member_ids {
for object in self.expand(member, depth.saturating_add(1)) {
if !out.contains(&object) {
out.push(object);
}
}
}
out
}
}
fn resolve_member(scope: &SearchScope, script: &Path, member: &Path) -> PathBuf {
let fs = RealFileSystem;
let context = SearchContext {
search_paths: &scope.search_paths,
sysroot: scope.sysroot.as_deref(),
naming: LibraryNaming::Elf,
fs: &fs,
};
if let Some(name) = member.to_str().and_then(|m| m.strip_prefix("-l")) {
return context.find_library(name, false).unwrap_or_default();
}
let text = member.as_os_str();
if text
.to_str()
.is_some_and(|t| t.starts_with('=') || t.starts_with("$SYSROOT"))
{
return crate::input::search::apply_sysroot(member, scope.sysroot.as_deref());
}
if member.is_absolute() {
if let Some(sysroot) = &scope.sysroot
&& let Ok(root) = std::fs::canonicalize(sysroot)
&& script.starts_with(&root)
{
let relative = member.strip_prefix("/").unwrap_or(member);
return root.join(relative);
}
return member.to_path_buf();
}
if let Some(dir) = script.parent() {
let beside = dir.join(member);
if beside.is_file() {
return beside;
}
}
context.find_script(member).unwrap_or_default()
}
fn scan_file(path: &Path, wanted: &FastSet<'_>) -> Scan {
let display = path
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_default();
let mut scan = Scan {
display,
..Scan::default()
};
let table = FileTable::new();
let Ok(id) = table.load_path(path) else {
return scan;
};
let Some(file) = table.get(id) else {
return scan;
};
let data = file.data();
match file.format() {
FileFormat::Elf(ident) if ident.is_shared() => {
scan.kind = Some(LibraryKind::Shared);
let source = Source::new(path);
let _ = match ElfKind::identify(data) {
Some(ElfKind::Elf64Le) => scan_shared::<Elf64Le>(data, source, wanted, &mut scan),
Some(ElfKind::Elf64Be) => scan_shared::<Elf64Be>(data, source, wanted, &mut scan),
Some(ElfKind::Elf32Le) => scan_shared::<Elf32Le>(data, source, wanted, &mut scan),
Some(ElfKind::Elf32Be) => scan_shared::<Elf32Be>(data, source, wanted, &mut scan),
None => None,
};
}
FileFormat::Archive | FileFormat::ThinArchive => {
scan.kind = Some(LibraryKind::Static);
scan_archive(path, data, wanted, &mut scan);
}
FileFormat::Text(_) => {
if let Ok(script) = parse_script(data, path, &mut NoIncludes) {
scan.script = true;
for command in &script.commands {
let (CommandKind::Group(files) | CommandKind::Input(files)) = &command.kind
else {
continue;
};
for input in files {
scan.members.push(match &input.name {
InputName::Path(bytes) => bytes_to_path(bytes),
InputName::Library(name) => {
let mut flag = b"-l".to_vec();
flag.extend_from_slice(name);
bytes_to_path(&flag)
}
});
}
}
}
}
_ => {}
}
scan
}
fn scan_shared<F: ElfFormat>(
data: &[u8],
source: Source<'_>,
wanted: &FastSet<'_>,
scan: &mut Scan,
) -> Option<()> {
let object = SharedObject::<F>::parse(data, source).ok()?;
if let Some(soname) = object.soname() {
scan.display = String::from_utf8_lossy(soname).into_owned();
}
let symbols = object.symbols();
for (index, raw) in symbols.iter_raw().enumerate() {
if raw.st_shndx == 0
|| raw.binding() == STB_LOCAL
|| matches!(raw.visibility(), STV_HIDDEN | STV_INTERNAL)
{
continue;
}
let Ok(name) = symbols.name(index, &raw) else {
continue;
};
if !wanted.contains(name) {
continue;
}
let Ok(version) = object.symbol_version(index) else {
continue;
};
if version.index == 0 {
continue;
}
let version_name = version
.info
.filter(|info| !info.is_base())
.map(|info| String::from_utf8_lossy(info.name).into_owned());
scan.definitions.push((
name.to_vec(),
Definition {
object: 0,
version: version_name,
hidden: version.hidden,
member: None,
},
));
}
Some(())
}
fn scan_archive(path: &Path, data: &[u8], wanted: &FastSet<'_>, scan: &mut Scan) {
let Ok(archive) = Archive::parse(path, data) else {
return;
};
let Some(symbols) = archive.symbol_index() else {
return;
};
for symbol in symbols.iter() {
let Ok(symbol) = symbol else {
break;
};
if !wanted.contains(symbol.name) {
continue;
}
let member = archive
.member_at(symbol.member_offset)
.ok()
.map(|member| member.display_name());
scan.definitions.push((
symbol.name.to_vec(),
Definition {
object: 0,
version: None,
hidden: false,
member,
},
));
}
}
#[cfg(unix)]
fn bytes_to_path(bytes: &[u8]) -> PathBuf {
use std::os::unix::ffi::OsStrExt;
PathBuf::from(std::ffi::OsStr::from_bytes(bytes))
}
#[cfg(not(unix))]
fn bytes_to_path(bytes: &[u8]) -> PathBuf {
PathBuf::from(String::from_utf8_lossy(bytes).into_owned())
}