1use std::array;
2
3use bytes::{Buf, BufMut, BytesMut};
4use chrono::{DateTime, Duration, Utc};
5use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Clone, PartialEq, Eq)]
11#[repr(C)]
12pub struct Outmp {
13 pub out_line: [u8; Self::MAX_TTY_NAME_LEN],
15 pub out_name: [u8; Self::MAX_USER_ID_LEN],
17 pub out_time: i32,
19}
20
21impl Outmp {
22 pub const MAX_TTY_NAME_LEN: usize = 8;
23 pub const MAX_USER_ID_LEN: usize = 8;
24}
25
26#[derive(Debug, Clone, PartialEq, Eq)]
30#[repr(C)]
31pub struct Whoent {
32 pub we_utmp: Outmp,
34 pub we_idle: i32,
36}
37
38impl Whoent {
39 pub const SIZE: usize = std::mem::size_of::<Self>();
40
41 fn zeroed() -> Self {
42 Self {
43 we_utmp: Outmp {
44 out_line: [0u8; Outmp::MAX_TTY_NAME_LEN],
45 out_name: [0u8; Outmp::MAX_USER_ID_LEN],
46 out_time: 0,
47 },
48 we_idle: 0,
49 }
50 }
51
52 fn is_zeroed(&self) -> bool {
53 self.we_utmp.out_line.iter().all(|&b| b == 0)
54 && self.we_utmp.out_name.iter().all(|&b| b == 0)
55 && self.we_utmp.out_time == 0
56 && self.we_idle == 0
57 }
58}
59
60#[derive(Debug, Clone, PartialEq, Eq)]
64#[repr(C)]
65pub struct Whod {
66 pub wd_vers: u8,
68 pub wd_type: u8,
70 pub wd_pad: [u8; 2],
71 pub wd_sendtime: i32,
73 pub wd_recvtime: i32,
75 pub wd_hostname: [u8; Self::MAX_HOSTNAME_LEN],
77 pub wd_loadav: [i32; 3],
79 pub wd_boottime: i32,
81 pub wd_we: [Whoent; Self::MAX_WHOENTRIES],
82}
83
84impl Whod {
85 pub const HEADER_SIZE: usize = 1 + 1 + 2 + 4 + 4 + Self::MAX_HOSTNAME_LEN + 4 * 3 + 4;
86 pub const MAX_SIZE: usize = std::mem::size_of::<Self>();
87
88 pub const MAX_HOSTNAME_LEN: usize = 32;
89 pub const MAX_WHOENTRIES: usize = 1024 / std::mem::size_of::<Whoent>();
90
91 pub const WHODVERSION: u8 = 1;
92
93 pub const WHODTYPE_STATUS: u8 = 1;
95
96 pub fn new(
97 sendtime: i32,
98 recvtime: i32,
99 hostname: [u8; Self::MAX_HOSTNAME_LEN],
100 loadav: [i32; 3],
101 boottime: i32,
102 whoentries: [Whoent; Self::MAX_WHOENTRIES],
103 ) -> Self {
104 debug_assert!(
105 whoentries
106 .iter()
107 .skip_while(|entry| !entry.is_zeroed())
108 .all(|entry| entry.is_zeroed())
109 );
110
111 Self {
112 wd_vers: Self::WHODVERSION,
113 wd_type: Self::WHODTYPE_STATUS,
114 wd_pad: [0u8; 2],
115 wd_sendtime: sendtime,
116 wd_recvtime: recvtime,
117 wd_hostname: hostname,
118 wd_loadav: loadav,
119 wd_boottime: boottime,
120 wd_we: whoentries,
121 }
122 }
123
124 pub fn to_bytes(&self) -> Vec<u8> {
125 let mut buf = BytesMut::with_capacity(Whod::MAX_SIZE);
126 buf.put_u8(self.wd_vers);
127 buf.put_u8(self.wd_type);
128 buf.put_slice(&self.wd_pad);
129 buf.put_i32(self.wd_sendtime);
130 buf.put_i32(self.wd_recvtime);
131 buf.put_slice(&self.wd_hostname);
132 buf.put_i32(self.wd_loadav[0]);
133 buf.put_i32(self.wd_loadav[1]);
134 buf.put_i32(self.wd_loadav[2]);
135 buf.put_i32(self.wd_boottime);
136
137 for whoent in self.wd_we.iter().take_while(|entry| !entry.is_zeroed()) {
138 buf.put_slice(&whoent.we_utmp.out_line);
139 buf.put_slice(&whoent.we_utmp.out_name);
140 buf.put_i32(whoent.we_utmp.out_time);
141 buf.put_i32(whoent.we_idle);
142 }
143
144 buf.to_vec()
145 }
146
147 pub fn from_bytes(input: &[u8]) -> anyhow::Result<Self> {
148 if input.len() < Self::HEADER_SIZE {
149 return Err(anyhow::anyhow!(
150 "Not enough bytes to parse packet header: {} < {}",
151 input.len(),
152 Self::HEADER_SIZE
153 ));
154 }
155
156 if input.len() > Self::MAX_SIZE {
157 return Err(anyhow::anyhow!(
158 "Too many bytes to parse packet: {} > {}",
159 input.len(),
160 Self::MAX_SIZE
161 ));
162 }
163
164 if !(input.len() - Self::HEADER_SIZE).is_multiple_of(Whoent::SIZE) {
165 return Err(anyhow::anyhow!(
166 "Invalid packet length: {} (not aligned with struct sizes, should be {} + N * {})",
167 input.len(),
168 Self::HEADER_SIZE,
169 Whoent::SIZE,
170 ));
171 }
172
173 let mut bytes = bytes::Bytes::copy_from_slice(input);
174
175 let wd_vers = bytes.get_u8();
176 if wd_vers != Self::WHODVERSION {
177 return Err(anyhow::anyhow!(
178 "Unsupported whod protocol version: {}",
179 wd_vers
180 ));
181 }
182
183 let wd_type = bytes.get_u8();
184 if wd_type != Self::WHODTYPE_STATUS {
185 return Err(anyhow::anyhow!("Unsupported whod packet type: {}", wd_type));
186 }
187
188 bytes.advance(2); let wd_sendtime = bytes.get_i32();
191 let wd_recvtime = bytes.get_i32();
192 let mut wd_hostname = [0u8; Self::MAX_HOSTNAME_LEN];
193 bytes.copy_to_slice(&mut wd_hostname);
194 let wd_loadav = [bytes.get_i32(), bytes.get_i32(), bytes.get_i32()];
195 let wd_boottime = bytes.get_i32();
196
197 debug_assert!(bytes.remaining() + Self::HEADER_SIZE == input.len());
198
199 let mut wd_we = array::from_fn(|_| Whoent::zeroed());
200
201 for (byte_chunk, whoent) in bytes.chunks_exact(Whoent::SIZE).zip(wd_we.iter_mut()) {
202 let mut chunk_bytes = bytes::Bytes::copy_from_slice(byte_chunk);
203
204 let mut out_line = [0u8; Outmp::MAX_TTY_NAME_LEN];
205 chunk_bytes.copy_to_slice(&mut out_line);
206 let mut out_name = [0u8; Outmp::MAX_USER_ID_LEN];
207 chunk_bytes.copy_to_slice(&mut out_name);
208 let out_time = chunk_bytes.get_i32();
209
210 let we_utmp = Outmp {
211 out_line,
212 out_name,
213 out_time,
214 };
215 let we_idle = chunk_bytes.get_i32();
216
217 *whoent = Whoent { we_utmp, we_idle };
218 }
219
220 let result = Whod::new(
221 wd_sendtime,
222 wd_recvtime,
223 wd_hostname,
224 wd_loadav,
225 wd_boottime,
226 wd_we,
227 );
228
229 Ok(result)
230 }
231}
232
233pub type LoadAverage = (i32, i32, i32);
238
239#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
244pub struct WhodStatusUpdate {
245 pub sendtime: DateTime<Utc>,
248
249 pub recvtime: Option<DateTime<Utc>>,
251
252 pub hostname: String,
254
255 pub load_average: LoadAverage,
257
258 pub boot_time: DateTime<Utc>,
260
261 pub users: Vec<WhodUserEntry>,
263}
264
265impl WhodStatusUpdate {
266 pub fn new(
267 sendtime: DateTime<Utc>,
268 recvtime: Option<DateTime<Utc>>,
269 hostname: String,
270 load_average: LoadAverage,
271 boot_time: DateTime<Utc>,
272 users: Vec<WhodUserEntry>,
273 ) -> Self {
274 Self {
275 sendtime,
276 recvtime,
277 hostname,
278 load_average,
279 boot_time,
280 users,
281 }
282 }
283}
284
285#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
290pub struct WhodUserEntry {
291 pub tty: String,
293
294 pub user_id: String,
296
297 pub login_time: DateTime<Utc>,
299
300 pub idle_time: Duration,
302}
303
304impl WhodUserEntry {
305 pub fn new(
306 tty: String,
307 user_id: String,
308 login_time: DateTime<Utc>,
309 idle_time: Duration,
310 ) -> Self {
311 Self {
312 tty,
313 user_id,
314 login_time,
315 idle_time,
316 }
317 }
318}
319
320impl TryFrom<Whoent> for WhodUserEntry {
321 type Error = String;
322
323 fn try_from(value: Whoent) -> Result<Self, Self::Error> {
324 let tty_end = value
325 .we_utmp
326 .out_line
327 .iter()
328 .position(|&c| c == 0)
329 .unwrap_or(value.we_utmp.out_line.len());
330 let tty = String::from_utf8(value.we_utmp.out_line[..tty_end].to_vec())
331 .map_err(|e| format!("Invalid UTF-8 in TTY name: {}", e))?;
332
333 let user_id_end = value
334 .we_utmp
335 .out_name
336 .iter()
337 .position(|&c| c == 0)
338 .unwrap_or(value.we_utmp.out_name.len());
339 let user_id = String::from_utf8(value.we_utmp.out_name[..user_id_end].to_vec())
340 .map_err(|e| format!("Invalid UTF-8 in user ID: {}", e))?;
341
342 let login_time = DateTime::from_timestamp_secs(value.we_utmp.out_time as i64).ok_or(
343 format!("Invalid login time timestamp: {}", value.we_utmp.out_time),
344 )?;
345
346 Ok(WhodUserEntry {
347 tty,
348 user_id,
349 login_time,
350 idle_time: Duration::seconds(value.we_idle as i64),
351 })
352 }
353}
354
355impl TryFrom<Whod> for WhodStatusUpdate {
356 type Error = String;
357
358 fn try_from(value: Whod) -> Result<Self, Self::Error> {
359 if value.wd_vers != Whod::WHODVERSION {
360 return Err(format!(
361 "Unsupported whod protocol version: {}",
362 value.wd_vers
363 ));
364 }
365
366 let sendtime = DateTime::from_timestamp_secs(value.wd_sendtime as i64).ok_or(format!(
367 "Invalid send time timestamp: {}",
368 value.wd_sendtime
369 ))?;
370
371 let recvtime = if value.wd_recvtime == 0 {
372 None
373 } else {
374 Some(
375 DateTime::from_timestamp_secs(value.wd_recvtime as i64).ok_or(format!(
376 "Invalid receive time timestamp: {}",
377 value.wd_recvtime
378 ))?,
379 )
380 };
381
382 let hostname_end = value
383 .wd_hostname
384 .iter()
385 .position(|&c| c == 0)
386 .unwrap_or(value.wd_hostname.len());
387 let hostname = String::from_utf8(value.wd_hostname[..hostname_end].to_vec())
388 .map_err(|e| format!("Invalid UTF-8 in hostname: {}", e))?;
389
390 let boot_time = DateTime::from_timestamp_secs(value.wd_boottime as i64).ok_or(format!(
391 "Invalid boot time timestamp: {}",
392 value.wd_boottime
393 ))?;
394
395 let users = value
396 .wd_we
397 .iter()
398 .take_while(|whoent| !whoent.is_zeroed())
399 .cloned()
400 .map(WhodUserEntry::try_from)
401 .collect::<Result<Vec<WhodUserEntry>, String>>()?;
402
403 Ok(WhodStatusUpdate {
404 sendtime,
405 recvtime,
406 hostname,
407 load_average: value.wd_loadav.into(),
408 boot_time,
409 users,
410 })
411 }
412}
413
414impl TryFrom<WhodUserEntry> for Whoent {
415 type Error = String;
416
417 fn try_from(value: WhodUserEntry) -> Result<Self, Self::Error> {
418 let mut out_line = [0u8; Outmp::MAX_TTY_NAME_LEN];
419 let tty_bytes = value.tty.as_bytes();
420 out_line[..tty_bytes.len().min(Outmp::MAX_TTY_NAME_LEN)].copy_from_slice(tty_bytes);
421
422 let mut out_name = [0u8; Outmp::MAX_USER_ID_LEN];
423 let user_id_bytes = value.user_id.as_bytes();
424 out_name[..user_id_bytes.len().min(Outmp::MAX_USER_ID_LEN)].copy_from_slice(user_id_bytes);
425
426 let out_time = value
427 .login_time
428 .timestamp()
429 .clamp(i32::MIN as i64, i32::MAX as i64) as i32;
430
431 let we_idle = value
432 .idle_time
433 .num_seconds()
434 .clamp(i32::MIN as i64, i32::MAX as i64) as i32;
435
436 Ok(Whoent {
437 we_utmp: Outmp {
438 out_line,
439 out_name,
440 out_time,
441 },
442 we_idle,
443 })
444 }
445}
446
447impl TryFrom<WhodStatusUpdate> for Whod {
448 type Error = String;
449
450 fn try_from(value: WhodStatusUpdate) -> Result<Self, Self::Error> {
451 let mut wd_hostname = [0u8; Whod::MAX_HOSTNAME_LEN];
452 let hostname_bytes = value.hostname.as_bytes();
453 wd_hostname[..hostname_bytes.len().min(Whod::MAX_HOSTNAME_LEN)]
454 .copy_from_slice(hostname_bytes);
455
456 let wd_sendtime = value
457 .sendtime
458 .timestamp()
459 .clamp(i32::MIN as i64, i32::MAX as i64) as i32;
460
461 let wd_recvtime = value.recvtime.map_or(0, |dt| {
462 dt.timestamp().clamp(i32::MIN as i64, i32::MAX as i64) as i32
463 });
464
465 let wd_boottime = value
466 .boot_time
467 .timestamp()
468 .clamp(i32::MIN as i64, i32::MAX as i64) as i32;
469
470 let wd_we = value
471 .users
472 .into_iter()
473 .map(Whoent::try_from)
474 .chain(std::iter::repeat(Ok(Whoent::zeroed())))
475 .take(Whod::MAX_WHOENTRIES)
476 .collect::<Result<Vec<Whoent>, String>>()?
477 .try_into()
478 .expect("Length mismatch, this should never happen");
479
480 Ok(Whod {
481 wd_vers: Whod::WHODVERSION,
482 wd_type: Whod::WHODTYPE_STATUS,
483 wd_pad: [0u8; 2],
484 wd_sendtime,
485 wd_recvtime,
486 wd_hostname,
487 wd_loadav: value.load_average.into(),
488 wd_boottime,
489 wd_we,
490 })
491 }
492}
493
494#[cfg(test)]
495mod tests {
496 use super::*;
497 use chrono::TimeZone;
498
499 #[test]
500 fn test_whod_serialization_roundtrip() {
501 let original_status = WhodStatusUpdate::new(
502 Utc.with_ymd_and_hms(2024, 6, 1, 12, 0, 0).unwrap(),
503 Some(Utc.with_ymd_and_hms(2024, 6, 1, 12, 5, 0).unwrap()),
504 "testhost".to_string(),
505 (25, 20, 18),
506 Utc.with_ymd_and_hms(2024, 5, 31, 8, 0, 0).unwrap(),
507 vec![
508 WhodUserEntry::new(
509 "tty1".to_string(),
510 "user1".to_string(),
511 Utc.with_ymd_and_hms(2024, 6, 1, 10, 0, 0).unwrap(),
512 Duration::minutes(5),
513 ),
514 WhodUserEntry::new(
515 "tty2".to_string(),
516 "user2".to_string(),
517 Utc.with_ymd_and_hms(2024, 6, 1, 11, 0, 0).unwrap(),
518 Duration::minutes(10),
519 ),
520 ],
521 );
522
523 let whod_struct =
524 Whod::try_from(original_status.clone()).expect("Conversion to Whod failed");
525 let bytes = whod_struct.to_bytes();
526 let parsed_whod = Whod::from_bytes(&bytes).expect("Parsing from bytes failed");
527 let final_status =
528 WhodStatusUpdate::try_from(parsed_whod).expect("Conversion from Whod failed");
529
530 assert_eq!(original_status, final_status);
531 }
532
533 #[test]
534 fn test_parser_invalid_bytes() {
535 let short_bytes = vec![0u8; Whod::HEADER_SIZE - 1];
537 assert!(Whod::from_bytes(&short_bytes).is_err());
538
539 let long_bytes = vec![0u8; Whod::MAX_SIZE + 1];
541 assert!(Whod::from_bytes(&long_bytes).is_err());
542
543 let misaligned_bytes = vec![0u8; Whod::HEADER_SIZE + 1];
545 assert!(Whod::from_bytes(&misaligned_bytes).is_err());
546
547 let mut invalid_version_bytes = vec![0u8; Whod::HEADER_SIZE];
549 invalid_version_bytes[0] = 99; assert!(Whod::from_bytes(&invalid_version_bytes).is_err());
551
552 let mut invalid_type_bytes = vec![0u8; Whod::HEADER_SIZE];
554 invalid_type_bytes[0] = Whod::WHODVERSION;
555 invalid_type_bytes[1] = 99; assert!(Whod::from_bytes(&invalid_type_bytes).is_err());
557 }
558}