1
mod time_codec;
2

            
3
use std::array;
4

            
5
use bytes::{Buf, BufMut, BytesMut};
6
use chrono::{DateTime, Duration, Utc};
7
use serde::{Deserialize, Serialize};
8

            
9
use crate::{
10
    proto::rwhod_protocol::time_codec::{
11
        RwhodTimestamps, decode_rwhod_timestamp_near_time, decode_rwhod_timestamps,
12
        encode_rwhod_timestamp,
13
    },
14
    util::duration_serde,
15
};
16

            
17
/// Classic C struct for utmp data for a single user session.
18
///
19
/// This struct is used in the rwhod protocol by being interpreted as raw bytes to be sent over UDP.
20
#[derive(Debug, Clone, PartialEq, Eq)]
21
#[repr(C)]
22
pub struct Outmp {
23
    /// tty name
24
    pub out_line: [u8; Self::MAX_TTY_NAME_LEN],
25
    /// user id
26
    pub out_name: [u8; Self::MAX_USER_ID_LEN],
27
    /// time on
28
    pub out_time: i32,
29
}
30

            
31
impl Outmp {
32
    pub const MAX_TTY_NAME_LEN: usize = 8;
33
    pub const MAX_USER_ID_LEN: usize = 8;
34
}
35

            
36
/// Classic C struct for a single user session.
37
///
38
/// This struct is used in the rwhod protocol by being interpreted as raw bytes to be sent over UDP.
39
#[derive(Debug, Clone, PartialEq, Eq)]
40
#[repr(C)]
41
pub struct Whoent {
42
    /// active tty info
43
    pub we_utmp: Outmp,
44
    /// tty idle time
45
    pub we_idle: i32,
46
}
47

            
48
impl Whoent {
49
    pub const SIZE: usize = std::mem::size_of::<Self>();
50

            
51
52
    fn zeroed() -> Self {
52
52
        Self {
53
52
            we_utmp: Outmp {
54
52
                out_line: [0u8; Outmp::MAX_TTY_NAME_LEN],
55
52
                out_name: [0u8; Outmp::MAX_USER_ID_LEN],
56
52
                out_time: 0,
57
52
            },
58
52
            we_idle: 0,
59
52
        }
60
52
    }
61

            
62
58
    fn is_zeroed(&self) -> bool {
63
107
        self.we_utmp.out_line.iter().all(|&b| b == 0)
64
56
            && self.we_utmp.out_name.iter().all(|&b| b == 0)
65
7
            && self.we_utmp.out_time == 0
66
7
            && self.we_idle == 0
67
58
    }
68
}
69

            
70
/// Classic C struct for a rwhod status update.
71
///
72
/// This struct is used in the rwhod protocol by being interpreted as raw bytes to be sent over UDP.
73
#[derive(Debug, Clone, PartialEq, Eq)]
74
#[repr(C)]
75
pub struct Whod {
76
    /// protocol version
77
    pub wd_vers: u8,
78
    /// packet type, see below
79
    pub wd_type: u8,
80
    pub wd_pad: [u8; 2],
81
    /// time stamp by sender
82
    pub wd_sendtime: i32,
83
    /// time stamp applied by receiver
84
    pub wd_recvtime: i32,
85
    /// host's name
86
    pub wd_hostname: [u8; Self::MAX_HOSTNAME_LEN],
87
    /// load average as in uptime
88
    pub wd_loadav: [i32; 3],
89
    /// time system booted
90
    pub wd_boottime: i32,
91
    pub wd_we: [Whoent; Self::MAX_WHOENTRIES],
92
}
93

            
94
impl Whod {
95
    pub const HEADER_SIZE: usize = 1 + 1 + 2 + 4 + 4 + Self::MAX_HOSTNAME_LEN + 4 * 3 + 4;
96
    pub const MAX_SIZE: usize = std::mem::size_of::<Self>();
97

            
98
    pub const MAX_HOSTNAME_LEN: usize = 32;
99
    pub const MAX_WHOENTRIES: usize = 1024 / std::mem::size_of::<Whoent>();
100

            
101
    pub const WHODVERSION: u8 = 1;
102

            
103
    // NOTE: there was probably meant to be more packet types, but only status is defined.
104
    pub const WHODTYPE_STATUS: u8 = 1;
105

            
106
1
    pub fn new(
107
1
        sendtime: i32,
108
1
        recvtime: i32,
109
1
        hostname: [u8; Self::MAX_HOSTNAME_LEN],
110
1
        loadav: [i32; 3],
111
1
        boottime: i32,
112
1
        whoentries: [Whoent; Self::MAX_WHOENTRIES],
113
1
    ) -> Self {
114
1
        debug_assert!(
115
            whoentries
116
                .iter()
117
                .skip_while(|entry| !entry.is_zeroed())
118
                .all(|entry| entry.is_zeroed())
119
        );
120

            
121
1
        Self {
122
1
            wd_vers: Self::WHODVERSION,
123
1
            wd_type: Self::WHODTYPE_STATUS,
124
1
            wd_pad: [0u8; 2],
125
1
            wd_sendtime: sendtime,
126
1
            wd_recvtime: recvtime,
127
1
            wd_hostname: hostname,
128
1
            wd_loadav: loadav,
129
1
            wd_boottime: boottime,
130
1
            wd_we: whoentries,
131
1
        }
132
1
    }
133

            
134
1
    pub fn to_bytes(&self) -> Vec<u8> {
135
1
        let mut buf = BytesMut::with_capacity(Whod::MAX_SIZE);
136
1
        buf.put_u8(self.wd_vers);
137
1
        buf.put_u8(self.wd_type);
138
1
        buf.put_slice(&self.wd_pad);
139
1
        buf.put_i32(self.wd_sendtime);
140
1
        buf.put_i32(self.wd_recvtime);
141
1
        buf.put_slice(&self.wd_hostname);
142
1
        buf.put_i32(self.wd_loadav[0]);
143
1
        buf.put_i32(self.wd_loadav[1]);
144
1
        buf.put_i32(self.wd_loadav[2]);
145
1
        buf.put_i32(self.wd_boottime);
146

            
147
3
        for whoent in self.wd_we.iter().take_while(|entry| !entry.is_zeroed()) {
148
2
            buf.put_slice(&whoent.we_utmp.out_line);
149
2
            buf.put_slice(&whoent.we_utmp.out_name);
150
2
            buf.put_i32(whoent.we_utmp.out_time);
151
2
            buf.put_i32(whoent.we_idle);
152
2
        }
153

            
154
1
        buf.to_vec()
155
1
    }
156

            
157
6
    pub fn from_bytes(input: &[u8]) -> anyhow::Result<Self> {
158
6
        if input.len() < Self::HEADER_SIZE {
159
1
            return Err(anyhow::anyhow!(
160
1
                "Not enough bytes to parse packet header: {} < {}",
161
1
                input.len(),
162
1
                Self::HEADER_SIZE
163
1
            ));
164
5
        }
165

            
166
5
        if input.len() > Self::MAX_SIZE {
167
1
            return Err(anyhow::anyhow!(
168
1
                "Too many bytes to parse packet: {} > {}",
169
1
                input.len(),
170
1
                Self::MAX_SIZE
171
1
            ));
172
4
        }
173

            
174
4
        if !(input.len() - Self::HEADER_SIZE).is_multiple_of(Whoent::SIZE) {
175
1
            return Err(anyhow::anyhow!(
176
1
                "Invalid packet length: {} (not aligned with struct sizes, should be {} + N * {})",
177
1
                input.len(),
178
1
                Self::HEADER_SIZE,
179
1
                Whoent::SIZE,
180
1
            ));
181
3
        }
182

            
183
3
        let mut bytes = bytes::Bytes::copy_from_slice(input);
184

            
185
3
        let wd_vers = bytes.get_u8();
186
3
        if wd_vers != Self::WHODVERSION {
187
1
            return Err(anyhow::anyhow!(
188
1
                "Unsupported whod protocol version: {}",
189
1
                wd_vers
190
1
            ));
191
2
        }
192

            
193
2
        let wd_type = bytes.get_u8();
194
2
        if wd_type != Self::WHODTYPE_STATUS {
195
1
            return Err(anyhow::anyhow!("Unsupported whod packet type: {}", wd_type));
196
1
        }
197

            
198
1
        bytes.advance(2); // skip wd_pad
199

            
200
1
        let wd_sendtime = bytes.get_i32();
201
1
        let wd_recvtime = bytes.get_i32();
202
1
        let mut wd_hostname = [0u8; Self::MAX_HOSTNAME_LEN];
203
1
        bytes.copy_to_slice(&mut wd_hostname);
204
1
        let wd_loadav = [bytes.get_i32(), bytes.get_i32(), bytes.get_i32()];
205
1
        let wd_boottime = bytes.get_i32();
206

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

            
209
42
        let mut wd_we = array::from_fn(|_| Whoent::zeroed());
210

            
211
2
        for (byte_chunk, whoent) in bytes
212
1
            .as_chunks::<{ Whoent::SIZE }>()
213
1
            .0
214
1
            .iter()
215
1
            .zip(wd_we.iter_mut())
216
2
        {
217
2
            let mut chunk_bytes = bytes::Bytes::copy_from_slice(byte_chunk);
218
2

            
219
2
            let mut out_line = [0u8; Outmp::MAX_TTY_NAME_LEN];
220
2
            chunk_bytes.copy_to_slice(&mut out_line);
221
2
            let mut out_name = [0u8; Outmp::MAX_USER_ID_LEN];
222
2
            chunk_bytes.copy_to_slice(&mut out_name);
223
2
            let out_time = chunk_bytes.get_i32();
224
2

            
225
2
            let we_utmp = Outmp {
226
2
                out_line,
227
2
                out_name,
228
2
                out_time,
229
2
            };
230
2
            let we_idle = chunk_bytes.get_i32();
231
2

            
232
2
            *whoent = Whoent { we_utmp, we_idle };
233
2
        }
234

            
235
1
        let result = Whod::new(
236
1
            wd_sendtime,
237
1
            wd_recvtime,
238
1
            wd_hostname,
239
1
            wd_loadav,
240
1
            wd_boottime,
241
1
            wd_we,
242
        );
243

            
244
1
        Ok(result)
245
6
    }
246
}
247

            
248
// ------------------------------------------------
249

            
250
/// Load average representation: (5 min, 10 min, 15 min)
251
/// All values are multiplied by 100.
252
pub type LoadAverage = (i32, i32, i32);
253

            
254
/// High-level representation of a rwhod status update.
255
///
256
/// This struct is intended for easier use in Rust code, with proper types and dynamic arrays.
257
/// It can be converted to and from the low-level [`Whod`] struct used for network transmission.
258
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
259
pub struct WhodStatusUpdate {
260
    // NOTE: there is only one defined packet type, so we just omit it here
261
    /// Timestamp by sender
262
    #[serde(with = "chrono::serde::ts_seconds")]
263
    pub sendtime: DateTime<Utc>,
264

            
265
    /// Timestamp applied by receiver
266
    #[serde(with = "chrono::serde::ts_seconds_option")]
267
    pub recvtime: Option<DateTime<Utc>>,
268

            
269
    /// Name of the host sending the status update (max 32 characters)
270
    pub hostname: String,
271

            
272
    /// load average over 5, 10, and 15 minutes multiplied by 100
273
    pub load_average: LoadAverage,
274

            
275
    /// Which time the system was booted
276
    #[serde(with = "chrono::serde::ts_seconds")]
277
    pub boot_time: DateTime<Utc>,
278

            
279
    /// List of users currently logged in to the host (max 42 entries)
280
    pub users: Vec<WhodUserEntry>,
281
}
282

            
283
impl WhodStatusUpdate {
284
17
    pub fn new(
285
17
        sendtime: DateTime<Utc>,
286
17
        recvtime: Option<DateTime<Utc>>,
287
17
        hostname: String,
288
17
        load_average: LoadAverage,
289
17
        boot_time: DateTime<Utc>,
290
17
        users: Vec<WhodUserEntry>,
291
17
    ) -> Self {
292
17
        Self {
293
17
            sendtime,
294
17
            recvtime,
295
17
            hostname,
296
17
            load_average,
297
17
            boot_time,
298
17
            users,
299
17
        }
300
17
    }
301
}
302

            
303
/// High-level representation of a single user session in a rwhod status update.
304
///
305
/// This struct is intended for easier use in Rust code, with proper types.
306
/// It can be converted to and from the low-level [`Whoent`] struct used for network transmission.
307
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
308
pub struct WhodUserEntry {
309
    /// TTY name (max 8 characters)
310
    pub tty: String,
311

            
312
    /// User ID (max 8 characters)
313
    pub user_id: String,
314

            
315
    /// Time when the user logged in
316
    #[serde(with = "chrono::serde::ts_seconds")]
317
    pub login_time: DateTime<Utc>,
318

            
319
    /// How long since the user last typed on the TTY
320
    #[serde(with = "duration_serde::duration_seconds")]
321
    pub idle_time: Duration,
322
}
323

            
324
impl WhodUserEntry {
325
62
    pub fn new(
326
62
        tty: String,
327
62
        user_id: String,
328
62
        login_time: DateTime<Utc>,
329
62
        idle_time: Duration,
330
62
    ) -> Self {
331
62
        Self {
332
62
            tty,
333
62
            user_id,
334
62
            login_time,
335
62
            idle_time,
336
62
        }
337
62
    }
338
}
339

            
340
impl TryFrom<Whoent> for WhodUserEntry {
341
    type Error = String;
342

            
343
55
    fn try_from(value: Whoent) -> Result<Self, Self::Error> {
344
55
        let tty_end = value
345
55
            .we_utmp
346
55
            .out_line
347
55
            .iter()
348
308
            .position(|&c| c == 0)
349
55
            .unwrap_or(value.we_utmp.out_line.len());
350
55
        let tty = String::from_utf8(value.we_utmp.out_line[..tty_end].to_vec())
351
55
            .map_err(|e| format!("Invalid UTF-8 in TTY name: {}", e))?;
352

            
353
54
        let user_id_end = value
354
54
            .we_utmp
355
54
            .out_name
356
54
            .iter()
357
351
            .position(|&c| c == 0)
358
54
            .unwrap_or(value.we_utmp.out_name.len());
359
54
        let user_id = String::from_utf8(value.we_utmp.out_name[..user_id_end].to_vec())
360
54
            .map_err(|e| format!("Invalid UTF-8 in user ID: {}", e))?;
361

            
362
54
        let now = Utc::now();
363
54
        let login_time = decode_rwhod_timestamp_near_time(value.we_utmp.out_time, now)?;
364

            
365
54
        Ok(WhodUserEntry {
366
54
            tty,
367
54
            user_id,
368
54
            login_time,
369
54
            idle_time: Duration::seconds(value.we_idle as i64),
370
54
        })
371
55
    }
372
}
373

            
374
impl TryFrom<Whod> for WhodStatusUpdate {
375
    type Error = String;
376

            
377
7
    fn try_from(value: Whod) -> Result<Self, Self::Error> {
378
7
        if value.wd_vers != Whod::WHODVERSION {
379
            return Err(format!(
380
                "Unsupported whod protocol version: {}",
381
                value.wd_vers
382
            ));
383
7
        }
384

            
385
7
        let now = Utc::now();
386
        let RwhodTimestamps {
387
7
            boottime,
388
7
            sendtime,
389
7
            recvtime,
390
7
        } = decode_rwhod_timestamps(value.wd_boottime, value.wd_sendtime, value.wd_recvtime, now)?;
391

            
392
7
        let hostname_end = value
393
7
            .wd_hostname
394
7
            .iter()
395
86
            .position(|&c| c == 0)
396
7
            .unwrap_or(value.wd_hostname.len());
397
7
        let hostname = String::from_utf8(value.wd_hostname[..hostname_end].to_vec())
398
7
            .map_err(|e| format!("Invalid UTF-8 in hostname: {}", e))?;
399

            
400
7
        let users = value
401
7
            .wd_we
402
7
            .iter()
403
55
            .take_while(|whoent| !whoent.is_zeroed())
404
49
            .map(|whoent| {
405
49
                let mut user = WhodUserEntry::try_from(whoent.clone())?;
406
49
                user.login_time = decode_rwhod_timestamp_near_time(whoent.we_utmp.out_time, now)?;
407
49
                Ok(user)
408
49
            })
409
7
            .collect::<Result<Vec<WhodUserEntry>, String>>()?;
410

            
411
7
        Ok(WhodStatusUpdate {
412
7
            sendtime,
413
7
            recvtime,
414
7
            hostname,
415
7
            load_average: value.wd_loadav.into(),
416
7
            boot_time: boottime,
417
7
            users,
418
7
        })
419
7
    }
420
}
421

            
422
impl TryFrom<WhodUserEntry> for Whoent {
423
    type Error = String;
424

            
425
52
    fn try_from(value: WhodUserEntry) -> Result<Self, Self::Error> {
426
52
        let mut out_line = [0u8; Outmp::MAX_TTY_NAME_LEN];
427
52
        let tty_bytes = value.tty.as_bytes();
428
52
        let tty_len = tty_bytes.len().min(Outmp::MAX_TTY_NAME_LEN);
429
52
        out_line[..tty_len].copy_from_slice(&tty_bytes[..tty_len]);
430

            
431
52
        let mut out_name = [0u8; Outmp::MAX_USER_ID_LEN];
432
52
        let user_id_bytes = value.user_id.as_bytes();
433
52
        let user_id_len = user_id_bytes.len().min(Outmp::MAX_USER_ID_LEN);
434
52
        out_name[..user_id_len].copy_from_slice(&user_id_bytes[..user_id_len]);
435

            
436
52
        let out_time = encode_rwhod_timestamp(value.login_time);
437

            
438
52
        let we_idle = value
439
52
            .idle_time
440
52
            .num_seconds()
441
52
            .clamp(i32::MIN as i64, i32::MAX as i64) as i32;
442

            
443
52
        Ok(Whoent {
444
52
            we_utmp: Outmp {
445
52
                out_line,
446
52
                out_name,
447
52
                out_time,
448
52
            },
449
52
            we_idle,
450
52
        })
451
52
    }
452
}
453

            
454
impl TryFrom<WhodStatusUpdate> for Whod {
455
    type Error = String;
456

            
457
7
    fn try_from(value: WhodStatusUpdate) -> Result<Self, Self::Error> {
458
7
        let mut wd_hostname = [0u8; Whod::MAX_HOSTNAME_LEN];
459
7
        let hostname_bytes = value.hostname.as_bytes();
460
7
        let hostname_len = hostname_bytes.len().min(Whod::MAX_HOSTNAME_LEN);
461
7
        wd_hostname[..hostname_len].copy_from_slice(&hostname_bytes[..hostname_len]);
462

            
463
7
        let wd_sendtime = encode_rwhod_timestamp(value.sendtime);
464
7
        let wd_recvtime = value.recvtime.map_or(0, encode_rwhod_timestamp);
465
7
        let wd_boottime = encode_rwhod_timestamp(value.boot_time);
466

            
467
7
        let wd_we = value
468
7
            .users
469
7
            .into_iter()
470
7
            .map(Whoent::try_from)
471
7
            .chain(std::iter::repeat(Ok(Whoent::zeroed())))
472
7
            .take(Whod::MAX_WHOENTRIES)
473
7
            .collect::<Result<Vec<Whoent>, String>>()?
474
7
            .try_into()
475
7
            .expect("Length mismatch, this should never happen");
476

            
477
7
        Ok(Whod {
478
7
            wd_vers: Whod::WHODVERSION,
479
7
            wd_type: Whod::WHODTYPE_STATUS,
480
7
            wd_pad: [0u8; 2],
481
7
            wd_sendtime,
482
7
            wd_recvtime,
483
7
            wd_hostname,
484
7
            wd_loadav: value.load_average.into(),
485
7
            wd_boottime,
486
7
            wd_we,
487
7
        })
488
7
    }
489
}
490

            
491
#[cfg(test)]
492
mod tests {
493
    use super::*;
494
    use chrono::TimeZone;
495

            
496
    #[test]
497
1
    fn test_whod_serialization_roundtrip() {
498
1
        let original_status = WhodStatusUpdate::new(
499
1
            Utc.with_ymd_and_hms(2024, 6, 1, 12, 0, 0).unwrap(),
500
1
            Some(Utc.with_ymd_and_hms(2024, 6, 1, 12, 5, 0).unwrap()),
501
1
            "testhost".to_string(),
502
1
            (25, 20, 18),
503
1
            Utc.with_ymd_and_hms(2024, 5, 31, 8, 0, 0).unwrap(),
504
1
            vec![
505
1
                WhodUserEntry::new(
506
1
                    "tty1".to_string(),
507
1
                    "user1".to_string(),
508
1
                    Utc.with_ymd_and_hms(2024, 6, 1, 10, 0, 0).unwrap(),
509
1
                    Duration::minutes(5),
510
                ),
511
1
                WhodUserEntry::new(
512
1
                    "tty2".to_string(),
513
1
                    "user2".to_string(),
514
1
                    Utc.with_ymd_and_hms(2024, 6, 1, 11, 0, 0).unwrap(),
515
1
                    Duration::minutes(10),
516
                ),
517
            ],
518
        );
519

            
520
1
        let whod_struct =
521
1
            Whod::try_from(original_status.clone()).expect("Conversion to Whod failed");
522
1
        let bytes = whod_struct.to_bytes();
523
1
        let parsed_whod = Whod::from_bytes(&bytes).expect("Parsing from bytes failed");
524
1
        let final_status =
525
1
            WhodStatusUpdate::try_from(parsed_whod).expect("Conversion from Whod failed");
526

            
527
1
        assert_eq!(original_status, final_status);
528
1
    }
529

            
530
    #[test]
531
1
    fn test_parser_invalid_bytes() {
532
        // Too short
533
1
        let short_bytes = vec![0u8; Whod::HEADER_SIZE - 1];
534
1
        assert!(Whod::from_bytes(&short_bytes).is_err());
535

            
536
        // Too long
537
1
        let long_bytes = vec![0u8; Whod::MAX_SIZE + 1];
538
1
        assert!(Whod::from_bytes(&long_bytes).is_err());
539

            
540
        // Misaligned length
541
1
        let misaligned_bytes = vec![0u8; Whod::HEADER_SIZE + 1];
542
1
        assert!(Whod::from_bytes(&misaligned_bytes).is_err());
543

            
544
        // Invalid version
545
1
        let mut invalid_version_bytes = vec![0u8; Whod::HEADER_SIZE];
546
1
        invalid_version_bytes[0] = 99; // invalid version
547
1
        assert!(Whod::from_bytes(&invalid_version_bytes).is_err());
548

            
549
        // Invalid packet type
550
1
        let mut invalid_type_bytes = vec![0u8; Whod::HEADER_SIZE];
551
1
        invalid_type_bytes[0] = Whod::WHODVERSION;
552
1
        invalid_type_bytes[1] = 99; // invalid type
553
1
        assert!(Whod::from_bytes(&invalid_type_bytes).is_err());
554
1
    }
555

            
556
    #[test]
557
1
    fn test_whod_user_entry_conversion() {
558
1
        let user_entry = WhodUserEntry::new(
559
1
            "tty1".to_string(),
560
1
            "user1".to_string(),
561
1
            Utc.with_ymd_and_hms(2024, 6, 1, 10, 0, 0).unwrap(),
562
1
            Duration::minutes(5),
563
        );
564

            
565
1
        let whoent = Whoent::try_from(user_entry.clone()).expect("Conversion to Whoent failed");
566
1
        let converted_back =
567
1
            WhodUserEntry::try_from(whoent).expect("Conversion from Whoent failed");
568

            
569
1
        assert_eq!(user_entry, converted_back);
570
1
    }
571

            
572
    #[test]
573
1
    fn test_whod_status_update_conversion() {
574
1
        let status_update = WhodStatusUpdate::new(
575
1
            Utc.with_ymd_and_hms(2024, 6, 1, 12, 0, 0).unwrap(),
576
1
            Some(Utc.with_ymd_and_hms(2024, 6, 1, 12, 5, 0).unwrap()),
577
1
            "testhost".to_string(),
578
1
            (25, 20, 18),
579
1
            Utc.with_ymd_and_hms(2024, 5, 31, 8, 0, 0).unwrap(),
580
1
            vec![
581
1
                WhodUserEntry::new(
582
1
                    "tty1".to_string(),
583
1
                    "user1".to_string(),
584
1
                    Utc.with_ymd_and_hms(2024, 6, 1, 10, 0, 0).unwrap(),
585
1
                    Duration::minutes(5),
586
                ),
587
1
                WhodUserEntry::new(
588
1
                    "tty2".to_string(),
589
1
                    "user2".to_string(),
590
1
                    Utc.with_ymd_and_hms(2024, 6, 1, 11, 0, 0).unwrap(),
591
1
                    Duration::minutes(10),
592
                ),
593
            ],
594
        );
595

            
596
1
        let whod_struct = Whod::try_from(status_update.clone()).expect("Conversion to Whod failed");
597
1
        let converted_back =
598
1
            WhodStatusUpdate::try_from(whod_struct).expect("Conversion from Whod failed");
599

            
600
1
        assert_eq!(status_update, converted_back);
601
1
    }
602

            
603
    #[test]
604
1
    fn test_whod_user_entry_invalid_utf8() {
605
1
        let mut whoent = Whoent::zeroed();
606
1
        whoent.we_utmp.out_line = [0xff, 0xfe, 0xfd, 0, 0, 0, 0, 0]; // Invalid UTF-8
607
1
        whoent.we_utmp.out_name = [0xff, 0xfe, 0xfd, 0, 0, 0, 0, 0]; // Invalid UTF-8
608
1
        whoent.we_utmp.out_time = 1_700_000_000; // Some valid timestamp
609
1
        whoent.we_idle = 60; // 1 minute
610

            
611
1
        let result = WhodUserEntry::try_from(whoent);
612
1
        assert!(result.is_err());
613
1
    }
614

            
615
    #[test]
616
1
    fn test_whod_user_entry_conversion_username_gets_truncated() {
617
1
        let mut whoent = Whoent::zeroed();
618
1
        whoent.we_utmp.out_name = [b'a'; Outmp::MAX_USER_ID_LEN];
619
1
        whoent.we_utmp.out_time = 1_700_000_000;
620
1
        whoent.we_idle = 60;
621

            
622
1
        let result = WhodUserEntry::try_from(whoent);
623
1
        assert!(result.is_ok());
624
1
        assert_eq!(
625
1
            result.unwrap().user_id,
626
1
            [b'a'; Outmp::MAX_USER_ID_LEN]
627
1
                .iter()
628
8
                .map(|&c| c as char)
629
1
                .collect::<String>()
630
        );
631
1
    }
632

            
633
    #[test]
634
1
    fn test_whod_user_entry_conversion_tty_gets_truncated() {
635
1
        let mut whoent = Whoent::zeroed();
636
1
        whoent.we_utmp.out_line = [b'b'; Outmp::MAX_TTY_NAME_LEN];
637
1
        whoent.we_utmp.out_time = 1_700_000_000;
638
1
        whoent.we_idle = 60;
639

            
640
1
        let result = WhodUserEntry::try_from(whoent);
641
1
        assert!(result.is_ok());
642
1
        assert_eq!(
643
1
            result.unwrap().tty,
644
1
            [b'b'; Outmp::MAX_TTY_NAME_LEN]
645
1
                .iter()
646
8
                .map(|&c| c as char)
647
1
                .collect::<String>()
648
        );
649
1
    }
650

            
651
    #[test]
652
1
    fn test_whod_status_update_hostname_gets_truncated() {
653
1
        let long_hostname = "a".repeat(Whod::MAX_HOSTNAME_LEN + 10);
654
1
        let status_update = WhodStatusUpdate::new(
655
1
            Utc.with_ymd_and_hms(2024, 6, 1, 12, 0, 0).unwrap(),
656
1
            Some(Utc.with_ymd_and_hms(2024, 6, 1, 12, 5, 0).unwrap()),
657
1
            long_hostname.clone(),
658
1
            (25, 20, 18),
659
1
            Utc.with_ymd_and_hms(2024, 5, 31, 8, 0, 0).unwrap(),
660
1
            vec![],
661
        );
662

            
663
1
        let whod_struct = Whod::try_from(status_update.clone()).expect("Conversion to Whod failed");
664
1
        let converted_back =
665
1
            WhodStatusUpdate::try_from(whod_struct).expect("Conversion from Whod failed");
666

            
667
1
        assert_eq!(
668
            converted_back.hostname,
669
1
            long_hostname[..Whod::MAX_HOSTNAME_LEN].to_string()
670
        );
671
1
    }
672

            
673
    #[test]
674
1
    fn test_whod_status_update_users_get_truncated() {
675
1
        let users = (0..(Whod::MAX_WHOENTRIES + 10))
676
52
            .map(|i| {
677
52
                WhodUserEntry::new(
678
52
                    format!("tty{}", i),
679
52
                    format!("user{}", i),
680
52
                    Utc.with_ymd_and_hms(2024, 6, 1, 10, 0, 0).unwrap(),
681
52
                    Duration::minutes(i as i64),
682
                )
683
52
            })
684
1
            .collect::<Vec<_>>();
685

            
686
1
        let status_update = WhodStatusUpdate::new(
687
1
            Utc.with_ymd_and_hms(2024, 6, 1, 12, 0, 0).unwrap(),
688
1
            Some(Utc.with_ymd_and_hms(2024, 6, 1, 12, 5, 0).unwrap()),
689
1
            "testhost".to_string(),
690
1
            (25, 20, 18),
691
1
            Utc.with_ymd_and_hms(2024, 5, 31, 8, 0, 0).unwrap(),
692
1
            users,
693
        );
694

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

            
699
1
        assert_eq!(converted_back.users.len(), Whod::MAX_WHOENTRIES);
700

            
701
42
        for (i, user) in converted_back.users.iter().enumerate() {
702
42
            assert_eq!(user.tty, format!("tty{}", i));
703
42
            assert_eq!(user.user_id, format!("user{}", i));
704
        }
705
1
    }
706

            
707
    #[test]
708
1
    fn test_whod_status_update_usernames_and_ttys_get_truncated() {
709
1
        let long_tty = "a".repeat(Outmp::MAX_TTY_NAME_LEN + 10);
710
1
        let long_user_id = "b".repeat(Outmp::MAX_USER_ID_LEN + 10);
711

            
712
1
        let user_entry = WhodUserEntry::new(
713
1
            long_tty.clone(),
714
1
            long_user_id.clone(),
715
1
            Utc.with_ymd_and_hms(2024, 6, 1, 10, 0, 0).unwrap(),
716
1
            Duration::minutes(5),
717
        );
718

            
719
1
        let whoent = Whoent::try_from(user_entry.clone()).expect("Conversion to Whoent failed");
720
1
        let converted_back =
721
1
            WhodUserEntry::try_from(whoent).expect("Conversion from Whoent failed");
722

            
723
1
        assert_eq!(
724
            converted_back.tty,
725
1
            long_tty[..Outmp::MAX_TTY_NAME_LEN].to_string()
726
        );
727
1
        assert_eq!(
728
            converted_back.user_id,
729
1
            long_user_id[..Outmp::MAX_USER_ID_LEN].to_string()
730
        );
731
1
    }
732

            
733
4
    fn wrap_epoch(offset: i64) -> DateTime<Utc> {
734
4
        DateTime::from_timestamp((i32::MAX as i64 + 1) + offset, 0).unwrap()
735
4
    }
736

            
737
    #[test]
738
1
    fn test_whod_status_update_roundtrip_corrects_wrapped_timestamps() {
739
1
        let recvtime = wrap_epoch(60 * 60);
740
1
        let sendtime = recvtime - Duration::minutes(5);
741
1
        let boot_time = recvtime - Duration::days(2);
742
1
        let login_time = recvtime - Duration::hours(2);
743

            
744
1
        let original = WhodStatusUpdate::new(
745
1
            sendtime,
746
1
            Some(recvtime),
747
1
            "testhost".to_string(),
748
1
            (25, 20, 18),
749
1
            boot_time,
750
1
            vec![WhodUserEntry::new(
751
1
                "tty1".to_string(),
752
1
                "user".to_string(),
753
1
                login_time,
754
1
                Duration::seconds(60),
755
            )],
756
        );
757

            
758
1
        let whod = Whod::try_from(original.clone()).expect("Conversion to Whod failed");
759
1
        let raw_boot_time = whod.wd_boottime;
760
1
        let raw_login_time = whod.wd_we[0].we_utmp.out_time;
761
1
        let converted = WhodStatusUpdate::try_from(whod).expect("Conversion from Whod failed");
762

            
763
1
        assert_eq!(converted.sendtime, original.sendtime);
764
1
        assert_eq!(converted.recvtime, original.recvtime);
765
1
        assert_eq!(converted.hostname, original.hostname);
766
1
        assert_eq!(converted.load_average, original.load_average);
767
1
        assert_eq!(
768
            converted.boot_time,
769
1
            decode_rwhod_timestamp_near_time(raw_boot_time, recvtime).unwrap()
770
        );
771
1
        assert_eq!(converted.users.len(), 1);
772
1
        assert_eq!(converted.users[0].tty, original.users[0].tty);
773
1
        assert_eq!(converted.users[0].user_id, original.users[0].user_id);
774
1
        assert_eq!(converted.users[0].idle_time, original.users[0].idle_time);
775
1
        assert_eq!(
776
1
            converted.users[0].login_time,
777
1
            decode_rwhod_timestamp_near_time(raw_login_time, recvtime).unwrap()
778
        );
779
1
    }
780

            
781
    #[test]
782
1
    fn test_whod_status_update_roundtrip_sendtime_before_wrap_recvtime_after_wrap() {
783
1
        let recvtime = wrap_epoch(60);
784
1
        let sendtime = DateTime::from_timestamp(i32::MAX as i64 - 29, 0).unwrap();
785
1
        let boot_time = sendtime - Duration::days(1);
786
1
        let login_time = sendtime - Duration::minutes(1);
787

            
788
1
        let original = WhodStatusUpdate::new(
789
1
            sendtime,
790
1
            Some(recvtime),
791
1
            "testhost".to_string(),
792
1
            (25, 20, 18),
793
1
            boot_time,
794
1
            vec![WhodUserEntry::new(
795
1
                "tty1".to_string(),
796
1
                "user".to_string(),
797
1
                login_time,
798
1
                Duration::seconds(60),
799
            )],
800
        );
801

            
802
1
        let whod = Whod::try_from(original.clone()).expect("Conversion to Whod failed");
803
1
        let raw_send_time = whod.wd_sendtime;
804
1
        let raw_boot_time = whod.wd_boottime;
805
1
        let raw_login_time = whod.wd_we[0].we_utmp.out_time;
806
1
        let converted = WhodStatusUpdate::try_from(whod).expect("Conversion from Whod failed");
807

            
808
1
        assert_eq!(converted.recvtime, original.recvtime);
809
1
        assert_eq!(converted.hostname, original.hostname);
810
1
        assert_eq!(converted.load_average, original.load_average);
811
1
        assert_eq!(
812
            converted.sendtime,
813
1
            decode_rwhod_timestamp_near_time(raw_send_time, recvtime).unwrap()
814
        );
815
1
        assert_eq!(
816
            converted.boot_time,
817
1
            decode_rwhod_timestamp_near_time(raw_boot_time, recvtime).unwrap()
818
        );
819
1
        assert_eq!(converted.users.len(), 1);
820
1
        assert_eq!(converted.users[0].tty, original.users[0].tty);
821
1
        assert_eq!(converted.users[0].user_id, original.users[0].user_id);
822
1
        assert_eq!(converted.users[0].idle_time, original.users[0].idle_time);
823
1
        assert_eq!(
824
1
            converted.users[0].login_time,
825
1
            decode_rwhod_timestamp_near_time(raw_login_time, recvtime).unwrap()
826
        );
827
1
    }
828

            
829
    #[test]
830
1
    fn test_whod_status_update_roundtrip_corrects_wrapped_timestamps_without_recvtime() {
831
1
        let sendtime = wrap_epoch(60 * 60);
832
1
        let boot_time = sendtime - Duration::days(2);
833
1
        let login_time = sendtime - Duration::hours(2);
834

            
835
1
        let original = WhodStatusUpdate::new(
836
1
            sendtime,
837
1
            None,
838
1
            "testhost".to_string(),
839
1
            (25, 20, 18),
840
1
            boot_time,
841
1
            vec![WhodUserEntry::new(
842
1
                "tty1".to_string(),
843
1
                "user".to_string(),
844
1
                login_time,
845
1
                Duration::seconds(60),
846
            )],
847
        );
848

            
849
1
        let whod = Whod::try_from(original.clone()).expect("Conversion to Whod failed");
850
1
        let raw_boot_time = whod.wd_boottime;
851
1
        let raw_login_time = whod.wd_we[0].we_utmp.out_time;
852
1
        let converted = WhodStatusUpdate::try_from(whod).expect("Conversion from Whod failed");
853

            
854
1
        assert_eq!(converted.sendtime, original.sendtime);
855
1
        assert_eq!(converted.recvtime, None);
856
1
        assert_eq!(converted.hostname, original.hostname);
857
1
        assert_eq!(converted.load_average, original.load_average);
858
1
        assert_eq!(
859
            converted.boot_time,
860
1
            decode_rwhod_timestamp_near_time(raw_boot_time, sendtime).unwrap()
861
        );
862
1
        assert_eq!(converted.users.len(), 1);
863
1
        assert_eq!(converted.users[0].tty, original.users[0].tty);
864
1
        assert_eq!(converted.users[0].user_id, original.users[0].user_id);
865
1
        assert_eq!(converted.users[0].idle_time, original.users[0].idle_time);
866
1
        assert_eq!(
867
1
            converted.users[0].login_time,
868
1
            decode_rwhod_timestamp_near_time(raw_login_time, sendtime).unwrap()
869
        );
870
1
    }
871

            
872
    #[test]
873
1
    fn test_whod_user_entry_roundtrip_corrects_wrapped_timestamp() {
874
1
        let original = WhodUserEntry::new(
875
1
            "tty1".to_string(),
876
1
            "user".to_string(),
877
1
            wrap_epoch(60 * 60),
878
1
            Duration::seconds(60),
879
        );
880

            
881
1
        let whoent = Whoent::try_from(original.clone()).expect("Conversion to Whoent failed");
882
1
        let converted_back =
883
1
            WhodUserEntry::try_from(whoent.clone()).expect("Conversion from Whoent failed");
884

            
885
1
        assert_eq!(converted_back.tty, original.tty);
886
1
        assert_eq!(converted_back.user_id, original.user_id);
887
1
        assert_eq!(converted_back.idle_time, original.idle_time);
888
1
        assert_eq!(
889
            converted_back.login_time,
890
1
            decode_rwhod_timestamp_near_time(whoent.we_utmp.out_time, original.login_time).unwrap()
891
        );
892
1
    }
893
}