99
1010use clap:: Parser ;
1111use gettextrs:: { bind_textdomain_codeset, setlocale, textdomain, LocaleCategory } ;
12+ use std:: error:: Error ;
13+ use std:: fs;
1214use std:: io:: Write ;
15+ use std:: io:: { stderr, stdout, ErrorKind } ;
1316use std:: path:: PathBuf ;
14- use std:: { fs , io } ;
17+ use std:: path :: { Component , Path } ;
1518
1619/// readlink — display the contents of a symbolic link
1720#[ derive( Parser ) ]
@@ -21,33 +24,144 @@ struct Args {
2124 #[ arg( short, long) ]
2225 no_newline : bool ,
2326
27+ // Not POSIX, but implemented by BusyBox, FreeBSD, GNU Core Utilities, toybox, and others
28+ /// Canonicalize the provided path, resolving symbolic links repeatedly if needed. The absolute path of the resolved file is printed.
29+ #[ arg( short = 'f' ) ]
30+ canonicalize : bool ,
31+
32+ /// Print an error description to standard error when an error occurs and the specified file could not be resolved
33+ #[ arg( short = 'v' ) ]
34+ verbose : bool ,
35+
2436 /// The pathname of an existing symbolic link
2537 pathname : PathBuf ,
2638}
2739
28- fn do_readlink ( args : & Args ) -> Result < String , String > {
29- let path = PathBuf :: from ( & args. pathname ) ;
40+ // Behavior of "readlink -f /existent-directory/non-existent-file" varies
41+ // Most implementations: print hypothetical fully resolved absolute path, exit code 0
42+ // bsdutils/FreeBSD, toybox: print nothing, exit code 1
43+ //
44+ // Behavior of "readlink -f /non-existent-directory/non-existent-file" does not vary
45+ // All implementations: print nothing, exit code 1
46+ fn do_readlink ( args : Args ) -> Result < String , String > {
47+ let Args {
48+ no_newline,
49+ canonicalize,
50+ verbose,
51+ pathname,
52+ } = args;
53+
54+ let pathname_path = pathname. as_path ( ) ;
55+
56+ let format_error = |description : & str , error : Option < & dyn Error > | {
57+ let pathname_path_display = pathname_path. display ( ) ;
58+
59+ let st = if let Some ( er) = error {
60+ format ! ( "{pathname_path_display}: {description}: {er}" )
61+ } else {
62+ format ! ( "{pathname_path_display}: {description}" )
63+ } ;
64+
65+ Result :: < String , String > :: Err ( st)
66+ } ;
3067
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 " )
68+ let format_returned_path = |path_to_return : & Path | {
69+ let path_to_return_display = path_to_return. display ( ) ;
70+
71+ let st = if no_newline {
72+ format ! ( "{path_to_return_display}" )
73+ } else {
74+ format ! ( "{path_to_return_display}\n " )
75+ } ;
76+
77+ Result :: < String , String > :: Ok ( st)
78+ } ;
79+
80+ let map_io_error = |error : & std:: io:: Error | {
81+ match error. kind ( ) {
82+ ErrorKind :: NotFound => {
83+ // All or almost all other implementations do not print an error here
84+ // (but they do exit with exit code 1)
85+ if verbose {
86+ format_error ( "No such file or directory" , None )
87+ } else {
88+ Err ( String :: new ( ) )
89+ }
90+ }
91+ ErrorKind :: PermissionDenied => {
92+ if verbose {
93+ format_error ( "Permission denied" , None )
94+ } else {
95+ Err ( String :: new ( ) )
96+ }
3897 }
98+ _ => format_error ( "Unknown error" , Some ( & error) ) ,
3999 }
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( ) )
100+ } ;
101+
102+ if canonicalize {
103+ let recursively_resolved_path_buf = recursive_resolve ( pathname_path. to_owned ( ) ) ?;
104+
105+ match fs:: canonicalize ( recursively_resolved_path_buf. as_path ( ) ) {
106+ Ok ( pa) => format_returned_path ( pa. as_path ( ) ) ,
107+ Err ( er) => {
108+ let mut components = recursively_resolved_path_buf. components ( ) ;
109+
110+ // Check if the last component of the path is a "normal" component
111+ // (e.g. "normal-component" in "/prefix/normal-component/suffix")
112+ //
113+ // If so, the fallback path (since the path itself could not be canonicalized)
114+ // is to canonicalize the parent directory path, and append the last path component
115+ if let Some ( Component :: Normal ( last_component) ) = components. next_back ( ) {
116+ let parent_path = components. as_path ( ) ;
117+
118+ match fs:: canonicalize ( parent_path) {
119+ Ok ( parent_path_canonicalized) => {
120+ // Before printing the hypothetical resolved path:
121+ // ensure that the parent is actually a directory
122+ if !parent_path_canonicalized. is_dir ( ) {
123+ return format_error ( "Not a directory" , None ) ;
124+ }
125+
126+ let parent_path_canonicalized_with_last_component = {
127+ let mut pa = parent_path_canonicalized;
128+
129+ pa. push ( last_component) ;
130+
131+ pa
132+ } ;
133+
134+ format_returned_path (
135+ parent_path_canonicalized_with_last_component. as_path ( ) ,
136+ )
137+ }
138+ Err ( err) => map_io_error ( & err) ,
139+ }
140+ } else {
141+ map_io_error ( & er)
142+ }
143+ }
144+ }
145+ } else {
146+ match fs:: symlink_metadata ( pathname_path) {
147+ Ok ( me) => {
148+ if !me. is_symlink ( ) {
149+ // POSIX says:
150+ // "If file does not name a symbolic link, readlink shall write a diagnostic message to standard error and exit with non-zero status."
151+ // However, this is violated by almost all implementations
152+ return if verbose {
153+ format_error ( "Not a symbolic link" , None )
154+ } else {
155+ Err ( String :: new ( ) )
156+ } ;
44157 }
45- io:: ErrorKind :: InvalidInput => {
46- format ! ( "readlink: {}: Not a symbolic link\n " , path. display( ) )
158+
159+ match fs:: read_link ( pathname_path) {
160+ Ok ( pa) => format_returned_path ( pa. as_path ( ) ) ,
161+ Err ( er) => map_io_error ( & er) ,
47162 }
48- _ => format ! ( "readlink: {}: {}\n " , path. display( ) , e) ,
49- } ;
50- Err ( err_message)
163+ }
164+ Err ( er) => map_io_error ( & er) ,
51165 }
52166 }
53167}
@@ -59,19 +173,69 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
59173
60174 let args = Args :: parse ( ) ;
61175
62- let mut exit_code = 0 ;
63-
64- match do_readlink ( & args) {
176+ let exit_code = match do_readlink ( args) {
65177 Ok ( output) => {
66- print ! ( "{}" , output) ;
67- io:: stdout ( ) . flush ( ) . unwrap ( ) ;
178+ let mut stdout_lock = stdout ( ) . lock ( ) ;
179+
180+ write ! ( stdout_lock, "{output}" ) . unwrap ( ) ;
181+
182+ stdout_lock. flush ( ) . unwrap ( ) ;
183+
184+ 0_i32
68185 }
69- Err ( err) => {
70- eprint ! ( "{}" , err) ;
71- io:: stderr ( ) . flush ( ) . unwrap ( ) ;
72- exit_code = 1 ;
186+ Err ( error_description) => {
187+ if !error_description. is_empty ( ) {
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+
195+ 1_i32
73196 }
74- }
197+ } ;
75198
76199 std:: process:: exit ( exit_code) ;
77200}
201+
202+ fn recursive_resolve ( starting_path_buf : PathBuf ) -> Result < PathBuf , String > {
203+ let mut current_path_buf = starting_path_buf;
204+
205+ let mut recursion_level = 0_usize ;
206+
207+ #[ allow( clippy:: while_let_loop) ]
208+ loop {
209+ match fs:: read_link ( current_path_buf. as_path ( ) ) {
210+ Ok ( pa) => {
211+ recursion_level += 1_usize ;
212+
213+ // https://unix.stackexchange.com/questions/53087/how-do-you-increase-maxsymlinks
214+ if recursion_level == 40_usize {
215+ return Err ( format ! (
216+ "Symbolic link chain is circular or just too long, gave up at \" {}\" " ,
217+ current_path_buf. to_string_lossy( )
218+ ) ) ;
219+ }
220+
221+ if pa. is_absolute ( ) {
222+ current_path_buf = pa;
223+ } else {
224+ if !current_path_buf. pop ( ) {
225+ return Err ( format ! (
226+ "Could not remove last path segment from path \" {}\" " ,
227+ current_path_buf. to_string_lossy( )
228+ ) ) ;
229+ }
230+
231+ current_path_buf. push ( pa) ;
232+ }
233+ }
234+ Err ( _) => {
235+ break ;
236+ }
237+ }
238+ }
239+
240+ Ok ( current_path_buf)
241+ }
0 commit comments