Skip to content

Commit d11708e

Browse files
readlink: implement -f
1 parent a08d82d commit d11708e

File tree

1 file changed

+178
-29
lines changed

1 file changed

+178
-29
lines changed

tree/readlink.rs

Lines changed: 178 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,14 @@
99

1010
use clap::Parser;
1111
use gettextrs::{bind_textdomain_codeset, setlocale, textdomain, LocaleCategory};
12+
use std::collections::HashSet;
13+
use std::error::Error;
14+
use std::ffi::OsString;
15+
use std::fs;
1216
use std::io::Write;
17+
use std::io::{stderr, stdout, ErrorKind};
1318
use std::path::PathBuf;
14-
use std::{fs, io};
19+
use std::path::{Component, Path};
1520

1621
/// readlink — display the contents of a symbolic link
1722
#[derive(Parser)]
@@ -21,33 +26,143 @@ struct Args {
2126
#[arg(short, long)]
2227
no_newline: bool,
2328

29+
// Not POSIX, but implemented by BusyBox, FreeBSD, GNU Core Utilities, toybox, and others
30+
/// Canonicalize the provided path, resolving symbolic links repeatedly if needed. The absolute path of the resolved file is printed.
31+
#[arg(short = 'f')]
32+
canonicalize: bool,
33+
2434
/// The pathname of an existing symbolic link
2535
pathname: PathBuf,
2636
}
2737

