1use std::array;
2
3use bytes::{Buf, BufMut, BytesMut};
4use chrono::{DateTime, Duration, Utc};
5use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Clone, PartialEq, Eq)]
11#[repr(C)]
12pub struct Outmp {
13 pub out_line: [u8; Self::MAX_TTY_NAME_LEN],
15 pub out_name: [u8; Self::MAX_USER_ID_LEN],
17 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#[derive(Debug, Clone, PartialEq, Eq)]
30#[repr(C)]
31pub struct Whoent {
32 pub we_utmp: Outmp,
34 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#[derive(Debug, Clone, PartialEq, Eq)]
64#[repr(C)]
65pub struct Whod {
66 pub wd_vers: u8,
68 pub wd_type: u8,
70 pub wd_pad: [u8; 2],
71 pub wd_sendtime: i32,
73 pub wd_recvtime: i32,
75 pub wd_hostname: [u8; Self::MAX_HOSTNAME_LEN],
77 pub wd_loadav: [i32; 3],
79 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 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); 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
233pub type LoadAverage = (i32, i32, i32);
238
239const 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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
323pub struct WhodStatusUpdate {
324 pub sendtime: DateTime<Utc>,
327
328 pub recvtime: Option<DateTime<Utc>>,
330
331 pub hostname: String,
333
334 pub load_average: LoadAverage,
336
337 pub boot_time: DateTime<Utc>,
339
340 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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
369pub struct WhodUserEntry {
370 pub tty: String,
372
373 pub user_id: String,
375
376 pub login_time: DateTime<Utc>,
378
379 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 let short_bytes = vec![0u8; Whod::HEADER_SIZE - 1];
632 assert!(Whod::from_bytes(&short_bytes).is_err());
633
634 let long_bytes = vec![0u8; Whod::MAX_SIZE + 1];
636 assert!(Whod::from_bytes(&long_bytes).is_err());
637
638 let misaligned_bytes = vec![0u8; Whod::HEADER_SIZE + 1];
640 assert!(Whod::from_bytes(&misaligned_bytes).is_err());
641
642 let mut invalid_version_bytes = vec![0u8; Whod::HEADER_SIZE];
644 invalid_version_bytes[0] = 99; assert!(Whod::from_bytes(&invalid_version_bytes).is_err());
646
647 let mut invalid_type_bytes = vec![0u8; Whod::HEADER_SIZE];
649 invalid_type_bytes[0] = Whod::WHODVERSION;
650 invalid_type_bytes[1] = 99; 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]; whoent.we_utmp.out_name = [0xff, 0xfe, 0xfd, 0, 0, 0, 0, 0]; whoent.we_utmp.out_time = 1_700_000_000; whoent.we_idle = 60; 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}