From 9a676ec179f9e1e625ec8d3ffabc416a2bcf0b6a Mon Sep 17 00:00:00 2001 From: Malhar Vora Date: Wed, 22 Jul 2026 17:03:06 +0530 Subject: [PATCH] Add --type option Signed-off-by: Malhar Vora --- src/uu/lsns/src/errors.rs | 6 ++-- src/uu/lsns/src/lsns.rs | 70 ++++++++++++++++++++++++++++++++------ tests/by-util/test_lsns.rs | 32 +++++++++++++++++ 3 files changed, 94 insertions(+), 14 deletions(-) diff --git a/src/uu/lsns/src/errors.rs b/src/uu/lsns/src/errors.rs index 142f25bb..036a0bf7 100644 --- a/src/uu/lsns/src/errors.rs +++ b/src/uu/lsns/src/errors.rs @@ -14,8 +14,8 @@ pub enum LsnsError { IOError(String, std::io::Error), /// CString conversion error (null byte in string) NulError(String, std::ffi::NulError), - /// Invalid namespace type index - InvalidNamespaceType(usize), + /// Invalid namespace type + InvalidNamespaceType(String), /// Unsupported platform #[cfg(not(target_os = "linux"))] UnsupportedPlatform, @@ -56,7 +56,7 @@ impl fmt::Display for LsnsError { 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), + Self::InvalidNamespaceType(_type) => write!(f, "unknown namespace type: {}", _type), #[cfg(not(target_os = "linux"))] Self::UnsupportedPlatform => write!(f, "lsns is only supported on Linux"), Self::InvalidNamespaceInodeFormat(s) => { diff --git a/src/uu/lsns/src/lsns.rs b/src/uu/lsns/src/lsns.rs index 781693dd..03d62e8a 100644 --- a/src/uu/lsns/src/lsns.rs +++ b/src/uu/lsns/src/lsns.rs @@ -32,7 +32,7 @@ const USAGE: &str = help_usage!("lsns.md"); const PATH_PROC: &str = "/proc"; const NSNAMES: [&str; 8] = ["cgroup", "ipc", "mnt", "net", "pid", "user", "uts", "time"]; -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq)] enum NamespaceType { Cgroup = 0, Ipc = 1, @@ -86,17 +86,25 @@ struct Lsns { namespaces: Vec, noheadings: bool, persistent: bool, + namespace_type: Option, } #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { let matches = uu_app().try_get_matches_from(args)?; + let namespace_type = matches + .get_one::("type") + .map(|s| NamespaceType::try_from(s.as_str())) + .transpose()?; + + // Initialize lsns struct let mut lsns = Lsns { processes: Vec::new(), namespaces: Vec::new(), noheadings: matches.get_flag("noheadings"), persistent: matches.get_flag("persistent"), + namespace_type, }; read_processes(PATH_PROC, &mut lsns)?; @@ -128,6 +136,15 @@ pub fn uu_app() -> Command { .action(ArgAction::SetTrue) .help("namespaces without processes"), ) + .arg( + Arg::new("type") + .short('t') + .long("type") + .action(ArgAction::Set) + .required(false) + .value_name("name") + .help("namespace type (mnt, net, ipc, user, pid, uts, cgroup, time)"), + ) } /// Read information of all the processes from /proc @@ -243,16 +260,23 @@ fn get_ns_ino(pid: i32, nsname: &str) -> Result { fn get_process_command(pid: i32) -> String { // Try cmdline first (full command with arguments) let cmdline_path = format!("/proc/{}/cmdline", pid); - if let Ok(content) = fs::read(&cmdline_path) { - // cmdline uses null bytes as separators + if let Ok(mut content) = fs::read(&cmdline_path) { + // cmdline uses null bytes as separators between arguments if !content.is_empty() { - // Find the first null byte or use entire content - let end = content - .iter() - .position(|&b| b == 0) - .unwrap_or(content.len()); - if end > 0 - && let Ok(cmd) = String::from_utf8(content[..end].to_vec()) + // Remove trailing null byte if present + if content.last().copied() == Some(0) { + content.pop(); + } + + // Replace null bytes with spaces to show full command line + for byte in &mut content { + if *byte == 0 { + *byte = b' '; + } + } + + if !content.is_empty() + && let Ok(cmd) = String::from_utf8(content) { return cmd; } @@ -492,7 +516,25 @@ impl NamespaceType { 5 => Ok(NamespaceType::User), 6 => Ok(NamespaceType::Uts), 7 => Ok(NamespaceType::Time), - _ => Err(LsnsError::InvalidNamespaceType(idx)), + _ => Err(LsnsError::InvalidNamespaceType(idx.to_string())), + } + } +} + +impl TryFrom<&str> for NamespaceType { + type Error = LsnsError; + + fn try_from(s: &str) -> Result { + match s { + "cgroup" => Ok(NamespaceType::Cgroup), + "ipc" => Ok(NamespaceType::Ipc), + "mnt" => Ok(NamespaceType::Mnt), + "net" => Ok(NamespaceType::Net), + "pid" => Ok(NamespaceType::Pid), + "user" => Ok(NamespaceType::User), + "uts" => Ok(NamespaceType::Uts), + "time" => Ok(NamespaceType::Time), + _ => Err(LsnsError::InvalidNamespaceType(s.to_string())), } } } @@ -544,6 +586,12 @@ fn display_namespaces(lsns: &Lsns) -> Result<(), LsnsError> { continue; } + if let Some(namespace_type) = &lsns.namespace_type + && ns.ns_type != *namespace_type + { + continue; + } + // Get namespace type name let ns_type = NSNAMES[ns.ns_type as usize]; diff --git a/tests/by-util/test_lsns.rs b/tests/by-util/test_lsns.rs index 22bf9320..5976ad92 100644 --- a/tests/by-util/test_lsns.rs +++ b/tests/by-util/test_lsns.rs @@ -180,3 +180,35 @@ fn test_persistent_flag() { } } } + +#[test] +#[cfg(target_os = "linux")] +fn test_type_option() { + let res = new_ucmd!().arg("--type").arg("mnt").succeeds(); + let stdout = res.no_stderr().stdout_str(); + + // With --type mnt, only mount namespace should be shown + for line in stdout.lines().skip(1) { + if !line.is_empty() { + let columns: Vec<&str> = line.split_whitespace().collect(); + if columns.len() >= 2 { + assert_eq!( + columns[1], "mnt", + "With --type mnt, only mnt namespace should be shown" + ); + } + } + } +} + +#[test] +#[cfg(target_os = "linux")] +fn test_invalid_namespace_type() { + let res = new_ucmd!() + .arg("--type") + .arg("invalid-namespace-type") + .fails(); + let stdout = res.stderr_str(); + + assert!(stdout.contains("lsns: unknown namespace type: invalid-namespace-type")); +}