Skip to main content

leenfetch_core/modules/linux/
title.rs

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