diff --git a/base/stat.jl b/base/stat.jl index 56b960c4f74ea..3931b9d1f3583 100644 --- a/base/stat.jl +++ b/base/stat.jl @@ -326,6 +326,16 @@ otherwise returns `false`. This is the generalization of [`isfile`](@ref), [`isdir`](@ref) etc. """ ispath(st::StatStruct) = filemode(st) & 0xf000 != 0x0000 +function ispath(path::String) + # We use `access()` and `F_OK` to determine if a given path exists. `F_OK` comes from `unistd.h`. + F_OK = 0x00 + r = ccall(:jl_fs_access, Cint, (Cstring, Cint), path, F_OK) + if !(r in (0, Base.UV_ENOENT, Base.UV_ENOTDIR, Base.UV_EINVAL)) + uv_error(string("ispath(", repr(path), ")"), r) + end + return r == 0 +end +ispath(path::AbstractString) = ispath(String(path)) """ isfifo(path) -> Bool diff --git a/base/sysinfo.jl b/base/sysinfo.jl index 97e5e2a71bcbc..c5744873312d6 100644 --- a/base/sysinfo.jl +++ b/base/sysinfo.jl @@ -33,6 +33,8 @@ export BINDIR, iswindows, isjsvm, isexecutable, + isreadable, + iswriteable, username, which @@ -556,20 +558,81 @@ const WINDOWS_VISTA_VER = v"6.0" Return `true` if the given `path` has executable permissions. +!!! note + This permission may change before the user executes `path`, + so it is recommended to execute the file and handle the error if that fails, + rather than calling `isexecutable` first. + !!! note Prior to Julia 1.6, this did not correctly interrogate filesystem ACLs on Windows, therefore it would return `true` for any file. From Julia 1.6 on, it correctly determines whether the file is marked as executable or not. + +See also [`ispath`](@ref), [`isreadable`](@ref), [`iswriteable`](@ref). """ function isexecutable(path::String) # We use `access()` and `X_OK` to determine if a given path is # executable by the current user. `X_OK` comes from `unistd.h`. X_OK = 0x01 - return ccall(:jl_fs_access, Cint, (Ptr{UInt8}, Cint), path, X_OK) == 0 + return ccall(:jl_fs_access, Cint, (Cstring, Cint), path, X_OK) == 0 end isexecutable(path::AbstractString) = isexecutable(String(path)) +""" + Sys.isreadable(path::String) + +Return `true` if the access permissions for the given `path` permitted reading by the current user. + +!!! note + This permission may change before the user calls `open`, + so it is recommended to just call `open` alone and handle the error if that fails, + rather than calling `isreadable` first. + +!!! note + Currently this function does not correctly interrogate filesystem + ACLs on Windows, therefore it can return wrong results. + +!!! compat "Julia 1.11" + This function requires at least Julia 1.11. + +See also [`ispath`](@ref), [`isexecutable`](@ref), [`iswriteable`](@ref). +""" +function isreadable(path::String) + # We use `access()` and `R_OK` to determine if a given path is + # readable by the current user. `R_OK` comes from `unistd.h`. + R_OK = 0x04 + return ccall(:jl_fs_access, Cint, (Cstring, Cint), path, R_OK) == 0 +end +isreadable(path::AbstractString) = isreadable(String(path)) + +""" + Sys.iswriteable(path::String) + +Return `true` if the access permissions for the given `path` permitted writing by the current user. + +!!! note + This permission may change before the user calls `open`, + so it is recommended to just call `open` alone and handle the error if that fails, + rather than calling `iswriteable` first. + +!!! note + Currently this function does not correctly interrogate filesystem + ACLs on Windows, therefore it can return wrong results. + +!!! compat "Julia 1.11" + This function requires at least Julia 1.11. + +See also [`ispath`](@ref), [`isexecutable`](@ref), [`isreadable`](@ref). +""" +function iswriteable(path::String) + # We use `access()` and `W_OK` to determine if a given path is + # writeable by the current user. `W_OK` comes from `unistd.h`. + W_OK = 0x02 + return ccall(:jl_fs_access, Cint, (Cstring, Cint), path, W_OK) == 0 +end +iswriteable(path::AbstractString) = iswriteable(String(path)) + """ Sys.which(program_name::String) diff --git a/doc/src/base/base.md b/doc/src/base/base.md index 3fd2bbb12bb1f..556d51d3af0c1 100644 --- a/doc/src/base/base.md +++ b/doc/src/base/base.md @@ -374,6 +374,8 @@ Base.Sys.uptime Base.Sys.isjsvm Base.Sys.loadavg Base.Sys.isexecutable +Base.Sys.isreadable +Base.Sys.iswriteable Base.Sys.username Base.@static ``` diff --git a/test/file.jl b/test/file.jl index 2dfd881b8a487..d44c3772e03e4 100644 --- a/test/file.jl +++ b/test/file.jl @@ -1658,23 +1658,44 @@ else ) end -@testset "chmod/isexecutable" begin +@testset "chmod/isexecutable/isreadable/iswriteable" begin mktempdir() do dir - mkdir(joinpath(dir, "subdir")) + subdir = joinpath(dir, "subdir") fpath = joinpath(dir, "subdir", "foo") - # Test that we can actually set the executable bit on all platforms. + @test !ispath(subdir) + mkdir(subdir) + @test ispath(subdir) + + @test !ispath(fpath) touch(fpath) + @test ispath(fpath) + + # Test that we can actually set the executable/readable/writeable bit on all platforms. chmod(fpath, 0o644) @test !Sys.isexecutable(fpath) + @test Sys.isreadable(fpath) + Sys.iswindows() ? @test_skip(Sys.iswriteable(fpath)) : @test(Sys.iswriteable(fpath)) chmod(fpath, 0o755) @test Sys.isexecutable(fpath) + @test Sys.isreadable(fpath) + Sys.iswindows() ? @test_skip(Sys.iswriteable(fpath)) : @test(Sys.iswriteable(fpath)) + chmod(fpath, 0o444) + @test !Sys.isexecutable(fpath) + @test Sys.isreadable(fpath) + @test !Sys.iswriteable(fpath) + chmod(fpath, 0o244) + @test !Sys.isexecutable(fpath) + Sys.iswindows() ? @test_skip(!Sys.isreadable(fpath)) : @test(!Sys.isreadable(fpath)) + Sys.iswindows() ? @test_skip(Sys.iswriteable(fpath)) : @test(Sys.iswriteable(fpath)) # Ensure that, on Windows, where inheritance is default, # chmod still behaves as we expect. if Sys.iswindows() - chmod(joinpath(dir, "subdir"), 0o666) - @test Sys.isexecutable(fpath) + chmod(subdir, 0o666) + @test !Sys.isexecutable(fpath) + @test Sys.isreadable(fpath) + @test_skip Sys.iswriteable(fpath) end # Reset permissions to all at the end, so it can be deleted properly.