#[cfg(target_os = "linux")]
use std::collections::BTreeSet;
use std::path::Path;
#[cfg(target_os = "linux")]
use std::path::PathBuf;
use murmur_artifact::{DeclaredNeed, LockOrigin, ProgramNeeds};
#[cfg(target_os = "linux")]
use murmur_artifact::{NeedForm, SystemLocation};
pub const NOT_FOUND: &str = "not found on this host";
pub const PROGRAM_NOT_ON_PATH: &str = "program not found on PATH";
pub const FONT_LOCATIONS: &[&str] = &[
"/etc/fonts",
"/usr/share/fonts",
"/usr/local/share/fonts",
"/usr/share/fontconfig",
"/var/cache/fontconfig",
];
const OS_RELEASE_PATHS: &[&str] = &["/etc/os-release", "/usr/lib/os-release"];
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DeclaredToolNeeds {
pub artifact: String,
pub origin: LockOrigin,
pub programs: Vec<ProgramNeeds>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ResolvedProgramNeeds {
pub artifact: String,
pub program: String,
pub granted: Vec<ResolvedNeed>,
pub missing: Vec<MissingNeed>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ResolvedNeed {
pub need: DeclaredNeed,
pub paths: Vec<NeedPath>,
}
pub type NeedPath = crate::trust_store::TrustStorePath;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MissingNeed {
pub need: DeclaredNeed,
pub reason: String,
}
pub fn derive_declared_needs(
tools: &[DeclaredToolNeeds],
shell_allow: &[String],
capsule_writable: &[&Path],
pinned: &[crate::pinned_programs::ResolvedPinnedProgram],
) -> Vec<ResolvedProgramNeeds> {
#[cfg(target_os = "linux")]
{
let mut path_dirs = crate::pinned_programs::host_path_dirs();
path_dirs.extend(crate::pinned_programs::pinned_bin_dirs(pinned));
let host = NeedHost {
path_dirs,
search_dirs: crate::sandbox::DEFAULT_LIBRARY_SEARCH_DIRS
.iter()
.map(PathBuf::from)
.collect(),
font_locations: FONT_LOCATIONS.iter().map(PathBuf::from).collect(),
ca_candidates: crate::trust_store::TRUST_STORE_CANDIDATES,
etc_root: PathBuf::from("/etc"),
home: std::env::var_os("HOME")
.filter(|home| !home.is_empty())
.map(PathBuf::from),
capsule_writable: capsule_writable
.iter()
.map(|root| root.to_path_buf())
.collect(),
pinned_trees: crate::pinned_programs::pinned_trees(pinned),
};
derive_in(tools, shell_allow, &host)
}
#[cfg(not(target_os = "linux"))]
{
let _ = (tools, shell_allow, capsule_writable, pinned);
Vec::new()
}
}
#[cfg(target_os = "linux")]
pub(crate) struct NeedHost<'a> {
pub(crate) path_dirs: Vec<PathBuf>,
pub(crate) search_dirs: Vec<PathBuf>,
pub(crate) font_locations: Vec<PathBuf>,
pub(crate) ca_candidates: &'a [crate::trust_store::TrustStoreCandidate<'a>],
pub(crate) etc_root: PathBuf,
pub(crate) home: Option<PathBuf>,
pub(crate) capsule_writable: Vec<PathBuf>,
pub(crate) pinned_trees: Vec<PathBuf>,
}
#[cfg(target_os = "linux")]
pub(crate) fn derive_in(
tools: &[DeclaredToolNeeds],
shell_allow: &[String],
host: &NeedHost<'_>,
) -> Vec<ResolvedProgramNeeds> {
let unreachable = Unreachable::of(host);
let mut resolved = Vec::new();
for tool in tools {
if tool.origin != LockOrigin::Operator {
continue;
}
for program in &tool.programs {
if program.needs.is_empty() || !shell_allow.contains(&program.program) {
continue;
}
resolved.push(resolve_program(&tool.artifact, program, host, &unreachable));
}
}
resolved
}
#[cfg(target_os = "linux")]
fn resolve_program(
artifact: &str,
program: &ProgramNeeds,
host: &NeedHost<'_>,
unreachable: &Unreachable,
) -> ResolvedProgramNeeds {
let binaries = crate::sandbox::resolve_exec_allowlist_in(
std::slice::from_ref(&program.program),
&host.path_dirs,
);
let mut granted = Vec::new();
let mut missing = Vec::new();
for need in &program.needs {
let found = match &need.form {
NeedForm::Library(soname) => {
let direct = resolve_library(soname, &binaries, host);
let seeds: Vec<PathBuf> = direct
.iter()
.filter(|path| unreachable.reason(path).is_none())
.cloned()
.collect();
let Ok(closure) = crate::sandbox::walk_dependency_closure(
&seeds,
&host.search_dirs,
&unreachable.walk_excluded,
|path| -> Result<_, std::convert::Infallible> {
Ok(crate::sandbox::read_elf_dependencies(path))
},
);
let refused = direct.into_iter().filter(|path| !seeds.contains(path));
Ok(closure
.into_iter()
.chain(refused)
.map(|path| NeedPath {
path,
directory: false,
})
.collect::<Vec<_>>())
}
NeedForm::File { file, beside } => Ok(resolve_library(beside, &binaries, host)
.iter()
.filter_map(|library| library.parent())
.filter_map(|dir| existing(&dir.join(file)))
.filter(|path| !path.directory)
.collect()),
NeedForm::ProgramFolder(relative) => {
if binaries.is_empty() {
Err(PROGRAM_NOT_ON_PATH.to_string())
} else {
Ok(binaries
.iter()
.filter_map(|binary| binary.parent())
.filter_map(|dir| existing(&dir.join(relative)))
.collect())
}
}
NeedForm::System(SystemLocation::Fonts) => Ok(host
.font_locations
.iter()
.filter_map(|location| existing(location))
.collect()),
NeedForm::System(SystemLocation::CaBundle) => {
let writable: Vec<&Path> =
host.capsule_writable.iter().map(PathBuf::as_path).collect();
Ok(crate::trust_store::select_in(host.ca_candidates, &writable))
}
NeedForm::Etc(name) => Ok(existing(&host.etc_root.join(name)).into_iter().collect()),
};
let paths = match found {
Ok(paths) if !paths.is_empty() => paths,
Ok(_) => {
missing.push(MissingNeed {
need: need.clone(),
reason: NOT_FOUND.to_string(),
});
continue;
}
Err(reason) => {
missing.push(MissingNeed {
need: need.clone(),
reason,
});
continue;
}
};
let mut kept: BTreeSet<NeedPath> = BTreeSet::new();
let mut refusal: Option<String> = None;
for path in paths {
match unreachable.reason(&path.path) {
None => {
kept.insert(path);
}
Some(reason) => {
refusal.get_or_insert(reason);
}
}
}
if !kept.is_empty() {
granted.push(ResolvedNeed {
need: need.clone(),
paths: kept.into_iter().collect(),
});
}
if let Some(reason) = refusal {
missing.push(MissingNeed {
need: need.clone(),
reason,
});
}
}
ResolvedProgramNeeds {
artifact: artifact.to_string(),
program: program.program.clone(),
granted,
missing,
}
}
#[cfg(target_os = "linux")]
fn resolve_library(soname: &str, binaries: &[PathBuf], host: &NeedHost<'_>) -> Vec<PathBuf> {
let mut found = BTreeSet::new();
if binaries.is_empty() {
found.extend(crate::sandbox::resolve_soname(
soname,
Path::new("/"),
&[],
&host.search_dirs,
));
}
for binary in binaries {
let runpaths = crate::sandbox::read_elf_dependencies(binary)
.map(|deps| deps.runpaths)
.unwrap_or_default();
let origin = binary.parent().unwrap_or_else(|| Path::new("/"));
found.extend(crate::sandbox::resolve_soname(
soname,
origin,
&runpaths,
&host.search_dirs,
));
}
found.into_iter().collect()
}
#[cfg(target_os = "linux")]
fn existing(path: &Path) -> Option<NeedPath> {
let canonical = std::fs::canonicalize(path).ok()?;
let metadata = std::fs::metadata(&canonical).ok()?;
(metadata.is_dir() || metadata.is_file()).then(|| NeedPath {
path: canonical,
directory: metadata.is_dir(),
})
}
#[cfg(target_os = "linux")]
struct Unreachable {
roots: Vec<PathBuf>,
labels: Vec<String>,
pinned: Vec<PathBuf>,
walk_excluded: Vec<PathBuf>,
}
#[cfg(target_os = "linux")]
impl Unreachable {
fn of(host: &NeedHost<'_>) -> Self {
let mut unreachable = Self {
roots: Vec::new(),
labels: Vec::new(),
pinned: host
.pinned_trees
.iter()
.flat_map(|tree| {
[
tree.clone(),
crate::protected_paths::canonicalize_existing_prefix(tree),
]
})
.collect(),
walk_excluded: Vec::new(),
};
let mut add = |path: &Path, label: &str| {
for root in [
path.to_path_buf(),
crate::protected_paths::canonicalize_existing_prefix(path),
] {
if !unreachable.roots.contains(&root) {
unreachable
.labels
.push(format!("{label} {}", root.display()));
unreachable.roots.push(root);
}
}
};
for kernel in ["/proc", "/sys", "/dev"] {
add(Path::new(kernel), "the kernel's");
}
if let Some(home) = &host.home {
add(home, "the home folder");
}
for root in &host.capsule_writable {
add(root, "the capsule-writable tree");
}
for location in murmur_artifact::ETC_CREDENTIAL_LOCATIONS {
add(&host.etc_root.join(location), "the credential location");
}
unreachable.walk_excluded = unreachable
.roots
.iter()
.filter(|root| !unreachable.pinned.iter().any(|tree| tree.starts_with(root)))
.cloned()
.collect();
unreachable
}
fn reason(&self, path: &Path) -> Option<String> {
if path == Path::new("/") {
return Some("resolved to /, the filesystem root".to_string());
}
if self.pinned.iter().any(|tree| path.starts_with(tree)) {
return None;
}
self.roots
.iter()
.zip(&self.labels)
.find_map(|(root, label)| {
let relation = if path == root {
"which is"
} else if path.starts_with(root) {
"beneath"
} else if root.starts_with(path) {
"which holds"
} else {
return None;
};
Some(format!(
"resolved to {}, {relation} {label}, which no need reaches",
path.display()
))
})
}
}
#[must_use]
pub fn host_distributions() -> Vec<String> {
OS_RELEASE_PATHS
.iter()
.find_map(|path| std::fs::read_to_string(path).ok())
.map(|text| distributions_from_os_release(&text))
.unwrap_or_default()
}
#[must_use]
pub fn distributions_from_os_release(text: &str) -> Vec<String> {
let value = |key: &str| {
text.lines().find_map(|line| {
let (name, value) = line.trim().split_once('=')?;
(name.trim() == key).then(|| {
let value = value.trim();
value
.strip_prefix('"')
.and_then(|v| v.strip_suffix('"'))
.or_else(|| value.strip_prefix('\'').and_then(|v| v.strip_suffix('\'')))
.unwrap_or(value)
.to_string()
})
})
};
let mut distributions: Vec<String> = Vec::new();
let id = value("ID");
let like = value("ID_LIKE");
for word in id
.iter()
.chain(like.iter())
.flat_map(|value| value.split_whitespace())
{
if !distributions.iter().any(|known| known == word) {
distributions.push(word.to_string());
}
}
distributions
}
#[must_use]
pub fn package_for<'a>(
need: &'a DeclaredNeed,
distributions: &'a [String],
) -> Option<(&'a str, &'a str)> {
distributions.iter().find_map(|distribution| {
need.packages
.get(distribution)
.map(|package| (package.as_str(), distribution.as_str()))
})
}
#[cfg(all(test, target_os = "linux"))]
mod tests {
use super::*;
use murmur_artifact::{parse_program_needs, NeedForm};
use std::os::unix::fs::symlink;
struct Scratch {
_temp: tempfile::TempDir,
root: PathBuf,
}
impl Scratch {
fn new() -> Self {
let temp = tempfile::tempdir().unwrap();
let root = std::fs::canonicalize(temp.path()).unwrap();
for dir in [
"bin/share",
"lib",
"fonts",
"home/user",
"workdir",
"etc/ssl/private",
"etc/ssl/certs",
"etc/fonts",
] {
std::fs::create_dir_all(root.join(dir)).unwrap();
}
std::fs::copy(host_binary("true"), root.join("bin/fixprog")).unwrap();
std::fs::copy(host_library("libm.so.6"), root.join("lib/libfix.so.1")).unwrap();
std::fs::write(root.join("lib/libfix.chk"), b"chk").unwrap();
std::fs::copy(host_library("libm.so.6"), root.join("lib/libother.so.1")).unwrap();
std::fs::write(root.join("etc/fonts/fonts.conf"), b"<fontconfig/>").unwrap();
std::fs::write(root.join("etc/ssl/certs/ca.pem"), b"pem").unwrap();
Self { _temp: temp, root }
}
fn host(&self) -> NeedHost<'static> {
let mut search_dirs = vec![self.root.join("lib")];
search_dirs.extend(
crate::sandbox::DEFAULT_LIBRARY_SEARCH_DIRS
.iter()
.map(PathBuf::from),
);
NeedHost {
path_dirs: vec![self.root.join("bin")],
search_dirs,
font_locations: vec![self.root.join("fonts"), self.root.join("no-fonts")],
ca_candidates: &[],
etc_root: self.root.join("etc"),
home: Some(self.root.join("home/user")),
capsule_writable: vec![self.root.join("workdir")],
pinned_trees: Vec::new(),
}
}
}
fn host_binary(name: &str) -> PathBuf {
["/bin", "/usr/bin"]
.iter()
.map(|dir| Path::new(dir).join(name))
.find(|path| path.is_file())
.expect("the host has the binary")
}
fn host_library(soname: &str) -> PathBuf {
let dirs: Vec<PathBuf> = crate::sandbox::DEFAULT_LIBRARY_SEARCH_DIRS
.iter()
.map(PathBuf::from)
.collect();
crate::sandbox::resolve_soname(soname, Path::new("/"), &[], &dirs)
.expect("the host has the library")
}
fn tool(artifact: &str, origin: LockOrigin, block: &str) -> DeclaredToolNeeds {
DeclaredToolNeeds {
artifact: artifact.to_string(),
origin,
programs: parse_program_needs(&format!(
"name: t\nversion: 0.1.0\nruntime: tool\n{block}"
))
.unwrap(),
}
}
fn allow(names: &[&str]) -> Vec<String> {
names.iter().map(|name| (*name).to_string()).collect()
}
fn granted_paths(resolved: &ResolvedProgramNeeds) -> BTreeSet<PathBuf> {
resolved
.granted
.iter()
.flat_map(|need| need.paths.iter().map(|path| path.path.clone()))
.collect()
}
const FIXPROG: &str = "programs:
fixprog:
needs:
- library: libfix.so.1
- file: libfix.chk
beside: libfix.so.1
- program_folder: share
- system: fonts
";
#[test]
fn declared_needs_a_pinned_only_program_resolves_into_its_store_copy_under_home() {
let scratch = Scratch::new();
let tree = scratch.root.join("home/user/.murmur/programs/abc");
std::fs::create_dir_all(tree.join("files/fixture")).unwrap();
std::fs::create_dir_all(tree.join("bin")).unwrap();
std::fs::copy(host_binary("true"), tree.join("files/fixture/pinprog")).unwrap();
symlink("../files/fixture/pinprog", tree.join("bin/pinprog")).unwrap();
std::fs::create_dir_all(scratch.root.join("home/user/elsewhere")).unwrap();
symlink(
scratch.root.join("home/user/elsewhere"),
tree.join("files/fixture/outside"),
)
.unwrap();
let block = "programs:\n pinprog:\n needs:\n - program_folder: .\n - program_folder: outside\n";
let tools = [tool("local/pinned-tool", LockOrigin::Operator, block)];
let unpinned = derive_in(&tools, &allow(&["pinprog"]), &scratch.host());
assert_eq!(unpinned[0].missing[0].reason, PROGRAM_NOT_ON_PATH);
let mut host = scratch.host();
host.path_dirs.push(tree.join("bin"));
host.pinned_trees = vec![tree.clone()];
let resolved = derive_in(&tools, &allow(&["pinprog"]), &host);
assert_eq!(
granted_paths(&resolved[0]),
BTreeSet::from([tree.join("files/fixture")])
);
assert_eq!(resolved[0].missing.len(), 1);
assert_eq!(
resolved[0].missing[0].need.form,
NeedForm::ProgramFolder("outside".to_string())
);
assert!(
resolved[0].missing[0]
.reason
.contains("beneath the home folder"),
"{}",
resolved[0].missing[0].reason
);
}
#[test]
fn declared_needs_resolve_to_exactly_the_declared_paths_read_only() {
let scratch = Scratch::new();
let host = scratch.host();
let resolved = derive_in(
&[tool("local/needs-tool", LockOrigin::Operator, FIXPROG)],
&allow(&["fixprog"]),
&host,
);
assert_eq!(resolved.len(), 1);
let program = &resolved[0];
assert_eq!(
(program.artifact.as_str(), program.program.as_str()),
("local/needs-tool", "fixprog")
);
assert!(program.missing.is_empty(), "{:?}", program.missing);
let library = scratch.root.join("lib/libfix.so.1");
let Ok(closure) = crate::sandbox::walk_dependency_closure(
std::slice::from_ref(&library),
&host.search_dirs,
&[],
|path| -> Result<_, std::convert::Infallible> {
Ok(crate::sandbox::read_elf_dependencies(path))
},
);
assert!(closure.len() > 1, "libm's own closure: {closure:?}");
let mut expected: BTreeSet<PathBuf> = closure.into_iter().collect();
expected.insert(scratch.root.join("lib/libfix.chk"));
expected.insert(scratch.root.join("bin/share"));
expected.insert(scratch.root.join("fonts"));
assert_eq!(granted_paths(program), expected);
assert!(!expected.contains(&scratch.root.join("lib/libother.so.1")));
assert!(!expected.contains(&scratch.root.join("lib")));
let labels: Vec<String> = program
.granted
.iter()
.map(|need| need.need.form.to_string())
.collect();
assert_eq!(
labels,
[
"library libfix.so.1",
"file libfix.chk beside libfix.so.1",
"program_folder share",
"system fonts",
]
);
let grants = crate::sandbox::resolve_declared_need_grants(&resolved);
assert_eq!(grants.len(), expected.len());
for grant in &grants {
assert!(!grant.executable, "{grant:?}");
let directory = grant.path == scratch.root.join("bin/share")
|| grant.path == scratch.root.join("fonts");
assert_eq!(grant.list_dir, directory, "{grant:?}");
}
}
#[test]
fn declared_needs_an_unreachable_location_is_moved_to_missing() {
let scratch = Scratch::new();
let bin = scratch.root.join("bin");
for (link, target) in [
("root_link", PathBuf::from("/")),
("proc_link", PathBuf::from("/proc")),
("home_link", scratch.root.join("home/user")),
("home_parent", scratch.root.join("home")),
("writable_link", scratch.root.join("workdir")),
("cred_link", scratch.root.join("etc/ssl/private")),
] {
symlink(&target, bin.join(link)).unwrap();
}
symlink(
scratch.root.join("etc/ssl/private"),
scratch.root.join("etc/innocent"),
)
.unwrap();
let block = "programs:
fixprog:
needs:
- program_folder: root_link
- program_folder: proc_link
- program_folder: home_link
- program_folder: home_parent
- program_folder: writable_link
- program_folder: cred_link
- etc: innocent
- library: libmurmur-absent.so.9
- program_folder: share
ghostprog:
needs:
- program_folder: .
- library: libfix.so.1
";
let resolved = derive_in(
&[tool("local/needs-tool", LockOrigin::Operator, block)],
&allow(&["fixprog", "ghostprog"]),
&scratch.host(),
);
let fixprog = &resolved[0];
assert_eq!(
granted_paths(fixprog),
BTreeSet::from([scratch.root.join("bin/share")])
);
let reasons: Vec<(String, String)> = fixprog
.missing
.iter()
.map(|missing| (missing.need.form.to_string(), missing.reason.clone()))
.collect();
let root = scratch.root.display();
assert_eq!(
reasons,
[
(
"program_folder root_link".to_string(),
"resolved to /, the filesystem root".to_string()
),
(
"program_folder proc_link".to_string(),
"resolved to /proc, which is the kernel's /proc, which no need reaches"
.to_string()
),
(
"program_folder home_link".to_string(),
format!(
"resolved to {root}/home/user, which is the home folder \
{root}/home/user, which no need reaches"
)
),
(
"program_folder home_parent".to_string(),
format!(
"resolved to {root}/home, which holds the home folder {root}/home/user, \
which no need reaches"
)
),
(
"program_folder writable_link".to_string(),
format!(
"resolved to {root}/workdir, which is the capsule-writable tree \
{root}/workdir, which no need reaches"
)
),
(
"program_folder cred_link".to_string(),
format!(
"resolved to {root}/etc/ssl/private, which is the credential location \
{root}/etc/ssl/private, which no need reaches"
)
),
(
"etc innocent".to_string(),
format!(
"resolved to {root}/etc/ssl/private, which is the credential location \
{root}/etc/ssl/private, which no need reaches"
)
),
(
"library libmurmur-absent.so.9".to_string(),
NOT_FOUND.to_string()
),
]
);
let ghost = &resolved[1];
assert_eq!(ghost.missing.len(), 1);
assert_eq!(ghost.missing[0].reason, PROGRAM_NOT_ON_PATH);
assert!(granted_paths(ghost).contains(&scratch.root.join("lib/libfix.so.1")));
}
#[test]
fn declared_needs_reach_only_an_operator_pin_and_a_granted_program() {
let scratch = Scratch::new();
let bin = scratch.root.join("bin");
std::fs::copy(host_binary("true"), bin.join("cat")).unwrap();
std::fs::copy(host_binary("true"), bin.join("ls")).unwrap();
let block = "programs:
cat:
needs:
- program_folder: share
- etc: fonts/fonts.conf
ls:
needs: []
";
let host = scratch.host();
let operator = [tool("local/needs-tool", LockOrigin::Operator, block)];
assert_eq!(derive_in(&operator, &allow(&["ls"]), &host), Vec::new());
let both = derive_in(&operator, &allow(&["cat", "ls"]), &host);
assert_eq!(both.len(), 1);
assert_eq!(both[0].program, "cat");
assert_eq!(
granted_paths(&both[0]),
BTreeSet::from([
scratch.root.join("bin/share"),
scratch.root.join("etc/fonts/fonts.conf"),
])
);
let runtime = [tool(
"local/needs-tool",
LockOrigin::Runtime {
session: "ses_x".to_string(),
},
block,
)];
assert_eq!(derive_in(&runtime, &allow(&["cat"]), &host), Vec::new());
assert_eq!(derive_in(&operator, &allow(&["cat"]), &host), both);
assert_eq!(
derive_in(&operator, &allow(&["CAT", "cat2"]), &host),
Vec::new()
);
let two = derive_in(
&[
tool("local/needs-tool", LockOrigin::Operator, block),
tool(
"local/second",
LockOrigin::Operator,
"programs:\n cat:\n needs:\n - system: fonts\n",
),
],
&allow(&["cat"]),
&host,
);
let owners: Vec<(&str, &str)> = two
.iter()
.map(|entry| (entry.artifact.as_str(), entry.program.as_str()))
.collect();
assert_eq!(
owners,
[("local/needs-tool", "cat"), ("local/second", "cat")]
);
}
#[test]
fn declared_needs_ca_bundle_is_the_trust_store_without_its_network_gate() {
let scratch = Scratch::new();
let bundle = scratch.root.join("etc/ssl/certs/ca.pem");
let bundle_text = bundle.to_string_lossy().into_owned();
let certs_text = scratch
.root
.join("etc/ssl/certs")
.to_string_lossy()
.into_owned();
let candidates = [
crate::trust_store::TrustStoreCandidate {
path: &bundle_text,
directory: false,
},
crate::trust_store::TrustStoreCandidate {
path: &certs_text,
directory: true,
},
];
let mut host = scratch.host();
host.ca_candidates = &candidates;
let resolved = derive_in(
&[tool(
"local/needs-tool",
LockOrigin::Operator,
"programs:\n fixprog:\n needs:\n - system: ca_bundle\n",
)],
&allow(&["fixprog"]),
&host,
);
assert_eq!(
resolved[0].granted[0].paths,
[NeedPath {
path: scratch.root.join("etc/ssl/certs"),
directory: true,
}]
);
assert_eq!(
crate::trust_store::derive_in(&candidates, &allow(&["fixprog"]), &[], &[]),
Vec::new()
);
}
#[test]
fn declared_needs_os_release_names_id_then_id_like() {
assert_eq!(
distributions_from_os_release("NAME=\"Ubuntu\"\nID=ubuntu\nID_LIKE=debian\n"),
["ubuntu", "debian"]
);
assert_eq!(
distributions_from_os_release("ID=\"rocky\"\nID_LIKE=\"rhel centos fedora\"\n"),
["rocky", "rhel", "centos", "fedora"]
);
assert_eq!(
distributions_from_os_release("ID_LIKE=\"rhel fedora\"\nID='almalinux'\n"),
["almalinux", "rhel", "fedora"]
);
assert_eq!(distributions_from_os_release(""), Vec::<String>::new());
assert_eq!(
distributions_from_os_release("ID=fedora\nVERSION_ID=40\n"),
["fedora"]
);
}
#[test]
fn declared_needs_package_for_takes_the_first_named_distribution() {
let need = DeclaredNeed {
form: NeedForm::Library("libx.so.1".to_string()),
packages: [("debian", "libx1"), ("fedora", "x-libs")]
.into_iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect(),
};
let ubuntu = allow(&["ubuntu", "debian"]);
assert_eq!(package_for(&need, &ubuntu), Some(("libx1", "debian")));
assert_eq!(package_for(&need, &allow(&["arch"])), None);
}
}