From b9fc50d3303c25936a54969a6c493242c90b56c0 Mon Sep 17 00:00:00 2001 From: Jay Date: Mon, 2 Mar 2026 17:42:37 +0000 Subject: [PATCH 1/5] Implement wall Tries to mirror functionality with GNU, namely having timeout and nobanner options. TODO: - tests - buildable on windows --- Cargo.lock | 18 +- Cargo.toml | 14 +- src/uu/wall/Cargo.toml | 18 ++ src/uu/wall/src/main.rs | 1 + src/uu/wall/src/wall.rs | 379 ++++++++++++++++++++++++++++++++++++++++ src/uu/wall/wall.md | 7 + 6 files changed, 429 insertions(+), 8 deletions(-) create mode 100644 src/uu/wall/Cargo.toml create mode 100644 src/uu/wall/src/main.rs create mode 100644 src/uu/wall/src/wall.rs create mode 100644 src/uu/wall/wall.md diff --git a/Cargo.lock b/Cargo.lock index 4a5bffc9..5db53a16 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1288,7 +1288,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0136791f7c95b1f6dd99f9cc786b91bb81c3800b639b3478e561ddb7be95e5f1" dependencies = [ "fastrand", - "getrandom 0.3.2", + "getrandom 0.4.1", "once_cell", "rustix", "windows-sys 0.60.2", @@ -1426,9 +1426,9 @@ checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" [[package]] name = "unicode-width" -version = "0.2.0" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" [[package]] name = "unicode-xid" @@ -1485,6 +1485,7 @@ dependencies = [ "uu_setpgid", "uu_setsid", "uu_uuidgen", + "uu_wall", "uucore 0.2.2", "uuid", "uutests", @@ -1701,6 +1702,17 @@ dependencies = [ "windows", ] +[[package]] +name = "uu_wall" +version = "0.0.1" +dependencies = [ + "chrono", + "clap", + "libc", + "unicode-width", + "uucore 0.2.2", +] + [[package]] name = "uucore" version = "0.2.2" diff --git a/Cargo.toml b/Cargo.toml index 56d2898f..d74582c3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,6 +47,7 @@ feat_common_core = [ "setpgid", "setsid", "uuidgen", + "wall", ] [workspace.dependencies] @@ -86,10 +87,10 @@ clap = { workspace = true } clap_complete = { workspace = true } clap_mangen = { workspace = true } dns-lookup = { workspace = true } -parse_datetime = {workspace = true} +parse_datetime = { workspace = true } phf = { workspace = true } serde = { workspace = true } -serde_json = { workspace = true } +serde_json = { workspace = true } textwrap = { workspace = true } uucore = { workspace = true } @@ -113,13 +114,16 @@ nologin = { optional = true, version = "0.0.1", package = "uu_nologin", path = " renice = { optional = true, version = "0.0.1", package = "uu_renice", path = "src/uu/renice" } rev = { optional = true, version = "0.0.1", package = "uu_rev", path = "src/uu/rev" } setpgid = { optional = true, version = "0.0.1", package = "uu_setpgid", path = "src/uu/setpgid" } -setsid = { optional = true, version = "0.0.1", package = "uu_setsid", path ="src/uu/setsid" } -uuidgen = { optional = true, version = "0.0.1", package = "uu_uuidgen", path ="src/uu/uuidgen" } +setsid = { optional = true, version = "0.0.1", package = "uu_setsid", path = "src/uu/setsid" } +uuidgen = { optional = true, version = "0.0.1", package = "uu_uuidgen", path = "src/uu/uuidgen" } +wall = { optional = true, version = "0.0.1", package = "uu_wall", path = "src/uu/wall" } [dev-dependencies] ctor = "0.6.0" # dmesg test require fixed-boot-time feature turned on. -dmesg = { version = "0.0.1", package = "uu_dmesg", path = "src/uu/dmesg", features = ["fixed-boot-time"] } +dmesg = { version = "0.0.1", package = "uu_dmesg", path = "src/uu/dmesg", features = [ + "fixed-boot-time", +] } libc = { workspace = true } pretty_assertions = "1" rand = { workspace = true } diff --git a/src/uu/wall/Cargo.toml b/src/uu/wall/Cargo.toml new file mode 100644 index 00000000..ccf6a029 --- /dev/null +++ b/src/uu/wall/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "uu_wall" +version = "0.0.1" +edition = "2021" + +[lib] +path = "src/wall.rs" + +[[bin]] +name = "wall" +path = "src/main.rs" + +[dependencies] +uucore = { workspace = true } +clap = { workspace = true } +libc = { workspace = true } +chrono = { workspace = true } +unicode-width = "0.2.2" diff --git a/src/uu/wall/src/main.rs b/src/uu/wall/src/main.rs new file mode 100644 index 00000000..304f2b55 --- /dev/null +++ b/src/uu/wall/src/main.rs @@ -0,0 +1 @@ +uucore::bin!(uu_wall); diff --git a/src/uu/wall/src/wall.rs b/src/uu/wall/src/wall.rs new file mode 100644 index 00000000..a2e9a2d6 --- /dev/null +++ b/src/uu/wall/src/wall.rs @@ -0,0 +1,379 @@ +#![warn(clippy::all, clippy::pedantic)] + +use std::{ + ffi::{CStr, CString}, + fmt::Write as fw, + fs::{File, OpenOptions}, + io::{stdin, BufRead, BufReader, Read, Write}, + path::Path, + str::FromStr, + sync::{mpsc, Arc}, + time::{Duration, SystemTime}, +}; + +use chrono::{DateTime, Local}; +use clap::{crate_version, Arg, ArgAction, Command}; +use libc::{c_char, gid_t}; +use unicode_width::UnicodeWidthChar; +use uucore::{ + error::{UResult, USimpleError}, + format_usage, help_about, help_usage, +}; + +const TERM_WIDTH: usize = 79; +const BLANK: &str = unsafe { str::from_utf8_unchecked(&[b' '; TERM_WIDTH]) }; +fn blank(s: &mut String) { + *s += BLANK; + *s += "\r\n"; +} + +const ABOUT: &str = help_about!("wall.md"); +const USAGE: &str = help_usage!("wall.md"); + +#[must_use] +pub fn uu_app() -> Command { + Command::new(uucore::util_name()) + .version(crate_version!()) + .about(ABOUT) + .override_usage(format_usage(USAGE)) + .infer_long_args(true) + .arg( + Arg::new("input") + .value_name(" | ") + .help("file to read or literal message") + .num_args(1..) + .index(1), + ) + .arg( + Arg::new("group") + .short('g') + .long("group") + .help("only send mesage to group"), + ) + .arg( + Arg::new("nobanner") + .short('n') + .long("nobanner") + .action(ArgAction::SetTrue) + .help("do not print banner, works only for root"), + ) + .arg( + Arg::new("timeout") + .short('t') + .long("timeout") + .value_parser(clap::value_parser!(u64)) + .help("write timeout in seconds"), + ) +} + +#[uucore::main] +pub fn uumain(args: impl uucore::Args) -> UResult<()> { + let args = uu_app().try_get_matches_from_mut(args)?; + + // clap will reject non-integer values, so we just need to reject 0 + let timeout = args.get_one::("timeout"); + if timeout == Some(&0) { + return Err(USimpleError::new(1, "invalid timeout argument: 0")); + } + + // get nobanner flag and check user is root + let flag = args.get_flag("nobanner"); + let print_banner = if flag && unsafe { libc::geteuid() } != 0 { + eprintln!("wall: --nobanner is available only for root"); + true + } else { + !flag + }; + + // if group exists, map to corresponding gid + let group = args + .get_one::("group") + .map(get_group_gid) + .transpose()?; + + // If we have a single input arg and it exists on disk, treat as a file. + // If either is false, assume it is a literal string. + // If no input given, use stdin. + match args.get_many::("input") { + Some(v) => { + let vals: Vec<&str> = v.map(String::as_str).collect(); + + let fname = vals + .first() + .expect("clap guarantees at least 1 value for input"); + + let p = Path::new(fname); + if vals.len() == 1 && p.exists() { + // When we are not root, but suid or sgid, refuse to read files + // (e.g. device files) that the user may not have access to. + // After all, our invoker can easily do "wall < file" + // instead of "wall file". + unsafe { + let uid = libc::getuid(); + if uid > 0 && (uid != libc::geteuid() || libc::getgid() != libc::getegid()) { + return Err(USimpleError::new( + 1, + format!("will not read {fname} - use stdin"), + )); + } + } + + let f = File::open(p) + .map_err(|_| USimpleError::new(1, format!("cannot open {fname}")))?; + + wall(f, group, timeout, print_banner); + } else { + let mut s = vals.as_slice().join(" "); + s.push('\n'); + wall(s.as_bytes(), group, timeout, print_banner); + } + } + None => wall(stdin(), group, timeout, print_banner), + } + + Ok(()) +} + +// go through user entries and print to each tty once. +// if group is specified, only print to memebers of the group. +fn wall(input: R, group: Option, timeout: Option<&u64>, print_banner: bool) { + let msg = makemsg(input, print_banner); + let mut seen_ttys = Vec::with_capacity(16); + loop { + // get next user entry and check it is valid + let entry = unsafe { + let utmpptr = libc::getutxent(); + if utmpptr.is_null() { + break; + } + &*utmpptr + }; + + if entry.ut_user[0] == 0 || entry.ut_type != libc::USER_PROCESS { + continue; + } + + // make sure device is valid + let first = entry.ut_line[0].cast_unsigned(); + if first == 0 || first == b':' { + continue; + } + + // check group membership + if let Some(gid) = group { + if !is_gr_member(&entry.ut_user, gid) { + continue; + } + } + + // get tty + let tty = unsafe { + let len = entry + .ut_line + .iter() + .position(|&c| c == 0) + .unwrap_or(entry.ut_line.len()); + + let bytes = std::slice::from_raw_parts(entry.ut_line.as_ptr().cast(), len); + str::from_utf8_unchecked(bytes).to_owned() + }; + + // output message to device + if !seen_ttys.contains(&tty) { + if let Err(e) = ttymsg(&tty, msg.clone(), timeout) { + eprintln!("warn ({tty:?}): {e}"); + } + seen_ttys.push(tty); + } + } + unsafe { libc::endutxent() }; +} + +// Create the banner and sanitise input +fn makemsg(input: R, print_banner: bool) -> Arc { + let mut buf = String::with_capacity(256); + if print_banner { + let hostname = unsafe { + let mut buf = [0; 256]; + let ret = libc::gethostname(buf.as_mut_ptr(), buf.len()); + if ret == 0 { + CStr::from_ptr(buf.as_ptr()).to_string_lossy().into_owned() + } else { + "unknown".to_string() + } + }; + + let whom = unsafe { + let ruid = libc::getuid(); + let pw = libc::getpwuid(ruid); + if !pw.is_null() && !(*pw).pw_name.is_null() { + CStr::from_ptr((*pw).pw_name).to_string_lossy().into_owned() + } else { + eprintln!("cannot get passwd uid"); + "".to_string() + } + }; + + let whereat = unsafe { + let tty_ptr = libc::ttyname(libc::STDOUT_FILENO); + if tty_ptr.is_null() { + "somewhere".to_string() + } else { + let s = CStr::from_ptr(tty_ptr).to_string_lossy(); + s.strip_prefix("/dev/").unwrap_or(&s).to_string() + } + }; + + let date = DateTime::::from(SystemTime::now()).format("%a %b %e %T %Y"); + let banner = format!("Broadcast message from {whom}@{hostname} ({whereat}) ({date}):"); + + blank(&mut buf); + buf += &banner; + buf.extend(std::iter::repeat_n( + ' ', + TERM_WIDTH.saturating_sub(banner.len()), + )); + buf += "\x07\x07\r\n"; + } + + // we put a blank box around our input + blank(&mut buf); + let mut reader = BufReader::new(input).lines(); + while let Some(Ok(line)) = reader.next() { + buf += &sanitise_line(&line); + } + blank(&mut buf); + + Arc::new(buf) +} + +// this function does two things: +// - wraps lines by TERM_WIDTH +// - escapes control characters +fn sanitise_line(line: &str) -> String { + let mut buf = String::with_capacity(line.len()); + let mut col = 0; + + for ch in line.chars() { + // sanitise character + match ch { + '\x07' => buf.push(ch), + '\t' => { + buf.push(ch); + col += 7 - (col % 8); + } + _ if ch.is_ascii_control() => { + buf.push('^'); + buf.push((ch as u8 ^ 0x40) as char); + col += 2; + } + _ if (0x80_u8..=0x9F).contains(&(ch as u8)) => { + let _ = write!(buf, "\\x{:02X}", ch as u8); + col += 4; + } + _ if ch.is_control() => { + let _ = write!(buf, "\\u{:04X}", ch as u32); + col += 6; + } + _ => { + buf.push(ch); + col += ch.width_cjk().unwrap_or_default(); + } + } + + // wrap line + if col >= TERM_WIDTH { + buf += "\r\n"; + col = 0; + } + } + + // fill rest of line with spaces + buf.extend(std::iter::repeat_n(' ', TERM_WIDTH.saturating_sub(col))); + buf + "\r\n" +} + +// Determine if user is in specified group +fn is_gr_member(user: &[c_char], gid: gid_t) -> bool { + #![allow(clippy::cast_sign_loss)] + + // make sure user exists in database + let pw = unsafe { libc::getpwnam(user.as_ptr()) }; + if pw.is_null() { + return false; + } + + // if so, check if primary group matches + let group = unsafe { (*pw).pw_gid }; + if gid == group { + return true; + } + + #[cfg(target_os = "macos")] + let base_gid = group as libc::c_int; + #[cfg(not(target_os = "macos"))] + let base_gid = group; + + // otherwise check gid is in list of groups user belongs to + let mut ngroups = 16; + let mut groups: Vec = vec![0; ngroups as usize]; + while unsafe { + libc::getgrouplist( + user.as_ptr(), + base_gid, + groups.as_mut_ptr(), + &raw mut ngroups, + ) + } == -1 + { + // ret -1 means buffer was too small so we resize + // according to the returned ngroups value + groups.resize(ngroups as usize, 0); + } + groups.contains(&gid) +} + +// Try to get corresponding group gid. +fn get_group_gid(group: &String) -> UResult { + // first we try as a group name + let cname = + CString::from_str(group).map_err(|_| USimpleError::new(1, "invalid group argument"))?; + + let gr = unsafe { libc::getgrnam(cname.as_ptr()) }; + if !gr.is_null() { + return Ok(unsafe { (*gr).gr_gid }); + } + + // otherwise, try as literal gid + let gid = group + .parse::() + .map_err(|_| USimpleError::new(1, "invalid group argument"))?; + + if unsafe { libc::getgrgid(gid) }.is_null() { + return Err(USimpleError::new(1, format!("{group}: unknown gid"))); + } + Ok(gid) +} + +// Write to the tty device +fn ttymsg(tty: &str, msg: Arc, timeout: Option<&u64>) -> Result<(), &'static str> { + let (tx, rx) = mpsc::channel(); + let device = String::from("/dev/") + tty; + + // spawn thread to write to device + std::thread::spawn(move || { + let r = match OpenOptions::new().write(true).open(&device) { + Ok(mut f) => f.write_all(msg.as_bytes()).map_err(|_| "write failed"), + Err(_) => Err("open failed"), + }; + let _ = tx.send(r); + }); + + // wait with timeout if specified, otherwise block + if let Some(&t) = timeout { + rx.recv_timeout(Duration::from_secs(t)) + .map_err(|_| "write timeout")? + } else { + rx.recv().map_err(|_| "channel closed")? + } +} diff --git a/src/uu/wall/wall.md b/src/uu/wall/wall.md new file mode 100644 index 00000000..37481667 --- /dev/null +++ b/src/uu/wall/wall.md @@ -0,0 +1,7 @@ +# wall + +``` +wall [options] [ | ] +``` + +Write a mesage to all users. From c62af9ef05d3a3c500736c2d66fdf612e6b39979 Mon Sep 17 00:00:00 2001 From: Jay Date: Tue, 3 Mar 2026 17:03:43 +0000 Subject: [PATCH 2/5] wall: testing and ci - added some argument tests. - used `cfg` directives to build on windows. - minor readability tweaks - infer/cast types for macos - satisfy clippy --- rustfmt.toml | 0 src/uu/wall/src/wall.rs | 623 +++++++++++++++++++------------------ tests/by-util/test_wall.rs | 54 ++++ tests/tests.rs | 4 + 4 files changed, 377 insertions(+), 304 deletions(-) create mode 100644 rustfmt.toml create mode 100644 tests/by-util/test_wall.rs diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 00000000..e69de29b diff --git a/src/uu/wall/src/wall.rs b/src/uu/wall/src/wall.rs index a2e9a2d6..dbd3dd3d 100644 --- a/src/uu/wall/src/wall.rs +++ b/src/uu/wall/src/wall.rs @@ -1,35 +1,282 @@ -#![warn(clippy::all, clippy::pedantic)] - -use std::{ - ffi::{CStr, CString}, - fmt::Write as fw, - fs::{File, OpenOptions}, - io::{stdin, BufRead, BufReader, Read, Write}, - path::Path, - str::FromStr, - sync::{mpsc, Arc}, - time::{Duration, SystemTime}, -}; - -use chrono::{DateTime, Local}; use clap::{crate_version, Arg, ArgAction, Command}; -use libc::{c_char, gid_t}; -use unicode_width::UnicodeWidthChar; -use uucore::{ - error::{UResult, USimpleError}, - format_usage, help_about, help_usage, -}; - -const TERM_WIDTH: usize = 79; -const BLANK: &str = unsafe { str::from_utf8_unchecked(&[b' '; TERM_WIDTH]) }; -fn blank(s: &mut String) { - *s += BLANK; - *s += "\r\n"; -} +use uucore::{error::UResult, format_usage, help_about, help_usage}; + +#[cfg(unix)] +use uucore::error::USimpleError; const ABOUT: &str = help_about!("wall.md"); const USAGE: &str = help_usage!("wall.md"); +#[cfg(unix)] +mod unix { + use super::{UResult, USimpleError}; + + use chrono::{DateTime, Local}; + use libc::{c_char, gid_t}; + use std::{ + ffi::{CStr, CString}, + fmt::Write as fw, + fs::OpenOptions, + io::{BufRead, BufReader, Read, Write}, + str::FromStr, + sync::{mpsc, Arc}, + time::{Duration, SystemTime}, + }; + use unicode_width::UnicodeWidthChar; + + const TERM_WIDTH: usize = 79; + const BLANK: &str = unsafe { str::from_utf8_unchecked(&[b' '; TERM_WIDTH]) }; + fn blank(s: &mut String) { + *s += BLANK; + *s += "\r\n"; + } + + // go through user entries and print to each tty once. + // if group is specified, only print to memebers of the group. + pub fn wall( + input: R, + group: Option, + timeout: Option<&u64>, + print_banner: bool, + ) { + let msg = makemsg(input, print_banner); + let mut seen_ttys = Vec::with_capacity(16); + loop { + // get next user entry and check it is valid + let entry = unsafe { + let utmpptr = libc::getutxent(); + if utmpptr.is_null() { + break; + } + &*utmpptr + }; + + if entry.ut_user[0] == 0 || entry.ut_type != libc::USER_PROCESS { + continue; + } + + // make sure device is valid + let first = entry.ut_line[0].cast_unsigned(); + if first == 0 || first == b':' { + continue; + } + + // check group membership + if let Some(gid) = group { + if !is_gr_member(&entry.ut_user, gid) { + continue; + } + } + + // get tty + let tty = unsafe { + let len = entry + .ut_line + .iter() + .position(|&c| c == 0) + .unwrap_or(entry.ut_line.len()); + + let bytes = std::slice::from_raw_parts(entry.ut_line.as_ptr().cast(), len); + str::from_utf8_unchecked(bytes).to_owned() + }; + + // output message to device + if !seen_ttys.contains(&tty) { + if let Err(e) = ttymsg(&tty, msg.clone(), timeout) { + eprintln!("warn ({tty:?}): {e}"); + } + seen_ttys.push(tty); + } + } + unsafe { libc::endutxent() }; + } + + // Create the banner and sanitise input + fn makemsg(input: R, print_banner: bool) -> Arc { + let mut buf = String::with_capacity(256); + if print_banner { + let hostname = unsafe { + let mut buf = [0; 256]; + let ret = libc::gethostname(buf.as_mut_ptr(), buf.len()); + if ret == 0 { + CStr::from_ptr(buf.as_ptr()).to_string_lossy().into_owned() + } else { + "unknown".to_string() + } + }; + + let whom = unsafe { + let ruid = libc::getuid(); + let pw = libc::getpwuid(ruid); + if !pw.is_null() && !(*pw).pw_name.is_null() { + CStr::from_ptr((*pw).pw_name).to_string_lossy().into_owned() + } else { + eprintln!("cannot get passwd uid"); + "".to_string() + } + }; + + let whereat = unsafe { + let tty_ptr = libc::ttyname(libc::STDOUT_FILENO); + if tty_ptr.is_null() { + "somewhere".to_string() + } else { + let s = CStr::from_ptr(tty_ptr).to_string_lossy(); + s.strip_prefix("/dev/").unwrap_or(&s).to_string() + } + }; + + let date = DateTime::::from(SystemTime::now()).format("%a %b %e %T %Y"); + let banner = format!("Broadcast message from {whom}@{hostname} ({whereat}) ({date}):"); + + blank(&mut buf); + buf += &banner; + buf.extend(std::iter::repeat_n( + ' ', + TERM_WIDTH.saturating_sub(banner.len()), + )); + buf += "\x07\x07\r\n"; + } + + // we put a blank box around our input + blank(&mut buf); + let mut reader = BufReader::new(input).lines(); + while let Some(Ok(line)) = reader.next() { + buf += &sanitise_line(&line); + } + blank(&mut buf); + + Arc::new(buf) + } + + // this function does two things: + // - wraps lines by TERM_WIDTH + // - escapes control characters + fn sanitise_line(line: &str) -> String { + let mut buf = String::with_capacity(line.len()); + let mut col = 0; + + for ch in line.chars() { + // sanitise character + match ch { + '\x07' => buf.push(ch), + '\t' => { + buf.push(ch); + col += 7 - (col % 8); + } + _ if ch.is_ascii_control() => { + buf.push('^'); + buf.push((ch as u8 ^ 0x40) as char); + col += 2; + } + _ if (0x80_u8..=0x9F).contains(&(ch as u8)) => { + let _ = write!(buf, "\\x{:02X}", ch as u8); + col += 4; + } + _ if ch.is_control() => { + let _ = write!(buf, "\\u{:04X}", ch as u32); + col += 6; + } + _ => { + buf.push(ch); + col += ch.width_cjk().unwrap_or_default(); + } + } + + // wrap line + if col >= TERM_WIDTH { + buf += "\r\n"; + col = 0; + } + } + + // fill rest of line with spaces + buf.extend(std::iter::repeat_n(' ', TERM_WIDTH.saturating_sub(col))); + buf + "\r\n" + } + + // Determine if user is in specified group + #[allow(clippy::cast_sign_loss)] + fn is_gr_member(user: &[c_char], gid: gid_t) -> bool { + // make sure user exists in database + let pw = unsafe { libc::getpwnam(user.as_ptr()) }; + if pw.is_null() { + return false; + } + + // if so, check if primary group matches + let group = unsafe { (*pw).pw_gid }; + if gid == group { + return true; + } + + // on macos, getgrouplist takes c_int as its group argument + #[cfg(target_os = "macos")] + let group = group.cast_signed(); + + // otherwise check gid is in list of supplementary groups user belongs to + let mut ngroups = 16; + let mut groups = vec![0; ngroups as usize]; + while unsafe { + libc::getgrouplist(user.as_ptr(), group, groups.as_mut_ptr(), &raw mut ngroups) + } == -1 + { + // ret -1 means buffer was too small so we resize + // according to the returned ngroups value + groups.resize(ngroups as usize, 0); + } + + #[cfg(target_os = "macos")] + let gid = gid.cast_signed(); + groups.contains(&gid) + } + + // Try to get corresponding group gid. + pub fn get_group_gid(group: &String) -> UResult { + // first we try as a group name + let Ok(cname) = CString::from_str(group) else { + return Err(USimpleError::new(1, "invalid group argument")); + }; + + let gr = unsafe { libc::getgrnam(cname.as_ptr()) }; + if !gr.is_null() { + return Ok(unsafe { (*gr).gr_gid }); + } + + // otherwise, try as literal gid + let Ok(gid) = group.parse::() else { + return Err(USimpleError::new(1, "invalid group argument")); + }; + if unsafe { libc::getgrgid(gid) }.is_null() { + return Err(USimpleError::new(1, format!("{group}: unknown gid"))); + } + Ok(gid) + } + + // Write to the tty device + fn ttymsg(tty: &str, msg: Arc, timeout: Option<&u64>) -> Result<(), &'static str> { + let (tx, rx) = mpsc::channel(); + let device = String::from("/dev/") + tty; + + // spawn thread to write to device + std::thread::spawn(move || { + let r = match OpenOptions::new().write(true).open(&device) { + Ok(mut f) => f.write_all(msg.as_bytes()).map_err(|_| "write failed"), + Err(_) => Err("open failed"), + }; + let _ = tx.send(r); + }); + + // wait with timeout if specified, otherwise block + if let Some(&t) = timeout { + rx.recv_timeout(Duration::from_secs(t)) + .map_err(|_| "write timeout")? + } else { + rx.recv().map_err(|_| "channel closed")? + } + } +} + #[must_use] pub fn uu_app() -> Command { Command::new(uucore::util_name()) @@ -66,8 +313,21 @@ pub fn uu_app() -> Command { ) } +#[cfg(not(unix))] +#[uucore::main] +pub fn uumain(_args: impl uucore::Args) -> UResult<()> { + Err(uucore::error::USimpleError::new( + 1, + "`wall` is available only on Unix.", + )) +} + +#[cfg(unix)] #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { + use std::fs::File; + use std::path::Path; + let args = uu_app().try_get_matches_from_mut(args)?; // clap will reject non-integer values, so we just need to reject 0 @@ -76,7 +336,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { return Err(USimpleError::new(1, "invalid timeout argument: 0")); } - // get nobanner flag and check user is root + // get nobanner flag and check if user is root let flag = args.get_flag("nobanner"); let print_banner = if flag && unsafe { libc::geteuid() } != 0 { eprintln!("wall: --nobanner is available only for root"); @@ -88,292 +348,47 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { // if group exists, map to corresponding gid let group = args .get_one::("group") - .map(get_group_gid) + .map(unix::get_group_gid) .transpose()?; // If we have a single input arg and it exists on disk, treat as a file. // If either is false, assume it is a literal string. // If no input given, use stdin. - match args.get_many::("input") { - Some(v) => { - let vals: Vec<&str> = v.map(String::as_str).collect(); - - let fname = vals - .first() - .expect("clap guarantees at least 1 value for input"); - - let p = Path::new(fname); - if vals.len() == 1 && p.exists() { - // When we are not root, but suid or sgid, refuse to read files - // (e.g. device files) that the user may not have access to. - // After all, our invoker can easily do "wall < file" - // instead of "wall file". - unsafe { - let uid = libc::getuid(); - if uid > 0 && (uid != libc::geteuid() || libc::getgid() != libc::getegid()) { - return Err(USimpleError::new( - 1, - format!("will not read {fname} - use stdin"), - )); - } + if let Some(v) = args.get_many::("input") { + let vals: Vec<&str> = v.map(String::as_str).collect(); + + let fname = vals + .first() + .expect("clap guarantees at least 1 value for input"); + + let p = Path::new(fname); + if vals.len() == 1 && p.exists() { + // When we are not root, but suid or sgid, refuse to read files + // (e.g. device files) that the user may not have access to. + // After all, our invoker can easily do "wall < file" instead of "wall file". + unsafe { + let uid = libc::getuid(); + if uid > 0 && (uid != libc::geteuid() || libc::getgid() != libc::getegid()) { + return Err(USimpleError::new( + 1, + format!("will not read {fname} - use stdin"), + )); } - - let f = File::open(p) - .map_err(|_| USimpleError::new(1, format!("cannot open {fname}")))?; - - wall(f, group, timeout, print_banner); - } else { - let mut s = vals.as_slice().join(" "); - s.push('\n'); - wall(s.as_bytes(), group, timeout, print_banner); } - } - None => wall(stdin(), group, timeout, print_banner), - } - - Ok(()) -} - -// go through user entries and print to each tty once. -// if group is specified, only print to memebers of the group. -fn wall(input: R, group: Option, timeout: Option<&u64>, print_banner: bool) { - let msg = makemsg(input, print_banner); - let mut seen_ttys = Vec::with_capacity(16); - loop { - // get next user entry and check it is valid - let entry = unsafe { - let utmpptr = libc::getutxent(); - if utmpptr.is_null() { - break; - } - &*utmpptr - }; - - if entry.ut_user[0] == 0 || entry.ut_type != libc::USER_PROCESS { - continue; - } - // make sure device is valid - let first = entry.ut_line[0].cast_unsigned(); - if first == 0 || first == b':' { - continue; - } - - // check group membership - if let Some(gid) = group { - if !is_gr_member(&entry.ut_user, gid) { - continue; - } - } + let Ok(f) = File::open(p) else { + return Err(USimpleError::new(1, format!("cannot open {fname}"))); + }; - // get tty - let tty = unsafe { - let len = entry - .ut_line - .iter() - .position(|&c| c == 0) - .unwrap_or(entry.ut_line.len()); - - let bytes = std::slice::from_raw_parts(entry.ut_line.as_ptr().cast(), len); - str::from_utf8_unchecked(bytes).to_owned() - }; - - // output message to device - if !seen_ttys.contains(&tty) { - if let Err(e) = ttymsg(&tty, msg.clone(), timeout) { - eprintln!("warn ({tty:?}): {e}"); - } - seen_ttys.push(tty); + unix::wall(f, group, timeout, print_banner); + } else { + let mut s = vals.as_slice().join(" "); + s.push('\n'); + unix::wall(s.as_bytes(), group, timeout, print_banner); } - } - unsafe { libc::endutxent() }; -} - -// Create the banner and sanitise input -fn makemsg(input: R, print_banner: bool) -> Arc { - let mut buf = String::with_capacity(256); - if print_banner { - let hostname = unsafe { - let mut buf = [0; 256]; - let ret = libc::gethostname(buf.as_mut_ptr(), buf.len()); - if ret == 0 { - CStr::from_ptr(buf.as_ptr()).to_string_lossy().into_owned() - } else { - "unknown".to_string() - } - }; - - let whom = unsafe { - let ruid = libc::getuid(); - let pw = libc::getpwuid(ruid); - if !pw.is_null() && !(*pw).pw_name.is_null() { - CStr::from_ptr((*pw).pw_name).to_string_lossy().into_owned() - } else { - eprintln!("cannot get passwd uid"); - "".to_string() - } - }; - - let whereat = unsafe { - let tty_ptr = libc::ttyname(libc::STDOUT_FILENO); - if tty_ptr.is_null() { - "somewhere".to_string() - } else { - let s = CStr::from_ptr(tty_ptr).to_string_lossy(); - s.strip_prefix("/dev/").unwrap_or(&s).to_string() - } - }; - - let date = DateTime::::from(SystemTime::now()).format("%a %b %e %T %Y"); - let banner = format!("Broadcast message from {whom}@{hostname} ({whereat}) ({date}):"); - - blank(&mut buf); - buf += &banner; - buf.extend(std::iter::repeat_n( - ' ', - TERM_WIDTH.saturating_sub(banner.len()), - )); - buf += "\x07\x07\r\n"; - } - - // we put a blank box around our input - blank(&mut buf); - let mut reader = BufReader::new(input).lines(); - while let Some(Ok(line)) = reader.next() { - buf += &sanitise_line(&line); - } - blank(&mut buf); - - Arc::new(buf) -} - -// this function does two things: -// - wraps lines by TERM_WIDTH -// - escapes control characters -fn sanitise_line(line: &str) -> String { - let mut buf = String::with_capacity(line.len()); - let mut col = 0; - - for ch in line.chars() { - // sanitise character - match ch { - '\x07' => buf.push(ch), - '\t' => { - buf.push(ch); - col += 7 - (col % 8); - } - _ if ch.is_ascii_control() => { - buf.push('^'); - buf.push((ch as u8 ^ 0x40) as char); - col += 2; - } - _ if (0x80_u8..=0x9F).contains(&(ch as u8)) => { - let _ = write!(buf, "\\x{:02X}", ch as u8); - col += 4; - } - _ if ch.is_control() => { - let _ = write!(buf, "\\u{:04X}", ch as u32); - col += 6; - } - _ => { - buf.push(ch); - col += ch.width_cjk().unwrap_or_default(); - } - } - - // wrap line - if col >= TERM_WIDTH { - buf += "\r\n"; - col = 0; - } - } - - // fill rest of line with spaces - buf.extend(std::iter::repeat_n(' ', TERM_WIDTH.saturating_sub(col))); - buf + "\r\n" -} - -// Determine if user is in specified group -fn is_gr_member(user: &[c_char], gid: gid_t) -> bool { - #![allow(clippy::cast_sign_loss)] - - // make sure user exists in database - let pw = unsafe { libc::getpwnam(user.as_ptr()) }; - if pw.is_null() { - return false; - } - - // if so, check if primary group matches - let group = unsafe { (*pw).pw_gid }; - if gid == group { - return true; - } - - #[cfg(target_os = "macos")] - let base_gid = group as libc::c_int; - #[cfg(not(target_os = "macos"))] - let base_gid = group; - - // otherwise check gid is in list of groups user belongs to - let mut ngroups = 16; - let mut groups: Vec = vec![0; ngroups as usize]; - while unsafe { - libc::getgrouplist( - user.as_ptr(), - base_gid, - groups.as_mut_ptr(), - &raw mut ngroups, - ) - } == -1 - { - // ret -1 means buffer was too small so we resize - // according to the returned ngroups value - groups.resize(ngroups as usize, 0); - } - groups.contains(&gid) -} - -// Try to get corresponding group gid. -fn get_group_gid(group: &String) -> UResult { - // first we try as a group name - let cname = - CString::from_str(group).map_err(|_| USimpleError::new(1, "invalid group argument"))?; - - let gr = unsafe { libc::getgrnam(cname.as_ptr()) }; - if !gr.is_null() { - return Ok(unsafe { (*gr).gr_gid }); - } - - // otherwise, try as literal gid - let gid = group - .parse::() - .map_err(|_| USimpleError::new(1, "invalid group argument"))?; - - if unsafe { libc::getgrgid(gid) }.is_null() { - return Err(USimpleError::new(1, format!("{group}: unknown gid"))); - } - Ok(gid) -} - -// Write to the tty device -fn ttymsg(tty: &str, msg: Arc, timeout: Option<&u64>) -> Result<(), &'static str> { - let (tx, rx) = mpsc::channel(); - let device = String::from("/dev/") + tty; - - // spawn thread to write to device - std::thread::spawn(move || { - let r = match OpenOptions::new().write(true).open(&device) { - Ok(mut f) => f.write_all(msg.as_bytes()).map_err(|_| "write failed"), - Err(_) => Err("open failed"), - }; - let _ = tx.send(r); - }); - - // wait with timeout if specified, otherwise block - if let Some(&t) = timeout { - rx.recv_timeout(Duration::from_secs(t)) - .map_err(|_| "write timeout")? } else { - rx.recv().map_err(|_| "channel closed")? + unix::wall(std::io::stdin(), group, timeout, print_banner); } + + Ok(()) } diff --git a/tests/by-util/test_wall.rs b/tests/by-util/test_wall.rs new file mode 100644 index 00000000..6b07cbf0 --- /dev/null +++ b/tests/by-util/test_wall.rs @@ -0,0 +1,54 @@ +#[cfg(unix)] +mod tests { + use uutests::new_ucmd; + + #[test] + fn test_invalid_arg() { + new_ucmd!().arg("--definitely-invalid").fails().code_is(1); + } + + #[test] + fn test_fails_on_invalid_group() { + new_ucmd!() + .arg("-g") + .arg("fooblywoobly") // assuming this group doesnt exist + .fails() + .code_is(1) + .stderr_contains("wall: invalid group argument"); + } + + #[test] + fn test_fails_on_invalid_gid() { + new_ucmd!() + .arg("-g") + .arg("99999") // assuming this group doesnt exist + .fails() + .code_is(1) + .stderr_contains("wall: 99999: unknown gid"); + } + + #[test] + fn test_warns_on_nobanner() { + new_ucmd!() + .arg("-n") + .arg("some text to wall") + .succeeds() + .code_is(0) + .stderr_contains("wall: --nobanner is available only for root"); + } + + #[test] + fn test_fails_on_invalid_timeout() { + new_ucmd!() + .arg("-t") + .arg("0") + .fails() + .code_is(1) + .stderr_contains("wall: invalid timeout argument: 0"); + } + + #[test] + fn test_succeeds_no_stdout() { + new_ucmd!().pipe_in("pipe me").succeeds().stdout_is(""); + } +} diff --git a/tests/tests.rs b/tests/tests.rs index 8f94a09a..069ce4f8 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -90,3 +90,7 @@ mod test_mcookie; #[cfg(feature = "uuidgen")] #[path = "by-util/test_uuidgen.rs"] mod test_uuidgen; + +#[cfg(feature = "wall")] +#[path = "by-util/test_wall.rs"] +mod test_wall; From fcec3fc52fef3bd66b33bd09e12677215c8d0a38 Mon Sep 17 00:00:00 2001 From: Jay Date: Tue, 3 Mar 2026 17:17:25 +0000 Subject: [PATCH 3/5] wall: unneeded clippy directive, clearer var names --- src/uu/wall/src/wall.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/uu/wall/src/wall.rs b/src/uu/wall/src/wall.rs index dbd3dd3d..ef4837c1 100644 --- a/src/uu/wall/src/wall.rs +++ b/src/uu/wall/src/wall.rs @@ -105,7 +105,7 @@ mod unix { } }; - let whom = unsafe { + let user = unsafe { let ruid = libc::getuid(); let pw = libc::getpwuid(ruid); if !pw.is_null() && !(*pw).pw_name.is_null() { @@ -116,7 +116,7 @@ mod unix { } }; - let whereat = unsafe { + let tty = unsafe { let tty_ptr = libc::ttyname(libc::STDOUT_FILENO); if tty_ptr.is_null() { "somewhere".to_string() @@ -127,7 +127,7 @@ mod unix { }; let date = DateTime::::from(SystemTime::now()).format("%a %b %e %T %Y"); - let banner = format!("Broadcast message from {whom}@{hostname} ({whereat}) ({date}):"); + let banner = format!("Broadcast message from {user}@{hostname} ({tty}) ({date}):"); blank(&mut buf); buf += &banner; @@ -196,7 +196,6 @@ mod unix { } // Determine if user is in specified group - #[allow(clippy::cast_sign_loss)] fn is_gr_member(user: &[c_char], gid: gid_t) -> bool { // make sure user exists in database let pw = unsafe { libc::getpwnam(user.as_ptr()) }; From 32ebe556325f0e0d4dd1642a6b1ba00768394ec9 Mon Sep 17 00:00:00 2001 From: Jay Date: Tue, 3 Mar 2026 17:21:50 +0000 Subject: [PATCH 4/5] wall: specify stderr more in testing --- tests/by-util/test_wall.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/by-util/test_wall.rs b/tests/by-util/test_wall.rs index 6b07cbf0..86d80691 100644 --- a/tests/by-util/test_wall.rs +++ b/tests/by-util/test_wall.rs @@ -14,7 +14,7 @@ mod tests { .arg("fooblywoobly") // assuming this group doesnt exist .fails() .code_is(1) - .stderr_contains("wall: invalid group argument"); + .stderr_is("wall: invalid group argument\n"); } #[test] @@ -24,7 +24,7 @@ mod tests { .arg("99999") // assuming this group doesnt exist .fails() .code_is(1) - .stderr_contains("wall: 99999: unknown gid"); + .stderr_is("wall: 99999: unknown gid\n"); } #[test] @@ -34,7 +34,7 @@ mod tests { .arg("some text to wall") .succeeds() .code_is(0) - .stderr_contains("wall: --nobanner is available only for root"); + .stderr_is("wall: --nobanner is available only for root\n"); } #[test] @@ -44,7 +44,7 @@ mod tests { .arg("0") .fails() .code_is(1) - .stderr_contains("wall: invalid timeout argument: 0"); + .stderr_is("wall: invalid timeout argument: 0\n"); } #[test] From 6dbb75a51c8ea9e6f0d79684f5e452fde9b9426d Mon Sep 17 00:00:00 2001 From: Jay Date: Wed, 4 Mar 2026 19:27:44 +0000 Subject: [PATCH 5/5] wall: refactor - put deps in appropariate places - use `uucore` methods to simplify code and remove a lot of manual `unsafe { libc::... --- Cargo.toml | 1 + src/uu/wall/Cargo.toml | 5 +- src/uu/wall/src/wall.rs | 156 +++++++++---------------------------- tests/by-util/test_wall.rs | 4 +- 4 files changed, 44 insertions(+), 122 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index d74582c3..21db372c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -76,6 +76,7 @@ sysinfo = "0.38" tempfile = "3.9.0" textwrap = { version = "0.16.0", features = ["terminal_size"] } thiserror = "2.0" +unicode-width = { version = "0.2.2", default-features = false } uucore = "0.2.2" uuid = { version = "1.16.0", features = ["rng-rand"] } uutests = "0.6.0" diff --git a/src/uu/wall/Cargo.toml b/src/uu/wall/Cargo.toml index ccf6a029..178a4eb5 100644 --- a/src/uu/wall/Cargo.toml +++ b/src/uu/wall/Cargo.toml @@ -2,6 +2,7 @@ name = "uu_wall" version = "0.0.1" edition = "2021" +description = "wall ~ Write a mesage to all users." [lib] path = "src/wall.rs" @@ -11,8 +12,8 @@ name = "wall" path = "src/main.rs" [dependencies] -uucore = { workspace = true } +uucore = { workspace = true, features = ["process", "entries"] } clap = { workspace = true } libc = { workspace = true } chrono = { workspace = true } -unicode-width = "0.2.2" +unicode-width = { workspace = true } diff --git a/src/uu/wall/src/wall.rs b/src/uu/wall/src/wall.rs index ef4837c1..1949e40b 100644 --- a/src/uu/wall/src/wall.rs +++ b/src/uu/wall/src/wall.rs @@ -2,27 +2,30 @@ use clap::{crate_version, Arg, ArgAction, Command}; use uucore::{error::UResult, format_usage, help_about, help_usage}; #[cfg(unix)] -use uucore::error::USimpleError; +use uucore::{ + entries::{Group, Locate}, + error::USimpleError, + process, +}; const ABOUT: &str = help_about!("wall.md"); const USAGE: &str = help_usage!("wall.md"); #[cfg(unix)] mod unix { - use super::{UResult, USimpleError}; + use super::process; + + use uucore::entries::{uid2usr, Locate, Passwd}; + use uucore::utmpx::Utmpx; - use chrono::{DateTime, Local}; - use libc::{c_char, gid_t}; use std::{ - ffi::{CStr, CString}, - fmt::Write as fw, + ffi::CStr, fs::OpenOptions, io::{BufRead, BufReader, Read, Write}, - str::FromStr, sync::{mpsc, Arc}, - time::{Duration, SystemTime}, + time::Duration, }; - use unicode_width::UnicodeWidthChar; + use unicode_width::{UnicodeWidthChar, UnicodeWidthStr}; const TERM_WIDTH: usize = 79; const BLANK: &str = unsafe { str::from_utf8_unchecked(&[b' '; TERM_WIDTH]) }; @@ -35,51 +38,31 @@ mod unix { // if group is specified, only print to memebers of the group. pub fn wall( input: R, - group: Option, + group: Option, timeout: Option<&u64>, print_banner: bool, ) { let msg = makemsg(input, print_banner); let mut seen_ttys = Vec::with_capacity(16); - loop { - // get next user entry and check it is valid - let entry = unsafe { - let utmpptr = libc::getutxent(); - if utmpptr.is_null() { - break; - } - &*utmpptr - }; - - if entry.ut_user[0] == 0 || entry.ut_type != libc::USER_PROCESS { + for record in Utmpx::iter_all_records() { + if !record.is_user_process() { continue; } // make sure device is valid - let first = entry.ut_line[0].cast_unsigned(); - if first == 0 || first == b':' { + let tty = record.tty_device(); + if tty.is_empty() || tty.starts_with(':') { continue; } // check group membership if let Some(gid) = group { - if !is_gr_member(&entry.ut_user, gid) { - continue; + match Passwd::locate(record.user().as_str()) { + Ok(pw) if pw.gid == gid || pw.belongs_to().contains(&gid) => {} + _ => continue, } } - // get tty - let tty = unsafe { - let len = entry - .ut_line - .iter() - .position(|&c| c == 0) - .unwrap_or(entry.ut_line.len()); - - let bytes = std::slice::from_raw_parts(entry.ut_line.as_ptr().cast(), len); - str::from_utf8_unchecked(bytes).to_owned() - }; - // output message to device if !seen_ttys.contains(&tty) { if let Err(e) = ttymsg(&tty, msg.clone(), timeout) { @@ -88,7 +71,6 @@ mod unix { seen_ttys.push(tty); } } - unsafe { libc::endutxent() }; } // Create the banner and sanitise input @@ -105,16 +87,7 @@ mod unix { } }; - let user = unsafe { - let ruid = libc::getuid(); - let pw = libc::getpwuid(ruid); - if !pw.is_null() && !(*pw).pw_name.is_null() { - CStr::from_ptr((*pw).pw_name).to_string_lossy().into_owned() - } else { - eprintln!("cannot get passwd uid"); - "".to_string() - } - }; + let user = uid2usr(process::getuid()).unwrap_or("".to_string()); let tty = unsafe { let tty_ptr = libc::ttyname(libc::STDOUT_FILENO); @@ -126,14 +99,14 @@ mod unix { } }; - let date = DateTime::::from(SystemTime::now()).format("%a %b %e %T %Y"); + let date = chrono::Local::now().format("%a %b %e %T %Y"); let banner = format!("Broadcast message from {user}@{hostname} ({tty}) ({date}):"); blank(&mut buf); buf += &banner; buf.extend(std::iter::repeat_n( ' ', - TERM_WIDTH.saturating_sub(banner.len()), + TERM_WIDTH.saturating_sub(banner.width()), )); buf += "\x07\x07\r\n"; } @@ -153,6 +126,8 @@ mod unix { // - wraps lines by TERM_WIDTH // - escapes control characters fn sanitise_line(line: &str) -> String { + use std::fmt::Write; + let mut buf = String::with_capacity(line.len()); let mut col = 0; @@ -179,7 +154,7 @@ mod unix { } _ => { buf.push(ch); - col += ch.width_cjk().unwrap_or_default(); + col += ch.width().unwrap_or_default(); } } @@ -195,63 +170,6 @@ mod unix { buf + "\r\n" } - // Determine if user is in specified group - fn is_gr_member(user: &[c_char], gid: gid_t) -> bool { - // make sure user exists in database - let pw = unsafe { libc::getpwnam(user.as_ptr()) }; - if pw.is_null() { - return false; - } - - // if so, check if primary group matches - let group = unsafe { (*pw).pw_gid }; - if gid == group { - return true; - } - - // on macos, getgrouplist takes c_int as its group argument - #[cfg(target_os = "macos")] - let group = group.cast_signed(); - - // otherwise check gid is in list of supplementary groups user belongs to - let mut ngroups = 16; - let mut groups = vec![0; ngroups as usize]; - while unsafe { - libc::getgrouplist(user.as_ptr(), group, groups.as_mut_ptr(), &raw mut ngroups) - } == -1 - { - // ret -1 means buffer was too small so we resize - // according to the returned ngroups value - groups.resize(ngroups as usize, 0); - } - - #[cfg(target_os = "macos")] - let gid = gid.cast_signed(); - groups.contains(&gid) - } - - // Try to get corresponding group gid. - pub fn get_group_gid(group: &String) -> UResult { - // first we try as a group name - let Ok(cname) = CString::from_str(group) else { - return Err(USimpleError::new(1, "invalid group argument")); - }; - - let gr = unsafe { libc::getgrnam(cname.as_ptr()) }; - if !gr.is_null() { - return Ok(unsafe { (*gr).gr_gid }); - } - - // otherwise, try as literal gid - let Ok(gid) = group.parse::() else { - return Err(USimpleError::new(1, "invalid group argument")); - }; - if unsafe { libc::getgrgid(gid) }.is_null() { - return Err(USimpleError::new(1, format!("{group}: unknown gid"))); - } - Ok(gid) - } - // Write to the tty device fn ttymsg(tty: &str, msg: Arc, timeout: Option<&u64>) -> Result<(), &'static str> { let (tx, rx) = mpsc::channel(); @@ -337,7 +255,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { // get nobanner flag and check if user is root let flag = args.get_flag("nobanner"); - let print_banner = if flag && unsafe { libc::geteuid() } != 0 { + let print_banner = if flag && process::geteuid() != 0 { eprintln!("wall: --nobanner is available only for root"); true } else { @@ -347,7 +265,11 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { // if group exists, map to corresponding gid let group = args .get_one::("group") - .map(unix::get_group_gid) + .map(|g| { + Group::locate(g.as_str()) + .map(|g| g.gid) + .map_err(|_| USimpleError::new(1, format!("{g}: unknown group"))) + }) .transpose()?; // If we have a single input arg and it exists on disk, treat as a file. @@ -365,14 +287,12 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { // When we are not root, but suid or sgid, refuse to read files // (e.g. device files) that the user may not have access to. // After all, our invoker can easily do "wall < file" instead of "wall file". - unsafe { - let uid = libc::getuid(); - if uid > 0 && (uid != libc::geteuid() || libc::getgid() != libc::getegid()) { - return Err(USimpleError::new( - 1, - format!("will not read {fname} - use stdin"), - )); - } + let uid = process::getuid(); + if uid > 0 && (uid != process::geteuid() || process::getgid() != process::getegid()) { + return Err(USimpleError::new( + 1, + format!("will not read {fname} - use stdin"), + )); } let Ok(f) = File::open(p) else { diff --git a/tests/by-util/test_wall.rs b/tests/by-util/test_wall.rs index 86d80691..bcf9c9af 100644 --- a/tests/by-util/test_wall.rs +++ b/tests/by-util/test_wall.rs @@ -14,7 +14,7 @@ mod tests { .arg("fooblywoobly") // assuming this group doesnt exist .fails() .code_is(1) - .stderr_is("wall: invalid group argument\n"); + .stderr_is("wall: fooblywoobly: unknown group\n"); } #[test] @@ -24,7 +24,7 @@ mod tests { .arg("99999") // assuming this group doesnt exist .fails() .code_is(1) - .stderr_is("wall: 99999: unknown gid\n"); + .stderr_is("wall: 99999: unknown group\n"); } #[test]