1
use std::array;
2

            
3
use bytes::{Buf, BufMut, BytesMut};
4
use chrono::{DateTime, Duration, Utc};
5
use 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)]
12
pub 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

            
21
impl 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)]
31
pub struct Whoent {
32
    /// active tty info
33
    pub we_utmp: Outmp,
34
    /// tty idle time
35
    pub we_idle: i32,
36
}
37

            
38
impl Whoent {
39
    pub const SIZE: usize = std::mem::size_of::<Self>();
40

            
41
52
    fn zeroed() -> Self {
42
52
        Self {
43
52
            we_utmp: Outmp {
44
52
                out_line: [0u8; Outmp::MAX_TTY_NAME_LEN],
45
52
                out_name: [0u8; Outmp::MAX_USER_ID_LEN],
46
52
                out_time: 0,
47
52
            },
48
52
            we_idle: 0,
49
52
        }
50
52
    }
51

            
52
58
    fn is_zeroed(&self) -> bool {
53
107
        self.we_utmp.out_line.iter().all(|&b| b == 0)
54
56
            && self.we_utmp.out_name.iter().all(|&b| b == 0)
55
7
            && self.we_utmp.out_time == 0
56
7
            && self.we_idle == 0
57
58
    }
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)]
65
pub 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

            
84
impl 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
1
    pub fn new(
97
1
        sendtime: i32,
98
1
        recvtime: i32,
99
1
        hostname: [u8; Self::MAX_HOSTNAME_LEN],
100
1
        loadav: [i32; 3],
101
1
        boottime: i32,
102
1
        whoentries: [Whoent; Self::MAX_WHOENTRIES],
103
1
    ) -> Self {
104
1
        debug_assert!(
105
            whoentries
106
                .iter()
107
                .skip_while(|entry| !entry.is_zeroed())
108
                .all(|entry| entry.is_zeroed())
109
        );
110

            
111
1
        Self {
112
1
            wd_vers: Self::WHODVERSION,
113
1
            wd_type: Self::WHODTYPE_STATUS,
114
1
            wd_pad: [0u8; 2],
115
1
            wd_sendtime: sendtime,
116
1
            wd_recvtime: recvtime,
117
1
            wd_hostname: hostname,
118
1
            wd_loadav: loadav,
119
1
            wd_boottime: boottime,
120
1
            wd_we: whoentries,
121
1
        }
122
1
    }
123

            
124
1
    pub fn to_bytes(&self) -> Vec<u8> {
125
1
        let mut buf = BytesMut::with_capacity(Whod::MAX_SIZE);
126
1
        buf.put_u8(self.wd_vers);
127
1
        buf.put_u8(self.wd_type);
128
1
        buf.put_slice(&self.wd_pad);
129
1
        buf.put_i32(self.wd_sendtime);
130
1
        buf.put_i32(self.wd_recvtime);
131
1
        buf.put_slice(&self.wd_hostname);
132
1
        buf.put_i32(self.wd_loadav[0]);
133
1
        buf.put_i32(self.wd_loadav[1]);
134
1
        buf.put_i32(self.wd_loadav[2]);
135
1
        buf.put_i32(self.wd_boottime);
136

            
137
3
        for whoent in self.wd_we.iter().take_while(|entry| !entry.is_zeroed()) {
138
2
            buf.put_slice(&whoent.we_utmp.out_line);
139
2
            buf.put_slice(&whoent.we_utmp.out_name);
140
2
            buf.put_i32(whoent.we_utmp.out_time);
141
2
            buf.put_i32(whoent.we_idle);
142
2
        }
143

            
144
1
        buf.to_vec()
145
1
    }
