Skip to main content

roowho2_lib/proto/
rwhod_protocol.rs

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