Skip to content

Commit ababc82

Browse files
committed
system: Fix infinite loop in link-target (on Unix)
Also added `follow-links?` keyword argument and a test. `$EINVAL` became unused and started causing a warning. I removed it from *-magic-numbers.dylan for all platforms without testing since it's not exported and all Unixen share the same implementation of link-target, which was the only place it was used. Fixes #1408
1 parent 6062b4c commit ababc82

18 files changed

+94
-40
lines changed

documentation/source/library-reference/system/file-system.rst

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ properties of the file.
7979
- :func:`file-properties`
8080
- :func:`file-property`
8181
- :func:`file-type`
82-
- :func:`link-target`
82+
- :gf:`link-target`
8383
- :gf:`resolve-file`
8484

8585
File system locators
@@ -1072,7 +1072,7 @@ File-System module.
10721072
another file or directory.
10731073

10741074
This function does not resolve symbolic links. To find the file type of the link
1075-
target call :func:`link-target` or :gf:`resolve-file` on *file* first.
1075+
target call :gf:`link-target` or :gf:`resolve-file` on *file* first.
10761076

10771077
.. type:: <file-type>
10781078

@@ -1120,17 +1120,33 @@ File-System module.
11201120
permissions set on the file are incorrect or insufficient for
11211121
your operation.
11221122

1123-
.. function:: link-target
1123+
.. generic-function:: link-target
11241124

11251125
Returns the target of a symbolic link.
11261126

1127-
:signature: link-target *file* => *target*
1127+
.. note:: On Windows this function is not implemented; it always signals an error.
1128+
1129+
:signature: link-target *file* #key *follow-links?* => *target*
11281130
:parameter file: An instance of type :type:`<pathname>`.
1129-
:value target: An instance of type :type:`<pathname>`.
1131+
:parameter #key follow-links?: An instance of type :drm:`<boolean>`. The default is
1132+
:drm:`#t`.
1133+
:value target: :drm:`#f` or an instance of type :class:`<file-system-locator>`.
11301134
:description:
11311135

1132-
Repeatedly follows symbolic links starting with *file* until it finds a
1133-
non-link file or directory, or a non-existent link target.
1136+
Returns a locator identifying the target of symbolic link *file*.
1137+
1138+
Signals :class:`<file-system-error>` if the system call to read the link target
1139+
fails for any reason. For example, if *file* is not a symbolic link or if *file*
1140+
does not exist.
1141+
1142+
If ``follow-links?`` is true (the default) then links are followed until a file
1143+
that is not a symbolic link is found, and the locator for that file is returned. If
1144+
the final link in a chain of one or more symbolic links points to a non-existent
1145+
file, :drm:`#f` is returned.
1146+
1147+
If ``follow-links?`` is false, no attempt is made to follow the link or to check
1148+
whether the link target file exists. A locator representing the target is
1149+
returned.
11341150

11351151
:seealso:
11361152

@@ -1314,7 +1330,7 @@ File-System module.
13141330
- :func:`file-property-setter`
13151331
- :func:`file-type`
13161332
- :func:`home-directory`
1317-
- :func:`link-target`
1333+
- :gf:`link-target`
13181334
- :func:`rename-file`
13191335
- :func:`create-symbolic-link`
13201336

documentation/source/release-notes/2025.2.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,5 +54,11 @@ System
5454
to do the same thing as the Unix implementation. Previously it erroniously did the
5555
same thing as :gf:`resolve-locator`.
5656

57+
* `Issue 1408 <https://github.com/dylan-lang/opendylan/issues/1408>`_, which could result
58+
in an infinite loop in :gf:`link-target`, has been fixed. In addition, the
59+
:gf:`link-target` function accepts a new keyword argument, ``follow-links?``, which
60+
specifies whether to follow all links until a non-symlink file is found (the default)
61+
or just return the direct target of the given symlink.
62+
5763
Contributors
5864
============