28-
fn do_readlink(args: &Args) -> Result<String, String> {
29-
let path = PathBuf::from(&args.pathname);
38+
// Behavior of "readlink -f /existent-directory/non-existent-file" varies
39+
// Most implementations: print hypothetical fully resolved absolute path, exit code 0
40+
// bsdutils/FreeBSD, toybox: print nothing, exit code 1
41+
//
42+
// Behavior of "readlink -f /non-existent-directory/non-existent-file" does not vary
43+
// All implementations: print nothing, exit code 1
44+
fn do_readlink(args: Args) -> Result<String, String> {
45+
// TODO
46+
// Add --verbose option and mirror other implementations (which are quiet by default)?
47+
let verbose = true;
48+
49+
let Args {
50+
no_newline,
51+
canonicalize,
52+
pathname,
53+
} = args;
54+
55+
let pathname_path = pathname.as_path();
56+
57+
let format_error = |description: &str, error: Option<&dyn Error>| {
58+
let pathname_path_display = pathname_path.display();
59+
60+
let st = if let Some(er) = error {
61+
format!("{pathname_path_display}: {description}: {er}")
62+
} else {
63+
format!("{pathname_path_display}: {description}")
64+
};
3065

31-
match fs::read_link(&path) {
32-
Ok(target) => {
33-
let output = target.display().to_string();
34-
if args.no_newline {
35-
Ok(output)
36-
} else {
37-
Ok(output + "\n")
66+
Result::<String, String>::Err(st)
67+
};
68+
69+
let format_returned_path = |path_to_return: &Path| {
70+
let path_to_return_display = path_to_return.display();
71+
72+
let st = if no_newline {
73+
format!("{path_to_return_display}")
74+
} else {
75+
format!("{path_to_return_display}\n")
76+
};
77+
78+
Result::<String, String>::Ok(st)
79+
};
80+
81+
let map_io_error = |error: &std::io::Error| {
82+
match error.kind() {
83+
ErrorKind::NotFound => {
84+
// All or almost all other implementations do not print an error here
85+
// (but they do exit with exit code 1)
86+
if verbose {
87+
format_error("No such file or directory", None)
88+
} else {
89+
Err(String::new())
90+
}
3891
}
92+
ErrorKind::PermissionDenied => {
93+
if verbose {
94+
format_error("Permission denied", None)
95+
} else {
96+
Err(String::new())
97+
}
98+
}
99+
_ => format_error("Unknown error", Some(&error)),
39100
}
40-
Err(e) => {
41-
let err_message = match e.kind() {
42-
io::ErrorKind::NotFound => {
43-
format!("readlink: {}: No such file or directory\n", path.display())
101+
};
102+
103+
if canonicalize {
104+
let recursively_resolved_path_buf = recursive_resolve(pathname_path.to_owned())?;
105+
106+
match fs::canonicalize(recursively_resolved_path_buf.as_path()) {
107+
Ok(pa) => format_returned_path(pa.as_path()),
108+
Err(er) => {
109+
let mut components = recursively_resolved_path_buf.components();
110+
111+
// Check if the last component of the path is a "normal" component
112+
// (e.g. "normal-component" in "/prefix/normal-component/suffix")
113+
//
114+
// If so, the fallback path (since the path itself could not be canonicalized)
115+
// is to canonicalize the parent directory path, and append the last path component
116+
if let Some(Component::Normal(last_component)) = components.next_back() {
117+
let parent_path = components.as_path();
118+
119+
match fs::canonicalize(parent_path) {
120+
Ok(parent_path_canonicalized) => {
121+
// Before printing the hypothetical resolved path:
122+
// ensure that the parent is actually a directory
123+
if !parent_path_canonicalized.is_dir() {
124+
return format_error("Not a directory", None);
125+
}
126+
127+
let parent_path_canonicalized_with_last_component = {
128+
let mut pa = parent_path_canonicalized;
129+
130+
pa.push(last_component);
131+
132+
pa
133+
};
134+
135+
format_returned_path(
136+
parent_path_canonicalized_with_last_component.as_path(),
137+
)
138+
}
139+
Err(err) => map_io_error(&err),
140+
}
141+
} else {
142+
map_io_error(&er)
44143
}
45-
io::ErrorKind::InvalidInput => {
46-
format!("readlink: {}: Not a symbolic link\n", path.display())
144+
}
145+
}
146+
} else {
147+
match fs::symlink_metadata(pathname_path) {
148+
Ok(me) => {
149+
if !me.is_symlink() {
150+
// POSIX says:
151+
// "If file does not name a symbolic link, readlink shall write a diagnostic message to standard error and exit with non-zero status."
152+
// However, this is violated by almost all implementations
153+
return if verbose {
154+
format_error("Not a symbolic link", None)
155+
} else {
156+
Err(String::new())
157+
};
47158
}
48-
_ => format!("readlink: {}: {}\n", path.display(), e),
49-
};
50-
Err(err_message)
159+
160+
match fs::read_link(pathname_path) {
161+
Ok(pa) => format_returned_path(pa.as_path()),
162+
Err(er) => map_io_error(&er),
163+
}
164+
}
165+
Err(er) => map_io_error(&er),
51166
}
52167
}
53168
}
@@ -59,19 +174,53 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
59174

60175
let args = Args::parse();
61176

62-
let mut exit_code = 0;
63-
64-
match do_readlink(&args) {
177+
let exit_code = match do_readlink(args) {
65178
Ok(output) => {
66-
print!("{}", output);
67-
io::stdout().flush().unwrap();
179+
let mut stdout_lock = stdout().lock();
180+
181+
write!(stdout_lock, "{output}").unwrap();
182+
183+
stdout_lock.flush().unwrap();
184+
185+
0_i32
68186
}
69-
Err(err) => {
70-
eprint!("{}", err);
71-
io::stderr().flush().unwrap();
72-
exit_code = 1;
187+
Err(error_description) => {
188+
let mut stderr_lock = stderr().lock();
189+
190+
writeln!(&mut stderr_lock, "readlink: {error_description}").unwrap();
191+
192+
stderr_lock.flush().unwrap();
193+
194+
1_i32
73195
}
74-
}
196+
};
75197

76198
std::process::exit(exit_code);
77199
}
200+
201+
fn recursive_resolve(starting_path_buf: PathBuf) -> Result<PathBuf, String> {
202+
let mut current_path_buf = starting_path_buf;
203+
204+
let mut encountered_paths = HashSet::<OsString>::new();
205+
206+
#[allow(clippy::while_let_loop)]
207+
loop {
208+
match fs::read_link(current_path_buf.as_path()) {
209+
Ok(pa) => {
210+
if !encountered_paths.insert(pa.as_os_str().to_owned()) {
211+
return Err(format!(
212+
"Infinite symbolic link loop detected at \"{}\")",
213+
pa.to_string_lossy()
214+
));
215+
}
216+
217+
current_path_buf = pa;
218+
}
219+
Err(_) => {
220+
break;
221+
}
222+
}
223+
}
224+
225+
Ok(current_path_buf)
226+
}

0 commit comments

Comments
 (0)