use std::collections::{BTreeMap, BTreeSet, btree_map::Entry};
use std::fs;
use std::io;
use std::ops::ControlFlow;
use std::path::{Path, PathBuf};
use goblin::elf::program_header::{PF_W, PT_LOAD};
use onelf_format::catalog;
use onelf_format::rules::{Descriptor, ExclusionBase, ExclusionEntry, ResourceKind, SourceBase};
use onelf_sysroot::path::in_root;
use super::collect::{GroupCollection, visit_members};
use super::datadirs::path_like;
use super::{
CandidateHit, Finding, Outcome, completeness, dynstr_range, format_size, is_excluded,
libc_family_of_soname, library_dir_of, object_prefix, read_elf_class, resolve_within,
};
use crate::bundle::sysroot::SysrootUniverse;
pub(crate) const MAX_OBJECT_PATHS: usize = 1024;
pub(crate) const MAX_OBJECT_RESOLUTIONS: usize = 65536;
pub(crate) const MAX_INFERRED_DIRS: usize = 256;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum InferredKind {
Plugins,
Data,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct InferredDir {
pub source: PathBuf,
pub prefix: PathBuf,
pub rel: PathBuf,
pub destination: PathBuf,
pub kind: InferredKind,
pub size: u64,
pub compiled: BTreeSet<String>,
pub objects: BTreeSet<String>,
}
impl InferredDir {
pub(crate) fn hit(&self) -> CandidateHit {
CandidateHit {
candidate: 0,
base: SourceBase::InstallationPrefix,
kind: ResourceKind::Directory,
source: self.source.clone(),
resolved: self.source.clone(),
root: self.prefix.clone(),
destination: self.destination.clone(),
}
}
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct Scope<'a> {
pub sysroot: Option<&'a SysrootUniverse>,
pub trees: &'a [&'a Path],
pub exclude: &'a [String],
pub descriptors: &'a [&'a Descriptor],
pub lib_dir: &'a Path,
}
#[derive(Debug, Clone)]
pub(crate) struct Object<'a> {
pub name: &'a str,
pub file: &'a Path,
pub twin: Option<PathBuf>,
}
#[derive(Debug, Default)]
pub(crate) struct Inference {
dirs: BTreeMap<PathBuf, InferredDir>,
handed: BTreeSet<PathBuf>,
refused: BTreeMap<PathBuf, Refusal>,
}
#[derive(Debug)]
struct Refusal {
dir: InferredDir,
why: String,
}
impl Inference {
pub(crate) fn read(&mut self, objects: &[Object], scope: &Scope) -> io::Result<Vec<PathBuf>> {
let exclude: Vec<&str> = scope.exclude.iter().map(String::as_str).collect();
let mut fresh = Vec::new();
let reported = self.refused.keys().cloned().collect::<BTreeSet<_>>();
for object in objects {
let name = object.name;
for candidate in candidates_of(object, scope, &exclude)? {
let at_limit = self.dirs.len() >= MAX_INFERRED_DIRS;
match self.dirs.entry(candidate.source.clone()) {
Entry::Occupied(mut held) => {
let handed_out = self.handed.contains(&candidate.source);
let dir = held.get_mut();
let wins = !handed_out
&& (&candidate.prefix, &candidate.rel) < (&dir.prefix, &dir.rel);
if wins {
dir.prefix = candidate.prefix.clone();
dir.rel = candidate.rel.clone();
dir.destination = candidate.destination.clone();
}
dir.compiled.insert(candidate.compiled.clone());
dir.objects.insert(name.to_string());
if !wins {
continue;
}
match classify(held.get(), scope.sysroot) {
Ok((kind, size)) => {
let dir = held.get_mut();
dir.kind = kind;
dir.size = size;
}
Err(error) => {
let (_, dir) = held.remove_entry();
self.refused.insert(
candidate.source.clone(),
Refusal {
dir,
why: format!("it cannot be read: {error}"),
},
);
fresh.retain(|source| source != &candidate.source);
}
}
}
Entry::Vacant(slot) => {
match self.refused.entry(candidate.source.clone()) {
Entry::Occupied(mut failed) => {
let dir = &mut failed.get_mut().dir;
dir.compiled.insert(candidate.compiled.clone());
dir.objects.insert(name.to_string());
if (&candidate.prefix, &candidate.rel) >= (&dir.prefix, &dir.rel) {
continue;
}
dir.prefix = candidate.prefix.clone();
dir.rel = candidate.rel.clone();
dir.destination = candidate.destination.clone();
match classify(dir, scope.sysroot) {
Ok((kind, size)) => {
if at_limit {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!(
"{name} compiles in more directories than the \
{MAX_INFERRED_DIRS} one build may infer \
(MAX_INFERRED_DIRS); refusing unbounded inference"
),
));
}
let refusal = failed.get_mut();
refusal.dir.kind = kind;
refusal.dir.size = size;
let (_, refusal) = failed.remove_entry();
fresh.push(candidate.source.clone());
slot.insert(refusal.dir);
}
Err(error) => {
failed.get_mut().why =
format!("it cannot be read: {error}");
}
}
}
Entry::Vacant(unread) => {
if at_limit {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!(
"{name} compiles in more directories than the \
{MAX_INFERRED_DIRS} one build may infer \
(MAX_INFERRED_DIRS); refusing unbounded inference"
),
));
}
let mut dir = InferredDir {
source: candidate.source.clone(),
prefix: candidate.prefix.clone(),
rel: candidate.rel.clone(),
destination: candidate.destination.clone(),
kind: InferredKind::Data,
size: 0,
compiled: BTreeSet::from([candidate.compiled.clone()]),
objects: BTreeSet::from([name.to_string()]),
};
match classify(&dir, scope.sysroot) {
Ok((kind, size)) => {
dir.kind = kind;
dir.size = size;
fresh.push(candidate.source.clone());
slot.insert(dir);
}
Err(error) => {
unread.insert(Refusal {
dir,
why: format!("it cannot be read: {error}"),
});
}
}
}
}
}
}
}
}
fresh.sort();
self.handed.extend(fresh.iter().cloned());
for (source, refusal) in &self.refused {
if !reported.contains(source) {
omit(&refusal.dir, &refusal.why);
}
}
Ok(fresh)
}
pub(crate) fn get(&self, source: &Path) -> Option<&InferredDir> {
self.dirs.get(source)
}
pub(crate) fn all(&self) -> impl Iterator<Item = &InferredDir> {
self.dirs.values()
}
}
fn classify(
dir: &InferredDir,
sysroot: Option<&SysrootUniverse>,
) -> io::Result<(InferredKind, u64)> {
let mut size = 0u64;
let mut objects = false;
let hit = dir.hit();
visit_members(&hit, sysroot, &mut |rel, file| {
if read_elf_class(&file).is_some() {
objects = true;
return ControlFlow::Break(());
}
if file == hit.resolved.join(&rel) {
size += fs::metadata(&file).map(|meta| meta.len()).unwrap_or(0);
}
ControlFlow::Continue(())
})?;
Ok(if objects {
(InferredKind::Plugins, 0)
} else {
(InferredKind::Data, size)
})
}
pub(crate) fn omit(dir: &InferredDir, why: &str) {
completeness::record(Finding::new(
dir.destination.display().to_string(),
Outcome::OptionalOmission,
format!("omitted because {why}; {}", origin(dir)),
));
}
fn origin(dir: &InferredDir) -> String {
let objects: Vec<&str> = dir.objects.iter().map(String::as_str).collect();
let compiled: Vec<&str> = dir.compiled.iter().map(String::as_str).collect();
format!(
"inferred from {} below the prefix {}, compiled in by {} as {}",
dir.source.display(),
dir.prefix.display(),
objects.join(", "),
compiled.join(", ")
)
}
const SHOWN_SKIPS: usize = 3;
pub(crate) enum Carried<'a> {
Data,
Trimmed { kept: usize, total: usize },
Plugins(&'a GroupCollection, usize, Option<(usize, usize)>),
Held,
WithinTrimmed(&'a Path),
Within(&'a Path),
}
pub(crate) const PENDING_REDIRECTION: &str = "redirection pending";
pub(crate) fn report(dir: &InferredDir, size: u64, carried: Carried) {
let kind = match dir.kind {
InferredKind::Plugins => "plugin directory",
InferredKind::Data => "data directory",
};
let (outcome, mut what) = match carried {
Carried::Data => (
Outcome::Selected,
format!(
"{kind}, {}, carried, {PENDING_REDIRECTION}",
format_size(size)
),
),
Carried::Trimmed { kept, total } => (
Outcome::TraceLimited,
format!(
"{kind}, {kept} of {total} member(s) kept by the trace, {}; the rest \
the trace did not open; {PENDING_REDIRECTION}",
format_size(size)
),
),
Carried::Held => (
Outcome::Selected,
format!(
"{kind}, {}, held by the tree's own copy, {PENDING_REDIRECTION}",
format_size(size)
),
),
Carried::WithinTrimmed(outer) => (
Outcome::TraceLimited,
format!(
"{kind}, whole size {}, carried within {}, only what the trace opened \
rides, redirected with it",
format_size(size),
outer.display()
),
),
Carried::Within(outer) => (
Outcome::Selected,
format!(
"{kind}, {}, carried within {}, redirected with it",
format_size(size),
outer.display()
),
),
Carried::Plugins(collection, 0, None) if collection.plugins.is_empty() => (
Outcome::OptionalOmission,
"plugin directory with nothing the bundle can carry".to_string(),
),
Carried::Plugins(collection, files, None) => (
Outcome::Selected,
format!(
"plugin directory, {} object(s) and {files} other file(s), {}, \
{PENDING_REDIRECTION}",
collection.plugins.len(),
format_size(size)
),
),
Carried::Plugins(collection, files, Some((kept, total))) => (
Outcome::TraceLimited,
format!(
"plugin directory, {} object(s) and {files} other file(s), {}; {kept} of \
{total} member(s) kept by the trace; the rest the trace did not open; \
{PENDING_REDIRECTION}",
collection.plugins.len(),
format_size(size)
),
),
};
if let Carried::Plugins(collection, _, _) = carried {
let skipped = &collection.skipped;
for (name, reason) in skipped.iter().take(SHOWN_SKIPS) {
what.push_str(&format!("; {name} omitted: {reason}"));
}
if skipped.len() > SHOWN_SKIPS {
what.push_str(&format!("; {} more omitted", skipped.len() - SHOWN_SKIPS));
}
}
completeness::record(Finding::new(
dir.destination.display().to_string(),
outcome,
format!("{what}; {}", origin(dir)),
));
}
#[derive(Debug)]
struct Candidate {
compiled: String,
source: PathBuf,
prefix: PathBuf,
rel: PathBuf,
destination: PathBuf,
}
fn candidates_of(object: &Object, scope: &Scope, exclude: &[&str]) -> io::Result<Vec<Candidate>> {
let path = object.file;
let sysroot = scope.sysroot;
if libc_family_of_soname(object.name).is_some() {
return Ok(Vec::new());
}
let installed = object.twin.as_deref().unwrap_or(path);
let Some(own_prefix) = object_prefix(installed) else {
return Ok(Vec::new());
};
if scope
.trees
.iter()
.any(|tree| installed.starts_with(tree) && !own_prefix.starts_with(tree))
{
return Ok(Vec::new());
}
let Ok(data) = fs::read(path) else {
return Ok(Vec::new());
};
let source_root = sysroot.map_or(Path::new("/"), SysrootUniverse::root);
let below_source = |place: &Path| match scope
.trees
.iter()
.find_map(|tree| place.strip_prefix(tree).ok())
{
Some(rel) => source_root.join(rel),
None => place.to_path_buf(),
};
let prefix = below_source(&own_prefix);
let under_root = prefix == source_root;
let resolve = |place: &Path| -> Option<PathBuf> {
let from_root = place.strip_prefix(source_root).ok()?;
match sysroot {
Some(universe) => in_root(universe.root(), from_root).ok(),
None => resolve_within(Path::new("/"), from_root).ok(),
}
};
let own_dir = installed
.parent()
.map(below_source)
.map(|dir| resolve(&dir).unwrap_or(dir));
let own_rel = own_dir
.as_deref()
.and_then(|dir| dir.strip_prefix(&prefix).ok())
.map(|rel| merged(rel, under_root).to_path_buf());
let merged_parent = prefix
.file_name()
.is_some_and(|name| name == "usr")
.then(|| prefix.parent())
.flatten();
let mut seen: BTreeSet<&str> = BTreeSet::new();
let mut directories = 0usize;
let mut out = Vec::new();
for compiled in compiled_strings(&data) {
let from_root = Path::new(&compiled[1..]);
let declared = source_root.join(from_root);
let plausible = declared
.strip_prefix(&prefix)
.is_ok_and(|rel| in_resource_base(rel, under_root))
|| merged_parent
.and_then(|parent| declared.strip_prefix(parent).ok())
.is_some_and(|rel| in_resource_base(rel, true));
if !plausible || !seen.insert(compiled) {
continue;
}
if seen.len() > MAX_OBJECT_RESOLUTIONS {
return Err(bounded(
path,
MAX_OBJECT_RESOLUTIONS,
"MAX_OBJECT_RESOLUTIONS",
));
}
let Some(source) = resolve(&declared) else {
continue;
};
if !source.is_dir() {
continue;
}
directories += 1;
if directories > MAX_OBJECT_PATHS {
return Err(bounded(path, MAX_OBJECT_PATHS, "MAX_OBJECT_PATHS"));
}
let Ok(rel) = source.strip_prefix(&prefix) else {
continue;
};
let declared_rel = declared
.strip_prefix(&prefix)
.ok()
.map(|rel| merged(rel, under_root));
if !in_resource_base(rel, under_root)
|| dropped(merged(rel, under_root), own_rel.as_deref(), exclude)
|| described(merged(rel, under_root), scope.descriptors)
|| declared_rel.is_some_and(|rel| described(rel, scope.descriptors))
{
continue;
}
if let Some(universe) = sysroot {
let admitted = source
.strip_prefix(universe.root())
.is_ok_and(|rel| universe.admits(&rel.to_string_lossy()));
if !admitted {
continue;
}
}
let destination = destination_of(merged(rel, under_root), scope.lib_dir);
let rel = rel.to_path_buf();
out.push(Candidate {
compiled: compiled.to_string(),
prefix: prefix.clone(),
rel,
destination,
source,
});
}
Ok(out)
}
fn dropped(rel: &Path, own: Option<&Path>, exclude: &[&str]) -> bool {
let exclusions = catalog::inference_exclusions();
let library_dir = library_dir_of(rel);
let owned = |entry: &ExclusionEntry| {
let owner = match entry.base {
ExclusionBase::InstallationPrefix => PathBuf::from(entry.path.as_str()),
ExclusionBase::LibraryDirs => match &library_dir {
Some(dir) => dir.join(entry.path.as_str()),
None => return false,
},
};
rel.starts_with(&owner) || owner.starts_with(rel)
};
let base = library_dir.as_deref().unwrap_or(Path::new("share"));
let excluded = rel.strip_prefix(base).is_ok_and(|below| {
below
.components()
.any(|part| is_excluded(&part.as_os_str().to_string_lossy(), exclude))
});
own.is_some_and(|own| own.starts_with(rel))
|| excluded
|| exclusions
.shared_roots
.iter()
.any(|root| Path::new(root.as_str()).starts_with(rel))
|| exclusions
.shared_trees
.iter()
.chain(&exclusions.libc)
.chain(&exclusions.drivers)
.any(owned)
}
fn described(rel: &Path, descriptors: &[&Descriptor]) -> bool {
let library_dir = library_dir_of(rel);
descriptors
.iter()
.flat_map(|descriptor| {
let resources = &descriptor.resources;
resources
.plugins
.iter()
.chain(&resources.data)
.flat_map(|group| &group.candidates)
.chain(resources.helpers.iter().flat_map(|h| &h.candidates))
})
.any(|candidate| {
let declared = match candidate.base {
SourceBase::InstallationPrefix => PathBuf::from(candidate.path.as_str()),
SourceBase::LibraryDir => match &library_dir {
Some(dir) => dir.join(candidate.path.as_str()),
None => return false,
},
};
rel.starts_with(&declared) || declared.starts_with(rel)
})
}
fn destination_of(rel: &Path, lib_dir: &Path) -> PathBuf {
match library_dir_of(rel).and_then(|dir| rel.strip_prefix(dir).ok().map(Path::to_path_buf)) {
Some(below) => lib_dir.join(below),
None => rel.to_path_buf(),
}
}
fn bounded(path: &Path, cap: usize, name: &str) -> io::Error {
io::Error::new(
io::ErrorKind::InvalidData,
format!(
"{}: more than {cap} compiled-in paths ({name}); refusing unbounded inference",
path.display()
),
)
}
fn compiled_strings(data: &[u8]) -> impl Iterator<Item = &str> {
read_only_ranges(data)
.into_iter()
.flat_map(move |(start, end)| data[start..end].split(|&b| b == 0))
.filter_map(normalized_absolute)
}
#[cfg(test)]
pub(crate) fn test_elf(segments: &[(u32, &[&str])]) -> Vec<u8> {
tests::test_image(segments)
}
pub(crate) fn read_only_ranges(data: &[u8]) -> Vec<(usize, usize)> {
let Ok(elf) = goblin::elf::Elf::parse(data) else {
return Vec::new();
};
let mut ranges: Vec<(usize, usize)> = elf
.program_headers
.iter()
.filter(|ph| ph.p_type == PT_LOAD && ph.p_flags & PF_W == 0)
.filter_map(|ph| {
let start = usize::try_from(ph.p_offset).ok()?.min(data.len());
let size = usize::try_from(ph.p_filesz).ok()?;
Some((start, start.saturating_add(size).min(data.len())))
})
.filter(|(start, end)| start < end)
.collect();
ranges.sort_unstable();
let mut merged: Vec<(usize, usize)> = Vec::with_capacity(ranges.len());
for (start, end) in ranges {
match merged.last_mut() {
Some(last) if start <= last.1 => last.1 = last.1.max(end),
_ => merged.push((start, end)),
}
}
let mut skips: Vec<(usize, usize)> = dynstr_range(&elf).into_iter().collect();
if let Some(dynamic) = elf.dynamic.as_ref() {
let (strtab, strsz) = (dynamic.info.strtab, dynamic.info.strsz);
if strtab != 0
&& strsz != 0
&& let Some(end) = strtab.checked_add(strsz)
{
skips.push((strtab, end));
}
}
for (skip_start, skip_end) in skips {
merged = merged
.into_iter()
.flat_map(|(start, end)| [(start, end.min(skip_start)), (start.max(skip_end), end)])
.filter(|(start, end)| start < end)
.collect();
}
merged
}
fn normalized_absolute(piece: &[u8]) -> Option<&str> {
if piece.len() < 2 || !path_like(piece) {
return None;
}
let path = std::str::from_utf8(piece).ok()?;
path[1..]
.split('/')
.all(|part| !part.is_empty() && part != "." && part != "..")
.then_some(path)
}
fn merged(rel: &Path, under_root: bool) -> &Path {
match rel.strip_prefix("usr") {
Ok(below) if under_root => below,
_ => rel,
}
}
fn in_resource_base(rel: &Path, under_root: bool) -> bool {
let rel = merged(rel, under_root);
let base = match library_dir_of(rel) {
Some(dir) => dir,
None if rel.starts_with("share") => PathBuf::from("share"),
None => return false,
};
rel.starts_with(&base) && rel != base
}
#[cfg(test)]
mod tests {
use super::*;
use crate::bundle::collect::MAX_GROUP_DEPTH;
use crate::bundle::plan::TREE_WALK_LIMIT;
use crate::bundle::sysroot::fixture;
use onelf_format::rules::is_multiarch;
use std::os::unix::fs::symlink;
const READ: u32 = 4;
const READ_WRITE: u32 = 6;
#[derive(Clone, Copy, PartialEq)]
enum Dynstr {
Dynamic,
Section,
}
fn image(
class64: bool,
big_endian: bool,
segments: &[(u32, &[&str])],
runpath: Option<(&str, Dynstr)>,
) -> Vec<u8> {
let put = |data: &mut Vec<u8>, at: usize, value: u64, width: usize| {
let bytes = value.to_le_bytes();
let mut field = bytes[..width].to_vec();
if big_endian {
field.reverse();
}
data[at..at + width].copy_from_slice(&field);
};
let (ehsize, phentsize, word) = if class64 { (64, 56, 8) } else { (52, 32, 4) };
let dynamic = runpath.is_some_and(|(_, table)| table == Dynstr::Dynamic);
let phnum = segments.len() + usize::from(dynamic);
let mut data = vec![0u8; ehsize + phentsize * phnum];
data[0..4].copy_from_slice(b"\x7fELF");
data[4] = if class64 { 2 } else { 1 };
data[5] = if big_endian { 2 } else { 1 };
data[6] = 1;
put(&mut data, 16, 3, 2);
put(&mut data, 18, if class64 { 62 } else { 3 }, 2);
put(&mut data, 20, 1, 4);
let (phoff_at, sizes_at) = if class64 { (32, 52) } else { (28, 40) };
put(&mut data, phoff_at, ehsize as u64, word);
put(&mut data, sizes_at, ehsize as u64, 2);
put(&mut data, sizes_at + 2, phentsize as u64, 2);
put(&mut data, sizes_at + 4, phnum as u64, 2);
let mut dynstr = None;
let mut headers = Vec::new();
for (index, (flags, strings)) in segments.iter().enumerate() {
let offset = data.len();
data.push(0);
for s in *strings {
data.extend_from_slice(s.as_bytes());
data.push(0);
}
if let (0, Some((runpath, _))) = (index, runpath) {
dynstr = Some((data.len(), runpath.len() + 2));
data.push(0);
data.extend_from_slice(runpath.as_bytes());
data.push(0);
}
headers.push((PT_LOAD, *flags, offset, data.len() - offset));
}
if let Some((strtab, strsz)) = dynstr.filter(|_| dynamic) {
let offset = data.len();
for (tag, value) in [
(29u64, 1u64),
(5, strtab as u64),
(10, strsz as u64),
(0, 0),
] {
for v in [tag, value] {
let at = data.len();
data.extend(std::iter::repeat_n(0, word));
put(&mut data, at, v, word);
}
}
headers.push((2, READ, offset, data.len() - offset));
}
for (index, (kind, flags, offset, size)) in headers.into_iter().enumerate() {
let ph = ehsize + phentsize * index;
let (offset, size) = (offset as u64, size as u64);
put(&mut data, ph, u64::from(kind), 4);
if class64 {
put(&mut data, ph + 4, u64::from(flags), 4);
for (at, value) in [
(8, offset),
(16, offset),
(24, offset),
(32, size),
(40, size),
] {
put(&mut data, ph + at, value, 8);
}
} else {
for (at, value) in [
(4, offset),
(8, offset),
(12, offset),
(16, size),
(20, size),
] {
put(&mut data, ph + at, value, 4);
}
put(&mut data, ph + 24, u64::from(flags), 4);
}
}
if let Some((strtab, strsz)) = dynstr.filter(|_| !dynamic) {
const NAMES: &[u8] = b"\0.dynstr\0.shstrtab\0";
let names = data.len();
data.extend_from_slice(NAMES);
while !data.len().is_multiple_of(8) {
data.push(0);
}
let (shentsize, shoff_at, counts_at, offset_at, size_at) = if class64 {
(64, 40, 58, 24, 32)
} else {
(40, 32, 46, 16, 20)
};
let shoff = data.len();
put(&mut data, shoff_at, shoff as u64, word);
put(&mut data, counts_at, shentsize as u64, 2);
put(&mut data, counts_at + 2, 3, 2);
put(&mut data, counts_at + 4, 2, 2);
data.extend(std::iter::repeat_n(0, shentsize * 3));
for (index, name, offset, size) in [(1, 1, strtab, strsz), (2, 9, names, NAMES.len())] {
let sh = shoff + shentsize * index;
put(&mut data, sh, name, 4);
put(&mut data, sh + 4, 3, 4);
put(&mut data, sh + offset_at, offset as u64, word);
put(&mut data, sh + size_at, size as u64, word);
}
}
data
}
fn elf(segments: &[(u32, &[&str])]) -> Vec<u8> {
image(true, false, segments, None)
}
pub(crate) fn test_image(segments: &[(u32, &[&str])]) -> Vec<u8> {
elf(segments)
}
fn strings(data: &[u8]) -> Vec<&str> {
let set: BTreeSet<&str> = compiled_strings(data).collect();
set.into_iter().collect()
}
struct Tree(PathBuf);
impl Tree {
fn new(tag: &str) -> Self {
let path =
std::env::temp_dir().join(format!("onelf-infer-{tag}-{}", std::process::id()));
let _ = fs::remove_dir_all(&path);
fs::create_dir_all(&path).unwrap();
Self(path.canonicalize().unwrap())
}
fn path(&self, rel: &str) -> String {
self.0.join(rel).to_string_lossy().into_owned()
}
fn dir(&self, rel: &str) {
fs::create_dir_all(self.0.join(rel)).unwrap();
}
fn file(&self, rel: &str, bytes: &[u8]) {
let path = self.0.join(rel);
fs::create_dir_all(path.parent().unwrap()).unwrap();
fs::write(path, bytes).unwrap();
}
fn object(&self, rel: &str, paths: &[&str]) -> (String, PathBuf) {
let owned: Vec<String> = paths.iter().map(|p| self.path(p)).collect();
let strings: Vec<&str> = owned.iter().map(String::as_str).collect();
self.file(rel, &elf(&[(READ, &strings)]));
let path = self.0.join(rel);
let name = path.file_name().unwrap().to_str().unwrap().to_string();
(name, path)
}
}
impl Drop for Tree {
fn drop(&mut self) {
let _ = fs::remove_dir_all(&self.0);
}
}
fn discover(objects: &[Object], scope: &Scope) -> io::Result<Vec<InferredDir>> {
let mut inference = Inference::default();
inference.read(objects, scope)?;
Ok(inference.dirs.into_values().collect())
}
fn run(
objects: &[(String, PathBuf)],
sysroot: Option<&SysrootUniverse>,
) -> io::Result<Vec<InferredDir>> {
run_in(objects, sysroot, &[])
}
fn run_in(
objects: &[(String, PathBuf)],
sysroot: Option<&SysrootUniverse>,
trees: &[&Path],
) -> io::Result<Vec<InferredDir>> {
let objects: Vec<Object> = objects
.iter()
.map(|(name, path)| Object {
name,
file: path,
twin: None,
})
.collect();
discover(
&objects,
&Scope {
sysroot,
trees,
exclude: &[],
descriptors: &[],
lib_dir: Path::new("lib"),
},
)
}
fn rels(dirs: &[InferredDir]) -> Vec<String> {
dirs.iter()
.map(|d| d.rel.to_string_lossy().into_owned())
.collect()
}
#[test]
fn compiled_paths_are_whole_normalized_strings_of_read_only_segments() {
let image = elf(&[
(
READ,
&[
"/usr/share/examplefw",
"/usr/lib/examplefw/plugins",
"/usr/share/examplefw",
"/usr/bin/../lib/bfd-plugins",
"/usr/lib/..",
"/usr/lib64/../lib64",
"/usr/share//opencc",
"/usr/./share/b",
"/usr/share/d/",
"/usr/share/with space",
"relative/path",
"/",
],
),
(READ_WRITE, &["/usr/share/writable"]),
]);
assert_eq!(
strings(&image),
["/usr/lib/examplefw/plugins", "/usr/share/examplefw"]
);
assert!(strings(b"not an elf /usr/share/examplefw\0").is_empty());
}
#[test]
fn every_class_and_byte_order_reads_and_hostile_tables_do_not_panic() {
let paths: &[&str] = &["/usr/share/examplefw"];
for (class64, big_endian) in [(true, false), (false, false), (true, true), (false, true)] {
let data = image(class64, big_endian, &[(READ, paths)], None);
assert_eq!(strings(&data), paths, "class64 {class64} big {big_endian}");
}
let segments: Vec<(u32, &[&str])> = (0..1000).map(|_| (READ, paths)).collect();
let mut overlapping = image(true, false, &segments, None);
for index in 1..1000 {
let ph = 64 + 56 * index;
let first: Vec<u8> = overlapping[64 + 8..64 + 48].to_vec();
overlapping[ph + 8..ph + 48].copy_from_slice(&first);
}
assert_eq!(read_only_ranges(&overlapping).len(), 1);
assert_eq!(strings(&overlapping), paths);
let whole = image(true, false, &[(READ, paths), (READ, paths)], None);
assert!(strings(&whole[..64 + 56 + 8]).is_empty());
let mut past_end = whole.clone();
past_end[56..58].copy_from_slice(&u16::MAX.to_le_bytes());
assert!(strings(&past_end).is_empty());
let mut oversized = whole;
oversized[64 + 32..64 + 40].copy_from_slice(&u64::MAX.to_le_bytes());
let _ = strings(&oversized);
}
#[test]
fn run_paths_are_not_compiled_in_directories() {
let tree = Tree::new("runpath");
tree.dir("usr/lib/pulseaudio");
tree.dir("usr/share/examplefw");
let resource = tree.path("usr/share/examplefw");
let runpath = tree.path("usr/lib/pulseaudio");
for table in [Dynstr::Dynamic, Dynstr::Section] {
let data = image(
true,
false,
&[(READ, &[resource.as_str()])],
Some((&runpath, table)),
);
assert!(
data.windows(runpath.len()).any(|w| w == runpath.as_bytes()),
"the fixture carries the run path in a read-only segment"
);
assert_eq!(strings(&data), [resource.as_str()]);
tree.file("usr/lib/libpulse.so.0", &data);
let lib = (
"libpulse.so.0".to_string(),
tree.0.join("usr/lib/libpulse.so.0"),
);
assert_eq!(rels(&run(&[lib], None).unwrap()), ["share/examplefw"]);
}
}
#[test]
fn an_object_naming_too_many_directories_stops_with_the_bound_named() {
let tree = Tree::new("bound");
let mut paths: Vec<String> = (0..=MAX_OBJECT_PATHS)
.map(|i| format!("usr/share/p{i}"))
.collect();
for path in &paths {
tree.dir(path);
}
let refs: Vec<&str> = paths.iter().map(String::as_str).collect();
let lib = tree.object("usr/lib/libmany.so.1", &refs);
let error = run(&[lib], None).unwrap_err();
assert!(error.to_string().contains("MAX_OBJECT_PATHS"), "{error}");
paths.truncate(8);
paths.extend((0..2000).map(|i| format!("usr/colour{i}/1")));
paths.extend((0..2000).map(|i| format!("usr/lib/go/src/x/{i}.go")));
paths.extend((0..8).map(|i| format!("opt/other/share/p{i}")));
tree.dir("usr/lib/go/src/x");
let refs: Vec<&str> = paths.iter().map(String::as_str).collect();
let program = tree.object("usr/bin/tool", &refs);
assert_eq!(run(&[program], None).unwrap().len(), 8);
}
#[test]
fn an_object_driving_too_many_resolutions_stops_with_the_bound_named() {
let tree = Tree::new("resolutions");
let paths: Vec<String> = (0..=MAX_OBJECT_RESOLUTIONS)
.map(|i| format!("usr/share/p{i}"))
.collect();
let refs: Vec<&str> = paths.iter().map(String::as_str).collect();
let lib = tree.object("usr/lib/libmany.so.1", &refs);
let error = run(&[lib], None).unwrap_err();
assert!(
error.to_string().contains("MAX_OBJECT_RESOLUTIONS"),
"{error}"
);
}
#[test]
fn a_build_finding_too_many_directories_stops_with_the_bound_named() {
let tree = Tree::new("build-bound");
let paths: Vec<String> = (0..=MAX_INFERRED_DIRS)
.map(|i| format!("usr/share/fw{i}"))
.collect();
for path in &paths {
tree.dir(path);
}
let refs: Vec<&str> = paths.iter().map(String::as_str).collect();
let lib = tree.object("usr/lib/libmany.so.1", &refs[..MAX_INFERRED_DIRS]);
assert_eq!(
run(std::slice::from_ref(&lib), None).unwrap().len(),
MAX_INFERRED_DIRS
);
let more = tree.object("usr/lib/libmore.so.1", &refs[MAX_INFERRED_DIRS..]);
let error = run(&[lib, more], None).unwrap_err();
assert!(error.to_string().contains("MAX_INFERRED_DIRS"), "{error}");
}
#[test]
fn a_directory_past_the_walk_limit_is_never_read_whole() {
let tree = Tree::new("classify-bound");
for i in 0..=TREE_WALK_LIMIT {
tree.file(&format!("usr/share/big/f{i:06}"), b"");
}
let dir = InferredDir {
source: tree.0.join("usr/share/big"),
prefix: tree.0.clone(),
rel: PathBuf::from("usr/share/big"),
destination: PathBuf::from("usr/share/big"),
kind: InferredKind::Data,
size: 0,
compiled: BTreeSet::from([tree.path("usr/share/big")]),
objects: BTreeSet::from(["libprobe.so".to_string()]),
};
let error = classify(&dir, None).unwrap_err();
assert!(error.to_string().contains("past the walk limit"), "{error}");
}
#[test]
fn a_directory_named_from_two_prefixes_keeps_one_destination() {
let tree = Tree::new("two-prefixes");
tree.dir("p/share/sdk/share/fw");
let a = tree.object("p/lib/liba.so.1", &["p/share/sdk/share/fw"]);
let b = tree.object("p/share/sdk/lib/libb.so.1", &["p/share/sdk/share/fw"]);
let expected = InferredDir {
source: tree.0.join("p/share/sdk/share/fw"),
prefix: tree.0.join("p"),
rel: PathBuf::from("share/sdk/share/fw"),
destination: PathBuf::from("share/sdk/share/fw"),
kind: InferredKind::Data,
size: 0,
compiled: BTreeSet::from([tree.path("p/share/sdk/share/fw")]),
objects: BTreeSet::from(["liba.so.1".to_string(), "libb.so.1".to_string()]),
};
let dirs = run(&[a.clone(), b.clone()], None).unwrap();
assert_eq!(dirs.len(), 1, "{:?}", rels(&dirs));
assert_eq!(dirs[0].prefix, expected.prefix);
assert_eq!(dirs[0].destination, expected.destination);
let dirs = run(&[b, a], None).unwrap();
assert_eq!(dirs.len(), 1, "{:?}", rels(&dirs));
assert_eq!(dirs[0].prefix, expected.prefix, "the least prefix wins");
assert_eq!(
dirs[0].destination, expected.destination,
"the destination belongs to the winning prefix"
);
}
#[test]
fn a_directory_reclassified_under_the_winning_prefix() {
let tree = Tree::new("two-prefix-class");
tree.dir("p/share/sdk/share/fw");
tree.file("p/share/sdk/share/fw/conf", b"data");
tree.object("p/lib/plugin.so", &[]);
std::os::unix::fs::symlink(
"../../../../lib/plugin.so",
tree.0.join("p/share/sdk/share/fw/module.so"),
)
.unwrap();
let a = tree.object("p/lib/liba.so.1", &["p/share/sdk/share/fw"]);
let b = tree.object("p/share/sdk/lib/libb.so.1", &["p/share/sdk/share/fw"]);
for objects in [[a.clone(), b.clone()], [b.clone(), a.clone()]] {
let dirs = run(&objects, None).unwrap();
assert_eq!(dirs.len(), 1, "{:?}", rels(&dirs));
assert_eq!(dirs[0].prefix, tree.0.join("p"), "{:?}", rels(&dirs));
assert_eq!(dirs[0].destination, PathBuf::from("share/sdk/share/fw"));
assert_eq!(dirs[0].kind, InferredKind::Plugins, "{:?}", rels(&dirs));
}
}
#[test]
fn a_refused_directory_is_tried_again_under_a_smaller_pair() {
let tree = Tree::new("two-prefix-retry");
tree.dir("p/share/sdk/share/fw");
tree.file("p/share/sdk/share/fw/conf", b"data");
tree.object("p/lib/plugin.so", &[]);
std::os::unix::fs::symlink(
"../../../../lib/plugin.so",
tree.0.join("p/share/sdk/share/fw/0link"),
)
.unwrap();
let mut deep = tree.0.join("p/share/sdk/share/fw/zdeep");
for level in 0..=MAX_GROUP_DEPTH {
deep = deep.join(format!("d{level:02}"));
}
fs::create_dir_all(&deep).unwrap();
let a = tree.object("p/lib/liba.so.1", &["p/share/sdk/share/fw"]);
let b = tree.object("p/share/sdk/lib/libb.so.1", &["p/share/sdk/share/fw"]);
let expected = InferredDir {
source: tree.0.join("p/share/sdk/share/fw"),
prefix: tree.0.join("p"),
rel: PathBuf::from("share/sdk/share/fw"),
destination: PathBuf::from("share/sdk/share/fw"),
kind: InferredKind::Plugins,
size: 0,
compiled: BTreeSet::from([tree.path("p/share/sdk/share/fw")]),
objects: BTreeSet::from(["liba.so.1".to_string(), "libb.so.1".to_string()]),
};
for objects in [[a.clone(), b.clone()], [b.clone(), a.clone()]] {
let dirs = run(&objects, None).unwrap();
assert_eq!(dirs.len(), 1, "{:?}", rels(&dirs));
assert_eq!(dirs[0], expected, "{:?}", rels(&dirs));
}
}
#[test]
fn a_refusal_gains_spellings_past_the_directory_limit() {
let tree = Tree::new("refusal-full");
let mut deep = tree.0.join("p/share/sdk/deepfw/zdeep");
for level in 0..=MAX_GROUP_DEPTH {
deep = deep.join(format!("d{level:02}"));
}
fs::create_dir_all(&deep).unwrap();
let r1 = tree.object("p/lib/libr1.so.1", &["p/share/sdk/deepfw"]);
let fill: Vec<String> = (0..MAX_INFERRED_DIRS)
.map(|i| format!("usr/share/fw{i}"))
.collect();
for dir in &fill {
tree.dir(dir);
}
let fill_refs: Vec<&str> = fill.iter().map(String::as_str).collect();
let many = tree.object("usr/lib/libmany.so.1", &fill_refs);
let r2 = tree.object("p/lib/libr2.so.1", &["p/share/sdk/deepfw"]);
let dirs = run(&[r1, many, r2], None).unwrap();
assert_eq!(dirs.len(), MAX_INFERRED_DIRS, "{:?}", rels(&dirs));
assert!(!rels(&dirs).iter().any(|rel| rel.contains("deepfw")));
}
#[test]
fn a_directory_handed_out_keeps_its_pair_and_classification() {
let tree = Tree::new("two-prefix-handed");
tree.dir("p/share/sdk/share/fw");
tree.file("p/share/sdk/share/fw/conf", b"data");
tree.object("p/lib/plugin.so", &[]);
std::os::unix::fs::symlink(
"../../../../lib/plugin.so",
tree.0.join("p/share/sdk/share/fw/module.so"),
)
.unwrap();
let a = tree.object("p/lib/liba.so.1", &["p/share/sdk/share/fw"]);
let b = tree.object("p/share/sdk/lib/libb.so.1", &["p/share/sdk/share/fw"]);
let source = tree.0.join("p/share/sdk/share/fw");
let scope = Scope {
sysroot: None,
trees: &[],
exclude: &[],
descriptors: &[],
lib_dir: Path::new("lib"),
};
let mut inference = Inference::default();
let objects_b = [Object {
name: b.0.as_str(),
file: b.1.as_path(),
twin: None,
}];
let fresh = inference.read(&objects_b, &scope).unwrap();
assert_eq!(fresh.len(), 1, "{fresh:?}");
assert_eq!(fresh[0], source);
let carried = inference.get(&source).unwrap();
assert_eq!(carried.prefix, tree.0.join("p/share/sdk"));
assert_eq!(carried.destination, PathBuf::from("share/fw"));
assert_eq!(carried.kind, InferredKind::Data);
let objects_a = [Object {
name: a.0.as_str(),
file: a.1.as_path(),
twin: None,
}];
let fresh = inference.read(&objects_a, &scope).unwrap();
assert!(fresh.is_empty(), "the carried pair is not improved");
let carried = inference.get(&source).unwrap();
assert_eq!(carried.prefix, tree.0.join("p/share/sdk"));
assert_eq!(carried.destination, PathBuf::from("share/fw"));
assert_eq!(carried.kind, InferredKind::Data);
assert_eq!(
carried.objects.iter().collect::<Vec<_>>(),
["liba.so.1", "libb.so.1"]
);
}
#[test]
fn only_existing_directories_under_the_objects_prefix_are_candidates() {
let tree = Tree::new("prefix");
tree.dir("usr/share/examplefw");
tree.dir("opt/vendor/share/examplefw");
tree.file("usr/share/examplefw.conf", b"conf");
let lib = tree.object(
"usr/lib/libexamplefw.so.1",
&[
"usr/share/examplefw",
"opt/vendor/share/examplefw",
"usr/share/examplefw.conf",
"usr/share/missing",
],
);
let dirs = run(&[lib], None).unwrap();
assert_eq!(rels(&dirs), ["share/examplefw"]);
assert_eq!(dirs[0].prefix, tree.0.join("usr"));
assert_eq!(
dirs[0].objects.iter().collect::<Vec<_>>(),
["libexamplefw.so.1"]
);
}
#[test]
fn only_paths_strictly_below_a_resource_base_are_candidates() {
let tree = Tree::new("bases");
for dir in [
"usr/lib/x86_64-linux-gnu/examplefw",
"usr/lib64/examplefw",
"usr/share/examplefw",
"usr/etc/examplefw",
"usr/local/share/examplefw",
"usr/include/examplefw",
] {
tree.dir(dir);
}
let lib = tree.object(
"usr/lib/libexamplefw.so.1",
&[
"usr",
"usr/lib",
"usr/share",
"usr/lib/x86_64-linux-gnu",
"usr/lib/x86_64-linux-gnu/examplefw",
"usr/lib64/examplefw",
"usr/share/examplefw",
"usr/etc/examplefw",
"usr/local/share",
"usr/local/share/examplefw",
"usr/include/examplefw",
],
);
assert_eq!(
rels(&run(&[lib], None).unwrap()),
[
"lib/x86_64-linux-gnu/examplefw",
"lib64/examplefw",
"share/examplefw"
]
);
}
#[test]
fn a_root_prefix_counts_the_bases_below_usr_and_nothing_else() {
assert!(in_resource_base(Path::new("usr/share/foo"), true));
assert!(in_resource_base(Path::new("lib/foo"), true));
assert!(!in_resource_base(Path::new("usr/share/foo"), false));
for rel in [
"etc/pam.d",
"proc/self/fdinfo",
"usr/local/share",
"usr/lib/x86_64-linux-gnu",
"usr/share",
"usr",
] {
assert!(!in_resource_base(Path::new(rel), true), "{rel}");
}
assert!(is_multiarch("x86_64-linux-gnu"));
assert!(is_multiarch("arm-linux-gnueabihf"));
assert!(!is_multiarch("gstreamer-1.0"));
assert!(!is_multiarch("xorg"));
}
#[test]
fn a_lib_object_finds_usr_share_and_ignores_the_rest_of_the_root() {
let tree = Tree::new("root-prefix");
let root = tree.0.join("sys");
let compiled = [
"/etc/pam.d",
"/proc/self/fdinfo",
"/usr/local/share",
"/usr/share/foo",
];
tree.file("sys/lib/libfoo.so.1", &elf(&[(READ, &compiled)]));
let mut files = vec!["lib/libfoo.so.1".to_string()];
for dir in compiled {
files.push(format!("{}/file", &dir[1..]));
tree.file(&format!("sys{dir}/file"), b"x");
}
let refs: Vec<&str> = files.iter().map(String::as_str).collect();
let universe = fixture::populate_universe(
&root,
&[fixture::package("app", &refs)],
"app",
&[],
None,
None,
);
let lib = ("libfoo.so.1".to_string(), root.join("lib/libfoo.so.1"));
let dirs = run(&[lib], Some(&universe)).unwrap();
assert_eq!(rels(&dirs), ["usr/share/foo"]);
assert_eq!(dirs[0].prefix, root);
}
#[test]
fn linked_spellings_are_one_directory_and_escaping_links_are_dropped() {
let tree = Tree::new("canonical");
tree.dir("usr/lib/examplefw");
symlink("lib", tree.0.join("usr/lib64")).unwrap();
symlink("usr/lib", tree.0.join("lib")).unwrap();
tree.dir("usr/lib/mergedfw");
tree.dir("usr/share/xkeyboard-config-2");
tree.dir("usr/share/X11");
symlink("../xkeyboard-config-2", tree.0.join("usr/share/X11/xkb")).unwrap();
tree.dir("outside/escape");
symlink(
tree.0.join("outside/escape"),
tree.0.join("usr/share/escape"),
)
.unwrap();
tree.dir("share/climbed");
tree.dir("usr/share/a");
symlink("../../../share/climbed", tree.0.join("usr/share/a/x")).unwrap();
let lib = tree.object(
"usr/lib/libexamplefw.so.1",
&[
"usr/lib64/examplefw",
"usr/lib/examplefw",
"lib/mergedfw",
"usr/share/X11/xkb",
"usr/share/xkeyboard-config-2",
"usr/share/escape",
"usr/share/a/x",
],
);
let dirs = run(&[lib], None).unwrap();
assert_eq!(
rels(&dirs),
["lib/examplefw", "lib/mergedfw", "share/xkeyboard-config-2"]
);
assert_eq!(dirs[0].source, tree.0.join("usr/lib/examplefw"));
assert_eq!(
dirs[0].compiled.iter().cloned().collect::<Vec<_>>(),
[
tree.path("usr/lib/examplefw"),
tree.path("usr/lib64/examplefw")
]
);
assert_eq!(
dirs[1].compiled.iter().cloned().collect::<Vec<_>>(),
[tree.path("lib/mergedfw")]
);
assert_eq!(
dirs[2].compiled.iter().cloned().collect::<Vec<_>>(),
[
tree.path("usr/share/X11/xkb"),
tree.path("usr/share/xkeyboard-config-2")
]
);
}
#[test]
fn programs_are_read_and_plugins_beside_them_are_found() {
let tree = Tree::new("programs");
tree.dir("usr/lib/xorg/modules");
tree.dir("usr/share/toolfw");
let xorg = tree.object("usr/lib/xorg/Xorg", &["usr/lib/xorg/modules"]);
let tool = tree.object("usr/bin/tool", &["usr/share/toolfw"]);
let helper = tree.object("usr/libexec/tool/helper", &["usr/share/toolfw"]);
let dirs = run(&[xorg, tool, helper], None).unwrap();
assert_eq!(rels(&dirs), ["lib/xorg/modules", "share/toolfw"]);
assert_eq!(
dirs[1].objects.iter().collect::<Vec<_>>(),
["helper", "tool"]
);
}
#[test]
fn a_deep_module_has_the_prefix_of_its_library_directory() {
let tree = Tree::new("deep-module");
tree.dir("usr/share/modulefw");
let module = tree.object(
"usr/lib/fw/plugins/nested/module.so",
&["usr/share/modulefw"],
);
let dirs = run(&[module], None).unwrap();
assert_eq!(rels(&dirs), ["share/modulefw"]);
assert_eq!(dirs[0].prefix, tree.0.join("usr"));
}
#[test]
fn tree_programs_take_their_prefix_below_the_source_root() {
let tree = Tree::new("tree-objects");
let root = tree.0.join("sys");
let app = tree.0.join("app");
tree.file("sys/usr/share/X11/xkb/rules", b"r");
tree.file("app/usr/bin/tool", &elf(&[(READ, &["/usr/share/X11/xkb"])]));
let universe = fixture::populate_universe(
&root,
&[fixture::package("app", &["usr/share/X11/xkb/rules"])],
"app",
&[],
None,
None,
);
let objects = [("tool".to_string(), app.join("usr/bin/tool"))];
assert!(run(&objects, Some(&universe)).unwrap().is_empty());
let dirs = run_in(&objects, Some(&universe), &[&app]).unwrap();
assert_eq!(rels(&dirs), ["share/X11/xkb"]);
assert_eq!(dirs[0].prefix, root.join("usr"));
}
#[test]
fn a_tree_object_takes_no_prefix_above_the_tree() {
let tree = Tree::new("tree-climb");
let outside = tree.0.join("op/lib/hoststate");
tree.dir("op/lib/hoststate");
let app = tree.0.join("op/lib/ci/AppDir");
let spelled = outside.display().to_string();
tree.file(
"op/lib/ci/AppDir/opt/foo/foo",
&elf(&[(READ, &[spelled.as_str()])]),
);
let objects = [("foo".to_string(), app.join("opt/foo/foo"))];
assert!(run_in(&objects, None, &[&app]).unwrap().is_empty());
}
fn surviving(tag: &str, object: &str, dirs: &[&str], exclude: &[String]) -> Vec<String> {
let tree = Tree::new(tag);
for dir in dirs {
tree.dir(dir);
}
let object = tree.object(object, dirs);
let objects = [Object {
name: &object.0,
file: &object.1,
twin: None,
}];
let scope = Scope {
sysroot: None,
trees: &[],
exclude,
descriptors: &[],
lib_dir: Path::new("lib"),
};
rels(&discover(&objects, &scope).unwrap())
}
#[test]
fn roots_trees_libc_and_drivers_drop_what_the_table_says() {
let kept = surviving(
"table",
"usr/lib/libexamplefw.so.1",
&[
"usr/share/X11",
"usr/share/X11/xkb",
"usr/share/xml",
"usr/share/locale",
"usr/share/locale/de",
"usr/share/doc/slang/v2",
"usr/share/fonts/Type1",
"usr/share/zoneinfo",
"usr/share/drirc.d",
"usr/lib/gconv",
"usr/lib/locale",
"usr/lib/dri",
"usr/lib64/dri",
"usr/lib64/security",
"usr/lib/x86_64-linux-gnu/security",
"usr/lib/x86_64-linux-gnu/gconv",
"usr/lib/jvm/x",
"usr/lib/go",
"usr/lib/gcc/x86_64-linux-gnu/14",
"usr/lib/examplefw/plugins",
"usr/share/examplefw",
],
&[],
);
assert_eq!(
kept,
["lib/examplefw/plugins", "share/X11/xkb", "share/examplefw"]
);
}
#[test]
fn a_plugin_in_a_subdirectory_drops_driver_and_own_directories() {
let kept = surviving(
"plugin-subdir",
"usr/lib/gstreamer-1.0/libgstva.so",
&["usr/lib64/dri", "usr/lib/gstreamer-1.0", "usr/share/gstva"],
&[],
);
assert_eq!(kept, ["share/gstva"]);
}
#[test]
fn the_objects_own_directory_is_dropped_and_what_lies_below_it_kept() {
let kept = surviving(
"own-dir",
"usr/lib/xorg/Xorg",
&[
"usr/lib/xorg",
"usr/lib/xorg/modules",
"usr/lib/xorg/locale",
],
&[],
);
assert_eq!(kept, ["lib/xorg/locale", "lib/xorg/modules"]);
let kept = surviving(
"own-multiarch",
"usr/lib/x86_64-linux-gnu/libfoo.so.1",
&[
"usr/lib/x86_64-linux-gnu",
"usr/lib/x86_64-linux-gnu/security",
"usr/lib/x86_64-linux-gnu/foo",
],
&[],
);
assert_eq!(kept, ["lib/x86_64-linux-gnu/foo"]);
assert!(
dropped(Path::new("lib/foo"), Some(Path::new("lib/foo/bar")), &[]),
"a candidate containing the object's own directory is dropped"
);
assert!(!dropped(
Path::new("lib/foo/bar/plugins"),
Some(Path::new("lib/foo/bar")),
&[]
));
let kept = surviving(
"multiarch-base",
"usr/lib/libbar.so.1",
&["usr/lib/x86_64-linux-gnu"],
&[],
);
assert!(kept.is_empty(), "{kept:?}");
}
#[test]
fn libc_objects_are_skipped_and_recipe_exclusions_apply() {
assert!(surviving("libc", "usr/lib/libc.so.6", &["usr/share/examplefw"], &[]).is_empty());
assert!(
surviving(
"loader",
"usr/lib/ld-linux-x86-64.so.2",
&["usr/share/examplefw"],
&[]
)
.is_empty()
);
let kept = surviving(
"exclude",
"usr/lib/libexamplefw.so.1",
&["usr/lib/libdecor/plugins-1", "usr/share/examplefw"],
&["libdecor".to_string()],
);
assert_eq!(kept, ["share/examplefw"]);
let kept = surviving(
"exclude-base",
"usr/lib/libexamplefw.so.1",
&["usr/libx32/examplefw", "usr/lib/libxfoo"],
&["libx".to_string()],
);
assert_eq!(kept, ["libx32/examplefw"]);
}
#[test]
fn a_root_prefix_mirrors_the_table_and_the_own_directory_below_usr() {
let tree = Tree::new("root-mirror");
let root = tree.0.join("sys");
let compiled = [
"/usr/lib/firefox",
"/usr/share/X11",
"/usr/share/locale",
"/usr/lib/x86_64-linux-gnu/gconv",
"/usr/share/firefoxfw",
];
tree.file("sys/lib/firefox/libxul.so", &elf(&[(READ, &compiled)]));
let mut files = vec!["lib/firefox/libxul.so".to_string()];
for dir in compiled {
files.push(format!("{}/file", &dir[1..]));
tree.file(&format!("sys{dir}/file"), b"x");
}
let refs: Vec<&str> = files.iter().map(String::as_str).collect();
let universe = fixture::populate_universe(
&root,
&[fixture::package("app", &refs)],
"app",
&[],
None,
None,
);
let lib = ("libxul.so".to_string(), root.join("lib/firefox/libxul.so"));
let dirs = run(&[lib], Some(&universe)).unwrap();
assert_eq!(rels(&dirs), ["usr/share/firefoxfw"]);
}
#[test]
fn the_own_directory_is_resolved_through_links() {
let tree = Tree::new("own-link");
let root = tree.0.join("sys");
let app = tree.0.join("app");
tree.file("sys/usr/lib/foo/plugins/p", b"p");
symlink("lib", root.join("usr/lib64")).unwrap();
tree.file(
"app/usr/lib64/foo/prog",
&elf(&[(READ, &["/usr/lib/foo", "/usr/lib/foo/plugins"])]),
);
let universe = fixture::populate_universe(
&root,
&[fixture::package("app", &["usr/lib/foo/plugins/p"])],
"app",
&[],
None,
None,
);
let objects = [("prog".to_string(), app.join("usr/lib64/foo/prog"))];
let dirs = run_in(&objects, Some(&universe), &[&app]).unwrap();
assert_eq!(rels(&dirs), ["lib/foo/plugins"]);
}
#[test]
fn a_tree_programs_own_directory_is_taken_below_the_source_root() {
let tree = Tree::new("tree-own-dir");
let root = tree.0.join("sys");
let app = tree.0.join("app");
tree.file("sys/usr/lib/tool/plugins/p", b"p");
tree.file(
"app/usr/lib/tool/tool",
&elf(&[(READ, &["/usr/lib/tool", "/usr/lib/tool/plugins"])]),
);
let universe = fixture::populate_universe(
&root,
&[fixture::package("app", &["usr/lib/tool/plugins/p"])],
"app",
&[],
None,
None,
);
let objects = [("tool".to_string(), app.join("usr/lib/tool/tool"))];
let dirs = run_in(&objects, Some(&universe), &[&app]).unwrap();
assert_eq!(rels(&dirs), ["lib/tool/plugins"]);
}
fn omissions(destination: &str) -> Vec<String> {
crate::bundle::completeness::report(&[]);
crate::bundle::completeness::take_reported()
.into_iter()
.filter(|f| f.object == destination && f.outcome == Outcome::OptionalOmission)
.map(|f| f.reason)
.collect()
}
#[test]
fn a_directory_too_deep_to_walk_is_omitted_without_failing() {
let tree = Tree::new("too-deep");
let deep = format!("usr/share/deepfw/{}", vec!["d"; 20].join("/"));
tree.file(&format!("{deep}/f"), b"f");
tree.dir("usr/share/examplefw");
let lib = tree.object(
"usr/lib/libexamplefw.so.1",
&["usr/share/deepfw", "usr/share/examplefw"],
);
omissions("");
assert_eq!(rels(&run(&[lib], None).unwrap()), ["share/examplefw"]);
let omitted = omissions("share/deepfw");
assert!(
omitted.len() == 1 && omitted[0].contains("cannot be read"),
"{omitted:?}"
);
}
#[test]
fn an_unreadable_directory_is_omitted_without_failing() {
use std::os::unix::fs::{MetadataExt, PermissionsExt};
if fs::metadata("/proc/self").is_ok_and(|m| m.uid() == 0) {
return;
}
let tree = Tree::new("unreadable");
tree.file("usr/share/lockedfw/sub/f", b"f");
tree.dir("usr/share/examplefw");
let lib = tree.object(
"usr/lib/libexamplefw.so.1",
&["usr/share/lockedfw", "usr/share/examplefw"],
);
let locked = tree.0.join("usr/share/lockedfw/sub");
fs::set_permissions(&locked, fs::Permissions::from_mode(0o000)).unwrap();
omissions("");
let dirs = run(&[lib], None);
fs::set_permissions(&locked, fs::Permissions::from_mode(0o755)).unwrap();
assert_eq!(rels(&dirs.unwrap()), ["share/examplefw"]);
let omitted = omissions("share/lockedfw");
assert!(
omitted.len() == 1 && omitted[0].contains("cannot be read"),
"{omitted:?}"
);
}
#[test]
fn discovery_is_deterministic() {
let tree = Tree::new("determinism");
tree.dir("usr/share/examplefw");
tree.dir("usr/share/otherfw");
let one = tree.object(
"usr/lib/libone.so.1",
&["usr/share/otherfw", "usr/share/examplefw"],
);
let two = tree.object("usr/lib/libtwo.so.1", &["usr/share/examplefw"]);
let forward = run(&[one.clone(), two.clone()], None).unwrap();
let backward = run(&[two, one], None).unwrap();
assert_eq!(forward, backward);
assert_eq!(rels(&forward), ["share/examplefw", "share/otherfw"]);
assert_eq!(
forward[0].objects.iter().collect::<Vec<_>>(),
["libone.so.1", "libtwo.so.1"]
);
let root = tree.0.join("sys");
let image = elf(&[(READ, &["/usr/share/examplefw"])]);
tree.file("sys/lib/libroot.so.1", &image);
tree.file("sys/usr/lib/libusr.so.1", &image);
tree.file("sys/usr/share/examplefw/a.conf", b"a");
let universe = fixture::populate_universe(
&root,
&[fixture::package(
"app",
&[
"lib/libroot.so.1",
"usr/lib/libusr.so.1",
"usr/share/examplefw/a.conf",
],
)],
"app",
&[],
None,
None,
);
let below_root = ("libroot.so.1".to_string(), root.join("lib/libroot.so.1"));
let below_usr = ("libusr.so.1".to_string(), root.join("usr/lib/libusr.so.1"));
let forward = run(&[below_root.clone(), below_usr.clone()], Some(&universe)).unwrap();
let backward = run(&[below_usr, below_root], Some(&universe)).unwrap();
assert_eq!(forward, backward);
assert_eq!(forward[0].prefix, root);
assert_eq!(rels(&forward), ["usr/share/examplefw"]);
}
#[test]
fn a_sysroot_build_finds_only_admitted_directories() {
let tree = Tree::new("sysroot");
let root = tree.0.join("sys");
let image = elf(&[(READ, &["/usr/share/examplefw", "/usr/share/unselected"])]);
tree.file("sys/usr/lib/libexamplefw.so.1", &image);
tree.file("sys/usr/share/examplefw/a.conf", b"a");
tree.file("sys/usr/share/unselected/b.conf", b"b");
let app = fixture::package(
"app",
&["usr/lib/libexamplefw.so.1", "usr/share/examplefw/a.conf"],
);
let other = fixture::package("other", &["usr/share/unselected/b.conf"]);
let universe = fixture::populate_universe(&root, &[app, other], "app", &[], None, None);
let lib = (
"libexamplefw.so.1".to_string(),
root.join("usr/lib/libexamplefw.so.1"),
);
let dirs = run(&[lib], Some(&universe)).unwrap();
assert_eq!(rels(&dirs), ["share/examplefw"]);
assert_eq!(dirs[0].source, root.join("usr/share/examplefw"));
}
#[test]
fn the_trim_names_a_member_by_any_spelling_the_trace_recorded() {
use super::super::{Trim, trimmed_out};
use onelf_sysroot::Policy;
let dir = InferredDir {
source: PathBuf::from("/dist/usr/lib/examplefw/plugins"),
prefix: PathBuf::from("/dist/usr"),
rel: PathBuf::from("lib/examplefw/plugins"),
destination: PathBuf::from("lib/examplefw/plugins"),
kind: InferredKind::Data,
size: 0,
compiled: BTreeSet::from(["/dist/usr/lib/examplefw/plugins".to_string()]),
objects: BTreeSet::from(["libexamplefw.so.1".to_string()]),
};
let a = Path::new("a.conf");
let opened = Trim::from_paths(
vec![PathBuf::from("/dist/usr/lib/examplefw/plugins/a.conf")],
None,
false,
);
assert!(!trimmed_out(&dir, a, None, Some(&opened)));
let keep_txt = std::env::temp_dir().join(format!(
"onelf-trim-keep-{}-{}.txt",
std::process::id(),
a.display()
));
std::fs::write(&keep_txt, "dist/usr/lib/examplefw/plugins/**\n").unwrap();
let keep = Some(Policy::load(&keep_txt).unwrap());
let _ = std::fs::remove_file(&keep_txt);
let globbed = Trim {
opened: Default::default(),
keep,
tree: None,
objects_only: false,
};
assert!(!trimmed_out(&dir, a, None, Some(&globbed)));
let empty = Trim::from_paths(Vec::new(), None, false);
assert!(trimmed_out(&dir, a, None, Some(&empty)));
let shadowed = Trim::from_paths(
vec![PathBuf::from("/tree/lib/a.conf")],
Some(Path::new("/tree")),
false,
);
assert!(!trimmed_out(&dir, a, None, Some(&shadowed)));
let other = Trim::from_paths(
vec![PathBuf::from("/tree/lib/b.conf")],
Some(Path::new("/tree")),
false,
);
assert!(trimmed_out(&dir, a, None, Some(&other)));
let outside = Trim::from_paths(
vec![PathBuf::from("/elsewhere/a.conf")],
Some(Path::new("/tree")),
false,
);
assert!(trimmed_out(&dir, a, None, Some(&outside)));
assert!(!trimmed_out(&dir, a, None, None));
}
}