sources/system/aarch64-linux-magic-numbers.dylan

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ License: Public Domain
77
define constant $ENOENT = 2;
88
define constant $EINTR = 4;
99
define constant $EACCES = 13;
10-
define constant $EINVAL = 22;
1110
define constant $ETXTBSY = 26;
1211
define constant $EROFS = 30;
1312
define constant $path-max = 4096;

sources/system/arm-linux-magic-numbers.dylan

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ License: Public Domain
77
define constant $ENOENT = 2;
88
define constant $EINTR = 4;
99
define constant $EACCES = 13;
10-
define constant $EINVAL = 22;
1110
define constant $ETXTBSY = 26;
1211
define constant $EROFS = 30;
1312
define constant $path-max = 4096;

sources/system/dump-magic-numbers.c

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ main(void) {
2929
PRINT_CONSTANT(ENOENT, "$ENOENT");
3030
PRINT_CONSTANT(EINTR, "$EINTR");
3131
PRINT_CONSTANT(EACCES, "$EACCES");
32-
PRINT_CONSTANT(EINVAL, "$EINVAL");
3332
PRINT_CONSTANT(ETXTBSY, "$ETXTBSY");
3433
PRINT_CONSTANT(EROFS, "$EROFS");
3534

sources/system/file-system/file-system.dylan

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,12 @@ Warranty: Distributed WITHOUT WARRANTY OF ANY KIND
88

99
/// Types
1010

11-
/// Needs a better name, I think ...
11+
// Needs a better name, I think ...
12+
//
13+
// I'm guessing the author of the above comment was thinking of the potential confusion
14+
// between "file type" and "file extension", which in turn is related to the confusion
15+
// between "pathname" or "locator" and "file". Locators uses the "extension" terminology
16+
// consistently so I think <file-type> is okay. <file-system-entity-type>? ;) --cgay
1217
define constant <file-type> = one-of(#"file", #"directory", #"link");
1318

1419
define constant <copy/rename-disposition> = one-of(#"signal", #"replace");
@@ -144,14 +149,20 @@ end method file-type;
144149

145150

146151
///
147-
define generic link-target (link :: <pathname>) => (target :: <pathname>);
148-
149-
define method link-target (link :: <file-system-locator>) => (target :: <pathname>)
150-
%link-target(link)
152+
define generic link-target
153+
(link :: <pathname>, #key follow-links?)
154+
=> (target :: false-or(<file-system-locator>));
155+
156+
define method link-target
157+
(link :: <file-system-locator>, #key follow-links? :: <boolean> = #t)
158+
=> (target :: false-or(<file-system-locator>))
159+
%link-target(link, follow-links?)
151160
end method link-target;
152161

153-
define method link-target (link :: <string>) => (target :: <pathname>)
154-
link-target(as(<file-system-locator>, link))
162+
define method link-target
163+
(link :: <string>, #key follow-links? :: <boolean> = #t)
164+
=> (target :: false-or(<file-system-locator>))
165+
link-target(as(<file-system-locator>, link), follow-links?: follow-links?)
155166
end method link-target;
156167

157168

sources/system/file-system/unix-file-system.dylan

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ end function;
150150

151151
define function %file-type
152152
(file :: <posix-file-system-locator>, #key if-does-not-exist = unsupplied())
153-
=> (file-type :: <file-type>)
153+
=> (file-type :: <object>)
154154
with-stack-stat (st, file)
155155
if (primitive-raw-as-boolean
156156
(%call-c-function ("system_lstat") (path :: <raw-byte-string>, st :: <raw-c-pointer>)
@@ -175,11 +175,12 @@ end function %file-type;
175175

176176

177177
define function %link-target
178-
(link :: <posix-file-system-locator>) => (target :: <posix-file-system-locator>)
179-
while (%file-type(link, if-does-not-exist: #f) == #"link")
180-
let bufsize = 8192;
178+
(link :: <posix-file-system-locator>, follow-links? :: <boolean>)
179+
=> (target :: false-or(<posix-file-system-locator>))
180+
iterate loop (link = link)
181+
let bufsize = $path-max;
181182
let buffer = make(<byte-string>, size: bufsize, fill: '\0');
182-
let count
183+
let length
183184
= raw-as-integer(%call-c-function ("readlink")
184185
(path :: <raw-byte-string>, buffer :: <raw-byte-string>,
185186
bufsize :: <raw-c-size-t>)
@@ -188,16 +189,25 @@ define function %link-target
188189
primitive-string-as-raw(buffer),
189190
integer-as-raw(bufsize))
190191
end);
191-
if (count == -1)
192-
unless (unix-errno() == $ENOENT | unix-errno() == $EINVAL)
193-
unix-file-error("readlink", "%s", link)
194-
end
192+
if (length == -1)
193+
unix-file-error("readlink", "%s", link);
195194
else
196-
let target = as(<file-system-locator>, copy-sequence(buffer, end: count));
197-
link := merge-locators(target, link)
195+
let locator = as(<file-system-locator>, copy-sequence(buffer, end: length));
196+
let target = merge-locators(locator, link);
197+
if (~follow-links?)
198+
target
199+
else
200+
let type = %file-type(target, if-does-not-exist: #f);
201+
if (~type)
202+
#f
203+
elseif (type == #"link")
204+
loop(target)
205+
else
206+
target
207+
end
208+
end
198209
end
199-
end;
200-
link
210+
end iterate
201211
end function %link-target;
202212

203213

sources/system/file-system/win32-file-system.dylan

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,8 @@ end function %file-type;
158158

159159
///
160160
define function %link-target
161-
(link :: <microsoft-file-system-locator>) => (target :: <microsoft-file-system-locator>)
161+
(link :: <microsoft-file-system-locator>, follow-links? :: <boolean>)
162+
=> (target :: <microsoft-file-system-locator>)
162163
error(make(<file-system-error>,
163164
format-string: "link-target is not available on this platform",
164165
format-arguments: #()))

sources/system/riscv64-linux-magic-numbers.dylan

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ License: Public Domain
77
define constant $ENOENT = 2;
88
define constant $EINTR = 4;
99
define constant $EACCES = 13;
10-
define constant $EINVAL = 22;
1110
define constant $ETXTBSY = 26;
1211
define constant $EROFS = 30;
1312
define constant $path-max = 4096;

sources/system/tests/file-system.dylan

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,25 @@ define test test-file-exists? ()
115115
assert-true(file-exists?(symlink-bad, follow-links?: #f));
116116
end test;
117117

118+
define test test-link-target ()
119+
if ($os-name ~== #"win32") // link-target is not implemented on Windows
120+
let tmp = test-temp-directory();
121+
let aaa = file-locator(tmp, "aaa");
122+
let bbb = file-locator(tmp, "bbb");
123+
let ccc = file-locator(tmp, "ccc");
124+
let ddd = file-locator(tmp, "ddd");
125+
write-test-file(ddd, contents: "ddd");
126+
create-symbolic-link(ddd, ccc);
127+
create-symbolic-link(ccc, bbb);
128+
create-symbolic-link(bbb, aaa);
129+
assert-equal(link-target(aaa), ddd);
130+
assert-equal(link-target(aaa, follow-links?: #f), bbb);
131+
assert-signals(<file-system-error>, link-target(ddd), "ddd is not a symbolic link");
132+
delete-file(ddd);
133+
assert-false(link-target(aaa), "link target does not exist");
134+
end if;
135+
end test;
136+
118137
define test test-symbolic-link ()
119138
let tmp-dir = test-temp-directory();
120139
let symlink = file-locator(tmp-dir, "symlink-to-tmp");

0 commit comments

Comments
 (0)