146

            
147
6
    pub fn from_bytes(input: &[u8]) -> anyhow::Result<Self> {
148
6
        if input.len() < Self::HEADER_SIZE {
149
1
            return Err(anyhow::anyhow!(
150
1
                "Not enough bytes to parse packet header: {} < {}",
151
1
                input.len(),
152
1
                Self::HEADER_SIZE
153
1
            ));
154
5
        }
155

            
156
5
        if input.len() > Self::MAX_SIZE {
157
1
            return Err(anyhow::anyhow!(
158
1
                "Too many bytes to parse packet: {} > {}",
159
1
                input.len(),
160
1
                Self::MAX_SIZE
161
1
            ));
162
4
        }
163

            
164
4
        if !(input.len() - Self::HEADER_SIZE).is_multiple_of(Whoent::SIZE) {
165
1
            return Err(anyhow::anyhow!(
166
1
                "Invalid packet length: {} (not aligned with struct sizes, should be {} + N * {})",
167
1
                input.len(),
168
1
                Self::HEADER_SIZE,
169
1
                Whoent::SIZE,
170
1
            ));
171
3
        }
172

            
173
3
        let mut bytes = bytes::Bytes::copy_from_slice(input);
174

            
175
3
        let wd_vers = bytes.get_u8();
176
3
        if wd_vers != Self::WHODVERSION {
177
1
            return Err(anyhow::anyhow!(
178
1
                "Unsupported whod protocol version: {}",
179
1
                wd_vers
180
1
            ));
181
2
        }
182

            
183
2
        let wd_type = bytes.get_u8();
184
2
        if wd_type != Self::WHODTYPE_STATUS {
185
1
            return Err(anyhow::anyhow!("Unsupported whod packet type: {}", wd_type));
186
1
        }
187

            
188
1
        bytes.advance(2); // skip wd_pad
189

            
190
1
        let wd_sendtime = bytes.get_i32();
191
1
        let wd_recvtime = bytes.get_i32();
192
1
        let mut wd_hostname = [0u8; Self::MAX_HOSTNAME_LEN];
193
1
        bytes.copy_to_slice(&mut wd_hostname);
194
1
        let wd_loadav = [bytes.get_i32(), bytes.get_i32(), bytes.get_i32()];
195
1
        let wd_boottime = bytes.get_i32();
196

            
197
1
        debug_assert!(bytes.remaining() + Self::HEADER_SIZE == input.len());
198

            
199
42
        let mut wd_we = array::from_fn(|_| Whoent::zeroed());
200

            
201
2
        for (byte_chunk, whoent) in bytes.chunks_exact(Whoent::SIZE).zip(wd_we.iter_mut()) {
202
2
            let mut chunk_bytes = bytes::Bytes::copy_from_slice(byte_chunk);
203
2

            
204
2
            let mut out_line = [0u8; Outmp::MAX_TTY_NAME_LEN];
205
2
            chunk_bytes.copy_to_slice(&mut out_line);
206
2
            let mut out_name = [0u8; Outmp::MAX_USER_ID_LEN];
207
2
            chunk_bytes.copy_to_slice(&mut out_name);
208
2
            let out_time = chunk_bytes.get_i32();
209
2

            
210
2
            let we_utmp = Outmp {
211
2
                out_line,
212
2
                out_name,
213
2
                out_time,
214
2
            };
215
2
            let we_idle = chunk_bytes.get_i32();
216
2

            
217
2
            *whoent = Whoent { we_utmp, we_idle };
218
2
        }
219

            
220
1
        let result = Whod::new(
221
1
            wd_sendtime,
222
1
            wd_recvtime,
223
1
            wd_hostname,
224
1
            wd_loadav,
225
1
            wd_boottime,
226
1
            wd_we,
227
        );
228

            
229
1
        Ok(result)
230
6
    }
