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 #[arg(short = 'l', long)]
23 long: bool,
24
25 #[arg(short = 'm', long)]
27 modes: bool,
28
29 #[arg(short = 'n', long)]
31 nosymlinks: bool,
32
33 #[arg(short = 'o', long)]
35 owners: bool,
36
37 #[arg(short = 'v', long)]
39 vertical: bool,
40
41 #[arg(short = 'x', long)]
43 mountpoints: bool,
44
45 #[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 current.push("/");
237 let stat_result = stat_path(¤t);
238 match &stat_result {
239 Ok(st) => {
240 let mp =
241 opts.mountpoints && is_mountpoint(¤t, 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(¤t);
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(¤t, st.st_dev);
269 let tc = file_type_char(&st, mp, opts);
270
271 if ft == FileType::Symlink {
272 match fs::read_link(¤t) {
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 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}