9
9
10
10
use clap:: Parser ;
11
11
use gettextrs:: { bind_textdomain_codeset, setlocale, textdomain, LocaleCategory } ;
12
+ use std:: error:: Error ;
13
+ use std:: fs;
12
14
use std:: io:: Write ;
15
+ use std:: io:: { stderr, stdout, ErrorKind } ;
13
16
use std:: path:: PathBuf ;
14
- use std:: { fs , io } ;
17
+ use std:: path :: { Component , Path } ;
15
18
16
19
/// readlink — display the contents of a symbolic link
17
20
#[ derive( Parser ) ]
@@ -21,33 +24,144 @@ struct Args {
21
24
#[ arg( short, long) ]
22
25
no_newline : bool ,
23
26
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
+
24
36
/// The pathname of an existing symbolic link
25
37
pathname : PathBuf ,
26
38
}
27
39
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
+ } ;
30
67
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
+ }
38
97
}
98
+ _ => format_error ( "Unknown error" , Some ( & error) ) ,
39
99
}
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
+ } ;
44
157
}
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) ,
47
162
}
48
- _ => format ! ( "readlink: {}: {}\n " , path. display( ) , e) ,
49
- } ;
50
- Err ( err_message)
163
+ }
164
+ Err ( er) => map_io_error ( & er) ,
51
165
}
52
166
}
53
167
}
@@ -59,19 +173,69 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
59
173
60
174
let args = Args :: parse ( ) ;
61
175
62
- let mut exit_code = 0 ;
63
-
64
- match do_readlink ( & args) {
176
+ let exit_code = match do_readlink ( args) {
65
177
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
68
185
}
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
73
196
}
74
- }
197
+ } ;
75
198
76
199
std:: process:: exit ( exit_code) ;
77
200
}
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