Skip to main content

roowho2_lib/proto/
rwhod_protocol.rs

1mod time_codec;
2
3use std::array;
4
5use bytes::{Buf, BufMut, BytesMut};
6use chrono::{DateTime, Duration, Utc};
7use serde::{Deserialize, Serialize};
8
9use crate::{
10    proto::rwhod_protocol::time_codec::{
11        RwhodTimestamps, decode_rwhod_timestamp_near_time, decode_rwhod_timestamps,
12        encode_rwhod_timestamp,
13    },
14    util::duration_serde,
15};
16
17/// Classic C struct for utmp data for a single user session.
18///
19/// This struct is used in the rwhod protocol by being interpreted as raw bytes to be sent over UDP.
20#[derive(Debug, Clone, PartialEq, Eq)]
21#[repr(C)]
22pub struct Outmp {
23    /// tty name
24    pub out_line: [u8; Self::MAX_TTY_NAME_LEN],
25    /// user id
26    pub out_name: [u8; Self::MAX_USER_ID_LEN],
27    /// time on
28    pub out_time: i32,
29}
30
31impl Outmp {
32    pub const MAX_TTY_NAME_LEN: usize = 8;
33    pub const MAX_USER_ID_LEN: usize = 8;
34}
35
36/// Classic C struct for a single user session.
37///
38/// This struct is used in the rwhod protocol by being interpreted as raw bytes to be sent over UDP.
39#[derive(Debug, Clone, PartialEq, Eq)]
40#[repr(C)]
41pub struct Whoent {
42    /// active tty info
43    pub we_utmp: Outmp,
44    /// tty idle time
45    pub we_idle: i32,
46}
47
48impl Whoent {
49    pub const SIZE: usize = std::mem::size_of::<Self>();
50
51    fn zeroed() -> Self {
52        Self {
53            we_utmp: Outmp {
54                out_line: [0u8; Outmp::MAX_TTY_NAME_LEN],
55                out_name: [0u8; Outmp::MAX_USER_ID_LEN],
56                out_time: 0,
57            },
58            we_idle: 0,
59        }
60    }
61
62    fn is_zeroed(&self) -> bool {
63        self.we_utmp.out_line.iter().all(|&b| b == 0)
64            && self.we_utmp.out_name.iter().all(|&b| b == 0)
65            && self.we_utmp.out_time == 0
66            && self.we_idle == 0
67    }
68}
69
70/// Classic C struct for a rwhod status update.
71///
72/// This struct is used in the rwhod protocol by being interpreted as raw bytes to be sent over UDP.
73#[derive(Debug, Clone, PartialEq, Eq)]
74#[repr(C)]
75pub struct Whod {
76    /// protocol version
77    pub wd_vers: u8,
78    /// packet type, see below
79    pub wd_type: u8,
80    pub wd_pad: [u8; 2],
81    /// time stamp by sender
82    pub wd_sendtime: i32,
83    /// time stamp applied by receiver
84    pub wd_recvtime: i32,
85    /// host's name
86    pub wd_hostname: [u8; Self::MAX_HOSTNAME_LEN],
87    /// load average as in uptime
88    pub wd_loadav: [i32; 3],
89    /// time system booted
90    pub wd_boottime: i32,
91    pub wd_we: [Whoent; Self::MAX_WHOENTRIES],
92}
93
94impl Whod {
95    pub const HEADER_SIZE: usize = 1 + 1 + 2 + 4 + 4 + Self::MAX_HOSTNAME_LEN + 4 * 3 + 4;
96    pub const MAX_SIZE: usize = std::mem::size_of::<Self>();
97
98    pub const MAX_HOSTNAME_LEN: usize = 32;
99    pub const MAX_WHOENTRIES: usize = 1024 / std::mem::size_of::<Whoent>();
100
101    pub const WHODVERSION: u8 = 1;
102
103    // NOTE: there was probably meant to be more packet types, but only status is defined.
104    pub const WHODTYPE_STATUS: u8 = 1;
105
106    pub fn new(
107        sendtime: i32,
108        recvtime: i32,
109        hostname: [u8; Self::MAX_HOSTNAME_LEN],
110        loadav: [i32; 3],
111        boottime: i32,
112        whoentries: [Whoent; Self::MAX_WHOENTRIES],
113    ) -> Self {
114        debug_assert!(
115            whoentries
116                .iter()
117                .skip_while(|entry| !entry.is_zeroed())
118                .all(|entry| entry.is_zeroed())
119        );
120
121        Self {
122            wd_vers: Self::WHODVERSION,
123            wd_type: Self::WHODTYPE_STATUS,
124            wd_pad: [0u8; 2],
125            wd_sendtime: sendtime,
126            wd_recvtime: recvtime,
127            wd_hostname: hostname,
128            wd_loadav: loadav,
129            wd_boottime: boottime,
130            wd_we: whoentries,
131        }
132    }
133
134    pub fn to_bytes(&self) -> Vec<u8> {
135        let mut buf = BytesMut::with_capacity(Whod::MAX_SIZE);
136        buf.put_u8(self.wd_vers);
137        buf.put_u8(self.wd_type);
138        buf.put_slice(&self.wd_pad);
139        buf.put_i32(self.wd_sendtime);
140        buf.put_i32(self.wd_recvtime);
141        buf.put_slice(&self.wd_hostname);
142        buf.put_i32(self.wd_loadav[0]);
143        buf.put_i32(self.wd_loadav[1]);
144        buf.put_i32(self.wd_loadav[2]);
145        buf.put_i32(self.wd_boottime);
146
147        for whoent in self.wd_we.iter().take_while(|entry| !entry.is_zeroed()) {
148            buf.put_slice(&whoent.we_utmp.out_line);
149            buf.put_slice(&whoent.we_utmp.out_name);
150            buf.put_i32(whoent.we_utmp.out_time);
151            buf.put_i32(whoent.we_idle);
152        }
153
154        buf.to_vec()
155    }
156
157    pub fn from_bytes(input: &[u8]) -> anyhow::Result<Self> {
158        if input.len() < Self::HEADER_SIZE {
159            return Err(anyhow::anyhow!(
160                "Not enough bytes to parse packet header: {} < {}",
161                input.len(),
162                Self::HEADER_SIZE
163            ));
164        }
165
166        if input.len() > Self::MAX_SIZE {
167            return Err(anyhow::anyhow!(
168                "Too many bytes to parse packet: {} > {}",
169                input.len(),
170                Self::MAX_SIZE
171            ));
172        }
173
174        if !(input.len() - Self::HEADER_SIZE).is_multiple_of(Whoent::SIZE) {
175            return Err(anyhow::anyhow!(
176                "Invalid packet length: {} (not aligned with struct sizes, should be {} + N * {})",
177                input.len(),
178                Self::HEADER_SIZE,
179                Whoent::SIZE,
180            ));
181        }
182
183        let mut bytes = bytes::Bytes::copy_from_slice(input);
184
185        let wd_vers = bytes.get_u8();
186        if wd_vers != Self::WHODVERSION {
187            return Err(anyhow::anyhow!(
188                "Unsupported whod protocol version: {}",
189                wd_vers
190            ));
191        }
192
193        let wd_type = bytes.get_u8();
194        if wd_type != Self::WHODTYPE_STATUS {
195            return Err(anyhow::anyhow!("Unsupported whod packet type: {}", wd_type));
196        }
197
198        bytes.advance(2); // skip wd_pad
199
200        let wd_sendtime = bytes.get_i32();
201        let wd_recvtime = bytes.get_i32();
202        let mut wd_hostname = [0u8; Self::MAX_HOSTNAME_LEN];
203        bytes.copy_to_slice(&mut wd_hostname);
204        let wd_loadav = [bytes.get_i32(), bytes.get_i32(), bytes.get_i32()];
205        let wd_boottime = bytes.get_i32();
206
207        debug_assert!(bytes.remaining() + Self::HEADER_SIZE == input.len());
208
209        let mut wd_we = array::from_fn(|_| Whoent::zeroed());
210
211        for (byte_chunk, whoent) in bytes
212            .as_chunks::<{ Whoent::SIZE }>()
213            .0
214            .iter()
215            .zip(wd_we.iter_mut())
216        {
217            let mut chunk_bytes = bytes::Bytes::copy_from_slice(byte_chunk);
218
219            let mut out_line = [0u8; Outmp::MAX_TTY_NAME_LEN];
220            chunk_bytes.copy_to_slice(&mut out_line);
221            let mut out_name = [0u8; Outmp::MAX_USER_ID_LEN];
222            chunk_bytes.copy_to_slice(&mut out_name);
223            let out_time = chunk_bytes.get_i32();
224
225            let we_utmp = Outmp {
226                out_line,
227                out_name,
228                out_time,
229            };
230            let we_idle = chunk_bytes.get_i32();
231
232            *whoent = Whoent { we_utmp, we_idle };
233        }
234
235        let result = Whod::new(
236            wd_sendtime,
237            wd_recvtime,
238            wd_hostname,
239            wd_loadav,
240            wd_boottime,
241            wd_we,
242        );
243
244        Ok(result)
245    }
246}
247
248// ------------------------------------------------
249
250/// Load average representation: (5 min, 10 min, 15 min)
251/// All values are multiplied by 100.
252pub type LoadAverage = (i32, i32, i32);
253
254/// High-level representation of a rwhod status update.
255///
256/// This struct is intended for easier use in Rust code, with proper types and dynamic arrays.
257/// It can be converted to and from the low-level [`Whod`] struct used for network transmission.
258#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
259pub struct WhodStatusUpdate {
260    // NOTE: there is only one defined packet type, so we just omit it here
261    /// Timestamp by sender
262    #[serde(with = "chrono::serde::ts_seconds")]
263    pub sendtime: DateTime<Utc>,
264
265    /// Timestamp applied by receiver
266    #[serde(with = "chrono::serde::ts_seconds_option")]
267    pub recvtime: Option<DateTime<Utc>>,
268
269    /// Name of the host sending the status update (max 32 characters)
270    pub hostname: String,
271
272    /// load average over 5, 10, and 15 minutes multiplied by 100
273    pub load_average: LoadAverage,
274
275    /// Which time the system was booted
276    #[serde(with = "chrono::serde::ts_seconds")]
277    pub boot_time: DateTime<Utc>,
278
279    /// List of users currently logged in to the host (max 42 entries)
280    pub users: Vec<WhodUserEntry>,
281}
282
283impl WhodStatusUpdate {
284    pub fn new(
285        sendtime: DateTime<Utc>,
286        recvtime: Option<DateTime<Utc>>,
287        hostname: String,
288        load_average: LoadAverage,
289        boot_time: DateTime<Utc>,
290        users: Vec<WhodUserEntry>,
291    ) -> Self {
292        Self {
293            sendtime,
294            recvtime,
295            hostname,
296            load_average,
297            boot_time,
298            users,
299        }
300    }
301}
302
303/// High-level representation of a single user session in a rwhod status update.
304///
305/// This struct is intended for easier use in Rust code, with proper types.
306/// It can be converted to and from the low-level [`Whoent`] struct used for network transmission.
307#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
308pub struct WhodUserEntry {
309    /// TTY name (max 8 characters)
310    pub tty: String,
311
312    /// User ID (max 8 characters)
313    pub user_id: String,
314
315    /// Time when the user logged in
316    #[serde(with = "chrono::serde::ts_seconds")]
317    pub login_time: DateTime<Utc>,
318
319    /// How long since the user last typed on the TTY
320    #[serde(with = "duration_serde::duration_seconds")]
321    pub idle_time: Duration,
322}
323
324impl WhodUserEntry {
325    pub fn new(
326        tty: String,
327        user_id: String,
328        login_time: DateTime<Utc>,
329        idle_time: Duration,
330    ) -> Self {
331        Self {
332            tty,
333            user_id,
334            login_time,
335            idle_time,
336        }
337    }
338}
339
340impl TryFrom<Whoent> for WhodUserEntry {
341    type Error = String;
342
343    fn try_from(value: Whoent) -> Result<Self, Self::Error> {
344        let tty_end = value
345            .we_utmp
346            .out_line
347            .iter()
348            .position(|&c| c == 0)
349            .unwrap_or(value.we_utmp.out_line.len());
350        let tty = String::from_utf8(value.we_utmp.out_line[..tty_end].to_vec())
351            .map_err(|e| format!("Invalid UTF-8 in TTY name: {}", e))?;
352
353        let user_id_end = value
354            .we_utmp
355            .out_name
356            .iter()
357            .position(|&c| c == 0)
358            .unwrap_or(value.we_utmp.out_name.len());
359        let user_id = String::from_utf8(value.we_utmp.out_name[..user_id_end].to_vec())
360            .map_err(|e| format!("Invalid UTF-8 in user ID: {}", e))?;
361
362        let now = Utc::now();
363        let login_time = decode_rwhod_timestamp_near_time(value.we_utmp.out_time, now)?;
364
365        Ok(WhodUserEntry {
366            tty,
367            user_id,
368            login_time,
369            idle_time: Duration::seconds(value.we_idle as i64),
370        })
371    }
372}
373
374impl TryFrom<Whod> for WhodStatusUpdate {
375    type Error = String;
376
377    fn try_from(value: Whod) -> Result<Self, Self::Error> {
378        if value.wd_vers != Whod::WHODVERSION {
379            return Err(format!(
380                "Unsupported whod protocol version: {}",
381                value.wd_vers
382            ));
383        }
384
385        let now = Utc::now();
386        let RwhodTimestamps {
387            boottime,
388            sendtime,
389            recvtime,
390        } = decode_rwhod_timestamps(value.wd_boottime, value.wd_sendtime, value.wd_recvtime, now)?;
391
392        let hostname_end = value
393            .wd_hostname
394            .iter()
395            .position(|&c| c == 0)
396            .unwrap_or(value.wd_hostname.len());
397        let hostname = String::from_utf8(value.wd_hostname[..hostname_end].to_vec())
398            .map_err(|e| format!("Invalid UTF-8 in hostname: {}", e))?;
399
400        let users = value
401            .wd_we
402            .iter()
403            .take_while(|whoent| !whoent.is_zeroed())
404            .map(|whoent| {
405                let mut user = WhodUserEntry::try_from(whoent.clone())?;
406                user.login_time = decode_rwhod_timestamp_near_time(whoent.we_utmp.out_time, now)?;
407                Ok(user)
408            })
409            .collect::<Result<Vec<WhodUserEntry>, String>>()?;
410
411        Ok(WhodStatusUpdate {
412            sendtime,
413            recvtime,
414            hostname,
415            load_average: value.wd_loadav.into(),
416            boot_time: boottime,
417            users,
418        })
419    }
420}
421
422impl TryFrom<WhodUserEntry> for Whoent {
423    type Error = String;
424
425    fn try_from(value: WhodUserEntry) -> Result<Self, Self::Error> {
426        let mut out_line = [0u8; Outmp::MAX_TTY_NAME_LEN];
427        let tty_bytes = value.tty.as_bytes();
428        let tty_len = tty_bytes.len().min(Outmp::MAX_TTY_NAME_LEN);
429        out_line[..tty_len].copy_from_slice(&tty_bytes[..tty_len]);
430
431        let mut out_name = [0u8; Outmp::MAX_USER_ID_LEN];
432        let user_id_bytes = value.user_id.as_bytes();
433        let user_id_len = user_id_bytes.len().min(Outmp::MAX_USER_ID_LEN);
434        out_name[..user_id_len].copy_from_slice(&user_id_bytes[..user_id_len]);
435
436        let out_time = encode_rwhod_timestamp(value.login_time);
437
438        let we_idle = value
439            .idle_time
440            .num_seconds()
441            .clamp(i32::MIN as i64, i32::MAX as i64) as i32;
442
443        Ok(Whoent {
444            we_utmp: Outmp {
445                out_line,
446                out_name,
447                out_time,
448            },
449            we_idle,
450        })
451    }
452}
453
454impl TryFrom<WhodStatusUpdate> for Whod {
455    type Error = String;
456
457    fn try_from(value: WhodStatusUpdate) -> Result<Self, Self::Error> {
458        let mut wd_hostname = [0u8; Whod::MAX_HOSTNAME_LEN];
459        let hostname_bytes = value.hostname.as_bytes();
460        let hostname_len = hostname_bytes.len().min(Whod::MAX_HOSTNAME_LEN);
461        wd_hostname[..hostname_len].copy_from_slice(&hostname_bytes[..hostname_len]);
462
463        let wd_sendtime = encode_rwhod_timestamp(value.sendtime);
464        let wd_recvtime = value.recvtime.map_or(0, encode_rwhod_timestamp);
465        let wd_boottime = encode_rwhod_timestamp(value.boot_time);
466
467        let wd_we = value
468            .users
469            .into_iter()
470            .map(Whoent::try_from)
471            .chain(std::iter::repeat(Ok(Whoent::zeroed())))
472            .take(Whod::MAX_WHOENTRIES)
473            .collect::<Result<Vec<Whoent>, String>>()?
474            .try_into()
475            .expect("Length mismatch, this should never happen");
476
477        Ok(Whod {
478            wd_vers: Whod::WHODVERSION,
479            wd_type: Whod::WHODTYPE_STATUS,
480            wd_pad: [0u8; 2],
481            wd_sendtime,
482            wd_recvtime,
483            wd_hostname,
484            wd_loadav: value.load_average.into(),
485            wd_boottime,
486            wd_we,
487        })
488    }
489}
490
491#[cfg(test)]
492mod tests {
493    use super::*;
494    use chrono::TimeZone;
495
496    #[test]
497    fn test_whod_serialization_roundtrip() {
498        let original_status = WhodStatusUpdate::new(
499            Utc.with_ymd_and_hms(2024, 6, 1, 12, 0, 0).unwrap(),
500            Some(Utc.with_ymd_and_hms(2024, 6, 1, 12, 5, 0).unwrap()),
501            "testhost".to_string(),
502            (25, 20, 18),
503            Utc.with_ymd_and_hms(2024, 5, 31, 8, 0, 0).unwrap(),
504            vec![
505                WhodUserEntry::new(
506                    "tty1".to_string(),
507                    "user1".to_string(),
508                    Utc.with_ymd_and_hms(2024, 6, 1, 10, 0, 0).unwrap(),
509                    Duration::minutes(5),
510                ),
511                WhodUserEntry::new(
512                    "tty2".to_string(),
513                    "user2".to_string(),
514                    Utc.with_ymd_and_hms(2024, 6, 1, 11, 0, 0).unwrap(),
515                    Duration::minutes(10),
516                ),
517            ],
518        );
519
520        let whod_struct =
521            Whod::try_from(original_status.clone()).expect("Conversion to Whod failed");
522        let bytes = whod_struct.to_bytes();
523        let parsed_whod = Whod::from_bytes(&bytes).expect("Parsing from bytes failed");
524        let final_status =
525            WhodStatusUpdate::try_from(parsed_whod).expect("Conversion from Whod failed");
526
527        assert_eq!(original_status, final_status);
528    }
529
530    #[test]
531    fn test_parser_invalid_bytes() {
532        // Too short
533        let short_bytes = vec![0u8; Whod::HEADER_SIZE - 1];
534        assert!(Whod::from_bytes(&short_bytes).is_err());
535
536        // Too long
537        let long_bytes = vec![0u8; Whod::MAX_SIZE + 1];
538        assert!(Whod::from_bytes(&long_bytes).is_err());
539
540        // Misaligned length
541        let misaligned_bytes = vec![0u8; Whod::HEADER_SIZE + 1];
542        assert!(Whod::from_bytes(&misaligned_bytes).is_err());
543
544        // Invalid version
545        let mut invalid_version_bytes = vec![0u8; Whod::HEADER_SIZE];
546        invalid_version_bytes[0] = 99; // invalid version
547        assert!(Whod::from_bytes(&invalid_version_bytes).is_err());
548
549        // Invalid packet type
550        let mut invalid_type_bytes = vec![0u8; Whod::HEADER_SIZE];
551        invalid_type_bytes[0] = Whod::WHODVERSION;
552        invalid_type_bytes[1] = 99; // invalid type
553        assert!(Whod::from_bytes(&invalid_type_bytes).is_err());
554    }
555
556    #[test]
557    fn test_whod_user_entry_conversion() {
558        let user_entry = WhodUserEntry::new(
559            "tty1".to_string(),
560            "user1".to_string(),
561            Utc.with_ymd_and_hms(2024, 6, 1, 10, 0, 0).unwrap(),
562            Duration::minutes(5),
563        );
564
565        let whoent = Whoent::try_from(user_entry.clone()).expect("Conversion to Whoent failed");
566        let converted_back =
567            WhodUserEntry::try_from(whoent).expect("Conversion from Whoent failed");
568
569        assert_eq!(user_entry, converted_back);
570    }
571
572    #[test]
573    fn test_whod_status_update_conversion() {
574        let status_update = WhodStatusUpdate::new(
575            Utc.with_ymd_and_hms(2024, 6, 1, 12, 0, 0).unwrap(),
576            Some(Utc.with_ymd_and_hms(2024, 6, 1, 12, 5, 0).unwrap()),
577            "testhost".to_string(),
578            (25, 20, 18),
579            Utc.with_ymd_and_hms(2024, 5, 31, 8, 0, 0).unwrap(),
580            vec![
581                WhodUserEntry::new(
582                    "tty1".to_string(),
583                    "user1".to_string(),
584                    Utc.with_ymd_and_hms(2024, 6, 1, 10, 0, 0).unwrap(),
585                    Duration::minutes(5),
586                ),
587                WhodUserEntry::new(
588                    "tty2".to_string(),
589                    "user2".to_string(),
590                    Utc.with_ymd_and_hms(2024, 6, 1, 11, 0, 0).unwrap(),
591                    Duration::minutes(10),
592                ),
593            ],
594        );
595
596        let whod_struct = Whod::try_from(status_update.clone()).expect("Conversion to Whod failed");
597        let converted_back =
598            WhodStatusUpdate::try_from(whod_struct).expect("Conversion from Whod failed");
599
600        assert_eq!(status_update, converted_back);
601    }
602
603    #[test]
604    fn test_whod_user_entry_invalid_utf8() {
605        let mut whoent = Whoent::zeroed();
606        whoent.we_utmp.out_line = [0xff, 0xfe, 0xfd, 0, 0, 0, 0, 0]; // Invalid UTF-8
607        whoent.we_utmp.out_name = [0xff, 0xfe, 0xfd, 0, 0, 0, 0, 0]; // Invalid UTF-8
608        whoent.we_utmp.out_time = 1_700_000_000; // Some valid timestamp
609        whoent.we_idle = 60; // 1 minute
610
611        let result = WhodUserEntry::try_from(whoent);
612        assert!(result.is_err());
613    }
614
615    #[test]
616    fn test_whod_user_entry_conversion_username_gets_truncated() {
617        let mut whoent = Whoent::zeroed();
618        whoent.we_utmp.out_name = [b'a'; Outmp::MAX_USER_ID_LEN];
619        whoent.we_utmp.out_time = 1_700_000_000;
620        whoent.we_idle = 60;
621
622        let result = WhodUserEntry::try_from(whoent);
623        assert!(result.is_ok());
624        assert_eq!(
625            result.unwrap().user_id,
626            [b'a'; Outmp::MAX_USER_ID_LEN]
627                .iter()
628                .map(|&c| c as char)
629                .collect::<String>()
630        );
631    }
632
633    #[test]
634    fn test_whod_user_entry_conversion_tty_gets_truncated() {
635        let mut whoent = Whoent::zeroed();
636        whoent.we_utmp.out_line = [b'b'; Outmp::MAX_TTY_NAME_LEN];
637        whoent.we_utmp.out_time = 1_700_000_000;
638        whoent.we_idle = 60;
639
640        let result = WhodUserEntry::try_from(whoent);
641        assert!(result.is_ok());
642        assert_eq!(
643            result.unwrap().tty,
644            [b'b'; Outmp::MAX_TTY_NAME_LEN]
645                .iter()
646                .map(|&c| c as char)
647                .collect::<String>()
648        );
649    }
650
651    #[test]
652    fn test_whod_status_update_hostname_gets_truncated() {
653        let long_hostname = "a".repeat(Whod::MAX_HOSTNAME_LEN + 10);
654        let status_update = WhodStatusUpdate::new(
655            Utc.with_ymd_and_hms(2024, 6, 1, 12, 0, 0).unwrap(),
656            Some(Utc.with_ymd_and_hms(2024, 6, 1, 12, 5, 0).unwrap()),
657            long_hostname.clone(),
658            (25, 20, 18),
659            Utc.with_ymd_and_hms(2024, 5, 31, 8, 0, 0).unwrap(),
660            vec![],
661        );
662
663        let whod_struct = Whod::try_from(status_update.clone()).expect("Conversion to Whod failed");
664        let converted_back =
665            WhodStatusUpdate::try_from(whod_struct).expect("Conversion from Whod failed");
666
667        assert_eq!(
668            converted_back.hostname,
669            long_hostname[..Whod::MAX_HOSTNAME_LEN].to_string()
670        );
671    }
672
673    #[test]
674    fn test_whod_status_update_users_get_truncated() {
675        let users = (0..(Whod::MAX_WHOENTRIES + 10))
676            .map(|i| {
677                WhodUserEntry::new(
678                    format!("tty{}", i),
679                    format!("user{}", i),
680                    Utc.with_ymd_and_hms(2024, 6, 1, 10, 0, 0).unwrap(),
681                    Duration::minutes(i as i64),
682                )
683            })
684            .collect::<Vec<_>>();
685
686        let status_update = WhodStatusUpdate::new(
687            Utc.with_ymd_and_hms(2024, 6, 1, 12, 0, 0).unwrap(),
688            Some(Utc.with_ymd_and_hms(2024, 6, 1, 12, 5, 0).unwrap()),
689            "testhost".to_string(),
690            (25, 20, 18),
691            Utc.with_ymd_and_hms(2024, 5, 31, 8, 0, 0).unwrap(),
692            users,
693        );
694
695        let whod_struct = Whod::try_from(status_update.clone()).expect("Conversion to Whod failed");
696        let converted_back =
697            WhodStatusUpdate::try_from(whod_struct).expect("Conversion from Whod failed");
698
699        assert_eq!(converted_back.users.len(), Whod::MAX_WHOENTRIES);
700
701        for (i, user) in converted_back.users.iter().enumerate() {
702            assert_eq!(user.tty, format!("tty{}", i));
703            assert_eq!(user.user_id, format!("user{}", i));
704        }
705    }
706
707    #[test]
708    fn test_whod_status_update_usernames_and_ttys_get_truncated() {
709        let long_tty = "a".repeat(Outmp::MAX_TTY_NAME_LEN + 10);
710        let long_user_id = "b".repeat(Outmp::MAX_USER_ID_LEN + 10);
711
712        let user_entry = WhodUserEntry::new(
713            long_tty.clone(),
714            long_user_id.clone(),
715            Utc.with_ymd_and_hms(2024, 6, 1, 10, 0, 0).unwrap(),
716            Duration::minutes(5),
717        );
718
719        let whoent = Whoent::try_from(user_entry.clone()).expect("Conversion to Whoent failed");
720        let converted_back =
721            WhodUserEntry::try_from(whoent).expect("Conversion from Whoent failed");
722
723        assert_eq!(
724            converted_back.tty,
725            long_tty[..Outmp::MAX_TTY_NAME_LEN].to_string()
726        );
727        assert_eq!(
728            converted_back.user_id,
729            long_user_id[..Outmp::MAX_USER_ID_LEN].to_string()
730        );
731    }
732
733    fn wrap_epoch(offset: i64) -> DateTime<Utc> {
734        DateTime::from_timestamp((i32::MAX as i64 + 1) + offset, 0).unwrap()
735    }
736
737    #[test]
738    fn test_whod_status_update_roundtrip_corrects_wrapped_timestamps() {
739        let recvtime = wrap_epoch(60 * 60);
740        let sendtime = recvtime - Duration::minutes(5);
741        let boot_time = recvtime - Duration::days(2);
742        let login_time = recvtime - Duration::hours(2);
743
744        let original = WhodStatusUpdate::new(
745            sendtime,
746            Some(recvtime),
747            "testhost".to_string(),
748            (25, 20, 18),
749            boot_time,
750            vec![WhodUserEntry::new(
751                "tty1".to_string(),
752                "user".to_string(),
753                login_time,
754                Duration::seconds(60),
755            )],
756        );
757
758        let whod = Whod::try_from(original.clone()).expect("Conversion to Whod failed");
759        let raw_boot_time = whod.wd_boottime;
760        let raw_login_time = whod.wd_we[0].we_utmp.out_time;
761        let converted = WhodStatusUpdate::try_from(whod).expect("Conversion from Whod failed");
762
763        assert_eq!(converted.sendtime, original.sendtime);
764        assert_eq!(converted.recvtime, original.recvtime);
765        assert_eq!(converted.hostname, original.hostname);
766        assert_eq!(converted.load_average, original.load_average);
767        assert_eq!(
768            converted.boot_time,
769            decode_rwhod_timestamp_near_time(raw_boot_time, recvtime).unwrap()
770        );
771        assert_eq!(converted.users.len(), 1);
772        assert_eq!(converted.users[0].tty, original.users[0].tty);
773        assert_eq!(converted.users[0].user_id, original.users[0].user_id);
774        assert_eq!(converted.users[0].idle_time, original.users[0].idle_time);
775        assert_eq!(
776            converted.users[0].login_time,
777            decode_rwhod_timestamp_near_time(raw_login_time, recvtime).unwrap()
778        );
779    }
780
781    #[test]
782    fn test_whod_status_update_roundtrip_sendtime_before_wrap_recvtime_after_wrap() {
783        let recvtime = wrap_epoch(60);
784        let sendtime = DateTime::from_timestamp(i32::MAX as i64 - 29, 0).unwrap();
785        let boot_time = sendtime - Duration::days(1);
786        let login_time = sendtime - Duration::minutes(1);
787
788        let original = WhodStatusUpdate::new(
789            sendtime,
790            Some(recvtime),
791            "testhost".to_string(),
792            (25, 20, 18),
793            boot_time,
794            vec![WhodUserEntry::new(
795                "tty1".to_string(),
796                "user".to_string(),
797                login_time,
798                Duration::seconds(60),
799            )],
800        );
801
802        let whod = Whod::try_from(original.clone()).expect("Conversion to Whod failed");
803        let raw_send_time = whod.wd_sendtime;
804        let raw_boot_time = whod.wd_boottime;
805        let raw_login_time = whod.wd_we[0].we_utmp.out_time;
806        let converted = WhodStatusUpdate::try_from(whod).expect("Conversion from Whod failed");
807
808        assert_eq!(converted.recvtime, original.recvtime);
809        assert_eq!(converted.hostname, original.hostname);
810        assert_eq!(converted.load_average, original.load_average);
811        assert_eq!(
812            converted.sendtime,
813            decode_rwhod_timestamp_near_time(raw_send_time, recvtime).unwrap()
814        );
815        assert_eq!(
816            converted.boot_time,
817            decode_rwhod_timestamp_near_time(raw_boot_time, recvtime).unwrap()
818        );
819        assert_eq!(converted.users.len(), 1);
820        assert_eq!(converted.users[0].tty, original.users[0].tty);
821        assert_eq!(converted.users[0].user_id, original.users[0].user_id);
822        assert_eq!(converted.users[0].idle_time, original.users[0].idle_time);
823        assert_eq!(
824            converted.users[0].login_time,
825            decode_rwhod_timestamp_near_time(raw_login_time, recvtime).unwrap()
826        );
827    }
828
829    #[test]
830    fn test_whod_status_update_roundtrip_corrects_wrapped_timestamps_without_recvtime() {
831        let sendtime = wrap_epoch(60 * 60);
832        let boot_time = sendtime - Duration::days(2);
833        let login_time = sendtime - Duration::hours(2);
834
835        let original = WhodStatusUpdate::new(
836            sendtime,
837            None,
838            "testhost".to_string(),
839            (25, 20, 18),
840            boot_time,
841            vec![WhodUserEntry::new(
842                "tty1".to_string(),
843                "user".to_string(),
844                login_time,
845                Duration::seconds(60),
846            )],
847        );
848
849        let whod = Whod::try_from(original.clone()).expect("Conversion to Whod failed");
850        let raw_boot_time = whod.wd_boottime;
851        let raw_login_time = whod.wd_we[0].we_utmp.out_time;
852        let converted = WhodStatusUpdate::try_from(whod).expect("Conversion from Whod failed");
853
854        assert_eq!(converted.sendtime, original.sendtime);
855        assert_eq!(converted.recvtime, None);
856        assert_eq!(converted.hostname, original.hostname);
857        assert_eq!(converted.load_average, original.load_average);
858        assert_eq!(
859            converted.boot_time,
860            decode_rwhod_timestamp_near_time(raw_boot_time, sendtime).unwrap()
861        );
862        assert_eq!(converted.users.len(), 1);
863        assert_eq!(converted.users[0].tty, original.users[0].tty);
864        assert_eq!(converted.users[0].user_id, original.users[0].user_id);
865        assert_eq!(converted.users[0].idle_time, original.users[0].idle_time);
866        assert_eq!(
867            converted.users[0].login_time,
868            decode_rwhod_timestamp_near_time(raw_login_time, sendtime).unwrap()
869        );
870    }
871
872    #[test]
873    fn test_whod_user_entry_roundtrip_corrects_wrapped_timestamp() {
874        let original = WhodUserEntry::new(
875            "tty1".to_string(),
876            "user".to_string(),
877            wrap_epoch(60 * 60),
878            Duration::seconds(60),
879        );
880
881        let whoent = Whoent::try_from(original.clone()).expect("Conversion to Whoent failed");
882        let converted_back =
883            WhodUserEntry::try_from(whoent.clone()).expect("Conversion from Whoent failed");
884
885        assert_eq!(converted_back.tty, original.tty);
886        assert_eq!(converted_back.user_id, original.user_id);
887        assert_eq!(converted_back.idle_time, original.idle_time);
888        assert_eq!(
889            converted_back.login_time,
890            decode_rwhod_timestamp_near_time(whoent.we_utmp.out_time, original.login_time).unwrap()
891        );
892    }
893}