diff --git a/Cargo.lock b/Cargo.lock index 5fd1ed75857..040158f8bf6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -156,6 +156,15 @@ dependencies = [ "compare", ] +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + [[package]] name = "bindgen" version = "0.69.4" @@ -423,6 +432,7 @@ checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" name = "coreutils" version = "0.0.26" dependencies = [ + "bincode", "chrono", "clap", "clap_complete", @@ -444,6 +454,8 @@ dependencies = [ "rlimit", "rstest", "selinux", + "serde", + "serde-big-array", "sha1", "tempfile", "textwrap", @@ -2065,18 +2077,27 @@ checksum = "e25dfac463d778e353db5be2449d1cce89bd6fd23c9f1ea21310ce6e5a1b29c4" [[package]] name = "serde" -version = "1.0.193" +version = "1.0.202" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89" +checksum = "226b61a0d411b2ba5ff6d7f73a476ac4f8bb900373459cd00fab8512828ba395" dependencies = [ "serde_derive", ] +[[package]] +name = "serde-big-array" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11fc7cc2c76d73e0f27ee52abbd64eec84d46f370c88371120433196934e4b7f" +dependencies = [ + "serde", +] + [[package]] name = "serde_derive" -version = "1.0.193" +version = "1.0.202" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" +checksum = "6048858004bcff69094cd972ed40a32500f153bd3be9f716b2eed2e8217c4838" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 229d6b9f57c..09d0c11ac89 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -490,8 +490,16 @@ sha1 = { version = "0.10.6", features = ["std"] } tempfile = { workspace = true } time = { workspace = true, features = ["local-offset"] } unindent = "0.2.3" -uucore = { workspace = true, features = ["entries", "process", "signals"] } +uucore = { workspace = true, features = [ + "entries", + "process", + "signals", + "utmpx", +] } +serde = { version = "1.0.202", features = ["derive"] } walkdir = { workspace = true } +bincode = { version = "1.3.3" } +serde-big-array = "0.5.1" hex-literal = "0.4.1" rstest = { workspace = true } diff --git a/src/uu/uptime/src/platform/unix.rs b/src/uu/uptime/src/platform/unix.rs index 7df34eb1a30..f8fc9eb4da4 100644 --- a/src/uu/uptime/src/platform/unix.rs +++ b/src/uu/uptime/src/platform/unix.rs @@ -109,7 +109,7 @@ fn uptime_with_file(file_path: &OsString) -> UResult<()> { print_loadavg(); return Ok(()); } - + print_time(); let (boot_time, user_count) = process_utmpx_from_file(file_path); if let Some(time) = boot_time { let upsecs = get_uptime_from_boot_time(time); @@ -211,6 +211,13 @@ fn process_utmpx_from_file(file: &OsString) -> (Option, usize) { match line.record_type() { USER_PROCESS => nusers += 1, BOOT_TIME => { + // Macos "getutxent" initializes all fields of the struct to 0, if it can't find any + // utmpx record from the file. + #[cfg(target_os = "macos")] + if line.into_inner().ut_tv.tv_sec == 0 { + continue; + } + let dt = line.login_time(); if dt.unix_timestamp() > 0 { boot_time = Some(dt.unix_timestamp() as time_t); diff --git a/tests/by-util/test_uptime.rs b/tests/by-util/test_uptime.rs index 23e492ab2db..402d4355409 100644 --- a/tests/by-util/test_uptime.rs +++ b/tests/by-util/test_uptime.rs @@ -3,7 +3,13 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. use crate::common::util::TestScenario; + use regex::Regex; +use serde::Serialize; +use serde_big_array::BigArray; +use std::fs::File; +use std::{io::Write, path::PathBuf}; +use uucore::utmpx::{BOOT_TIME, RUN_LVL, USER_PROCESS, UT_HOSTSIZE, UT_LINESIZE, UT_NAMESIZE}; #[test] fn test_invalid_arg() { @@ -75,32 +81,134 @@ fn test_uptime_with_non_existent_file() { #[test] #[cfg(not(target_os = "openbsd"))] -fn test_uptime_with_file_containing_valid_utmpx_record() { +fn test_uptime_with_file_containing_valid_boot_time_utmpx_record() { let ts = TestScenario::new(util_name!()); - let re = Regex::new(r"up {1,2}[(\d){1,} days]*\d{1,2}:\d\d").unwrap(); - ts.ucmd() - .arg("validRecord.txt") - .succeeds() - .stdout_matches(&re) - .stdout_contains("load average"); -} + let at = &ts.fixtures; + // 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. -/// Assuming /var/log/wtmp has multiple records, /var/log/wtmp doesn't seem to exist in macos -#[test] -#[cfg(not(target_os = "openbsd"))] -fn test_uptime_with_file_containing_multiple_valid_utmpx_record() { - let ts = TestScenario::new(util_name!()); - // Checking for up 00:00 [can be any time] - let re = Regex::new(r"up {1,2}[(\d){1,} days]*\d{1,2}:\d\d").unwrap(); - // Can be multiple users, for double digit users, only matches the last digit. - let re_users = Regex::new(r"\d user[s]?").unwrap(); + let re = Regex::new(r"up [(\d){1,} days]*\d{1,2}:\d\d").unwrap(); + + utmp(&at.plus("test")); ts.ucmd() - .arg("validMultipleRecords.txt") + .arg("test") .succeeds() .stdout_matches(&re) - .stdout_matches(&re_users) .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.into_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 + #[derive(Serialize)] + #[repr(C)] + pub struct time_val { + pub tv_sec: i32, + pub tv_usec: i32, + } + + #[derive(Serialize)] + #[repr(C, align(4))] + pub struct exit_status { + e_termination: i16, + e_exit: i16, + } + #[derive(Serialize)] + #[repr(C, align(4))] + pub struct utmp { + pub ut_type: i32, + pub ut_pid: i32, + pub ut_line: [i8; UT_LINESIZE], + pub ut_id: [i8; 4], + + pub ut_user: [i8; UT_NAMESIZE], + #[serde(with = "BigArray")] + pub ut_host: [i8; UT_HOSTSIZE], + pub ut_exit: exit_status, + + pub ut_session: i32, + pub ut_tv: time_val, + + pub ut_addr_v6: [i32; 4], + glibc_reserved: [i8; 20], + } + + let utmp = utmp { + ut_type: BOOT_TIME as i32, + 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: exit_status { + e_termination: 0, + e_exit: 0, + }, + ut_session: 0, + ut_tv: time_val { + tv_sec: 1716371201, + tv_usec: 290913, + }, + ut_addr_v6: [127, 0, 0, 1], + glibc_reserved: [0; 20], + }; + let utmp1 = utmp { + ut_type: RUN_LVL as i32, + 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: exit_status { + e_termination: 0, + e_exit: 0, + }, + ut_session: 0, + ut_tv: time_val { + tv_sec: 1716371209, + tv_usec: 162250, + }, + ut_addr_v6: [0, 0, 0, 0], + glibc_reserved: [0; 20], + }; + let utmp2 = utmp { + ut_type: USER_PROCESS as i32, + 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: exit_status { + e_termination: 0, + e_exit: 0, + }, + ut_session: 0, + ut_tv: time_val { + tv_sec: 1716371283, + tv_usec: 858764, + }, + ut_addr_v6: [0, 0, 0, 0], + glibc_reserved: [0; 20], + }; + + let mut buf = bincode::serialize(&utmp).unwrap(); + buf.append(&mut bincode::serialize(&utmp1).unwrap()); + buf.append(&mut bincode::serialize(&utmp2).unwrap()); + let mut f = File::create(path).unwrap(); + f.write_all(&mut buf).unwrap(); + } } + #[test] #[cfg(not(target_os = "openbsd"))] fn test_uptime_with_extra_argument() { @@ -112,26 +220,6 @@ fn test_uptime_with_extra_argument() { .fails() .stderr_contains("extra operand 'b'"); } -/// Here we test if partial records are parsed properly and this may return an uptime of hours or -/// days, assuming /var/log/wtmp contains multiple records -#[test] -#[cfg(not(target_os = "openbsd"))] -fn test_uptime_with_file_containing_multiple_valid_utmpx_record_with_partial_records() { - let ts = TestScenario::new(util_name!()); - - let re_users = Regex::new(r"\d user[s]?").unwrap(); - // 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_uptime = Regex::new(r"up {1,2}[(\d){1,} days]*\d{1,2}:\d\d").unwrap(); - ts.ucmd() - .arg("validMultiplePartialRecords.txt") - .succeeds() - .stdout_contains("load average") - .stdout_matches(&re_users) - .stdout_matches(&re_uptime); -} - /// Checks whether uptime displays the correct stderr msg when its called with a directory #[test] #[cfg(not(target_os = "openbsd"))] diff --git a/tests/fixtures/uptime/validMultiplePartialRecords.txt b/tests/fixtures/uptime/validMultiplePartialRecords.txt deleted file mode 100644 index ef9ea9083c5..00000000000 Binary files a/tests/fixtures/uptime/validMultiplePartialRecords.txt and /dev/null differ diff --git a/tests/fixtures/uptime/validMultipleRecords.txt b/tests/fixtures/uptime/validMultipleRecords.txt deleted file mode 100644 index 344e4b4d033..00000000000 Binary files a/tests/fixtures/uptime/validMultipleRecords.txt and /dev/null differ diff --git a/tests/fixtures/uptime/validRecord.txt b/tests/fixtures/uptime/validRecord.txt deleted file mode 100644 index dcf63164394..00000000000 Binary files a/tests/fixtures/uptime/validRecord.txt and /dev/null differ