use std::{
fs::File,
os::unix::{ffi::OsStrExt, io::{AsRawFd, RawFd}},
path::{Path, PathBuf},
};
use super::{
connection::Connection,
error::{Error, Result},
protocol::{
construction::{AsyncConstructible, SyncConstructible},
AsyncProtocolInit, ProtocolState,
},
socket::NetlinkSocket,
};
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum NamespaceSpec<'a> {
Default,
Named(&'a str),
Path(&'a Path),
Pid(u32),
}
impl<'a> NamespaceSpec<'a> {
pub fn connection<P: ProtocolState + Default + SyncConstructible>(&self) -> Result<Connection<P>> {
match self {
NamespaceSpec::Default => Connection::<P>::new(),
NamespaceSpec::Named(name) => connection_for(name),
NamespaceSpec::Path(path) => connection_for_path(path),
NamespaceSpec::Pid(pid) => connection_for_pid(*pid),
}
}
pub async fn connection_async<P: AsyncProtocolInit + AsyncConstructible>(
&self,
) -> Result<Connection<P>> {
match self {
NamespaceSpec::Default => Connection::<P>::new_async().await,
NamespaceSpec::Named(name) => connection_for_async(name).await,
NamespaceSpec::Path(path) => connection_for_path_async(path).await,
NamespaceSpec::Pid(pid) => connection_for_pid_async(*pid).await,
}
}
#[inline]
pub fn is_default(&self) -> bool {
matches!(self, NamespaceSpec::Default)
}
pub fn spawn(&self, cmd: std::process::Command) -> Result<std::process::Child> {
match self {
NamespaceSpec::Default => {
let mut cmd = cmd;
cmd.spawn().map_err(Error::Io)
}
NamespaceSpec::Named(name) => spawn(name, cmd),
NamespaceSpec::Path(path) => spawn_path(path, cmd),
NamespaceSpec::Pid(pid) => {
let path = format!("/proc/{}/ns/net", pid);
spawn_path(&path, cmd)
}
}
}
pub fn spawn_output(&self, cmd: std::process::Command) -> Result<std::process::Output> {
match self {
NamespaceSpec::Default => {
let mut cmd = cmd;
cmd.stdout(std::process::Stdio::piped());
cmd.stderr(std::process::Stdio::piped());
let child = cmd.spawn().map_err(Error::Io)?;
child.wait_with_output().map_err(Error::Io)
}
NamespaceSpec::Named(name) => spawn_output(name, cmd),
NamespaceSpec::Path(path) => spawn_output_path(path, cmd),
NamespaceSpec::Pid(pid) => {
let path = format!("/proc/{}/ns/net", pid);
spawn_output_path(&path, cmd)
}
}
}
pub fn spawn_with_etc(&self, cmd: std::process::Command) -> Result<std::process::Child> {
match self {
NamespaceSpec::Default => {
let mut cmd = cmd;
cmd.spawn().map_err(Error::Io)
}
NamespaceSpec::Named(name) => spawn_with_etc(name, cmd),
NamespaceSpec::Path(path) => spawn_path(path, cmd),
NamespaceSpec::Pid(pid) => {
let path = format!("/proc/{}/ns/net", pid);
spawn_path(&path, cmd)
}
}
}
pub fn spawn_output_with_etc(
&self,
cmd: std::process::Command,
) -> Result<std::process::Output> {
match self {
NamespaceSpec::Default => {
let mut cmd = cmd;
cmd.stdout(std::process::Stdio::piped());
cmd.stderr(std::process::Stdio::piped());
let child = cmd.spawn().map_err(Error::Io)?;
child.wait_with_output().map_err(Error::Io)
}
NamespaceSpec::Named(name) => spawn_output_with_etc(name, cmd),
NamespaceSpec::Path(path) => spawn_output_path(path, cmd),
NamespaceSpec::Pid(pid) => {
let path = format!("/proc/{}/ns/net", pid);
spawn_output_path(&path, cmd)
}
}
}
}
pub const NETNS_RUN_DIR: &str = "/var/run/netns";
pub fn connection_for<P: ProtocolState + Default + SyncConstructible>(name: &str) -> Result<Connection<P>> {
let path = PathBuf::from(NETNS_RUN_DIR).join(name);
connection_for_path(&path)
}
pub fn connection_for_path<P: ProtocolState + Default + SyncConstructible, T: AsRef<Path>>(
path: T,
) -> Result<Connection<P>> {
Connection::<P>::new_in_namespace_path(path)
}
pub fn connection_for_pid<P: ProtocolState + Default + SyncConstructible>(pid: u32) -> Result<Connection<P>> {
let path = format!("/proc/{}/ns/net", pid);
connection_for_path(&path)
}
pub async fn connection_for_async<P: AsyncProtocolInit + AsyncConstructible>(name: &str) -> Result<Connection<P>> {
let path = PathBuf::from(NETNS_RUN_DIR).join(name);
connection_for_path_async(&path).await
}
pub async fn connection_for_path_async<P: AsyncProtocolInit + AsyncConstructible, T: AsRef<Path>>(
path: T,
) -> Result<Connection<P>> {
let socket = NetlinkSocket::new_in_namespace_path(P::PROTOCOL, path)?;
let state = P::resolve_async(&socket).await?;
Ok(Connection::from_parts(socket, state))
}
pub async fn connection_for_pid_async<P: AsyncProtocolInit + AsyncConstructible>(pid: u32) -> Result<Connection<P>> {
let path = format!("/proc/{}/ns/net", pid);
connection_for_path_async(&path).await
}
pub fn open(name: &str) -> Result<NamespaceFd> {
let path = PathBuf::from(NETNS_RUN_DIR).join(name);
open_path(&path)
}
pub(crate) fn namespace_open_error(path: &Path, e: std::io::Error) -> Error {
if e.kind() == std::io::ErrorKind::NotFound {
Error::NamespaceNotFound {
name: path.display().to_string(),
}
} else {
Error::Io(e)
}
}
pub fn open_path<P: AsRef<Path>>(path: P) -> Result<NamespaceFd> {
let file =
File::open(path.as_ref()).map_err(|e| namespace_open_error(path.as_ref(), e))?;
Ok(NamespaceFd { file })
}
pub fn open_pid(pid: u32) -> Result<NamespaceFd> {
let path = format!("/proc/{}/ns/net", pid);
open_path(&path)
}
#[derive(Debug)]
pub struct NamespaceFd {
file: File,
}
impl NamespaceFd {
pub fn as_raw_fd(&self) -> RawFd {
self.file.as_raw_fd()
}
}
impl AsRawFd for NamespaceFd {
fn as_raw_fd(&self) -> RawFd {
self.file.as_raw_fd()
}
}
pub fn enter(name: &str) -> Result<NamespaceGuard> {
let path = PathBuf::from(NETNS_RUN_DIR).join(name);
enter_path(&path)
}
pub fn enter_path<P: AsRef<Path>>(path: P) -> Result<NamespaceGuard> {
let original = File::open("/proc/thread-self/ns/net")
.map_err(|e| Error::InvalidMessage(format!("cannot open current namespace: {}", e)))?;
let target =
File::open(path.as_ref()).map_err(|e| namespace_open_error(path.as_ref(), e))?;
let ret = unsafe { libc::setns(target.as_raw_fd(), libc::CLONE_NEWNET) };
if ret < 0 {
return Err(Error::Io(std::io::Error::last_os_error()));
}
Ok(NamespaceGuard {
original: Some(original),
_thread_pinned: std::marker::PhantomData,
})
}
#[derive(Debug)]
pub struct NamespaceGuard {
original: Option<File>,
_thread_pinned: std::marker::PhantomData<*mut ()>,
}
impl NamespaceGuard {
pub fn restore(mut self) -> Result<()> {
let result = self.do_restore();
self.original = None;
result
}
fn do_restore(&self) -> Result<()> {
let Some(original) = &self.original else {
return Ok(());
};
let ret = unsafe { libc::setns(original.as_raw_fd(), libc::CLONE_NEWNET) };
if ret < 0 {
return Err(Error::Io(std::io::Error::last_os_error()));
}
Ok(())
}
}
impl Drop for NamespaceGuard {
fn drop(&mut self) {
if self.original.is_none() {
return; }
if let Err(e) = self.do_restore() {
tracing::error!(
error = %e,
"NamespaceGuard::drop: failed to restore original namespace; thread may be stuck"
);
}
}
}
pub fn exists(name: &str) -> bool {
let path = PathBuf::from(NETNS_RUN_DIR).join(name);
path.exists()
}
fn path_to_cstring(path: &Path) -> Result<std::ffi::CString> {
std::ffi::CString::new(path.as_os_str().as_bytes())
.map_err(|_| Error::InvalidMessage(format!("invalid namespace path '{}'", path.display())))
}
const NSFS_MAGIC: u64 = 0x6e_73_66_73;
pub fn is_namespace_path<P: AsRef<Path>>(path: P) -> bool {
let Ok(c_path) = path_to_cstring(path.as_ref()) else {
return false; };
let mut buf: libc::statfs = unsafe { std::mem::zeroed() };
let rc = unsafe { libc::statfs(c_path.as_ptr(), &mut buf) };
rc == 0 && buf.f_type as u64 == NSFS_MAGIC
}
pub fn is_namespace(name: &str) -> bool {
is_namespace_path(PathBuf::from(NETNS_RUN_DIR).join(name))
}
pub fn create(name: &str) -> Result<()> {
create_path(PathBuf::from(NETNS_RUN_DIR).join(name))
}
fn already_exists_error(path: &Path) -> Error {
Error::Io(std::io::Error::new(
std::io::ErrorKind::AlreadyExists,
format!("namespace '{}' already exists", path.display()),
))
}
fn topmost_missing_ancestor(dir: &Path) -> PathBuf {
let mut topmost = dir.to_path_buf();
let mut cursor = dir;
while let Some(parent) = cursor.parent() {
if parent.exists() {
break;
}
topmost = parent.to_path_buf();
cursor = parent;
}
topmost
}
fn remove_empty_dirs_upward(parent: &Path, topmost: &Path) {
let mut cursor = parent;
loop {
if std::fs::remove_dir(cursor).is_err() {
return;
}
if cursor == topmost {
return;
}
let Some(next) = cursor.parent() else { return };
cursor = next;
}
}
pub fn create_path<P: AsRef<Path>>(path: P) -> Result<()> {
let ns_path = path.as_ref().to_path_buf();
if ns_path.exists() {
return Err(already_exists_error(&ns_path));
}
let created_dirs = match ns_path.parent() {
Some(parent) if !parent.exists() => {
let topmost = topmost_missing_ancestor(parent);
std::fs::create_dir_all(parent).map_err(|e| {
Error::InvalidMessage(format!("cannot create {}: {}", parent.display(), e))
})?;
Some((parent.to_path_buf(), topmost))
}
_ => None,
};
let rollback_dirs = |created: &Option<(PathBuf, PathBuf)>| {
if let Some((parent, topmost)) = created {
remove_empty_dirs_upward(parent, topmost);
}
};
std::fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(&ns_path)
.map_err(|e| {
rollback_dirs(&created_dirs);
if e.kind() == std::io::ErrorKind::AlreadyExists {
already_exists_error(&ns_path)
} else {
Error::InvalidMessage(format!(
"cannot create namespace file '{}': {}",
ns_path.display(),
e
))
}
})?;
let ns_path_owned = ns_path.clone();
let label = ns_path.display().to_string();
let thread_result = std::thread::spawn(move || -> Result<()> {
create_namespace_in_current_thread(&label, &ns_path_owned)
})
.join();
match thread_result {
Ok(Ok(())) => Ok(()),
Ok(Err(e)) => {
let _ = std::fs::remove_file(&ns_path);
rollback_dirs(&created_dirs);
Err(e)
}
Err(_panic) => {
let _ = std::fs::remove_file(&ns_path);
rollback_dirs(&created_dirs);
Err(Error::InvalidMessage(format!(
"namespace '{}' worker thread panicked during create",
ns_path.display()
)))
}
}
}
fn create_namespace_in_current_thread(name: &str, ns_path: &Path) -> Result<()> {
let original_ns = File::open("/proc/thread-self/ns/net").map_err(|e| {
let _ = std::fs::remove_file(ns_path);
Error::InvalidMessage(format!("cannot save current namespace: {}", e))
})?;
let ret = unsafe { libc::unshare(libc::CLONE_NEWNET) };
if ret < 0 {
let _ = std::fs::remove_file(ns_path);
return Err(Error::Io(std::io::Error::last_os_error()));
}
let ns_path_cstr = path_to_cstring(ns_path).inspect_err(|_| {
let _ = std::fs::remove_file(ns_path);
})?;
let self_ns = std::ffi::CString::new("/proc/thread-self/ns/net").unwrap();
let ret = unsafe {
libc::mount(
self_ns.as_ptr(),
ns_path_cstr.as_ptr(),
std::ptr::null(),
libc::MS_BIND,
std::ptr::null(),
)
};
if ret < 0 {
let err = std::io::Error::last_os_error();
unsafe { libc::setns(original_ns.as_raw_fd(), libc::CLONE_NEWNET) };
let _ = std::fs::remove_file(ns_path);
return Err(Error::Io(err));
}
let ret = unsafe { libc::setns(original_ns.as_raw_fd(), libc::CLONE_NEWNET) };
if ret < 0 {
let restore_err = std::io::Error::last_os_error();
if let Ok(c) = path_to_cstring(ns_path) {
unsafe { libc::umount2(c.as_ptr(), libc::MNT_DETACH) };
}
let _ = std::fs::remove_file(ns_path);
return Err(Error::InvalidMessage(format!(
"namespace '{}' failed to restore original namespace: {}",
name, restore_err
)));
}
Ok(())
}
pub fn delete(name: &str) -> Result<()> {
delete_path(PathBuf::from(NETNS_RUN_DIR).join(name))
}
pub fn delete_path<P: AsRef<Path>>(path: P) -> Result<()> {
let ns_path = path.as_ref();
if !ns_path.exists() {
return Err(Error::NamespaceNotFound {
name: ns_path.display().to_string(),
});
}
let ns_path_cstr = path_to_cstring(ns_path)?;
let ret = unsafe { libc::umount2(ns_path_cstr.as_ptr(), libc::MNT_DETACH) };
if ret < 0 {
let err = std::io::Error::last_os_error();
if err.raw_os_error() != Some(libc::EINVAL) {
return Err(Error::Io(err));
}
}
std::fs::remove_file(ns_path)
.map_err(|e| Error::InvalidMessage(format!("cannot remove namespace file: {}", e)))?;
Ok(())
}
pub fn execute_in<F, T>(name: &str, f: F) -> Result<T>
where
F: FnOnce() -> T,
{
let guard = enter(name)?;
let result = f();
guard.restore()?;
Ok(result)
}
pub fn execute_in_path<F, T, P: AsRef<Path>>(path: P, f: F) -> Result<T>
where
F: FnOnce() -> T,
{
let guard = enter_path(path)?;
let result = f();
guard.restore()?;
Ok(result)
}
pub fn list() -> Result<Vec<String>> {
let dir = match std::fs::read_dir(NETNS_RUN_DIR) {
Ok(d) => d,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
return Ok(Vec::new());
}
Err(e) => {
return Err(Error::Io(e));
}
};
let mut names = Vec::new();
for entry in dir {
let entry = entry.map_err(Error::Io)?;
let name = entry.file_name().to_string_lossy().to_string();
if name != "." && name != ".." {
names.push(name);
}
}
names.sort();
Ok(names)
}
fn run_in_namespace_thread<T, F>(path: PathBuf, f: F) -> Result<T>
where
F: FnOnce() -> Result<T> + Send + 'static,
T: Send + 'static,
{
let thread_result = std::thread::spawn(move || -> Result<T> {
let target = File::open(&path).map_err(|e| namespace_open_error(&path, e))?;
let ret = unsafe { libc::setns(target.as_raw_fd(), libc::CLONE_NEWNET) };
if ret < 0 {
return Err(Error::Io(std::io::Error::last_os_error()));
}
f()
})
.join();
match thread_result {
Ok(result) => result,
Err(_panic) => Err(Error::InvalidMessage(
"namespace worker thread panicked".to_string(),
)),
}
}
pub fn get_sysctl(ns_name: &str, key: &str) -> Result<String> {
get_sysctl_path(PathBuf::from(NETNS_RUN_DIR).join(ns_name), key)
}
pub fn set_sysctl(ns_name: &str, key: &str, value: &str) -> Result<()> {
set_sysctl_path(PathBuf::from(NETNS_RUN_DIR).join(ns_name), key, value)
}
pub fn set_sysctls(ns_name: &str, entries: &[(&str, &str)]) -> Result<()> {
set_sysctls_path(PathBuf::from(NETNS_RUN_DIR).join(ns_name), entries)
}
pub fn get_sysctl_path<P: AsRef<Path>>(path: P, key: &str) -> Result<String> {
let key = key.to_owned();
run_in_namespace_thread(path.as_ref().to_path_buf(), move || {
super::sysctl::get(&key)
})
}
pub fn set_sysctl_path<P: AsRef<Path>>(path: P, key: &str, value: &str) -> Result<()> {
let (key, value) = (key.to_owned(), value.to_owned());
run_in_namespace_thread(path.as_ref().to_path_buf(), move || {
super::sysctl::set(&key, &value)
})
}
pub fn set_sysctls_path<P: AsRef<Path>>(path: P, entries: &[(&str, &str)]) -> Result<()> {
let entries: Vec<(String, String)> = entries
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect();
run_in_namespace_thread(path.as_ref().to_path_buf(), move || {
let borrowed: Vec<(&str, &str)> = entries
.iter()
.map(|(k, v)| (k.as_str(), v.as_str()))
.collect();
super::sysctl::set_many(&borrowed)
})
}
pub fn spawn(ns_name: &str, cmd: std::process::Command) -> Result<std::process::Child> {
let path = PathBuf::from(NETNS_RUN_DIR).join(ns_name);
if !path.exists() {
return Err(Error::NamespaceNotFound {
name: ns_name.to_string(),
});
}
spawn_path(&path, cmd)
}
pub fn spawn_output(ns_name: &str, mut cmd: std::process::Command) -> Result<std::process::Output> {
cmd.stdout(std::process::Stdio::piped());
cmd.stderr(std::process::Stdio::piped());
let child = spawn(ns_name, cmd)?;
child.wait_with_output().map_err(Error::Io)
}
pub fn spawn_path<P: AsRef<Path>>(
path: P,
mut cmd: std::process::Command,
) -> Result<std::process::Child> {
use std::os::unix::process::CommandExt;
let ns_fd = open_path(path)?;
let raw_fd = ns_fd.as_raw_fd();
unsafe {
cmd.pre_exec(move || {
if libc::setns(raw_fd, libc::CLONE_NEWNET) != 0 {
return Err(std::io::Error::last_os_error());
}
Ok(())
});
}
let child = cmd.spawn().map_err(Error::Io)?;
drop(ns_fd);
Ok(child)
}
pub fn spawn_output_path<P: AsRef<Path>>(
path: P,
mut cmd: std::process::Command,
) -> Result<std::process::Output> {
cmd.stdout(std::process::Stdio::piped());
cmd.stderr(std::process::Stdio::piped());
let child = spawn_path(path, cmd)?;
child.wait_with_output().map_err(Error::Io)
}
fn prepare_etc_binds(ns_name: &str) -> Result<Vec<(std::ffi::CString, std::ffi::CString)>> {
let etc_netns = PathBuf::from("/etc/netns").join(ns_name);
let entries = match std::fs::read_dir(&etc_netns) {
Ok(entries) => entries,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()),
Err(e) => return Err(Error::Io(e)),
};
let mut binds = Vec::new();
for entry in entries {
let entry = entry.map_err(Error::Io)?;
let file_name = entry.file_name();
let src = entry.path();
let dst = Path::new("/etc").join(&file_name);
if !dst.exists() {
continue;
}
let src_c = std::ffi::CString::new(src.as_os_str().as_encoded_bytes())
.map_err(|_| Error::InvalidMessage("null byte in path".into()))?;
let dst_c = std::ffi::CString::new(dst.as_os_str().as_encoded_bytes())
.map_err(|_| Error::InvalidMessage("null byte in path".into()))?;
binds.push((src_c, dst_c));
}
Ok(binds)
}
pub fn spawn_with_etc(ns_name: &str, cmd: std::process::Command) -> Result<std::process::Child> {
let path = PathBuf::from(NETNS_RUN_DIR).join(ns_name);
if !path.exists() {
return Err(Error::NamespaceNotFound {
name: ns_name.to_string(),
});
}
spawn_path_with_etc(&path, ns_name, cmd)
}
pub fn spawn_output_with_etc(
ns_name: &str,
mut cmd: std::process::Command,
) -> Result<std::process::Output> {
cmd.stdout(std::process::Stdio::piped());
cmd.stderr(std::process::Stdio::piped());
let child = spawn_with_etc(ns_name, cmd)?;
child.wait_with_output().map_err(Error::Io)
}
pub fn spawn_path_with_etc<P: AsRef<Path>>(
path: P,
ns_name: &str,
mut cmd: std::process::Command,
) -> Result<std::process::Child> {
use std::os::unix::process::CommandExt;
let ns_fd = open_path(path)?;
let raw_fd = ns_fd.as_raw_fd();
let bind_mounts = prepare_etc_binds(ns_name)?;
let ns_name_c = std::ffi::CString::new(ns_name)
.map_err(|_| Error::InvalidMessage("null byte in namespace name".into()))?;
let c_root = std::ffi::CString::new("/").unwrap();
let c_none = std::ffi::CString::new("none").unwrap();
let c_sys = std::ffi::CString::new("/sys").unwrap();
let c_sysfs = std::ffi::CString::new("sysfs").unwrap();
unsafe {
cmd.pre_exec(move || {
if libc::setns(raw_fd, libc::CLONE_NEWNET) != 0 {
return Err(std::io::Error::last_os_error());
}
if bind_mounts.is_empty() {
return Ok(());
}
if libc::unshare(libc::CLONE_NEWNS) != 0 {
return Err(std::io::Error::last_os_error());
}
if libc::mount(
c_none.as_ptr(),
c_root.as_ptr(),
std::ptr::null(),
libc::MS_SLAVE | libc::MS_REC,
std::ptr::null(),
) != 0
{
return Err(std::io::Error::last_os_error());
}
libc::umount2(c_sys.as_ptr(), libc::MNT_DETACH);
if libc::mount(
ns_name_c.as_ptr(),
c_sys.as_ptr(),
c_sysfs.as_ptr(),
0,
std::ptr::null(),
) != 0
{
}
for (src, dst) in &bind_mounts {
if libc::mount(
src.as_ptr(),
dst.as_ptr(),
std::ptr::null(),
libc::MS_BIND,
std::ptr::null(),
) != 0
{
return Err(std::io::Error::last_os_error());
}
}
Ok(())
});
}
let child = cmd.spawn().map_err(Error::Io)?;
drop(ns_fd);
Ok(child)
}
pub fn spawn_output_path_with_etc<P: AsRef<Path>>(
path: P,
ns_name: &str,
mut cmd: std::process::Command,
) -> Result<std::process::Output> {
cmd.stdout(std::process::Stdio::piped());
cmd.stderr(std::process::Stdio::piped());
let child = spawn_path_with_etc(path, ns_name, cmd)?;
child.wait_with_output().map_err(Error::Io)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_netns_run_dir() {
assert_eq!(NETNS_RUN_DIR, "/var/run/netns");
}
#[test]
fn test_list_namespaces() {
let result = list();
assert!(result.is_ok());
}
#[test]
fn test_exists_nonexistent() {
assert!(!exists("definitely_does_not_exist_12345"));
}
#[test]
fn test_topmost_missing_ancestor() {
let base = std::env::temp_dir().join(format!("nlink-tma-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&base);
assert_eq!(topmost_missing_ancestor(&base.join("a/b")), base);
std::fs::create_dir_all(&base).unwrap();
assert_eq!(topmost_missing_ancestor(&base.join("a/b")), base.join("a"));
let _ = std::fs::remove_dir_all(&base);
}
#[test]
fn test_create_path_rejects_existing_marker() {
let path = std::env::temp_dir().join(format!("nlink-exists-{}", std::process::id()));
std::fs::File::create(&path).unwrap();
let err = create_path(&path).expect_err("existing path must be rejected");
assert!(err.to_string().contains("already exists"));
assert!(
err.is_already_exists(),
"rejection must be classifiable via is_already_exists()"
);
let _ = std::fs::remove_file(&path);
}
#[test]
fn test_remove_empty_dirs_upward_full_chain() {
let base = std::env::temp_dir().join(format!("nlink-rb-full-{}", std::process::id()));
let parent = base.join("a/b");
std::fs::create_dir_all(&parent).unwrap();
remove_empty_dirs_upward(&parent, &base);
assert!(!base.exists(), "empty chain should be removed up to topmost");
}
#[test]
fn test_remove_empty_dirs_upward_stops_at_nonempty() {
let base = std::env::temp_dir().join(format!("nlink-rb-stop-{}", std::process::id()));
let parent = base.join("a/b");
std::fs::create_dir_all(&parent).unwrap();
let sibling = base.join("a/sibling-marker");
std::fs::File::create(&sibling).unwrap();
remove_empty_dirs_upward(&parent, &base);
assert!(!parent.exists(), "empty leaf dir should be removed");
assert!(sibling.exists(), "sibling marker must survive rollback");
assert!(base.join("a").exists(), "non-empty ancestor must survive");
let _ = std::fs::remove_dir_all(&base);
}
#[test]
fn test_delete_path_missing_is_not_found() {
let path =
std::env::temp_dir().join(format!("nlink-missing-del-{}", std::process::id()));
let _ = std::fs::remove_file(&path);
let err = delete_path(&path).expect_err("missing path must be rejected");
assert!(err.is_not_found());
}
#[test]
fn is_namespace_path_false_for_missing() {
let path =
std::env::temp_dir().join(format!("nlink-isns-missing-{}", std::process::id()));
let _ = std::fs::remove_file(&path);
assert!(!is_namespace_path(&path));
}
#[test]
fn is_namespace_path_false_for_plain_file() {
let path = std::env::temp_dir().join(format!("nlink-isns-plain-{}", std::process::id()));
std::fs::File::create(&path).unwrap();
assert!(!is_namespace_path(&path));
let _ = std::fs::remove_file(&path);
}
#[test]
fn is_namespace_false_for_nonexistent_name() {
assert!(!is_namespace("definitely_does_not_exist_12345"));
}
#[test]
fn open_path_missing_is_not_found() {
let path =
std::env::temp_dir().join(format!("nlink-open-missing-{}", std::process::id()));
let _ = std::fs::remove_file(&path);
let err = open_path(&path).expect_err("missing path must fail");
assert!(err.is_not_found(), "got unclassifiable error: {err}");
}
#[test]
fn enter_path_missing_is_not_found() {
let path =
std::env::temp_dir().join(format!("nlink-enter-missing-{}", std::process::id()));
let _ = std::fs::remove_file(&path);
let err = enter_path(&path).expect_err("missing path must fail");
assert!(err.is_not_found(), "got unclassifiable error: {err}");
}
#[test]
fn connection_for_missing_name_is_not_found() {
let Err(err) = connection_for::<crate::Route>("definitely_does_not_exist_12345") else {
panic!("missing namespace must fail");
};
assert!(err.is_not_found(), "got unclassifiable error: {err}");
}
#[test]
fn is_namespace_path_true_for_own_net_ns() {
assert!(is_namespace_path("/proc/self/ns/net"));
}
}