diff --git a/tests/by-util/test_uptime.rs b/tests/by-util/test_uptime.rs index 063d3f0ef0..d92f6e7c60 100644 --- a/tests/by-util/test_uptime.rs +++ b/tests/by-util/test_uptime.rs @@ -4,7 +4,9 @@ // file that was distributed with this source code. // // spell-checker:ignore utmp runlevel testusr testx boottime -#![allow(clippy::cast_possible_wrap, clippy::unreadable_literal)] + +#[cfg(all(target_os = "linux", target_env = "gnu"))] +use crate::utmp::{LinuxGlibcUtmpRecord, write_linux_glibc_utmp}; #[cfg(unix)] use uutests::at_and_ucmd; @@ -112,172 +114,44 @@ fn test_uptime_with_non_existent_file() { .stdout_contains("up ???? days ??:??"); } -// TODO create a similar test for macos -// This will pass #[test] -#[cfg(unix)] -#[cfg(not(any(target_os = "openbsd", target_os = "macos", target_os = "android")))] -#[cfg(not(target_env = "musl"))] +#[cfg(all(target_os = "linux", target_env = "gnu"))] #[cfg_attr( - all(target_arch = "aarch64", target_os = "linux"), + target_arch = "aarch64", ignore = "Issue #7159 - Test not supported on ARM64 Linux" )] -#[allow(clippy::too_many_lines, clippy::items_after_statements)] fn test_uptime_with_file_containing_valid_boot_time_utmpx_record() { - use std::fs::File; - use std::{io::Write, path::PathBuf}; - - // This test will pass for freebsd but we currently don't support changing the utmpx file for - // freebsd. let (at, mut ucmd) = at_and_ucmd!(); // Regex matches for "up 00::00" ,"up 12 days 00::00", the time can be any valid time and // the days can be more than 1 digit or not there. This will match even if the amount of whitespace is // wrong between the days and the time. let re = Regex::new(r"up [(\d){1,} days]*\d{1,2}:\d\d").unwrap(); - utmp(&at.plus("testx")); + let records = [ + LinuxGlibcUtmpRecord::new(uucore::utmpx::BOOT_TIME, 0, "~", "~~", "reboot", ""), + LinuxGlibcUtmpRecord::new( + uucore::utmpx::RUN_LVL, + i32::try_from(std::process::id()).unwrap(), + "~", + "~~", + "runlevel", + "", + ), + LinuxGlibcUtmpRecord::new( + uucore::utmpx::USER_PROCESS, + i32::try_from(std::process::id()).unwrap(), + ":1", + "~~", + "testusr", + "", + ), + ]; + write_linux_glibc_utmp(&at.plus("testx"), &records); ucmd.arg("testx") .succeeds() .stdout_matches(&re) .stdout_contains("load average"); - - // Helper function to create byte sequences - fn slice_32(slice: &[u8]) -> [i8; 32] { - let mut arr: [i8; 32] = [0; 32]; - - for (i, val) in slice.iter().enumerate() { - arr[i] = *val as i8; - } - arr - } - - // Creates a file utmp records of three different types including a valid BOOT_TIME entry - fn utmp(path: &PathBuf) { - // Definitions of our utmpx structs - const BOOT_TIME: i32 = 2; - const RUN_LVL: i32 = 1; - const USER_PROCESS: i32 = 7; - - #[repr(C)] - pub struct TimeVal { - pub tv_sec: i32, - pub tv_usec: i32, - } - - #[repr(C)] - pub struct ExitStatus { - e_termination: i16, - e_exit: i16, - } - - #[repr(C, align(4))] - pub struct Utmp { - pub ut_type: i32, - pub ut_pid: i32, - pub ut_line: [i8; 32], - pub ut_id: [i8; 4], - - pub ut_user: [i8; 32], - pub ut_host: [i8; 256], - pub ut_exit: ExitStatus, - pub ut_session: i32, - pub ut_tv: TimeVal, - - pub ut_addr_v6: [i32; 4], - glibc_reserved: [i8; 20], - } - - let utmp = Utmp { - ut_type: BOOT_TIME, - ut_pid: 0, - ut_line: slice_32("~".as_bytes()), - ut_id: [126, 126, 0, 0], - ut_user: slice_32("reboot".as_bytes()), - ut_host: [0; 256], - ut_exit: ExitStatus { - e_termination: 0, - e_exit: 0, - }, - ut_session: 0, - ut_tv: TimeVal { - tv_sec: 1716371201, - tv_usec: 290913, - }, - ut_addr_v6: [127, 0, 0, 1], - glibc_reserved: [0; 20], - }; - let utmp1 = Utmp { - ut_type: RUN_LVL, - ut_pid: std::process::id() as i32, - ut_line: slice_32("~".as_bytes()), - ut_id: [126, 126, 0, 0], - ut_user: slice_32("runlevel".as_bytes()), - ut_host: [0; 256], - ut_exit: ExitStatus { - e_termination: 0, - e_exit: 0, - }, - ut_session: 0, - ut_tv: TimeVal { - tv_sec: 1716371209, - tv_usec: 162250, - }, - ut_addr_v6: [0, 0, 0, 0], - glibc_reserved: [0; 20], - }; - let utmp2 = Utmp { - ut_type: USER_PROCESS, - ut_pid: std::process::id() as i32, - ut_line: slice_32(":1".as_bytes()), - ut_id: [126, 126, 0, 0], - ut_user: slice_32("testusr".as_bytes()), - ut_host: [0; 256], - ut_exit: ExitStatus { - e_termination: 0, - e_exit: 0, - }, - ut_session: 0, - ut_tv: TimeVal { - tv_sec: 1716371283, - tv_usec: 858764, - }, - ut_addr_v6: [0, 0, 0, 0], - glibc_reserved: [0; 20], - }; - - fn serialize_i8_arr(buf: &mut Vec, arr: &[i8]) { - for b in arr { - buf.push(*b as u8); - } - } - - fn serialize(utmp: &Utmp) -> Vec { - let mut buf = Vec::new(); - buf.extend_from_slice(&utmp.ut_type.to_ne_bytes()); - buf.extend_from_slice(&utmp.ut_pid.to_ne_bytes()); - serialize_i8_arr(&mut buf, &utmp.ut_line); - serialize_i8_arr(&mut buf, &utmp.ut_id); - serialize_i8_arr(&mut buf, &utmp.ut_user); - serialize_i8_arr(&mut buf, &utmp.ut_host); - buf.extend_from_slice(&utmp.ut_exit.e_termination.to_ne_bytes()); - buf.extend_from_slice(&utmp.ut_exit.e_exit.to_ne_bytes()); - buf.extend_from_slice(&utmp.ut_session.to_ne_bytes()); - buf.extend_from_slice(&utmp.ut_tv.tv_sec.to_ne_bytes()); - buf.extend_from_slice(&utmp.ut_tv.tv_usec.to_ne_bytes()); - for v in &utmp.ut_addr_v6 { - buf.extend_from_slice(&v.to_ne_bytes()); - } - serialize_i8_arr(&mut buf, &utmp.glibc_reserved); - buf - } - - let mut buf = serialize(&utmp); - buf.append(&mut serialize(&utmp1)); - buf.append(&mut serialize(&utmp2)); - let mut f = File::create(path).unwrap(); - f.write_all(&buf).unwrap(); - } } #[test] diff --git a/tests/by-util/test_who.rs b/tests/by-util/test_who.rs index fd511d49bd..6f13d67744 100644 --- a/tests/by-util/test_who.rs +++ b/tests/by-util/test_who.rs @@ -5,10 +5,17 @@ // spell-checker:ignore (flags) runlevel mesg +#[cfg(all(target_os = "linux", target_env = "gnu"))] +use crate::utmp::{LinuxGlibcUtmpRecord, write_linux_glibc_utmp}; +#[cfg(all(target_os = "linux", target_env = "gnu"))] +use uucore::utmpx; +#[cfg(all(target_os = "linux", target_env = "gnu"))] +use uutests::at_and_ucmd; use uutests::new_ucmd; use uutests::unwrap_or_return; use uutests::util::{TestScenario, expected_result, gnu_cmd_result}; use uutests::util_name; + #[test] fn test_invalid_arg() { new_ucmd!().arg("--definitely-invalid").fails_with_code(1); @@ -276,6 +283,78 @@ fn test_locale() { .stdout_is(&expected_stdout); } +#[cfg(all(target_os = "linux", target_env = "gnu"))] +#[test] +#[cfg_attr( + target_arch = "aarch64", + ignore = "Issue #7174 - Test not supported on ARM64 Linux" +)] +fn test_records_from_file() { + let (at, mut ucmd) = at_and_ucmd!(); + let path = at.plus("who.utmp"); + let runlevel_pid = i32::from(b'N') * 256 + i32::from(b'3'); + let records = [ + LinuxGlibcUtmpRecord::new(utmpx::RUN_LVL, runlevel_pid, "~", "~~", "runlevel", ""), + LinuxGlibcUtmpRecord::new(utmpx::BOOT_TIME, 0, "~", "~~", "reboot", "kernel"), + LinuxGlibcUtmpRecord::new(utmpx::NEW_TIME, 0, "}", "", "", ""), + LinuxGlibcUtmpRecord::new(utmpx::OLD_TIME, 0, "|", "", "", ""), + LinuxGlibcUtmpRecord::new(utmpx::INIT_PROCESS, 105, "ttyI", "i1", "", ""), + LinuxGlibcUtmpRecord::new(utmpx::LOGIN_PROCESS, 106, "ttyL", "l1", "", ""), + LinuxGlibcUtmpRecord::new( + utmpx::USER_PROCESS, + 107, + "missing-tty", + "u1", + "alice", + "localhost", + ), + LinuxGlibcUtmpRecord::new(utmpx::USER_PROCESS, 108, "null", "u2", "bob", ""), + LinuxGlibcUtmpRecord::new(utmpx::DEAD_PROCESS, 109, "ttyD", "d1", "", "") + .with_exit_status(9, 4), + LinuxGlibcUtmpRecord::new(utmpx::ACCOUNTING, 110, "ignored", "x1", "ignored", ""), + ]; + write_linux_glibc_utmp(&path, &records); + + ucmd.args(&["--all", "--heading"]) + .arg(&path) + .env("LC_ALL", "C") + .succeeds() + .stdout_contains("NAME") + .stdout_contains("run-level 3") + .stdout_contains("last=S") + .stdout_contains("system boot") + .stdout_contains("clock change") + .stdout_contains("ttyI") + .stdout_contains("LOGIN") + .stdout_contains("alice") + .stdout_contains("(localhost)") + .stdout_contains("bob") + .stdout_contains("term=9 exit=4") + .stdout_does_not_contain("ignored"); + + new_ucmd!() + .args(&["--count"]) + .arg(&path) + .env("LC_ALL", "C") + .succeeds() + .stdout_is("alice bob\n# users=2\n"); + + new_ucmd!() + .arg("--lookup") + .arg(&path) + .env("LC_ALL", "C") + .succeeds() + .stdout_contains("alice") + .stdout_contains("localhost"); + + new_ucmd!() + .arg(&path) + .env("LC_ALL", "C.UTF-8") + .succeeds() + .stdout_contains("alice") + .stdout_does_not_contain("system boot"); +} + #[cfg(target_os = "linux")] #[test] fn test_piped_to_dev_full() { diff --git a/tests/common/utmp.rs b/tests/common/utmp.rs new file mode 100644 index 0000000000..1dfb3fc269 --- /dev/null +++ b/tests/common/utmp.rs @@ -0,0 +1,92 @@ +// This file is part of the uutils coreutils package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +use std::mem::size_of; +use std::path::Path; + +use uucore::utmpx; + +const GLIBC_RESERVED_SIZE: usize = 20; +const UT_ADDR_V6_WORDS: usize = 4; +const UT_TYPE_PADDING: usize = size_of::() - size_of::(); + +pub(crate) struct LinuxGlibcUtmpRecord { + record_type: i16, + pid: i32, + line: [u8; utmpx::UT_LINESIZE], + id: [u8; utmpx::UT_IDSIZE], + user: [u8; utmpx::UT_NAMESIZE], + host: [u8; utmpx::UT_HOSTSIZE], + termination: i16, + exit: i16, + timestamp: i32, +} + +impl LinuxGlibcUtmpRecord { + pub(crate) fn new( + record_type: i16, + pid: i32, + line: &str, + id: &str, + user: &str, + host: &str, + ) -> Self { + Self { + record_type, + pid, + line: fixed_field(line), + id: fixed_field(id), + user: fixed_field(user), + host: fixed_field(host), + termination: 0, + exit: 0, + timestamp: 1_716_371_201, + } + } + + #[cfg(feature = "who")] + pub(crate) fn with_exit_status(mut self, termination: i16, exit: i16) -> Self { + self.termination = termination; + self.exit = exit; + self + } + + fn encode_into(&self, bytes: &mut Vec) { + let start = bytes.len(); + bytes.extend_from_slice(&self.record_type.to_ne_bytes()); + // glibc aligns ut_pid after the 16-bit ut_type field. + bytes.resize(bytes.len() + UT_TYPE_PADDING, 0); + bytes.extend_from_slice(&self.pid.to_ne_bytes()); + bytes.extend_from_slice(&self.line); + bytes.extend_from_slice(&self.id); + bytes.extend_from_slice(&self.user); + bytes.extend_from_slice(&self.host); + bytes.extend_from_slice(&self.termination.to_ne_bytes()); + bytes.extend_from_slice(&self.exit.to_ne_bytes()); + bytes.extend_from_slice(&0_i32.to_ne_bytes()); + bytes.extend_from_slice(&self.timestamp.to_ne_bytes()); + bytes.extend_from_slice(&0_i32.to_ne_bytes()); + for _ in 0..UT_ADDR_V6_WORDS { + bytes.extend_from_slice(&0_i32.to_ne_bytes()); + } + bytes.resize(bytes.len() + GLIBC_RESERVED_SIZE, 0); + assert_eq!(bytes.len() - start, size_of::()); + } +} + +fn fixed_field(value: &str) -> [u8; N] { + assert!(value.len() <= N); + let mut field = [0; N]; + field[..value.len()].copy_from_slice(value.as_bytes()); + field +} + +pub(crate) fn write_linux_glibc_utmp(path: &Path, records: &[LinuxGlibcUtmpRecord]) { + let mut bytes = Vec::with_capacity(size_of::() * records.len()); + for record in records { + record.encode_into(&mut bytes); + } + std::fs::write(path, bytes).unwrap(); +} diff --git a/tests/tests.rs b/tests/tests.rs index 14aae3c333..1f2c6d439b 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -18,6 +18,14 @@ fn init() { } } +#[cfg(all( + any(feature = "uptime", feature = "who"), + target_os = "linux", + target_env = "gnu" +))] +#[path = "common/utmp.rs"] +mod utmp; + #[cfg(feature = "arch")] #[path = "by-util/test_arch.rs"] mod test_arch;