Skip to content

Commit

Permalink
[file] Fix relative symlinks to directories on Windows (JuliaLang#39491)
Browse files Browse the repository at this point in the history
  • Loading branch information
staticfloat committed Feb 9, 2021
1 parent 7bc2110 commit 78c448f
Show file tree
Hide file tree
Showing 2 changed files with 81 additions and 16 deletions.
57 changes: 49 additions & 8 deletions base/file.jl
Original file line number Diff line number Diff line change
Expand Up @@ -977,36 +977,77 @@ function sendfile(src::AbstractString, dst::AbstractString)
end

if Sys.iswindows()
const UV_FS_SYMLINK_DIR = 0x0001
const UV_FS_SYMLINK_JUNCTION = 0x0002
const UV__EPERM = -4048
end

"""
symlink(target::AbstractString, link::AbstractString)
symlink(target::AbstractString, link::AbstractString; dir_target = false)
Creates a symbolic link to `target` with the name `link`.
On Windows, symlinks must be explicitly declared as referring to a directory
or not. If `target` already exists, by default the type of `link` will be auto-
detected, however if `target` does not exist, this function defaults to creating
a file symlink unless `dir_target` is set to `true`. Note that if the user
sets `dir_target` but `target` exists and is a file, a directory symlink will
still be created, but dereferencing the symlink will fail, just as if the user
creates a file symlink (by calling `symlink()` with `dir_target` set to `false`
before the directory is created) and tries to dereference it to a directory.
Additionally, there are two methods of making a link on Windows; symbolic links
and junction points. Junction points are slightly more efficient, but do not
support relative paths, so if a relative directory symlink is requested (as
denoted by `isabspath(target)` returning `false`) a symlink will be used, else
a junction point will be used. Best practice for creating symlinks on Windows
is to create them only after the files/directories they reference are already
created.
!!! note
This function raises an error under operating systems that do not support
soft symbolic links, such as Windows XP.
!!! compat "Julia 1.6"
The `dir_target` keyword argument was added in Julia 1.6. Prior to this,
symlinks to nonexistant paths on windows would always be file symlinks, and
relative symlinks to directories were not supported.
"""
function symlink(p::AbstractString, np::AbstractString)
function symlink(target::AbstractString, link::AbstractString;
dir_target::Bool = false)
@static if Sys.iswindows()
if Sys.windows_version() < Sys.WINDOWS_VISTA_VER
error("Windows XP does not support soft symlinks")
end
end
flags = 0
@static if Sys.iswindows()
if isdir(p)
flags |= UV_FS_SYMLINK_JUNCTION
p = abspath(p)
# If we're going to create a directory link, we need to know beforehand.
# First, if `target` is not an absolute path, let's immediately resolve
# it so that we can peek and see if it's a directory.
resolved_target = target
if !isabspath(target)
resolved_target = joinpath(dirname(link), target)
end

# If it is a directory (or `dir_target` is set), we'll need to add one
# of `UV_FS_SYMLINK_{DIR,JUNCTION}` to the flags, depending on whether
# `target` is an absolute path or not.
if (ispath(resolved_target) && isdir(resolved_target)) || dir_target
if isabspath(target)
flags |= UV_FS_SYMLINK_JUNCTION
else
flags |= UV_FS_SYMLINK_DIR
end
end
end
err = ccall(:jl_fs_symlink, Int32, (Cstring, Cstring, Cint), p, np, flags)
err = ccall(:jl_fs_symlink, Int32, (Cstring, Cstring, Cint), target, link, flags)
if err < 0
msg = "symlink($(repr(p)), $(repr(np)))"
msg = "symlink($(repr(target)), $(repr(link)))"
@static if Sys.iswindows()
if !isdir(p)
# creating file/directory symlinks requires Administrator privileges
# while junction points apparently do not
if !(flags & UV_FS_SYMLINK_JUNCTION) && err == UV__EPERM
msg = "On Windows, creating symlinks requires Administrator privileges.\n$msg"
end
end
Expand Down
40 changes: 32 additions & 8 deletions test/file.jl
Original file line number Diff line number Diff line change
Expand Up @@ -29,23 +29,49 @@ end
if !Sys.iswindows() || Sys.windows_version() >= Sys.WINDOWS_VISTA_VER
dirlink = joinpath(dir, "dirlink")
symlink(subdir, dirlink)
@test stat(dirlink) == stat(subdir)
@test readdir(dirlink) == readdir(subdir)

# relative link
cd(subdir)
relsubdirlink = joinpath(subdir, "rel_subdirlink")
reldir = joinpath("..", "adir2")
symlink(reldir, relsubdirlink)
cd(pwd_)
@test stat(relsubdirlink) == stat(subdir2)
@test readdir(relsubdirlink) == readdir(subdir2)

# creation of symlink to directory that does not yet exist
new_dir = joinpath(subdir, "new_dir")
foo_file = joinpath(subdir, "new_dir", "foo")
nedlink = joinpath(subdir, "non_existant_dirlink")
symlink("new_dir", nedlink; dir_target=true)
try
readdir(nedlink)
@test false
catch e
@test isa(e, Base.IOError)
# It's surprisingly difficult to know what numeric value this will be across platforms
# so we'll just check the string representation instead. :(
@test endswith(e.msg, "(ENOENT)")
end
mkdir(new_dir)
touch(foo_file)
@test readdir(new_dir) == readdir(nedlink)

rm(foo_file)
rm(new_dir)
rm(nedlink)
end

if !Sys.iswindows()
if !Sys.iswindows() || Sys.windows_version() >= Sys.WINDOWS_VISTA_VER
link = joinpath(dir, "afilelink.txt")
symlink(file, link)
@test stat(file) == stat(link)

# relative link
cd(subdir)
rellink = joinpath(subdir, "rel_afilelink.txt")
relfile = joinpath("..", "afile.txt")
symlink(relfile, rellink)
cd(pwd_)
@test stat(rellink) == stat(file)
end

using Random
Expand Down Expand Up @@ -1350,11 +1376,9 @@ end
############
# Clean up #
############
if !Sys.iswindows()
if !Sys.iswindows() || (Sys.windows_version() >= Sys.WINDOWS_VISTA_VER)
rm(link)
rm(rellink)
end
if !Sys.iswindows() || (Sys.windows_version() >= Sys.WINDOWS_VISTA_VER)
rm(dirlink)
rm(relsubdirlink)
end
Expand Down

0 comments on commit 78c448f

Please sign in to comment.