Skip to main content

linuxutils_misc/
namei.rs

1use linuxutils_common::man::ManContent;
2
3pub const MAN: ManContent = ManContent::empty();
4
5use clap::Parser;
6use rustix::fs::{FileType, RawMode, Stat};
7use std::{
8    collections::HashSet,
9    fs, io,
10    path::{Path, PathBuf},
11    process::ExitCode,
12};
13
14#[derive(Parser)]
15#[command(
16    name = "namei",
17    about = "Follow a pathname until a terminal point is found",
18    override_usage = "namei [options] <pathname>..."
19)]
20pub struct Args {
21    /// Long listing (equivalent to -m -o -v)
22    #[arg(short = 'l', long)]
23    long: bool,
24
25    /// Show the mode bits of each file type
26    #[arg(short = 'm', long)]
27    modes: bool,
28
29    /// Don't follow symlinks
30    #[arg(short = 'n', long)]
31    nosymlinks: bool,
32
33    /// Show owner and group name
34    #[arg(short = 'o', long)]
35    owners: bool,
36
37    /// Vertically align modes and owners
38    #[arg(short = 'v', long)]
39    vertical: bool,
40
41    /// Show mountpoints with 'D' instead of 'd'
42    #[arg(short = 'x', long)]
43    mountpoints: bool,
44
45    /// Pathnames to follow
46    #[arg(required = true)]
47    pathnames: Vec<String>,
48}
49
50struct Options {
51    modes: bool,
52    owners: bool,
53    vertical: bool,
54    nosymlinks: bool,
55    mountpoints: bool,
56}
57
58fn format_mode(mode: RawMode) -> String {
59    let mut s = String::with_capacity(9);
60    let perms = [
61        (0o400, 'r'),
62        (0o200, 'w'),
63        (0o100, 'x'),
64        (0o040, 'r'),
65        (0o020, 'w'),
66        (0o010, 'x'),
67        (0o004, 'r'),
68        (0o002, 'w'),
69        (0o001, 'x'),
70    ];
71    for (bit, ch) in perms {
72        if mode & bit != 0 {
73            s.push(ch);
74        } else {
75            s.push('-');
76        }
77    }
78    s
79}
80
81fn uid_to_name(uid: u32) -> String {
82    fs::read_to_string("/etc/passwd")
83        .ok()
84        .and_then(|content| {
85            for line in content.lines() {
86                let fields: Vec<&str> = line.split(':').collect();
87                if fields.len() >= 3
88                    && let Ok(u) = fields[2].parse::<u32>()
89                    && u == uid
90                {
91                    return Some(fields[0].to_string());
92                }
93            }
94            None
95        })
96        .unwrap_or_else(|| uid.to_string())
97}
98
99fn gid_to_name(gid: u32) -> String {
100    fs::read_to_string("/etc/group")
101        .ok()
102        .and_then(|content| {
103            for line in content.lines() {
104                let fields: Vec<&str> = line.split(':').collect();
105                if fields.len() >= 3
106                    && let Ok(g) = fields[2].parse::<u32>()
107                    && g == gid
108                {
109                    return Some(fields[0].to_string());
110                }
111            }
112            None
113        })
114        .unwrap_or_else(|| gid.to_string())
115}
116
117fn file_type_char(stat: &Stat, is_mountpoint: bool, opts: &Options) -> char {
118    let ft = FileType::from_raw_mode(stat.st_mode);
119    if ft == FileType::Directory {
120        if opts.mountpoints && is_mountpoint {
121            'D'
122        } else {
123            'd'
124        }
125    } else if ft == FileType::Symlink {
126        'l'
127    } else if ft == FileType::RegularFile {
128        '-'
129    } else if ft == FileType::Socket {
130        's'
131    } else if ft == FileType::BlockDevice {
132        'b'
133    } else if ft == FileType::CharacterDevice {
134        'c'
135    } else if ft == FileType::Fifo {
136        'p'
137    } else {
138        '?'
139    }
140}
141
142fn stat_path(path: &Path) -> io::Result<Stat> {
143    use rustix::fs::{AtFlags, CWD};
144    rustix::fs::statat(CWD, path, AtFlags::SYMLINK_NOFOLLOW)
145        .map_err(io::Error::from)
146}
147
148fn is_mountpoint(path: &Path, child_dev: u64) -> bool {
149    let parent = match path.parent() {
150        Some(p) if p == Path::new("") => Path::new("."),
151        Some(p) => p,
152        None => return true,
153    };
154    match stat_path(parent) {
155        Ok(parent_stat) => parent_stat.st_dev != child_dev,
156        Err(_) => false,
157    }
158}
159
160fn print_entry(
161    type_char: char,
162    name: &str,
163    stat: Option<&Stat>,
164    link_target: Option<&str>,
165    indent: usize,
166    opts: &Options,
167) {
168    let indent_str = " ".repeat(indent * 2);
169
170    if opts.modes || opts.owners {
171        if let Some(st) = stat {
172            if opts.vertical {
173                let mode_str = if opts.modes {
174                    format_mode(st.st_mode)
175                } else {
176                    String::new()
177                };
178                let owner_str = if opts.owners {
179                    format!(
180                        "{} {}",
181                        uid_to_name(st.st_uid),
182                        gid_to_name(st.st_gid)
183                    )
184                } else {
185                    String::new()
186                };
187                if opts.modes && opts.owners {
188                    print!("{indent_str}{mode_str} {owner_str} ");
189                } else if opts.modes {
190                    print!("{indent_str}{mode_str} ");
191                } else {
192                    print!("{indent_str}{owner_str} ");
193                }
194            } else {
195                let mut parts = Vec::new();
196                if opts.modes {
197                    parts.push(format_mode(st.st_mode));
198                }
199                if opts.owners {
200                    parts.push(format!(
201                        "{} {}",
202                        uid_to_name(st.st_uid),
203                        gid_to_name(st.st_gid)
204                    ));
205                }
206                print!("{indent_str}{} ", parts.join(" "));
207            }
208        } else {
209            print!("{indent_str}");
210        }
211    } else {
212        print!("{indent_str}");
213    }
214
215    print!("{type_char} {name}");
216    if let Some(target) = link_target {
217        print!(" -> {target}");
218    }
219    println!();
220}
221
222fn walk_path(
223    pathname: &str,
224    opts: &Options,
225    visited: &mut HashSet<PathBuf>,
226    indent: usize,
227) {
228    let components: Vec<&str> = pathname.split('/').collect();
229
230    let mut current = PathBuf::new();
231    let mut first = true;
232
233    for component in &components {
234        if first && component.is_empty() {
235            // Absolute path: starts with "/"
236            current.push("/");
237            let stat_result = stat_path(&current);
238            match &stat_result {
239                Ok(st) => {
240                    let mp =
241                        opts.mountpoints && is_mountpoint(&current, st.st_dev);
242                    let tc = file_type_char(st, mp, opts);
243                    print_entry(tc, "/", Some(st), None, indent, opts);
244                }
245                Err(e) => {
246                    print_entry('?', "/", None, None, indent, opts);
247                    eprintln!("namei: failed to stat /: {e}");
248                }
249            }
250            first = false;
251            continue;
252        }
253
254        if component.is_empty() {
255            first = false;
256            continue;
257        }
258
259        current.push(component);
260        first = false;
261
262        let stat_result = stat_path(&current);
263        match stat_result {
264            Ok(st) => {
265                let ft = FileType::from_raw_mode(st.st_mode);
266                let mp = opts.mountpoints
267                    && ft == FileType::Directory
268                    && is_mountpoint(&current, st.st_dev);
269                let tc = file_type_char(&st, mp, opts);
270
271                if ft == FileType::Symlink {
272                    match fs::read_link(&current) {
273                        Ok(target) => {
274                            let target_str =
275                                target.to_string_lossy().to_string();
276                            print_entry(
277                                tc,
278                                component,
279                                Some(&st),
280                                Some(&target_str),
281                                indent,
282                                opts,
283                            );
284
285                            if !opts.nosymlinks {
286                                let resolved = if target.is_absolute() {
287                                    target.clone()
288                                } else {
289                                    current
290                                        .parent()
291                                        .unwrap_or(Path::new("."))
292                                        .join(&target)
293                                };
294
295                                if visited.contains(&resolved) {
296                                    let warn_indent =
297                                        " ".repeat((indent + 1) * 2);
298                                    println!(
299                                        "{warn_indent}  [loop detected at {}]",
300                                        resolved.display()
301                                    );
302                                } else {
303                                    visited.insert(resolved.clone());
304                                    let walk_target =
305                                        resolved.to_string_lossy().to_string();
306                                    walk_path(
307                                        &walk_target,
308                                        opts,
309                                        visited,
310                                        indent + 1,
311                                    );
312                                }
313
314                                // After resolving the symlink, update current to point
315                                // through the resolved target for subsequent components
316                                current = fs::canonicalize(
317                                    current
318                                        .parent()
319                                        .unwrap_or(Path::new("."))
320                                        .join(&target),
321                                )
322                                .unwrap_or(current);
323                            }
324                        }
325                        Err(e) => {
326                            print_entry(
327                                tc,
328                                component,
329                                Some(&st),
330                                None,
331                                indent,
332                                opts,
333                            );
334                            eprintln!(
335                                "namei: failed to read link {}: {e}",
336                                current.display()
337                            );
338                        }
339                    }
340                } else {
341                    print_entry(tc, component, Some(&st), None, indent, opts);
342                }
343            }
344            Err(e) => {
345                print_entry('?', component, None, None, indent, opts);
346                eprintln!("namei: failed to stat {}: {e}", current.display());
347                return;
348            }
349        }
350    }
351}
352
353pub fn run(args: Args) -> ExitCode {
354    let opts = Options {
355        modes: args.modes || args.long,
356        owners: args.owners || args.long,
357        vertical: args.vertical || args.long,
358        nosymlinks: args.nosymlinks,
359        mountpoints: args.mountpoints,
360    };
361
362    for pathname in &args.pathnames {
363        println!("f: {pathname}");
364        let mut visited = HashSet::new();
365        walk_path(pathname, &opts, &mut visited, 0);
366    }
367
368    ExitCode::SUCCESS
369}