Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions src/uu/lsns/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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) => {
Expand Down
70 changes: 59 additions & 11 deletions src/uu/lsns/src/lsns.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -86,17 +86,25 @@ struct Lsns {
namespaces: Vec<Namespace>,
noheadings: bool,
persistent: bool,
namespace_type: Option<NamespaceType>,
}

#[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::<String>("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)?;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -243,16 +260,23 @@ fn get_ns_ino(pid: i32, nsname: &str) -> Result<u64, LsnsError> {
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;
}
Expand Down Expand Up @@ -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<Self, Self::Error> {
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())),
}
}
}
Expand Down Expand Up @@ -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];

Expand Down
32 changes: 32 additions & 0 deletions tests/by-util/test_lsns.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"));
}
Loading