231
}
232

            
233
// ------------------------------------------------
234

            
235
/// Load average representation: (5 min, 10 min, 15 min)
236
/// All values are multiplied by 100.
237
pub 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.
244
const RWHOD_TIMESTAMP_CORRECTION_WINDOW: i64 = 0x40000000_i64;
245
const RWHOD_TIMESTAMP_WRAP_INCREMENT: i64 = 0x70000000_i64 + 0x70000000_i64 + 0x20000000_i64;
246

            
247
7
fn decode_rwhod_timestamp(raw: i32, correction: i64) -> Result<DateTime<Utc>, String> {
248
7
    DateTime::from_timestamp_secs(i64::from(raw) + correction).ok_or(format!(
249
        "Invalid timestamp: {} with correction {}",
250
        raw, correction
251
    ))
252
7
}
253

            
254
65
fn rwhod_time_correction(now: DateTime<Utc>, recvtime: i32) -> i64 {
255
65
    let delta = now.timestamp() - i64::from(recvtime);
256

            
257
65
    if delta <= RWHOD_TIMESTAMP_CORRECTION_WINDOW {
258
55
        return 0;
259
10
    }
260

            
261
10
    let wraps = (delta - RWHOD_TIMESTAMP_CORRECTION_WINDOW - 1)
262
10
        .div_euclid(RWHOD_TIMESTAMP_WRAP_INCREMENT)
263
10
        + 1;
264

            
265
10
    wraps * RWHOD_TIMESTAMP_WRAP_INCREMENT
266
65
}
267

            
268
62
fn decode_rwhod_timestamp_not_after(
269
62
    raw: i32,
270
62
    base_correction: i64,
271
62
    upper_bound: DateTime<Utc>,
272
62
) -> Result<DateTime<Utc>, String> {
273
62
    let upper_bound = upper_bound.timestamp();
274

            
275
127
    for offset in [1_i64, 0, -1] {
276
127
        let correction = base_correction + offset * RWHOD_TIMESTAMP_WRAP_INCREMENT;
277
127
        let candidate = i64::from(raw) + correction;
278
127
        if candidate <= upper_bound {
279
62
            return DateTime::from_timestamp_secs(candidate).ok_or(format!(
280
                "Invalid timestamp: {} with correction {}",
281
                raw, correction
282
            ));
283
65
        }
284
    }
285

            
286
    decode_rwhod_timestamp(raw, base_correction - RWHOD_TIMESTAMP_WRAP_INCREMENT)
287
62
}
288

            
289
55
fn decode_rwhod_timestamp_near_time(
290
55
    raw: i32,
291
55
    time: DateTime<Utc>,
292
55
) -> Result<DateTime<Utc>, String> {
293
55
    let base_correction = rwhod_time_correction(time, raw);
294
55
    let mut best_candidate = None;
295

            
296
165
    for offset in [1_i64, 0, -1] {
297
165
        let correction = base_correction + offset * RWHOD_TIMESTAMP_WRAP_INCREMENT;
298
165
        let candidate = i64::from(raw) + correction;
299
165
        let distance = (time.timestamp() - candidate).abs();
300

            
301
110
        match best_candidate {
302
110
            Some((best_distance, _, _)) if best_distance <= distance => {}
303
110
            _ => best_candidate = Some((distance, candidate, correction)),
304
        }
305
    }
306

            
307
55
    let (_, candidate, correction) = best_candidate.expect("candidate list should not be empty");
308
55
    DateTime::from_timestamp_secs(candidate).ok_or(format!(
309
        "Invalid timestamp: {} with correction {}",
310
        raw, correction
311
    ))
312
55
}
313

            
314
73
fn encode_rwhod_timestamp(timestamp: DateTime<Utc>) -> i32 {
315
73
    timestamp.timestamp() as i32
316
73
}
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)]
323
pub 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

            
344
impl WhodStatusUpdate {
345
8
    pub fn new(
346
8
        sendtime: DateTime<Utc>,
347
8
        recvtime: Option<DateTime<Utc>>,
348
8
        hostname: String,
349
8
        load_average: LoadAverage,
350
8
        boot_time: DateTime<Utc>,
351
8
        users: Vec<WhodUserEntry>,
352
8
    ) -> Self {
353
8
        Self {
354
8
            sendtime,
355
8
            recvtime,
356
8
            hostname,
357
8
            load_average,
358
8
            boot_time,
359
8
            users,
360
8
        }
361
8
    }
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)]
369
pub 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

            
383
impl WhodUserEntry {
384
62
    pub fn new(
385
62
        tty: String,
386
62
        user_id: String,
387
62
        login_time: DateTime<Utc>,
388
62
        idle_time: Duration,
389
62
    ) -> Self {
390
62
        Self {
391
62
            tty,
392
62
            user_id,
393
62
            login_time,
394
62
            idle_time,
395
62
        }
396
62
    }
397
}
398

            
399
impl TryFrom<Whoent> for WhodUserEntry {
400
    type Error = String;
401

            
402
55
    fn try_from(value: Whoent) -> Result<Self, Self::Error> {
403
55
        let tty_end = value
404
55
            .we_utmp
405
55
            .out_line
406
55
            .iter()
407
308
            .position(|&c| c == 0)
408
55
            .unwrap_or(value.we_utmp.out_line.len());
409
55
        let tty = String::from_utf8(value.we_utmp.out_line[..tty_end].to_vec())
410
55
            .map_err(|e| format!("Invalid UTF-8 in TTY name: {}", e))?;
411

            
412
54
        let user_id_end = value
413
54
            .we_utmp
414
54
            .out_name
415
54
            .iter()
416
351
            .position(|&c| c == 0)
417
54
            .unwrap_or(value.we_utmp.out_name.len());
418
54
        let user_id = String::from_utf8(value.we_utmp.out_name[..user_id_end].to_vec())
419
54
            .map_err(|e| format!("Invalid UTF-8 in user ID: {}", e))?;
420

            
421
54
        let now = Utc::now();
422
54
        let login_time = decode_rwhod_timestamp_near_time(value.we_utmp.out_time, now)?;
423

            
424
54
        Ok(WhodUserEntry {
425
54
            tty,
426
54
            user_id,
427
54
            login_time,
428
54
            idle_time: Duration::seconds(value.we_idle as i64),
429
54
        })
430
55
    }
431
}
432

            
433
impl TryFrom<Whod> for WhodStatusUpdate {
434
    type Error = String;
435

            
436
7
    fn try_from(value: Whod) -> Result<Self, Self::Error> {
437
7
        if value.wd_vers != Whod::WHODVERSION {
438
            return Err(format!(
439
                "Unsupported whod protocol version: {}",
440
                value.wd_vers
441
            ));
442
7
        }
443

            
444
7
        let now = Utc::now();
445
7
        let recvtime_correction = rwhod_time_correction(now, value.wd_recvtime);
446

            
447
7
        let recvtime = if value.wd_recvtime == 0 {
448
1
            None
449
        } else {
450
6
            Some(decode_rwhod_timestamp(
451
6
                value.wd_recvtime,
452
6
                recvtime_correction,
453
            )?)
454
        };
455

            
456
7
        let recvtime_upper_bound = recvtime.unwrap_or(now);
457

            
458
7
        let sendtime = if recvtime.is_some() {
459
6
            decode_rwhod_timestamp_not_after(
460
6
                value.wd_sendtime,
461
6
                recvtime_correction,
462
6
                recvtime_upper_bound,
463
            )?
464
        } else {
465
1
            decode_rwhod_timestamp_near_time(value.wd_sendtime, now)?
466
        };
467

            
468
7
        let hostname_end = value
469
7
            .wd_hostname
470
7
            .iter()
471
86
            .position(|&c| c == 0)
472
7
            .unwrap_or(value.wd_hostname.len());
473
7
        let hostname = String::from_utf8(value.wd_hostname[..hostname_end].to_vec())
474
7
            .map_err(|e| format!("Invalid UTF-8 in hostname: {}", e))?;
475

            
476
7
        let boot_time = if recvtime.is_some() {
477
6
            decode_rwhod_timestamp_not_after(value.wd_boottime, recvtime_correction, sendtime)?
478
        } else {
479
1
            decode_rwhod_timestamp_not_after(
480
1
                value.wd_boottime,
481
1
                rwhod_time_correction(sendtime, value.wd_boottime),
482
1
                sendtime,
483
            )?
484
        };
485

            
486
7
        let users = value
487
7
            .wd_we
488
7
            .iter()
489
55
            .take_while(|whoent| !whoent.is_zeroed())
490
49
            .map(|whoent| {
491
49
                let mut user = WhodUserEntry::try_from(whoent.clone())?;
492
49
                user.login_time = if recvtime.is_some() {
493
48
                    decode_rwhod_timestamp_not_after(
494
48
                        whoent.we_utmp.out_time,
495
48
                        recvtime_correction,
496
48
                        recvtime_upper_bound,
497
                    )?
498
                } else {
499
1
                    decode_rwhod_timestamp_not_after(
500
1
                        whoent.we_utmp.out_time,
501
1
                        rwhod_time_correction(sendtime, whoent.we_utmp.out_time),
502
1
                        sendtime,
503
                    )?
504
                };
505
49
                Ok(user)
506
49
            })
507
7
            .collect::<Result<Vec<WhodUserEntry>, String>>()?;
508

            
509
7
        Ok(WhodStatusUpdate {
510
7
            sendtime,
511
7
            recvtime,
512
7
            hostname,
513
7
            load_average: value.wd_loadav.into(),
514
7
            boot_time,
515
7
            users,
516
7
        })
517
7
    }
518
}
519

            
520
impl TryFrom<WhodUserEntry> for Whoent {
521
    type Error = String;
522

            
523
52
    fn try_from(value: WhodUserEntry) -> Result<Self, Self::Error> {
524
52
        let mut out_line = [0u8; Outmp::MAX_TTY_NAME_LEN];
525
52
        let tty_bytes = value.tty.as_bytes();
526
52
        let tty_len = tty_bytes.len().min(Outmp::MAX_TTY_NAME_LEN);
527
52
        out_line[..tty_len].copy_from_slice(&tty_bytes[..tty_len]);
528

            
529
52
        let mut out_name = [0u8; Outmp::MAX_USER_ID_LEN];
530
52
        let user_id_bytes = value.user_id.as_bytes();
531
52
        let user_id_len = user_id_bytes.len().min(Outmp::MAX_USER_ID_LEN);
532
52
        out_name[..user_id_len].copy_from_slice(&user_id_bytes[..user_id_len]);
533

            
534
52
        let out_time = encode_rwhod_timestamp(value.login_time);
535

            
536
52
        let we_idle = value
537
52
            .idle_time
538
52
            .num_seconds()
539
52
            .clamp(i32::MIN as i64, i32::MAX as i64) as i32;
540

            
541
52
        Ok(Whoent {
542
52
            we_utmp: Outmp {
543
52
                out_line,
544
52
                out_name,
545
52
                out_time,
546
52
            },
547
52
            we_idle,
548
52
        })
549
52
    }
550
}
551

            
552
impl TryFrom<WhodStatusUpdate> for Whod {
553
    type Error = String;
554

            
555
7
    fn try_from(value: WhodStatusUpdate) -> Result<Self, Self::Error> {
556
7
        let mut wd_hostname = [0u8; Whod::MAX_HOSTNAME_LEN];
557
7
        let hostname_bytes = value.hostname.as_bytes();
558
7
        let hostname_len = hostname_bytes.len().min(Whod::MAX_HOSTNAME_LEN);
559
7
        wd_hostname[..hostname_len].copy_from_slice(&hostname_bytes[..hostname_len]);
560

            
561
7
        let wd_sendtime = encode_rwhod_timestamp(value.sendtime);
562
7
        let wd_recvtime = value.recvtime.map_or(0, encode_rwhod_timestamp);
563
7
        let wd_boottime = encode_rwhod_timestamp(value.boot_time);
564

            
565
7
        let wd_we = value
566
7
            .users
567
7
            .into_iter()
568
7
            .map(Whoent::try_from)
569
7
            .chain(std::iter::repeat(Ok(Whoent::zeroed())))
570
7
            .take(Whod::MAX_WHOENTRIES)
571
7
            .collect::<Result<Vec<Whoent>, String>>()?
572
7
            .try_into()
573
7
            .expect("Length mismatch, this should never happen");
574

            
575
7
        Ok(Whod {
576
7
            wd_vers: Whod::WHODVERSION,
577
7
            wd_type: Whod::WHODTYPE_STATUS,
578
7
            wd_pad: [0u8; 2],
579
7
            wd_sendtime,
580
7
            wd_recvtime,
581
7
            wd_hostname,
582
7
            wd_loadav: value.load_average.into(),
583
7
            wd_boottime,
584
7
            wd_we,
585
7
        })
586
7
    }
587
}
588

            
589
#[cfg(test)]
590
mod tests {
591
    use super::*;
592
    use chrono::TimeZone;
593

            
594
    #[test]
595
1
    fn test_whod_serialization_roundtrip() {
596
1
        let original_status = WhodStatusUpdate::new(
597
1
            Utc.with_ymd_and_hms(2024, 6, 1, 12, 0, 0).unwrap(),
598
1
            Some(Utc.with_ymd_and_hms(2024, 6, 1, 12, 5, 0).unwrap()),
599
1
            "testhost".to_string(),
600
1
            (25, 20, 18),
601
1
            Utc.with_ymd_and_hms(2024, 5, 31, 8, 0, 0).unwrap(),
602
1
            vec![
603
1
                WhodUserEntry::new(
604
1
                    "tty1".to_string(),
605
1
                    "user1".to_string(),
606
1
                    Utc.with_ymd_and_hms(2024, 6, 1, 10, 0, 0).unwrap(),
607
1
                    Duration::minutes(5),
608
                ),
609
1
                WhodUserEntry::new(
610
1
                    "tty2".to_string(),
611
1
                    "user2".to_string(),
612
1
                    Utc.with_ymd_and_hms(2024, 6, 1, 11, 0, 0).unwrap(),
613
1
                    Duration::minutes(10),
614
                ),
615
            ],
616
        );
617

            
618
1
        let whod_struct =
619
1
            Whod::try_from(original_status.clone()).expect("Conversion to Whod failed");
620
1
        let bytes = whod_struct.to_bytes();
621
1
        let parsed_whod = Whod::from_bytes(&bytes).expect("Parsing from bytes failed");
622
1
        let final_status =
623
1
            WhodStatusUpdate::try_from(parsed_whod).expect("Conversion from Whod failed");
624

            
625
1
        assert_eq!(original_status, final_status);
626
1
    }
627

            
628
    #[test]
629
1
    fn test_parser_invalid_bytes() {
630
        // Too short
631
1
        let short_bytes = vec![0u8; Whod::HEADER_SIZE - 1];
632
1
        assert!(Whod::from_bytes(&short_bytes).is_err());
633

            
634
        // Too long
635
1
        let long_bytes = vec![0u8; Whod::MAX_SIZE + 1];
636
1
        assert!(Whod::from_bytes(&long_bytes).is_err());
637

            
638
        // Misaligned length
639
1
        let misaligned_bytes = vec![0u8; Whod::HEADER_SIZE + 1];
640
1
        assert!(Whod::from_bytes(&misaligned_bytes).is_err());
641

            
642
        // Invalid version
643
1
        let mut invalid_version_bytes = vec![0u8; Whod::HEADER_SIZE];
644
1
        invalid_version_bytes[0] = 99; // invalid version
645
1
        assert!(Whod::from_bytes(&invalid_version_bytes).is_err());
646

            
647
        // Invalid packet type
648
1
        let mut invalid_type_bytes = vec![0u8; Whod::HEADER_SIZE];
649
1
        invalid_type_bytes[0] = Whod::WHODVERSION;
650
1
        invalid_type_bytes[1] = 99; // invalid type
651
1
        assert!(Whod::from_bytes(&invalid_type_bytes).is_err());
652
1
    }
653

            
654
    #[test]
655
1
    fn test_whod_user_entry_conversion() {
656
1
        let user_entry = WhodUserEntry::new(
657
1
            "tty1".to_string(),
658
1
            "user1".to_string(),
659
1
            Utc.with_ymd_and_hms(2024, 6, 1, 10, 0, 0).unwrap(),
660
1
            Duration::minutes(5),
661
        );
662

            
663
1
        let whoent = Whoent::try_from(user_entry.clone()).expect("Conversion to Whoent failed");
664
1
        let converted_back =
665
1
            WhodUserEntry::try_from(whoent).expect("Conversion from Whoent failed");
666

            
667
1
        assert_eq!(user_entry, converted_back);
668
1
    }
669

            
670
    #[test]
671
1
    fn test_whod_status_update_conversion() {
672
1
        let status_update = WhodStatusUpdate::new(
673
1
            Utc.with_ymd_and_hms(2024, 6, 1, 12, 0, 0).unwrap(),
674
1
            Some(Utc.with_ymd_and_hms(2024, 6, 1, 12, 5, 0).unwrap()),
675
1
            "testhost".to_string(),
676
1
            (25, 20, 18),
677
1
            Utc.with_ymd_and_hms(2024, 5, 31, 8, 0, 0).unwrap(),
678
1
            vec![
679
1
                WhodUserEntry::new(
680
1
                    "tty1".to_string(),
681
1
                    "user1".to_string(),
682
1
                    Utc.with_ymd_and_hms(2024, 6, 1, 10, 0, 0).unwrap(),
683
1
                    Duration::minutes(5),
684
                ),
685
1
                WhodUserEntry::new(
686
1
                    "tty2".to_string(),
687
1
                    "user2".to_string(),
688
1
                    Utc.with_ymd_and_hms(2024, 6, 1, 11, 0, 0).unwrap(),
689
1
                    Duration::minutes(10),
690
                ),
691
            ],
692
        );
693

            
694
1
        let whod_struct = Whod::try_from(status_update.clone()).expect("Conversion to Whod failed");
695
1
        let converted_back =
696
1
            WhodStatusUpdate::try_from(whod_struct).expect("Conversion from Whod failed");
697

            
698
1
        assert_eq!(status_update, converted_back);
699
1
    }
700

            
701
    #[test]
702
1
    fn test_whod_user_entry_invalid_utf8() {
703
1
        let mut whoent = Whoent::zeroed();
704
1
        whoent.we_utmp.out_line = [0xff, 0xfe, 0xfd, 0, 0, 0, 0, 0]; // Invalid UTF-8
705
1
        whoent.we_utmp.out_name = [0xff, 0xfe, 0xfd, 0, 0, 0, 0, 0]; // Invalid UTF-8
706
1
        whoent.we_utmp.out_time = 1_700_000_000; // Some valid timestamp
707
1
        whoent.we_idle = 60; // 1 minute
708

            
709
1
        let result = WhodUserEntry::try_from(whoent);
710
1
        assert!(result.is_err());
711
1
    }
712

            
713
    #[test]
714
1
    fn test_whod_user_entry_conversion_username_gets_truncated() {
715
1
        let mut whoent = Whoent::zeroed();
716
1
        whoent.we_utmp.out_name = [b'a'; Outmp::MAX_USER_ID_LEN];
717
1
        whoent.we_utmp.out_time = 1_700_000_000;
718
1
        whoent.we_idle = 60;
719

            
720
1
        let result = WhodUserEntry::try_from(whoent);
721
1
        assert!(result.is_ok());
722
1
        assert_eq!(
723
1
            result.unwrap().user_id,
724
1
            [b'a'; Outmp::MAX_USER_ID_LEN]
725
1
                .iter()
726
8
                .map(|&c| c as char)
727
1
                .collect::<String>()
728
        );
729
1
    }
730

            
731
    #[test]
732
1
    fn test_whod_user_entry_conversion_tty_gets_truncated() {
733
1
        let mut whoent = Whoent::zeroed();
734
1
        whoent.we_utmp.out_line = [b'b'; Outmp::MAX_TTY_NAME_LEN];
735
1
        whoent.we_utmp.out_time = 1_700_000_000;
736
1
        whoent.we_idle = 60;
737

            
738
1
        let result = WhodUserEntry::try_from(whoent);
739
1
        assert!(result.is_ok());
740
1
        assert_eq!(
741
1
            result.unwrap().tty,
742
1
            [b'b'; Outmp::MAX_TTY_NAME_LEN]
743
1
                .iter()
744
8
                .map(|&c| c as char)
745
1
                .collect::<String>()
746
        );
747
1
    }
748

            
749
    #[test]
750
1
    fn test_whod_status_update_hostname_gets_truncated() {
751
1
        let long_hostname = "a".repeat(Whod::MAX_HOSTNAME_LEN + 10);
752
1
        let status_update = WhodStatusUpdate::new(
753
1
            Utc.with_ymd_and_hms(2024, 6, 1, 12, 0, 0).unwrap(),
754
1
            Some(Utc.with_ymd_and_hms(2024, 6, 1, 12, 5, 0).unwrap()),
755
1
            long_hostname.clone(),
756
1
            (25, 20, 18),
757
1
            Utc.with_ymd_and_hms(2024, 5, 31, 8, 0, 0).unwrap(),
758
1
            vec![],
759
        );
760

            
761
1
        let whod_struct = Whod::try_from(status_update.clone()).expect("Conversion to Whod failed");
762
1
        let converted_back =
763
1
            WhodStatusUpdate::try_from(whod_struct).expect("Conversion from Whod failed");
764

            
765
1
        assert_eq!(
766
            converted_back.hostname,
767
1
            long_hostname[..Whod::MAX_HOSTNAME_LEN].to_string()
768
        );
769
1
    }
770

            
771
    #[test]
772
1
    fn test_whod_status_update_users_get_truncated() {
773
1
        let users = (0..(Whod::MAX_WHOENTRIES + 10))
774
52
            .map(|i| {
775
52
                WhodUserEntry::new(
776
52
                    format!("tty{}", i),
777
52
                    format!("user{}", i),
778
52
                    Utc.with_ymd_and_hms(2024, 6, 1, 10, 0, 0).unwrap(),
779
52
                    Duration::minutes(i as i64),
780
                )
781
52
            })
782
1
            .collect::<Vec<_>>();
783

            
784
1
        let status_update = WhodStatusUpdate::new(
785
1
            Utc.with_ymd_and_hms(2024, 6, 1, 12, 0, 0).unwrap(),
786
1
            Some(Utc.with_ymd_and_hms(2024, 6, 1, 12, 5, 0).unwrap()),
787
1
            "testhost".to_string(),
788
1
            (25, 20, 18),
789
1
            Utc.with_ymd_and_hms(2024, 5, 31, 8, 0, 0).unwrap(),
790
1
            users,
791
        );
792

            
793
1
        let whod_struct = Whod::try_from(status_update.clone()).expect("Conversion to Whod failed");
794
1
        let converted_back =
795
1
            WhodStatusUpdate::try_from(whod_struct).expect("Conversion from Whod failed");
796

            
797
1
        assert_eq!(converted_back.users.len(), Whod::MAX_WHOENTRIES);
798

            
799
42
        for (i, user) in converted_back.users.iter().enumerate() {
800
42
            assert_eq!(user.tty, format!("tty{}", i));
801
42
            assert_eq!(user.user_id, format!("user{}", i));
802
        }
803
1
    }
804

            
805
    #[test]
806
1
    fn test_whod_status_update_usernames_and_ttys_get_truncated() {
807
1
        let long_tty = "a".repeat(Outmp::MAX_TTY_NAME_LEN + 10);
808
1
        let long_user_id = "b".repeat(Outmp::MAX_USER_ID_LEN + 10);
809

            
810
1
        let user_entry = WhodUserEntry::new(
811
1
            long_tty.clone(),
812
1
            long_user_id.clone(),
813
1
            Utc.with_ymd_and_hms(2024, 6, 1, 10, 0, 0).unwrap(),
814
1
            Duration::minutes(5),
815
        );
816

            
817
1
        let whoent = Whoent::try_from(user_entry.clone()).expect("Conversion to Whoent failed");
818
1
        let converted_back =
819
1
            WhodUserEntry::try_from(whoent).expect("Conversion from Whoent failed");
820

            
821
1
        assert_eq!(
822
            converted_back.tty,
823
1
            long_tty[..Outmp::MAX_TTY_NAME_LEN].to_string()
824
        );
825
1
        assert_eq!(
826
            converted_back.user_id,
827
1
            long_user_id[..Outmp::MAX_USER_ID_LEN].to_string()
828
        );
829
1
    }
830

            
831
    #[test]
832
1
    fn test_rwhod_timestamp_correction_for_received_packets() {
833
1
        let now = Utc.with_ymd_and_hms(2045, 1, 1, 0, 0, 0).unwrap();
834
1
        let corrected_recvtime = now - chrono::Duration::days(1);
835
1
        let raw_recvtime = corrected_recvtime.timestamp() as i32;
836
1
        let correction = rwhod_time_correction(now, raw_recvtime);
837

            
838
1
        assert_eq!(correction, 1i64 << 32);
839
1
        assert_eq!(
840
1
            decode_rwhod_timestamp(raw_recvtime, correction).unwrap(),
841
            corrected_recvtime
842
        );
843
1
    }
844

            
845
    #[test]
846
1
    fn test_whod_status_update_roundtrip_corrects_wrapped_timestamps() {
847
1
        let original = WhodStatusUpdate::new(
848
1
            Utc.with_ymd_and_hms(2044, 12, 31, 23, 0, 0).unwrap(),
849
1
            Some(Utc.with_ymd_and_hms(2045, 1, 1, 0, 0, 0).unwrap()),
850
1
            "testhost".to_string(),
851
1
            (25, 20, 18),
852
1
            Utc.with_ymd_and_hms(2044, 12, 30, 0, 0, 0).unwrap(),
853
1
            vec![WhodUserEntry::new(
854
1
                "tty1".to_string(),
855
1
                "user".to_string(),
856
1
                Utc.with_ymd_and_hms(2044, 12, 31, 22, 0, 0).unwrap(),
857
1
                Duration::seconds(60),
858
            )],
859
        );
860

            
861
1
        let whod = Whod::try_from(original.clone()).expect("Conversion to Whod failed");
862
1
        let converted = WhodStatusUpdate::try_from(whod).expect("Conversion from Whod failed");
863

            
864
1
        assert_eq!(converted, original);
865
1
    }
866

            
867
    #[test]
868
1
    fn test_whod_status_update_roundtrip_sendtime_before_wrap_recvtime_after_wrap() {
869
1
        let original = WhodStatusUpdate::new(
870
1
            Utc.with_ymd_and_hms(2038, 1, 19, 3, 13, 0).unwrap(),
871
1
            Some(Utc.with_ymd_and_hms(2038, 1, 19, 3, 14, 30).unwrap()),
872
1
            "testhost".to_string(),
873
1
            (25, 20, 18),
874
1
            Utc.with_ymd_and_hms(2038, 1, 18, 0, 0, 0).unwrap(),
875
1
            vec![WhodUserEntry::new(
876
1
                "tty1".to_string(),
877
1
                "user".to_string(),
878
1
                Utc.with_ymd_and_hms(2038, 1, 19, 3, 12, 0).unwrap(),
879
1
                Duration::seconds(60),
880
            )],
881
        );
882

            
883
1
        let whod = Whod::try_from(original.clone()).expect("Conversion to Whod failed");
884
1
        let converted = WhodStatusUpdate::try_from(whod).expect("Conversion from Whod failed");
885

            
886
1
        assert_eq!(converted, original);
887
1
    }
888

            
889
    #[test]
890
1
    fn test_whod_status_update_roundtrip_corrects_wrapped_timestamps_without_recvtime() {
891
1
        let original = WhodStatusUpdate::new(
892
1
            Utc.with_ymd_and_hms(2045, 1, 1, 0, 0, 0).unwrap(),
893
1
            None,
894
1
            "testhost".to_string(),
895
1
            (25, 20, 18),
896
1
            Utc.with_ymd_and_hms(2044, 12, 30, 0, 0, 0).unwrap(),
897
1
            vec![WhodUserEntry::new(
898
1
                "tty1".to_string(),
899
1
                "user".to_string(),
900
1
                Utc.with_ymd_and_hms(2044, 12, 31, 22, 0, 0).unwrap(),
901
1
                Duration::seconds(60),
902
            )],
903
        );
904

            
905
1
        let whod = Whod::try_from(original.clone()).expect("Conversion to Whod failed");
906
1
        let converted = WhodStatusUpdate::try_from(whod).expect("Conversion from Whod failed");
907

            
908
1
        assert_eq!(converted, original);
909
1
    }
910

            
911
    #[test]
912
1
    fn test_whod_user_entry_roundtrip_corrects_wrapped_timestamp() {
913
1
        let original = WhodUserEntry::new(
914
1
            "tty1".to_string(),
915
1
            "user".to_string(),
916
1
            Utc.with_ymd_and_hms(2045, 1, 1, 0, 0, 0).unwrap(),
917
1
            Duration::seconds(60),
918
        );
919

            
920
1
        let whoent = Whoent::try_from(original.clone()).expect("Conversion to Whoent failed");
921
1
        let converted_back =
922
1
            WhodUserEntry::try_from(whoent).expect("Conversion from Whoent failed");
923

            
924
1
        assert_eq!(converted_back, original);
925
1
    }
926

            
927
    #[test]
928
1
    fn test_encode_rwhod_timestamp_wraps_like_i32_cast() {
929
1
        let timestamp = Utc.with_ymd_and_hms(2045, 1, 1, 0, 0, 0).unwrap();
930
1
        assert_eq!(
931
1
            encode_rwhod_timestamp(timestamp),
932
1
            timestamp.timestamp() as i32
933
        );
934
1
    }
935
}