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#[derive(Debug, Clone, PartialEq, Eq)]
21#[repr(C)]
22pub struct Outmp {
23 pub out_line: [u8; Self::MAX_TTY_NAME_LEN],
25 pub out_name: [u8; Self::MAX_USER_ID_LEN],
27 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#[derive(Debug, Clone, PartialEq, Eq)]
40#[repr(C)]
41pub struct Whoent {
42 pub we_utmp: Outmp,
44 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#[derive(Debug, Clone, PartialEq, Eq)]
74#[repr(C)]
75pub struct Whod {
76 pub wd_vers: u8,
78 pub wd_type: u8,
80 pub wd_pad: [u8; 2],
81 pub wd_sendtime: i32,
83 pub wd_recvtime: i32,
85 pub wd_hostname: [u8; Self::MAX_HOSTNAME_LEN],
87 pub wd_loadav: [i32; 3],
89 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 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); 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
248pub type LoadAverage = (i32, i32, i32);
253
254#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
259pub struct WhodStatusUpdate {
260 #[serde(with = "chrono::serde::ts_seconds")]
263 pub sendtime: DateTime<Utc>,
264
265 #[serde(with = "chrono::serde::ts_seconds_option")]
267 pub recvtime: Option<DateTime<Utc>>,
268
269 pub hostname: String,
271
272 pub load_average: LoadAverage,
274
275 #[serde(with = "chrono::serde::ts_seconds")]
277 pub boot_time: DateTime<Utc>,
278
279 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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
308pub struct WhodUserEntry {
309 pub tty: String,
311
312 pub user_id: String,
314
315 #[serde(with = "chrono::serde::ts_seconds")]
317 pub login_time: DateTime<Utc>,
318
319 #[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 let short_bytes = vec![0u8; Whod::HEADER_SIZE - 1];
534 assert!(Whod::from_bytes(&short_bytes).is_err());
535
536 let long_bytes = vec![0u8; Whod::MAX_SIZE + 1];
538 assert!(Whod::from_bytes(&long_bytes).is_err());
539
540 let misaligned_bytes = vec![0u8; Whod::HEADER_SIZE + 1];
542 assert!(Whod::from_bytes(&misaligned_bytes).is_err());
543
544 let mut invalid_version_bytes = vec![0u8; Whod::HEADER_SIZE];
546 invalid_version_bytes[0] = 99; assert!(Whod::from_bytes(&invalid_version_bytes).is_err());
548
549 let mut invalid_type_bytes = vec![0u8; Whod::HEADER_SIZE];
551 invalid_type_bytes[0] = Whod::WHODVERSION;
552 invalid_type_bytes[1] = 99; 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]; 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);
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}