Implement basic lsns utility (#554)

---------

Signed-off-by: Malhar Vora <mlvora.2010@gmail.com>
This commit is contained in:
Malhar Vora
2026-06-09 09:39:55 +02:00
committed by GitHub
parent f77b451f9a
commit 34e2b369b3
10 changed files with 955 additions and 0 deletions
Generated
+10
View File
@@ -1545,6 +1545,7 @@ dependencies = [
"uu_lsipc",
"uu_lslocks",
"uu_lsmem",
"uu_lsns",
"uu_mcookie",
"uu_mesg",
"uu_mountpoint",
@@ -1688,6 +1689,15 @@ dependencies = [
"uucore 0.2.2",
]
[[package]]
name = "uu_lsns"
version = "0.0.1"
dependencies = [
"clap",
"smartcols-sys",
"uucore 0.2.2",
]
[[package]]
name = "uu_mcookie"
version = "0.0.1"
+2
View File
@@ -38,6 +38,7 @@ feat_common_core = [
"lsipc",
"lslocks",
"lsmem",
"lsns",
"mcookie",
"mesg",
"mountpoint",
@@ -110,6 +111,7 @@ lscpu = { optional = true, version = "0.0.1", package = "uu_lscpu", path = "src/
lsipc = { optional = true, version = "0.0.1", package = "uu_lsipc", path = "src/uu/lsipc" }
lslocks = { optional = true, version = "0.0.1", package = "uu_lslocks", path = "src/uu/lslocks" }
lsmem = { optional = true, version = "0.0.1", package = "uu_lsmem", path = "src/uu/lsmem" }
lsns = { optional = true, version = "0.0.1", package = "uu_lsns", path = "src/uu/lsns" }
mcookie = { optional = true, version = "0.0.1", package = "uu_mcookie", path = "src/uu/mcookie" }
mesg = { optional = true, version = "0.0.1", package = "uu_mesg", path = "src/uu/mesg" }
mountpoint = { optional = true, version = "0.0.1", package = "uu_mountpoint", path = "src/uu/mountpoint" }
+16
View File
@@ -0,0 +1,16 @@
[package]
name = "uu_lsns"
version = "0.0.1"
edition = "2024"
[lib]
path = "src/lsns.rs"
[[bin]]
name = "lsns"
path = "src/main.rs"
[dependencies]
uucore = { workspace = true, features = ["entries"] }
clap = { workspace = true }
smartcols-sys = { workspace = true }
+7
View File
@@ -0,0 +1,7 @@
# lsns
```
lsns [OPTION]...
```
List the namespaces in the system.
+105
View File
@@ -0,0 +1,105 @@
// This file is part of the uutils util-linux package.
//
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
use std::ffi::c_int;
use std::fmt;
use uucore::error::UError;
#[derive(Debug)]
pub enum LsnsError {
/// Generic I/O error with context message
IOError(String, std::io::Error),
/// CString conversion error (null byte in string)
NulError(String, std::ffi::NulError),
/// Invalid namespace type index
InvalidNamespaceType(usize),
/// Unsupported platform
#[cfg(not(target_os = "linux"))]
UnsupportedPlatform,
/// Invalid namespace inode format
InvalidNamespaceInodeFormat(String),
/// Invalid process stat format
InvalidProcessStatFormat(String),
/// Failed to get UID from directory entry
FailedToGetUid(String),
/// Failed to get PID from directory entry
FailedToGetPid(String),
/// Failed to read process information
FailedToReadProcess(String),
}
impl LsnsError {
/// Create an I/O error with a context message
pub(crate) fn io0(message: impl Into<String>, error: impl Into<std::io::Error>) -> Self {
Self::IOError(message.into(), error.into())
}
/// Helper to convert negative errno to Result
pub(crate) fn io_from_neg_errno(
message: impl Into<String>,
result: c_int,
) -> Result<usize, LsnsError> {
if let Ok(result) = usize::try_from(result) {
Ok(result)
} else {
let err = std::io::Error::from_raw_os_error(-result);
Err(Self::IOError(message.into(), err))
}
}
}
impl fmt::Display for LsnsError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::IOError(message, err) => write!(f, "{message}: {err}"),
Self::NulError(message, err) => write!(f, "{message}: {err}"),
Self::InvalidNamespaceType(idx) => write!(f, "Invalid namespace type index: {}", idx),
#[cfg(not(target_os = "linux"))]
Self::UnsupportedPlatform => write!(f, "lsns is only supported on Linux"),
Self::InvalidNamespaceInodeFormat(s) => {
write!(f, "Invalid namespace inode format: {}", s)
}
Self::InvalidProcessStatFormat(s) => {
write!(f, "Invalid process stat format: {}", s)
}
Self::FailedToGetUid(s) => {
write!(f, "Failed to get UID from directory entry: {}", s)
}
Self::FailedToGetPid(s) => {
write!(f, "Failed to get PID from directory entry: {}", s)
}
Self::FailedToReadProcess(s) => {
write!(f, "Failed to read process information: {}", s)
}
}
}
}
impl UError for LsnsError {
fn code(&self) -> i32 {
1
}
fn usage(&self) -> bool {
false
}
}
impl std::error::Error for LsnsError {}
// Implement From trait for automatic conversion from std::io::Error
impl From<std::io::Error> for LsnsError {
fn from(err: std::io::Error) -> Self {
Self::IOError(String::new(), err)
}
}
// Implement From trait for automatic conversion from std::ffi::NulError
impl From<std::ffi::NulError> for LsnsError {
fn from(err: std::ffi::NulError) -> Self {
Self::NulError(String::new(), err)
}
}
File diff suppressed because it is too large Load Diff
+1
View File
@@ -0,0 +1 @@
uucore::bin!(uu_lsns);
+86
View File
@@ -0,0 +1,86 @@
// This file is part of the uutils util-linux package.
//
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
use std::ffi::{CStr, c_int, c_uint};
use std::ptr::NonNull;
use std::{io, ptr};
use smartcols_sys::{
libscols_column, libscols_line, libscols_table, scols_init_debug, scols_line_set_data,
scols_new_table, scols_print_table, scols_table_new_column, scols_table_new_line,
scols_unref_table,
};
use crate::errors::LsnsError;
pub(crate) fn initialize() {
unsafe { scols_init_debug(0) };
}
#[repr(transparent)]
pub(crate) struct Table(NonNull<libscols_table>);
impl Table {
pub(crate) fn new() -> Result<Self, LsnsError> {
NonNull::new(unsafe { scols_new_table() })
.ok_or_else(|| LsnsError::io0("scols_new_table", io::ErrorKind::OutOfMemory))
.map(Self)
}
}
impl TableOperations for Table {
fn as_ptr(&self) -> *mut libscols_table {
self.0.as_ptr()
}
}
impl Drop for Table {
fn drop(&mut self) {
unsafe { scols_unref_table(self.0.as_ptr()) }
}
}
pub(crate) trait TableOperations: Sized {
fn as_ptr(&self) -> *mut libscols_table;
fn new_column(
&mut self,
name: &CStr,
width_hint: f64,
flags: c_uint,
) -> Result<ColumnRef, LsnsError> {
NonNull::new(unsafe {
scols_table_new_column(self.as_ptr(), name.as_ptr(), width_hint, flags as c_int)
})
.ok_or_else(|| LsnsError::io0("scols_table_new_column", io::ErrorKind::OutOfMemory))
.map(ColumnRef)
}
fn new_line(&mut self, parent: Option<&mut LineRef>) -> Result<LineRef, LsnsError> {
let parent = parent.map_or(ptr::null_mut(), |parent| parent.0.as_ptr());
NonNull::new(unsafe { scols_table_new_line(self.as_ptr(), parent) })
.ok_or_else(|| LsnsError::io0("scols_table_new_line", io::ErrorKind::OutOfMemory))
.map(LineRef)
}
fn print(&self) -> Result<(), LsnsError> {
let r = unsafe { scols_print_table(self.as_ptr()) };
LsnsError::io_from_neg_errno("scols_print_table", r).map(|_| ())
}
}
#[repr(transparent)]
pub(crate) struct LineRef(NonNull<libscols_line>);
impl LineRef {
pub(crate) fn set_data(&mut self, cell_index: usize, data: &CStr) -> Result<(), LsnsError> {
let r = unsafe { scols_line_set_data(self.0.as_ptr(), cell_index, data.as_ptr()) };
LsnsError::io_from_neg_errno("scols_line_set_data", r).map(|_| ())
}
}
#[repr(transparent)]
pub(crate) struct ColumnRef(NonNull<libscols_column>);
+145
View File
@@ -0,0 +1,145 @@
// This file is part of the uutils util-linux package.
//
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
use uutests::new_ucmd;
#[test]
fn test_invalid_arg() {
new_ucmd!().arg("--definitely-invalid").fails().code_is(1);
}
#[test]
#[cfg(target_os = "linux")]
fn test_basic_output() {
let res = new_ucmd!().succeeds();
let stdout = res.no_stderr().stdout_str();
// Check for header columns
assert!(stdout.contains("NS"));
assert!(stdout.contains("TYPE"));
assert!(stdout.contains("NPROCS"));
assert!(stdout.contains("PID"));
assert!(stdout.contains("USER"));
assert!(stdout.contains("COMMAND"));
}
#[test]
#[cfg(target_os = "linux")]
fn test_namespace_types() {
let res = new_ucmd!().succeeds();
let stdout = res.no_stderr().stdout_str();
// We should see at least some common namespace types
// Note: Not all may be present on all systems, so we check for at least one
let has_namespace = stdout.contains("mnt")
|| stdout.contains("net")
|| stdout.contains("pid")
|| stdout.contains("uts")
|| stdout.contains("ipc")
|| stdout.contains("user")
|| stdout.contains("cgroup");
assert!(
has_namespace,
"Expected to see at least one namespace type in output"
);
}
#[test]
#[cfg(target_os = "linux")]
fn test_output_has_processes() {
let res = new_ucmd!().succeeds();
let stdout = res.no_stderr().stdout_str();
// The output should have at least one process (the test process itself)
// Count lines (excluding header)
let lines: Vec<&str> = stdout.lines().collect();
assert!(
lines.len() >= 2,
"Expected at least header line and one namespace entry"
);
}
#[test]
#[cfg(not(target_os = "linux"))]
fn test_unsupported_platform() {
// On non-Linux platforms, lsns should fail with an appropriate error
new_ucmd!().fails();
}
#[test]
#[cfg(target_os = "linux")]
fn test_output_format() {
let res = new_ucmd!().succeeds();
let stdout = res.no_stderr().stdout_str();
// Verify the output has proper table format
/*
Each line should have multiple columns separated by whitespace.We check
for at least 4 columns as the minimum (NS, TYPE, NPROCS, PID). These
fields are always present. For mnt namespaces the PID and COMMAND column
will be empty.
*/
for line in stdout.lines().skip(1) {
// Skip header
if !line.is_empty() {
let columns: Vec<&str> = line.split_whitespace().collect();
assert!(
columns.len() >= 4,
"Each namespace entry should have at least 4 columns"
);
}
}
}
#[test]
#[cfg(target_os = "linux")]
fn test_namespace_ids_are_numeric() {
let res = new_ucmd!().succeeds();
let stdout = res.no_stderr().stdout_str();
/*
The first column of each line should be the namespace ID which is an inod number
and it should be numeric.
*/
// Skip the header line and check that namespace IDs are numeric
for line in stdout.lines().skip(1) {
if !line.is_empty() {
let columns: Vec<&str> = line.split_whitespace().collect();
if !columns.is_empty() {
let ns_id = columns[0];
assert!(
ns_id.chars().all(|c| c.is_ascii_digit()),
"Namespace ID should be numeric: {}",
ns_id
);
}
}
}
}
#[test]
#[cfg(target_os = "linux")]
fn test_user_column_not_empty() {
let res = new_ucmd!().succeeds();
let stdout = res.no_stderr().stdout_str();
// Check that USER column (5th column) is not empty for entries with processes
for line in stdout.lines().skip(1) {
if !line.is_empty() {
let columns: Vec<&str> = line.split_whitespace().collect();
if columns.len() >= 5 {
// If there's a PID (4th column is not empty), there should be a user
if !columns[3].is_empty() && columns[3] != "0" {
assert!(
!columns[4].is_empty(),
"User column should not be empty when PID is present"
);
}
}
}
}
}
+4
View File
@@ -27,6 +27,10 @@ mod test_lsmem;
#[path = "by-util/test_lslocks.rs"]
mod test_lslocks;
#[cfg(feature = "lsns")]
#[path = "by-util/test_lsns.rs"]
mod test_lsns;
#[cfg(feature = "mesg")]
#[path = "by-util/test_mesg.rs"]
mod test_mesg;