99
1010use clap:: Parser ;
1111use 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;
1216use std:: io:: Write ;
17+ use std:: io:: { stderr, stdout, ErrorKind } ;
1318use 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