Skip to content

Commit 10495a5

Browse files
authored
os: implement readlink/1 on !windows (#26405)
1 parent 15222a9 commit 10495a5

4 files changed

Lines changed: 91 additions & 4 deletions

File tree

‎vlib/os/os.c.v‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -431,6 +431,16 @@ pub fn system(cmd string) int {
431431
}
432432

433433
// exists returns true if `path` (file or directory) exists.
434+
//
435+
// Note that when used on symlinks, if the target of the symlink does not exist, the behavior of this function is complex.
436+
// On linux, mac, and similar systems, on such a symlink, this function returns false.
437+
// (This may make sense, in the sense that such a path does not contain anything readable.
438+
// It is also the same as the libc 'access' function, and may be familiar from that regard.
439+
// On the other hand, this case may be surprising, since it means this function can return "false" for a path that exists enough
440+
// that trying to create a new file or dir there subsequently (even aside from TOCTOU issues) will fail with EEXIST.)
441+
// On windows systems, a symlink with a target that does not exist, this function returns true.
442+
// Consider using lstat() for a less ambiguous result about whether a path is occupied or not;
443+
// whether lstat returns any result or a "not exists" error gives a clear indication of whether the path is occupied.
434444
pub fn exists(path string) bool {
435445
$if windows {
436446
p := path.replace('/', '\\')

‎vlib/os/os_nix.c.v‎

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ fn C.uname(name &C.utsname) int
5151

5252
fn C.symlink(&char, &char) int
5353

54+
fn C.readlink(&char, &char, int) int
55+
5456
fn C.link(&char, &char) int
5557

5658
fn C.gethostname(&char, int) int
@@ -362,16 +364,58 @@ pub fn raw_execute(cmd string) Result {
362364
return execute(cmd)
363365
}
364366

365-
// symlink creates a symbolic link named target, which points to origin.
367+
// symlink creates a symbolic link named link_name, which points to target.
366368
// It returns a POSIX error message, if it can not do so.
367-
pub fn symlink(origin string, target string) ! {
368-
res := C.symlink(&char(origin.str), &char(target.str))
369+
pub fn symlink(target string, link_name string) ! {
370+
res := C.symlink(&char(target.str), &char(link_name.str))
369371
if res == 0 {
370372
return
371373
}
372374
return error(posix_get_error_msg(C.errno))
373375
}
374376

377+
// readlink reads the target of a symbolic link.
378+
// It returns a POSIX error message if it can not do so.
379+
//
380+
// Note that the target of a symbolic link can be any string:
381+
// it is often used to point to another path, but the target is not guaranteed
382+
// to resolve as a path, nor to point to a path that exists.
383+
@[manualfree]
384+
pub fn readlink(path string) !string {
385+
// Use a region of stack to get information into; we'll return new memory of more precise size later.
386+
mut buf := [max_path_buffer_size]u8{}
387+
// readlink returns the number of bytes written into buf, or -1 for errors.
388+
res := C.readlink(&char(path.str), &char(&buf[0]), max_path_buffer_size)
389+
if res < 0 {
390+
return last_error()
391+
}
392+
// Common case: we got a complete read into our buffer on the stack.
393+
// In this case, copy the data into a new heap-allocated string that's right-sized
394+
// (we can't return memory from our stack).
395+
if res < max_path_buffer_size {
396+
return unsafe { (&buf[0]).vstring_with_len(res).clone() }
397+
}
398+
// If the number of bytes read wasn't less than as many as we said we'd accept: that means we might not have gotten a complete read.
399+
// In this case, we have to start doing heap allocations, increasingly large, and simply check until we get a complete one.
400+
// Whenever we do succeed: we'll return a string that refers to a subset of that possibly excessively sized buffer,
401+
// because we're already on the heap and returning it is valid; and because allocating a new buffer just
402+
// to save some resident memory is usually a poor trade of spending of time just to reclaim a very minor amount of space.
403+
mut size := max_path_buffer_size
404+
for {
405+
size *= 2
406+
mut buf2 := unsafe { &char(malloc_noscan(size)) }
407+
res2 := C.readlink(&char(path.str), buf2, size)
408+
if res2 < 0 {
409+
return last_error()
410+
}
411+
if res2 < size {
412+
return unsafe { tos(&u8(&buf2[0]), res2) }
413+
}
414+
unsafe { free(buf2) } // and then loop around to try again with a larger one.
415+
}
416+
return error('${@METHOD} unreachable code')
417+
}
418+
375419
// link creates a new link (also known as a hard link) to an existing file.
376420
// It returns a POSIX error message, if it can not do so.
377421
pub fn link(origin string, target string) ! {

‎vlib/os/os_test.c.v‎

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -615,6 +615,34 @@ fn test_symlink() {
615615
}
616616
}
617617

618+
fn test_readlink() {
619+
$if windows {
620+
eprintln('skipping ${@METHOD} on windows, api not supported')
621+
return
622+
}
623+
os.symlink('some_target_string', 'some_symlink')!
624+
defer { os.rm('some_symlink') or { panic(err) } }
625+
assert os.readlink('some_symlink')! == 'some_target_string'
626+
}
627+
628+
fn test_exists_symlink_dangling() {
629+
$if msvc {
630+
eprintln('skipping ${@METHOD} on windows + msvc; TODO: investigate why os.lstat/1 behaves differently than for gcc/clang')
631+
return
632+
}
633+
os.symlink('nonexistent', 'dangling_symlink') or { handle_privilege_error(err) or { return } }
634+
// sanity check that the symlink truly does exist. the lack of error alone is the check.
635+
// (on linux, `.get_filetype() == os.FileType.symbolic_link` is true, but on windows, a dangling symlink is reported as a regular file.)
636+
os.lstat('dangling_symlink')!
637+
// the exists function says false in this scenario... on linux and linux-like systems.
638+
// it says true on windows!
639+
$if windows {
640+
assert os.exists('dangling_symlink') == true
641+
} $else {
642+
assert os.exists('dangling_symlink') == false
643+
}
644+
}
645+
618646
fn test_is_executable_writable_readable() {
619647
file_name := 'rwxfile.exe'
620648
create_file(file_name)!
@@ -1026,7 +1054,6 @@ fn test_execute_fc_get_output() {
10261054
return
10271055
}
10281056
result := os.execute('c:\\windows\\system32\\fc.exe /?')
1029-
dump(result)
10301057
assert result.output.contains('filename')
10311058
assert result.exit_code == -1
10321059
}

‎vlib/os/os_windows.c.v‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -431,6 +431,12 @@ pub fn symlink(origin string, target string) ! {
431431
return error('could not symlink')
432432
}
433433

434+
// readlink reads the target of a symbolic link.
435+
// TODO: implement this for windows too.
436+
pub fn readlink(path string) !string {
437+
return error('${@METHOD} not yet supported on windows')
438+
}
439+
434440
pub fn link(origin string, target string) ! {
435441
res := C.CreateHardLinkW(target.to_wide(), origin.to_wide(), C.NULL)
436442
// 1 = success, != 1 failure => https://stackoverflow.com/questions/33010440/createsymboliclink-on-windows-10

0 commit comments

Comments
 (0)