1
mod classic_formatter;
2
mod parser;
3

            
4
use std::path::PathBuf;
5

            
6
use chrono::{DateTime, TimeDelta, Utc};
7
use serde::{Deserialize, Serialize};
8

            
9
use 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)]
15
pub struct FingerRequest {
16
    long: bool,
17
    name: String,
18
}
19

            
20
impl FingerRequest {
21
4
    pub fn new(long: bool, name: String) -> Self {
22
4
        Self { long, name }
23
4
    }
24

            
25
2
    pub fn to_bytes(&self) -> Vec<u8> {
26
2
        let mut result = Vec::new();
27
2
        if self.long {
28
1
            result.extend(b"/W ");
29
1
        }
30

            
31
2
        result.extend(self.name.as_bytes());
32
2
        result.extend(b"\r\n");
33

            
34
2
        result
35
2
    }
36

            
37
2
    pub fn from_bytes(bytes: &[u8]) -> Self {
38
2
        let (long, name) = if &bytes[..3] == b"/W " {
39
1
            (true, &bytes[3..])
40
        } else {
41
1
            (false, bytes)
42
        };
43

            
44
2
        let name = match name.strip_suffix(b"\r\n") {
45
2
            Some(new_name) => new_name,
46
            None => name,
47
        };
48

            
49
2
        Self::new(long, String::from_utf8_lossy(name).to_string())
50
2
    }
51
}
52

            
53
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
54
pub struct RawFingerResponse(String);
55

            
56
impl RawFingerResponse {
57
12
    pub fn new(content: String) -> Self {
58
12
        Self(content)
59
12
    }
60

            
61
10
    pub fn get_inner(&self) -> &str {
62
10
        &self.0
63
10
    }
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
2
    pub fn from_bytes(bytes: &[u8]) -> Self {
74
2
        if bytes.is_empty() {
75
            return Self(String::new());
76
2
        }
77

            
78
56
        fn normalize(c: u8) -> u8 {
79
56
            if c == (b'\r' | 0x80) || c == (b'\n' | 0x80) {
80
                c & 0x7f
81
            } else {
82
56
                c
83
            }
84
56
        }
85

            
86
2
        let normalized: Vec<u8> = bytes
87
2
            .iter()
88
2
            .copied()
89
2
            .map(normalize)
90
2
            .chain(std::iter::once(normalize(*bytes.last().unwrap())))
91
54
            .map_windows(|[a, b]| {
92
54
                if *a == b'\r' && *b == b'\n' {
93
3
                    None
94
                } else {
95
51
                    Some(*a)
96
                }
97
54
            })
98
2
            .flatten()
99
2
            .collect();
100

            
101
2
        let result = String::from_utf8_lossy(&normalized).to_string();
102

            
103
2
        Self(result)
104
2
    }
105

            
106
2
    pub fn to_bytes(&self) -> Vec<u8> {
107
2
        let mut out = Vec::with_capacity(self.0.len() + 2);
108

            
109
51
        for &b in self.0.as_bytes() {
110
51
            if b == b'\n' {
111
3
                out.extend_from_slice(b"\r\n");
112
48
            } else {
113
48
                out.push(b);
114
48
            }
115
        }
116

            
117
2
        if !self.0.ends_with('\n') {
118
            out.extend_from_slice(b"\r\n");
119
2
        }
120

            
121
2
        out
122
2
    }
123
}
124

            
125
impl From<String> for RawFingerResponse {
126
10
    fn from(s: String) -> Self {
127
10
        Self::new(s)
128
10
    }
129
}
130

            
131
impl 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)]
138
pub enum FingerResponseUserEntry {
139
    Structured(Box<FingerResponseStructuredUserEntry>),
140
    Raw(String),
141
}
142

            
143
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
144
pub struct FingerResponseStructuredUserEntry {
145
    /// The unix username of this user, as noted in passwd
146
    pub username: String,
147

            
148
    /// The full name of this user, as noted in passwd
149
    pub full_name: String,
150

            
151
    /// The path to the home directory of this user, as noted in passwd
152
    pub home_dir: PathBuf,
153

            
154
    /// The path to the shell of this user, as noted in passwd
155
    pub shell: PathBuf,
156

            
157
    /// Office location, if available
158
    pub office: Option<String>,
159

            
160
    /// Office phone number, if available
161
    pub office_phone: Option<String>,
162

            
163
    /// Home phone number, if available
164
    pub home_phone: Option<String>,
165

            
166
    /// Whether the user has never logged in to this host
167
    pub never_logged_in: bool,
168

            
169
    /// A list of user sessions, sourced from utmp entries
170
    pub sessions: Vec<FingerResponseUserSession>,
171

            
172
    /// Contents of ~/.forward, if it exists
173
    pub forward_status: Option<String>,
174

            
175
    /// Whether the user has new or unread mail
176
    pub mail_status: Option<MailStatus>,
177

            
178
    /// Contents of ~/.pgpkey, if it exists
179
    pub pgp_key: Option<String>,
180

            
181
    /// Contents of ~/.project, if it exists
182
    pub project: Option<String>,
183

            
184
    /// Contents of ~/.plan, if it exists
185
    pub plan: Option<String>,
186
}
187

            
188
impl FingerResponseStructuredUserEntry {
189
    #[allow(clippy::too_many_arguments)]
190
11
    pub fn new(
191
11
        username: String,
192
11
        full_name: String,
193
11
        home_dir: PathBuf,
194
11
        shell: PathBuf,
195
11
        office: Option<String>,
196
11
        office_phone: Option<String>,
197
11
        home_phone: Option<String>,
198
11
        never_logged_in: bool,
199
11
        sessions: Vec<FingerResponseUserSession>,
200
11
        forward_status: Option<String>,
201
11
        mail_status: Option<MailStatus>,
202
11
        pgp_key: Option<String>,
203
11
        project: Option<String>,
204
11
        plan: Option<String>,
205
11
    ) -> Self {
206
11
        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
11
        Self {
212
11
            username,
213
11
            full_name,
214
11
            home_dir,
215
11
            shell,
216
11
            office,
217
11
            office_phone,
218
11
            home_phone,
219
11
            never_logged_in,
220
11
            sessions,
221
11
            forward_status,
222
11
            mail_status,
223
11
            pgp_key,
224
11
            project,
225
11
            plan,
226
11
        }
227
11
    }
228

            
229
    /// Try parsing a [FingerResponseUserEntry] from the text format used by bsd-finger.
230
10
    pub fn try_from_raw_finger_response(
231
10
        response: &RawFingerResponse,
232
10
        username: String,
233
10
    ) -> anyhow::Result<Self> {
234
10
        try_parse_structured_user_entry_from_raw_finger_response(response, username)
235
10
    }
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)]
243
pub 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)]
253
pub struct FingerResponseUserSession {
254
    /// The tty on which this session exists
255
    pub tty: String,
256

            
257
    /// When the user logged in and created this session
258
    pub login_time: DateTime<Utc>,
259

            
260
    /// The hostname or address of the machine from which the user is logged in, if available
261
    pub host: Option<String>,
262

            
263
    /// The amount of time since the use last interacted with the tty
264
    pub idle_time: Option<TimeDelta>,
265

            
266
    /// Whether this tty is writable, and thus can receive messages via `mesg(1)`
267
    pub messages_on: bool,
268
}
269

            
270
impl FingerResponseUserSession {
271
8
    pub fn new(
272
8
        tty: String,
273
8
        login_time: DateTime<Utc>,
274
8
        host: Option<String>,
275
8
        idle_time: Option<TimeDelta>,
276
8
        messages_on: bool,
277
8
    ) -> Self {
278
8
        Self {
279
8
            tty,
280
8
            login_time,
281
8
            host,
282
8
            idle_time,
283
8
            messages_on,
284
8
        }
285
8
    }
286
}
287

            
288
#[cfg(test)]
289
mod tests {
290
    use super::*;
291

            
292
    #[test]
293
1
    fn test_finger_raw_serialization_roundrip() {
294
1
        let request = FingerRequest::new(true, "alice".to_string());
295
1
        let bytes = request.to_bytes();
296
1
        let deserialized = FingerRequest::from_bytes(&bytes);
297
1
        assert_eq!(request, deserialized);
298

            
299
1
        let request2 = FingerRequest::new(false, "bob".to_string());
300
1
        let bytes2 = request2.to_bytes();
301
1
        let deserialized2 = FingerRequest::from_bytes(&bytes2);
302
1
        assert_eq!(request2, deserialized2);
303

            
304
1
        let response = RawFingerResponse::new("Hello, World!\nThis is a test.\n".to_string());
305
1
        let response_bytes = response.to_bytes();
306
1
        let deserialized_response = RawFingerResponse::from_bytes(&response_bytes);
307
1
        assert_eq!(response, deserialized_response);
308

            
309
1
        let response2 = RawFingerResponse::new("Single line response\n".to_string());
310
1
        let response_bytes2 = response2.to_bytes();
311
1
        let deserialized_response2 = RawFingerResponse::from_bytes(&response_bytes2);
312
1
        assert_eq!(response2, deserialized_response2);
313
1
    }
314
}