Skip to main content

leenfetch_core/modules/linux/
title.rs

1use std::env;
2use std::ffi::CString;
3use std::fs;
4
5/// Return (username, hostname, combined length) as a tuple.
6///
7/// FQDN is passed to `get_hostname` to determine whether to include the domain.
8///
9/// The length is the sum of the lengths of the username and hostname,
10/// plus one for the '@' separator.
11pub fn get_titles(fqdn: bool) -> (String, String) {
12    let user = get_user();
13    let host = get_hostname(fqdn);
14
15    (user, host)
16}
17
18fn get_user() -> String {
19    // 1. Try $USER environment variable (fastest)
20    if let Some(u) = env::var_os("USER") {
21        let s = u.to_string_lossy();
22        if !s.is_empty() {
23            return s.into();
24        }
25    }
26
27    // 2. Use getuid() + getpwuid() syscalls (no process spawn)
28    let uid = unsafe { libc::getuid() };
29    if uid > 0 {
30        // Try to get username from passwd entry
31        if let Some(pw) = get_pwuid(uid) {
32            return pw;
33        }
34    }
35
36    // 3. Fallback: extract from $HOME
37    if let Ok(home) = env::var("HOME") {
38        if let Some(name) = home.rsplit('/').find(|s| !s.is_empty()) {
39            return name.to_string();
40        }
41    }
42
43    // 4. Worst-case
44    "unknown".into()
45}
46
47fn get_pwuid(uid: libc::uid_t) -> Option<String> {
48    // Use getpwuid_r for thread-safe passwd lookup
49    let mut passwd = MaybeUninit::uninit();
50    let mut buf = vec![0u8; 1024];
51
52    let result = unsafe {
53        let mut pwd_ptr: *mut libc::passwd = std::ptr::null_mut();
54        libc::getpwuid_r(
55            uid,
56            passwd.as_mut_ptr(),
57            buf.as_mut_ptr() as *mut libc::c_char,
58            buf.len(),
59            &mut pwd_ptr,
60        )
61    };
62
63    if result == 0 && !passwd.as_ptr().is_null() {
64        let pwd = unsafe { passwd.assume_init() };
65        if !pwd.pw_name.is_null() {
66            let name = unsafe { CString::from_raw(pwd.pw_name) };
67            return Some(name.to_string_lossy().into_owned());
68        }
69    }
70    None
71}
72
73fn get_hostname(fqdn: bool) -> String {
74    // 1. Try gethostname() syscall first (fastest)
75    let mut buf = [0u8; 256];
76    let len = unsafe { libc::gethostname(buf.as_mut_ptr() as *mut libc::c_char, buf.len()) };
77    if len == 0 {
78        let s = String::from_utf8_lossy(&buf)
79            .trim_end_matches('\0')
80            .to_string();
81        if !s.is_empty() && (s != "localhost" || !fqdn) {
82            if fqdn {
83                // Try to get FQDN
84                if let Ok(fqdn_name) = fs::read_to_string("/etc/hostname") {
85                    let trimmed = fqdn_name.trim().to_string();
86                    if !trimmed.is_empty() && trimmed.contains('.') {
87                        return trimmed;
88                    }
89                }
90                // Try DNS domain from /etc/resolv.conf or nsswitch
91                if let Ok(domain) = fs::read_to_string("/etc/resolv.conf") {
92                    for line in domain.lines() {
93                        if line.starts_with("domain ") {
94                            let domain = line[7..].trim();
95                            if !domain.is_empty() {
96                                return format!("{}.{}", s, domain);
97                            }
98                        }
99                    }
100                }
101            }
102            return s;
103        }
104    }
105
106    // 2. Try HOSTNAME environment variable
107    if let Some(h) = env::var_os("HOSTNAME") {
108        let s = h.to_string_lossy();
109        if !s.is_empty() {
110            return s.into();
111        }
112    }
113
114    // 3. Fallback: read /etc/hostname
115    if let Ok(hostname) = fs::read_to_string("/etc/hostname") {
116        let s = hostname.trim().to_string();
117        if !s.is_empty() {
118            return s;
119        }
120    }
121
122    "localhost".into()
123}
124
125use std::mem::MaybeUninit;
126
127#[cfg(test)]
128mod tests {
129    use super::*;
130    use crate::test_utils::EnvLock;
131
132    #[test]
133    fn test_get_user_from_env() {
134        let env_lock = EnvLock::acquire(&["USER"]);
135        env_lock.set_var("USER", "testuser");
136        assert_eq!(get_user(), "testuser");
137        drop(env_lock);
138    }
139
140    #[test]
141    fn test_hostname_from_env() {
142        let env_lock = EnvLock::acquire(&["HOSTNAME"]);
143        env_lock.set_var("HOSTNAME", "testhost");
144        assert_eq!(get_hostname(false), "testhost");
145        drop(env_lock);
146    }
147
148    #[test]
149    fn test_hostname_command_fallback() {
150        let env_lock = EnvLock::acquire(&["HOSTNAME"]);
151        env_lock.remove_var("HOSTNAME");
152        let result = get_hostname(false);
153        assert!(!result.is_empty(), "Hostname should not be empty");
154        drop(env_lock);
155    }
156
157    #[test]
158    fn test_hostname_final_fallback() {
159        // This test can't force full fallback easily since `hostname` command always exists,
160        // but we can at least ensure it's non-empty
161        let result = get_hostname(false);
162        assert!(!result.is_empty());
163    }
164}