1mod classic_formatter;
2mod parser;
3
4use std::path::PathBuf;
5
6use chrono::{DateTime, TimeDelta, Utc};
7use serde::{Deserialize, Serialize};
8
9use crate::{
10 proto::finger_protocol::{
11 classic_formatter::classic_format_finger_response_structured_user_entry,
12 parser::try_parse_structured_user_entry_from_raw_finger_response,
13 },
14 util::duration_serde,
15};
16
17#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
18pub struct FingerRequest {
19 long: bool,
20 name: String,
21}
22
23impl FingerRequest {
24 pub fn new(long: bool, name: String) -> Self {
25 Self { long, name }
26 }
27
28 pub fn to_bytes(&self) -> Vec<u8> {
29 let mut result = Vec::new();
30 if self.long {
31 result.extend(b"/W ");
32 }
33
34 result.extend(self.name.as_bytes());
35 result.extend(b"\r\n");
36
37 result
38 }
39
40 pub fn from_bytes(bytes: &[u8]) -> Self {
41 let (long, name) = if &bytes[..3] == b"/W " {
42 (true, &bytes[3..])
43 } else {
44 (false, bytes)
45 };
46
47 let name = match name.strip_suffix(b"\r\n") {
48 Some(new_name) => new_name,
49 None => name,
50 };
51
52 Self::new(long, String::from_utf8_lossy(name).to_string())
53 }
54}
55
56#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
57pub struct RawFingerResponse(String);
58
59impl RawFingerResponse {
60 pub fn new(content: String) -> Self {
61 Self(content)
62 }
63
64 pub fn get_inner(&self) -> &str {
65 &self.0
66 }
67
68 pub fn into_inner(self) -> String {
69 self.0
70 }
71
72 pub fn is_empty(&self) -> bool {
73 self.0.is_empty()
74 }
75
76 pub fn from_bytes(bytes: &[u8]) -> Self {
77 if bytes.is_empty() {
78 return Self(String::new());
79 }
80
81 fn normalize(c: u8) -> u8 {
82 if c == (b'\r' | 0x80) || c == (b'\n' | 0x80) {
83 c & 0x7f
84 } else {
85 c
86 }
87 }
88
89 let normalized: Vec<u8> = bytes
90 .iter()
91 .copied()
92 .map(normalize)
93 .chain(std::iter::once(normalize(*bytes.last().unwrap())))
94 .map_windows(|[a, b]| {
95 if *a == b'\r' && *b == b'\n' {
96 None
97 } else {
98 Some(*a)
99 }
100 })
101 .flatten()
102 .collect();
103
104 let result = String::from_utf8_lossy(&normalized).to_string();
105
106 Self(result)
107 }
108
109 pub fn to_bytes(&self) -> Vec<u8> {
110 let mut out = Vec::with_capacity(self.0.len() + 2);
111
112 for &b in self.0.as_bytes() {
113 if b == b'\n' {
114 out.extend_from_slice(b"\r\n");
115 } else {
116 out.push(b);
117 }
118 }
119
120 if !self.0.ends_with('\n') {
121 out.extend_from_slice(b"\r\n");
122 }
123
124 out
125 }
126}
127
128impl From<String> for RawFingerResponse {
129 fn from(s: String) -> Self {
130 Self::new(s)
131 }
132}
133
134impl From<&str> for RawFingerResponse {
135 fn from(s: &str) -> Self {
136 Self::new(s.to_string())
137 }
138}
139
140#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
141pub enum FingerResponseUserEntry {
142 Structured(Box<FingerResponseStructuredUserEntry>),
143 Raw(String),
144}
145
146#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
147pub struct FingerResponseStructuredUserEntry {
148 pub username: String,
150
151 pub full_name: String,
153
154 pub home_dir: PathBuf,
156
157 pub shell: PathBuf,
159
160 pub office: Option<String>,
162
163 pub office_phone: Option<String>,
165
166 pub home_phone: Option<String>,
168
169 pub never_logged_in: bool,
171
172 pub sessions: Vec<FingerResponseUserSession>,
174
175 pub forward_status: Option<String>,
177
178 pub mail_status: Option<MailStatus>,
180
181 pub pgp_key: Option<String>,
183
184 pub project: Option<String>,
186
187 pub plan: Option<String>,
189}
190
191impl FingerResponseStructuredUserEntry {
192 #[allow(clippy::too_many_arguments)]
193 pub fn new(
194 username: String,
195 full_name: String,
196 home_dir: PathBuf,
197 shell: PathBuf,
198 office: Option<String>,
199 office_phone: Option<String>,
200 home_phone: Option<String>,
201 never_logged_in: bool,
202 sessions: Vec<FingerResponseUserSession>,
203 forward_status: Option<String>,
204 mail_status: Option<MailStatus>,
205 pgp_key: Option<String>,
206 project: Option<String>,
207 plan: Option<String>,
208 ) -> Self {
209 debug_assert!(
210 !never_logged_in || sessions.is_empty(),
211 "User cannot be marked as never logged in while having active sessions"
212 );
213
214 Self {
215 username,
216 full_name,
217 home_dir,
218 shell,
219 office,
220 office_phone,
221 home_phone,
222 never_logged_in,
223 sessions,
224 forward_status,
225 mail_status,
226 pgp_key,
227 project,
228 plan,
229 }
230 }
231
232 pub fn try_from_raw_finger_response(
234 response: &RawFingerResponse,
235 username: String,
236 ) -> anyhow::Result<Self> {
237 try_parse_structured_user_entry_from_raw_finger_response(response, username)
238 }
239
240 pub fn classic_format(&self) -> String {
241 classic_format_finger_response_structured_user_entry(self)
242 }
243}
244
245#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
246pub enum MailStatus {
247 NoMail,
248 NewMailReceived {
249 #[serde(with = "chrono::serde::ts_seconds")]
250 received_time: DateTime<Utc>,
251 #[serde(with = "chrono::serde::ts_seconds")]
252 unread_since: DateTime<Utc>,
253 },
254 MailLastRead(#[serde(with = "chrono::serde::ts_seconds")] DateTime<Utc>),
255}
256
257#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
258pub struct FingerResponseUserSession {
259 pub tty: String,
261
262 #[serde(with = "chrono::serde::ts_seconds")]
264 pub login_time: DateTime<Utc>,
265
266 pub host: Option<String>,
268
269 #[serde(with = "duration_serde::duration_seconds_option")]
271 pub idle_time: Option<TimeDelta>,
272
273 pub messages_on: bool,
275}
276
277impl FingerResponseUserSession {
278 pub fn new(
279 tty: String,
280 login_time: DateTime<Utc>,
281 host: Option<String>,
282 idle_time: Option<TimeDelta>,
283 messages_on: bool,
284 ) -> Self {
285 Self {
286 tty,
287 login_time,
288 host,
289 idle_time,
290 messages_on,
291 }
292 }
293}
294
295#[cfg(test)]
296mod tests {
297 use super::*;
298
299 #[test]
300 fn test_finger_raw_serialization_roundrip() {
301 let request = FingerRequest::new(true, "alice".to_string());
302 let bytes = request.to_bytes();
303 let deserialized = FingerRequest::from_bytes(&bytes);
304 assert_eq!(request, deserialized);
305
306 let request2 = FingerRequest::new(false, "bob".to_string());
307 let bytes2 = request2.to_bytes();
308 let deserialized2 = FingerRequest::from_bytes(&bytes2);
309 assert_eq!(request2, deserialized2);
310
311 let response = RawFingerResponse::new("Hello, World!\nThis is a test.\n".to_string());
312 let response_bytes = response.to_bytes();
313 let deserialized_response = RawFingerResponse::from_bytes(&response_bytes);
314 assert_eq!(response, deserialized_response);
315
316 let response2 = RawFingerResponse::new("Single line response\n".to_string());
317 let response_bytes2 = response2.to_bytes();
318 let deserialized_response2 = RawFingerResponse::from_bytes(&response_bytes2);
319 assert_eq!(response2, deserialized_response2);
320 }
321}