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