use std::collections::HashMap;
use std::fs;
use std::io;
use std::ops::ControlFlow;
use std::path::{Path, PathBuf};
use onelf_format::rules::{RelativePath, Requirement, ResourceGroup};
use super::elf::libc_family_of_soname;
use super::plan::TREE_WALK_LIMIT;
use super::{
AdmittedLibrary, AdmittedUniverse, CandidateHit, GroupQuery, LibcFamily, counterpart_spelling,
discover_group, is_nix_stub_ld, miss_text, parse_needed, read_elf_class, read_elf_machine,
resolve_within,
};
use crate::bundle::sysroot::SysrootUniverse;
const MAX_GROUP_MEMBERS: usize = 4096;
pub(crate) const MAX_GROUP_DEPTH: usize = 16;
const SHOWN_MISSES: usize = 8;
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct CollectedPlugin {
pub soname: String,
pub destination: PathBuf,
pub resolved: PathBuf,
}
#[derive(Debug, Default)]
pub(crate) struct GroupCollection {
pub rule: String,
pub group: String,
pub search_dirs: Vec<PathBuf>,
pub plugins: Vec<CollectedPlugin>,
pub skipped: Vec<(String, String)>,
pub misses: Vec<String>,
pub counterpart: String,
}
pub(crate) fn collect_group(
rule: &str,
declared: &ResourceGroup,
admitted: &[AdmittedLibrary],
sysroot: Option<&SysrootUniverse>,
target_class: Option<u8>,
target_machine: Option<u16>,
) -> io::Result<GroupCollection> {
let outcome = discover_group(
&GroupQuery {
rule,
group: declared.id.as_str(),
kind: declared.kind,
destination: &declared.destination,
candidates: &declared.candidates,
},
&AdmittedUniverse {
libraries: admitted,
sysroot,
},
)?;
let mut collection = collect_found(
rule,
declared.id.as_str(),
&declared.members,
&outcome.found,
sysroot,
target_class,
target_machine,
)?;
collection.misses = outcome
.misses
.iter()
.take(SHOWN_MISSES)
.map(|m| format!("{}: {}", m.root.display(), miss_text(&m.reason)))
.collect();
if collection.plugins.is_empty() && declared.requirement == Requirement::Required {
let mut text = format!(
"rule {rule} group {} is required but no usable plugin was found",
declared.id.as_str()
);
for reason in collection.misses.iter().take(3) {
text.push_str("; ");
text.push_str(reason);
}
for (name, reason) in collection.skipped.iter().take(3) {
text.push_str(&format!("; {name}: {reason}"));
}
return Err(io::Error::new(io::ErrorKind::NotFound, text));
}
Ok(collection)
}
pub(crate) fn collect_found(
rule: &str,
group: &str,
members_allowed: &[RelativePath],
found: &[CandidateHit],
sysroot: Option<&SysrootUniverse>,
target_class: Option<u8>,
target_machine: Option<u16>,
) -> io::Result<GroupCollection> {
let mut collection = GroupCollection {
rule: rule.to_string(),
group: group.to_string(),
search_dirs: Vec::new(),
plugins: Vec::new(),
skipped: Vec::new(),
misses: Vec::new(),
counterpart: found
.first()
.and_then(|hit| {
let declared = hit.source.strip_prefix(&hit.root).ok()?;
counterpart_spelling(hit.base, declared)
})
.unwrap_or_default(),
};
let mut taken: HashMap<PathBuf, PathBuf> = HashMap::new();
let target = (target_class, target_machine);
for hit in found {
collection.search_dirs.push(hit.resolved.clone());
for (rel, path) in members(hit, sysroot)? {
let named_so = rel
.file_name()
.and_then(|n| n.to_str())
.is_some_and(|n| n.ends_with(".so"));
if !named_so {
continue;
}
let shown = rel.to_string_lossy();
if !members_allowed.is_empty() && !members_allowed.iter().any(|m| m.as_str() == shown) {
continue;
}
take_member(&mut collection, &mut taken, hit, &rel, &path, target)?;
}
}
Ok(collection)
}
pub(crate) fn collect_objects(
rule: &str,
group: &str,
hit: &CandidateHit,
members: &[(PathBuf, PathBuf)],
(target_class, target_machine, target_libc): (Option<u8>, Option<u16>, Option<LibcFamily>),
) -> io::Result<GroupCollection> {
let mut collection = GroupCollection {
rule: rule.to_string(),
group: group.to_string(),
search_dirs: vec![hit.resolved.clone()],
..GroupCollection::default()
};
let mut taken: HashMap<PathBuf, PathBuf> = HashMap::new();
for (rel, path) in members {
if read_elf_class(path).is_some() {
if let Some(want) = target_libc
&& let Some(have) = parse_needed(path)
.unwrap_or_default()
.iter()
.find_map(|d| libc_family_of_soname(d))
&& have != want
{
collection.skipped.push((
rel.to_string_lossy().into_owned(),
format!("links {have:?} libc, the bundle runs {want:?}"),
));
continue;
}
take_member(
&mut collection,
&mut taken,
hit,
rel,
path,
(target_class, target_machine),
)?;
}
}
Ok(collection)
}
fn take_member(
collection: &mut GroupCollection,
taken: &mut HashMap<PathBuf, PathBuf>,
hit: &CandidateHit,
rel: &Path,
path: &Path,
(target_class, target_machine): (Option<u8>, Option<u16>),
) -> io::Result<()> {
let Some(name) = rel.file_name().and_then(|n| n.to_str()) else {
return Ok(());
};
let shown = rel.to_string_lossy().into_owned();
if let Err(reason) = compatible_object(path, target_class, target_machine) {
collection.skipped.push((shown, reason));
return Ok(());
}
let destination = hit.destination.join(rel);
if let Some(first) = taken.get(&destination) {
collection.skipped.push((
shown,
format!(
"{} is already collected from {}",
destination.display(),
first.display()
),
));
return Ok(());
}
if collection.plugins.len() >= MAX_GROUP_MEMBERS {
return Err(bounded(
&collection.rule,
&collection.group,
MAX_GROUP_MEMBERS,
));
}
taken.insert(destination.clone(), path.to_path_buf());
collection.plugins.push(CollectedPlugin {
soname: name.to_string(),
destination,
resolved: path.to_path_buf(),
});
Ok(())
}
fn bounded(rule: &str, group: &str, cap: usize) -> io::Error {
io::Error::new(
io::ErrorKind::InvalidData,
format!(
"rule {rule} group {group}: more than {cap} members in one plugin \
group; refusing unbounded collection"
),
)
}
pub(crate) fn members(
hit: &CandidateHit,
sysroot: Option<&SysrootUniverse>,
) -> io::Result<Vec<(PathBuf, PathBuf)>> {
let mut out = Vec::new();
visit_members(hit, sysroot, &mut |rel, path| {
out.push((rel, path));
ControlFlow::Continue(())
})?;
Ok(out)
}
pub(crate) fn visit_members(
hit: &CandidateHit,
sysroot: Option<&SysrootUniverse>,
visit: &mut dyn FnMut(PathBuf, PathBuf) -> ControlFlow<()>,
) -> io::Result<()> {
if let Some(universe) = sysroot {
let dir_rel = hit
.resolved
.strip_prefix(universe.root())
.map_err(|_| {
io::Error::new(
io::ErrorKind::InvalidData,
format!(
"{}: outside the sysroot it was found in",
hit.resolved.display()
),
)
})?
.to_string_lossy()
.into_owned();
for f in universe.admitted_under(&dir_rel) {
let rel = PathBuf::from(&f)
.strip_prefix(&dir_rel)
.unwrap_or(Path::new(&f))
.to_path_buf();
let Ok(path) = onelf_sysroot::path::in_root(universe.root(), Path::new(&f)) else {
continue;
};
if visit(rel, path).is_break() {
break;
}
}
return Ok(());
}
let mut visited = 0usize;
walk_members(
&hit.root,
&hit.resolved,
Path::new(""),
0,
&mut visited,
visit,
)
.map(|_| ())
}
fn walk_members(
root: &Path,
dir: &Path,
rel: &Path,
depth: usize,
visited: &mut usize,
visit: &mut dyn FnMut(PathBuf, PathBuf) -> ControlFlow<()>,
) -> io::Result<ControlFlow<()>> {
if depth > MAX_GROUP_DEPTH {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!(
"{}: deeper than the {MAX_GROUP_DEPTH} level group walk limit",
dir.display()
),
));
}
let mut entries: Vec<fs::DirEntry> = Vec::new();
for entry in fs::read_dir(dir)? {
*visited += 1;
if *visited > TREE_WALK_LIMIT {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!(
"{}: more than {TREE_WALK_LIMIT} entries, past the walk limit",
dir.display()
),
));
}
entries.push(entry?);
}
entries.sort_by_key(|e| e.file_name());
for entry in entries {
let child_rel = rel.join(entry.file_name());
let child = dir.join(entry.file_name());
let kind = entry.file_type()?;
let flow = if kind.is_dir() {
walk_members(root, &child, &child_rel, depth + 1, visited, visit)?
} else if kind.is_file() {
visit(child_rel, child)
} else if kind.is_symlink()
&& let Ok(inside) = child.strip_prefix(root)
&& let Ok(target) = resolve_within(root, inside)
&& target.is_file()
{
visit(child_rel, target)
} else {
ControlFlow::Continue(())
};
if flow.is_break() {
return Ok(flow);
}
}
Ok(ControlFlow::Continue(()))
}
fn compatible_object(
path: &Path,
target_class: Option<u8>,
target_machine: Option<u16>,
) -> Result<(), String> {
if is_nix_stub_ld(path) {
return Err("a stub loader that loads nothing".to_string());
}
let Some(class) = read_elf_class(path) else {
return Err("not an ELF object".to_string());
};
if let Some(want) = target_class
&& class != want
{
return Err(format!(
"ELF class {class}, the bundle builds for class {want}"
));
}
match (target_machine, read_elf_machine(path)) {
(Some(want), Some(machine)) if machine != want => Err(format!(
"machine {machine}, the bundle builds for machine {want}"
)),
(Some(_), None) => Err("not an ELF object".to_string()),
_ => Ok(()),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::bundle::sysroot::fixture;
use onelf_format::catalog;
use onelf_format::rules::{ResourceKind, SourceBase};
fn fake_elf(class: u8, machine: u16) -> Vec<u8> {
let mut v = vec![0u8; 64];
v[0..4].copy_from_slice(b"\x7fELF");
v[4] = class;
v[18..20].copy_from_slice(&machine.to_le_bytes());
v
}
fn write(path: &Path, bytes: &[u8]) {
fs::create_dir_all(path.parent().unwrap()).unwrap();
fs::write(path, bytes).unwrap();
}
fn admitted_lib(path: &Path) -> AdmittedLibrary {
AdmittedLibrary::new(path.file_name().unwrap().to_str().unwrap(), path)
}
fn builtin_group(rule: &str) -> ResourceGroup {
catalog::builtins()
.iter()
.find(|d| d.id.as_str() == rule)
.unwrap()
.resources
.plugins
.first()
.unwrap()
.clone()
}
fn collected<'a>(collection: &'a GroupCollection, soname: &str) -> &'a CollectedPlugin {
collection
.plugins
.iter()
.find(|p| p.soname == soname)
.unwrap()
}
#[test]
fn an_activated_group_collects_every_compatible_member() {
let dir =
std::env::temp_dir().join(format!("onelf-gather-{}-{}", std::process::id(), line!()));
let _ = fs::remove_dir_all(&dir);
let lib = dir.join("usr/lib");
write(&lib.join("libgstreamer-1.0.so.0"), &fake_elf(2, 62));
write(&lib.join("gstreamer-1.0/pulse.so"), &fake_elf(2, 62));
write(&lib.join("gstreamer-1.0/loop.so"), &fake_elf(2, 62));
write(&lib.join("gstreamer-1.0/README"), b"notes");
let libs = [admitted_lib(&lib.join("libgstreamer-1.0.so.0"))];
let collection = collect_group(
"gstreamer",
&builtin_group("gstreamer"),
&libs,
None,
Some(2),
Some(62),
)
.unwrap();
assert_eq!(collection.search_dirs, vec![lib.join("gstreamer-1.0")]);
assert_eq!(collection.plugins.len(), 2, "{:?}", collection.plugins);
assert_eq!(
collected(&collection, "pulse.so").destination,
PathBuf::from("lib/gstreamer-1.0/pulse.so")
);
assert!(collection.skipped.is_empty(), "{:?}", collection.skipped);
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn same_named_members_in_nested_layouts_are_all_collected() {
let dir =
std::env::temp_dir().join(format!("onelf-gather-{}-{}", std::process::id(), line!()));
let _ = fs::remove_dir_all(&dir);
let lib = dir.join("usr/lib");
write(&lib.join("libgstreamer-1.0.so.0"), &fake_elf(2, 62));
write(
&lib.join("gstreamer-1.0/x86-64-v2/cmyk.so"),
&fake_elf(2, 62),
);
write(
&lib.join("gstreamer-1.0/x86-64-v3/cmyk.so"),
&fake_elf(2, 62),
);
write(&lib.join("gstreamer-1.0/cmyk.so"), &fake_elf(2, 62));
let libs = [admitted_lib(&lib.join("libgstreamer-1.0.so.0"))];
let collection = collect_group(
"gstreamer",
&builtin_group("gstreamer"),
&libs,
None,
Some(2),
Some(62),
)
.unwrap();
let mut found: Vec<(PathBuf, PathBuf)> = collection
.plugins
.iter()
.map(|p| (p.destination.clone(), p.resolved.clone()))
.collect();
found.sort();
assert_eq!(
found,
vec![
(
PathBuf::from("lib/gstreamer-1.0/cmyk.so"),
lib.join("gstreamer-1.0/cmyk.so")
),
(
PathBuf::from("lib/gstreamer-1.0/x86-64-v2/cmyk.so"),
lib.join("gstreamer-1.0/x86-64-v2/cmyk.so")
),
(
PathBuf::from("lib/gstreamer-1.0/x86-64-v3/cmyk.so"),
lib.join("gstreamer-1.0/x86-64-v3/cmyk.so")
),
]
);
assert!(collection.skipped.is_empty(), "{:?}", collection.skipped);
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn a_symlinked_member_is_followed_inside_its_root() {
let dir =
std::env::temp_dir().join(format!("onelf-gather-{}-{}", std::process::id(), line!()));
let _ = fs::remove_dir_all(&dir);
let lib = dir.join("usr/lib");
let outside = dir.join("outside/libasound_module_pcm_far.so");
write(&lib.join("libasound.so.2"), &fake_elf(2, 62));
write(
&lib.join("gstreamer-1.0/libasound_module_pcm_pulse.so"),
&fake_elf(2, 62),
);
write(&outside, &fake_elf(2, 62));
std::os::unix::fs::symlink(
"libasound_module_pcm_pulse.so",
lib.join("gstreamer-1.0/libasound_module_pcm_alias.so"),
)
.unwrap();
std::os::unix::fs::symlink(
&outside,
lib.join("gstreamer-1.0/libasound_module_pcm_far.so"),
)
.unwrap();
let libs = [admitted_lib(&lib.join("libasound.so.2"))];
let collection = collect_group(
"gstreamer",
&builtin_group("gstreamer"),
&libs,
None,
Some(2),
Some(62),
)
.unwrap();
let alias = collected(&collection, "libasound_module_pcm_alias.so");
assert_eq!(
alias.destination,
PathBuf::from("lib/gstreamer-1.0/libasound_module_pcm_alias.so")
);
assert_eq!(
alias.resolved,
lib.join("gstreamer-1.0/libasound_module_pcm_pulse.so")
);
assert!(
!collection
.plugins
.iter()
.any(|p| p.soname == "libasound_module_pcm_far.so"),
"{:?}",
collection.plugins
);
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn nested_layout_is_preserved_into_the_destination() {
let dir =
std::env::temp_dir().join(format!("onelf-gather-{}-{}", std::process::id(), line!()));
let _ = fs::remove_dir_all(&dir);
let lib = dir.join("usr/lib/x86_64-linux-gnu");
write(&lib.join("gstreamer-1.0.so.1"), &fake_elf(2, 62));
write(&lib.join("gstreamer-1.0/plugins/x264.so"), &fake_elf(2, 62));
write(&lib.join("gstreamer-1.0/deep/a/avif.so"), &fake_elf(2, 62));
let libs = [admitted_lib(&lib.join("gstreamer-1.0.so.1"))];
let collection = collect_group(
"gstreamer",
&builtin_group("gstreamer"),
&libs,
None,
Some(2),
Some(62),
)
.unwrap();
assert_eq!(
collected(&collection, "x264.so").destination,
PathBuf::from("lib/gstreamer-1.0/plugins/x264.so"),
"the nested layout a distribution ships is kept"
);
assert_eq!(
collected(&collection, "avif.so").destination,
PathBuf::from("lib/gstreamer-1.0/deep/a/avif.so")
);
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn an_incompatible_member_is_refused_without_shadowing() {
let dir =
std::env::temp_dir().join(format!("onelf-gather-{}-{}", std::process::id(), line!()));
let _ = fs::remove_dir_all(&dir);
let lib = dir.join("usr/lib");
write(&lib.join("libgstreamer-1.0.so.0"), &fake_elf(2, 62));
write(&lib.join("gstreamer-1.0/arm.so"), &fake_elf(2, 183));
write(&lib.join("gstreamer-1.0/text.so"), b"not elf");
write(&lib.join("gstreamer-1.0/thin.so"), &fake_elf(1, 62));
write(&lib.join("gstreamer-1.0/core.so"), &fake_elf(2, 62));
write(&lib.join("gstreamer-1.0/helper"), b"not a candidate");
let libs = [admitted_lib(&lib.join("libgstreamer-1.0.so.0"))];
let collection = collect_group(
"gstreamer",
&builtin_group("gstreamer"),
&libs,
None,
Some(2),
Some(62),
)
.unwrap();
assert_eq!(collection.plugins.len(), 1);
assert_eq!(collection.plugins[0].soname, "core.so");
let refused: Vec<(&str, &str)> = collection
.skipped
.iter()
.map(|(n, r)| (n.as_str(), r.as_str()))
.collect();
assert_eq!(
refused,
[
("arm.so", "machine 183, the bundle builds for machine 62"),
("text.so", "not an ELF object"),
("thin.so", "ELF class 1, the bundle builds for class 2"),
],
"every refusal names its reason, in walk order"
);
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn a_sysroot_group_walks_only_admitted_members() {
let dir =
std::env::temp_dir().join(format!("onelf-gather-{}-{}", std::process::id(), line!()));
let _ = fs::remove_dir_all(&dir);
let root = dir.join("sys");
write(&root.join("usr/bin/app"), &fake_elf(2, 62));
write(&root.join("usr/lib/libgio-2.0.so.0"), &fake_elf(2, 62));
write(
&root.join("usr/lib/gio/modules/gnutls.so"),
&fake_elf(2, 62),
);
write(&root.join("usr/lib/gio/modules/other.so"), &fake_elf(2, 62));
let mut app = fixture::package("app", &["usr/bin/app", "usr/lib/libgio-2.0.so.0"]);
app.depends = vec!["gio".into()];
let mut gio = fixture::package(
"gio",
&["usr/lib/libgio-2.0.so.0", "usr/lib/gio/modules/gnutls.so"],
);
gio.provides = vec!["gio".into()];
let extra = fixture::package("extra-modules", &["usr/lib/gio/modules/other.so"]);
let universe =
fixture::populate_universe(&root, &[app, gio, extra], "app", &[], None, None);
let libs = [admitted_lib(&root.join("usr/lib/libgio-2.0.so.0"))];
let collection = collect_group(
"gio",
&builtin_group("gio"),
&libs,
Some(&universe),
Some(2),
Some(62),
)
.unwrap();
assert_eq!(collection.plugins.len(), 1, "{:?}", collection.plugins);
assert_eq!(collection.plugins[0].soname, "gnutls.so");
assert_eq!(
collection.plugins[0].destination,
PathBuf::from("lib/gio/modules/gnutls.so"),
"the unselected package's member never becomes a candidate"
);
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn a_required_group_with_nothing_usable_is_an_error() {
let dir =
std::env::temp_dir().join(format!("onelf-gather-{}-{}", std::process::id(), line!()));
let _ = fs::remove_dir_all(&dir);
let lib = dir.join("usr/lib");
write(&lib.join("libasound.so.2"), &fake_elf(2, 62));
write(&lib.join("gstreamer-1.0/arm.so"), &fake_elf(2, 183));
let libs = [admitted_lib(&lib.join("libasound.so.2"))];
let mut required = builtin_group("gstreamer");
required.requirement = Requirement::Required;
let err = collect_group("gstreamer", &required, &libs, None, Some(2), Some(62))
.expect_err("a required group with only an incompatible member fails");
assert!(
err.to_string()
.contains("rule gstreamer group plugins is required but no usable plugin"),
"{err}"
);
assert!(
err.to_string().contains("arm.so"),
"the refusal names the incompatible member: {err}"
);
fs::remove_dir_all(lib.join("gstreamer-1.0")).unwrap();
let err = collect_group("gstreamer", &required, &libs, None, Some(2), Some(62))
.expect_err("a required group with no directory fails");
assert!(
err.to_string().contains("absent"),
"the refusal names the absent location: {err}"
);
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn an_optional_group_with_nothing_usable_is_reported_not_failed() {
let dir =
std::env::temp_dir().join(format!("onelf-gather-{}-{}", std::process::id(), line!()));
let _ = fs::remove_dir_all(&dir);
let lib = dir.join("usr/lib");
write(&lib.join("libgstreamer-1.0.so.0"), &fake_elf(2, 62));
let libs = [admitted_lib(&lib.join("libgstreamer-1.0.so.0"))];
let collection = collect_group(
"gstreamer",
&builtin_group("gstreamer"),
&libs,
None,
Some(2),
Some(62),
)
.unwrap();
assert!(collection.plugins.is_empty());
assert_eq!(collection.misses.len(), 1, "{:?}", collection.misses);
assert!(collection.misses[0].contains("absent"));
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn the_same_member_in_two_roots_is_collected_once() {
let dir =
std::env::temp_dir().join(format!("onelf-gather-{}-{}", std::process::id(), line!()));
let _ = fs::remove_dir_all(&dir);
let one = dir.join("one/usr/lib");
let two = dir.join("two/usr/lib");
write(&one.join("libgio-2.0.so.0"), &fake_elf(2, 62));
write(&two.join("libgio-2.0.so.0"), &fake_elf(2, 62));
write(&one.join("gio/modules/shared.so"), &fake_elf(2, 62));
write(&two.join("gio/modules/shared.so"), &fake_elf(2, 62));
write(&two.join("gio/modules/other.so"), &fake_elf(2, 62));
let libs = [
admitted_lib(&one.join("libgio-2.0.so.0")),
admitted_lib(&two.join("libgio-2.0.so.0")),
];
let collection =
collect_group("gio", &builtin_group("gio"), &libs, None, Some(2), Some(62)).unwrap();
assert_eq!(collection.search_dirs.len(), 2);
assert_eq!(collection.plugins.len(), 2, "{:?}", collection.plugins);
assert_eq!(
collected(&collection, "shared.so").destination,
PathBuf::from("lib/gio/modules/shared.so"),
"the first root in admission order decides the soname"
);
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn a_member_allowlist_collects_only_the_listed_members() {
let dir =
std::env::temp_dir().join(format!("onelf-gather-{}-{}", std::process::id(), line!()));
let _ = fs::remove_dir_all(&dir);
let lib = dir.join("usr/lib");
write(&lib.join("libspa-0.2.so.0"), &fake_elf(2, 62));
write(
&lib.join("spa-0.2/support/libspa-support.so"),
&fake_elf(2, 62),
);
write(
&lib.join("spa-0.2/bluez5/libspa-bluez5.so"),
&fake_elf(2, 62),
);
write(&lib.join("spa-0.2/libspa-support.so"), &fake_elf(2, 62));
write(
&lib.join("spa-0.2/audioconvert/libspa-arm.so"),
&fake_elf(2, 183),
);
let libs = [admitted_lib(&lib.join("libspa-0.2.so.0"))];
let mut listed = builtin_group("spa");
listed.members = ["support/libspa-support.so", "audioconvert/libspa-arm.so"]
.into_iter()
.map(|m| onelf_format::rules::RelativePath::new(m).unwrap())
.collect();
let collection = collect_group("spa", &listed, &libs, None, Some(2), Some(62)).unwrap();
let found: Vec<&Path> = collection
.plugins
.iter()
.map(|p| p.destination.as_path())
.collect();
assert_eq!(found, [Path::new("lib/spa-0.2/support/libspa-support.so")]);
assert_eq!(
collection
.skipped
.iter()
.map(|(n, _)| n.as_str())
.collect::<Vec<_>>(),
["audioconvert/libspa-arm.so"],
"a listed incompatible member is still refused with its reason"
);
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn collection_is_bounded() {
let dir =
std::env::temp_dir().join(format!("onelf-gather-{}-{}", std::process::id(), line!()));
let _ = fs::remove_dir_all(&dir);
let lib = dir.join("usr/lib");
write(&lib.join("libgstreamer-1.0.so.0"), &fake_elf(2, 62));
for i in 0..MAX_GROUP_MEMBERS + 1 {
write(
&lib.join(format!("gstreamer-1.0/m{i}.so")),
&fake_elf(2, 62),
);
}
let libs = [admitted_lib(&lib.join("libgstreamer-1.0.so.0"))];
let err = collect_group(
"gstreamer",
&builtin_group("gstreamer"),
&libs,
None,
Some(2),
Some(62),
)
.expect_err("a group past the member cap is refused");
assert!(
err.to_string()
.contains("more than 4096 members in one plugin group"),
"{err}"
);
let _ = fs::remove_dir_all(&dir);
}
fn found(
base: SourceBase,
root: &Path,
path: &str,
member: &str,
destination: &str,
) -> CandidateHit {
write(&root.join(path).join(member), &fake_elf(2, 62));
CandidateHit {
candidate: 0,
base,
kind: ResourceKind::Directory,
source: root.join(path),
resolved: root.join(path),
root: root.to_path_buf(),
destination: PathBuf::from(destination),
}
}
#[test]
fn a_library_dir_base_spells_the_declared_path() {
let dir =
std::env::temp_dir().join(format!("onelf-gather-{}-{}", std::process::id(), line!()));
let _ = fs::remove_dir_all(&dir);
let lib = dir.join("usr/lib");
let collection = collect_found(
"alsa",
"plugins",
&[],
&[found(
SourceBase::LibraryDir,
&lib,
"alsa-lib",
"pulse.so",
"lib/alsa-lib",
)],
None,
Some(2),
Some(62),
)
.unwrap();
assert_eq!(collection.counterpart, "alsa-lib");
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn a_prefix_base_spells_below_its_library_directory() {
let dir =
std::env::temp_dir().join(format!("onelf-gather-{}-{}", std::process::id(), line!()));
let _ = fs::remove_dir_all(&dir);
let prefix = dir.join("usr");
let collection = collect_found(
"gstreamer",
"plugins",
&[],
&[found(
SourceBase::InstallationPrefix,
&prefix,
"lib/x86_64-linux-gnu/gstreamer-1.0",
"coreelements.so",
"lib/gstreamer-1.0",
)],
None,
Some(2),
Some(62),
)
.unwrap();
assert_eq!(collection.counterpart, "gstreamer-1.0");
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn a_prefix_base_outside_the_library_directories_spells_nothing() {
let dir =
std::env::temp_dir().join(format!("onelf-gather-{}-{}", std::process::id(), line!()));
let _ = fs::remove_dir_all(&dir);
let prefix = dir.join("usr");
let collection = collect_found(
"examplefw",
"data",
&[],
&[found(
SourceBase::InstallationPrefix,
&prefix,
"share/examplefw",
"keep.so",
"share/examplefw",
)],
None,
Some(2),
Some(62),
)
.unwrap();
assert_eq!(collection.counterpart, "");
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn the_first_hit_spells_the_counterpart() {
let dir =
std::env::temp_dir().join(format!("onelf-gather-{}-{}", std::process::id(), line!()));
let _ = fs::remove_dir_all(&dir);
let first = dir.join("opt/example");
let second = dir.join("usr/lib");
let collection = collect_found(
"alsa",
"plugins",
&[],
&[
found(
SourceBase::InstallationPrefix,
&first,
"lib/alsa-lib/deep",
"pulse.so",
"lib/alsa-lib",
),
found(
SourceBase::LibraryDir,
&second,
"alsa-lib",
"pulse.so",
"lib/alsa-lib",
),
],
None,
Some(2),
Some(62),
)
.unwrap();
assert_eq!(collection.counterpart, "alsa-lib/deep");
let _ = fs::remove_dir_all(&dir);
}
}