Skip to content

Commit

Permalink
Implement PathStruct and pass to _ishidden functions
Browse files Browse the repository at this point in the history
Instead of keeping track of both the file given by the user _and_ the
real path of the file (which some internal functions require, and
others necessarily don't), we can construct a PathStruct object and
pass that around internally.  This also allows us to perform error
checking within the PathStruct object, rather than in the ishidden
function.

Related: #19.

Still to do:
  - Write documentation for PathStruct and InvaliidRealPathError
  - Implement further tests
  - Refine error checking for PathStruct constructor with method
    PathStruct(path::AbstractString, rp::AbstractString)
  • Loading branch information
jakewilliami committed Sep 27, 2022
1 parent 2d4a64e commit d3fa718
Show file tree
Hide file tree
Showing 3 changed files with 83 additions and 30 deletions.
41 changes: 12 additions & 29 deletions src/HiddenFiles.jl
Original file line number Diff line number Diff line change
Expand Up @@ -25,19 +25,20 @@ On Unix-like systems, a file or directory is hidden if it starts with a full sto
ishidden

include("docs.jl")
include("path.jl")

@static if Sys.isunix()
include("utils/zfs.jl")
if iszfs() # @static breaks here # ZFS
error("not yet implemented")
_ishidden_zfs(f::AbstractString, rp::AbstractString) = error("not yet implemented")
_ishidden_zfs(ps::PathStruct) = error("not yet implemented")
_ishidden = _ishidden_zfs
end

# Trivial Unix check
_isdotfile(f::AbstractString) = startswith(basename(f), '.')
# Check dotfiles, but also account for ZFS
_ishidden_unix(f::AbstractString, rp::AbstractString) = _isdotfile(rp) || (iszfs() && _ishidden_zfs("", ""))
_ishidden_unix(ps::PathStruct) = _isdotfile(ps.realpath) || (iszfs() && _ishidden_zfs("", ""))

@static if Sys.isbsd() # BDS-related; this is true for macOS as well
# https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man2/chflags.2.html
Expand All @@ -64,7 +65,7 @@ include("docs.jl")
# https://github.com/davidkaya/corefx/blob/4fd3d39f831f3e14f311b0cdc0a33d662e684a9c/src/System.IO.FileSystem/src/System/IO/FileStatus.Unix.cs#L88
_isinvisible(f::AbstractString) = (_st_flags(f) & UF_HIDDEN) == UF_HIDDEN

_ishidden_bsd_related(f::AbstractString, rp::AbstractString) = _ishidden_unix(f, rp) || _isinvisible(rp)
_ishidden_bsd_related(ps::PathStruct) = _ishidden_unix(ps) || _isinvisible(ps.realpath)
end

@static if Sys.isapple() # macOS/Darwin
Expand Down Expand Up @@ -157,10 +158,10 @@ include("docs.jl")


#=== All macOS cases ===#
_ishidden_macos(f::AbstractString, rp::AbstractString) = _ishidden_bsd_related(f, rp) || _issystemfile(f) || _exists_inside_package_or_bundle(rp)
_ishidden_macos(ps::PathStruct) = _ishidden_bsd_related(ps) || _issystemfile(ps.path) || _exists_inside_package_or_bundle(ps.realpath)
_ishidden = _ishidden_macos
elseif Sys.isbsd() # BSD; this excludes macOS through control flow (as macOS is checked for first)
_ishidden_bsd(f::AbstractString, rp::AbstractString) = _ishidden_bsd_related(f, rp)
_ishidden_bsd(ps::PathStruct) = _ishidden_bsd_related(ps)
_ishidden = _ishidden_bsd
else # General UNIX
_ishidden = _ishidden_unix
Expand All @@ -172,10 +173,10 @@ elseif Sys.iswindows()
const FILE_ATTRIBUTE_HIDDEN = 0x2
const FILE_ATTRIBUTE_SYSTEM = 0x4

function _ishidden_windows(f::AbstractString, rp::AbstractString)
function _ishidden_windows(ps::PathStruct)
# https://docs.microsoft.com/en-gb/windows/win32/api/fileapi/nf-fileapi-getfileattributesa
# DWORD GetFileAttributesA([in] LPCSTR lpFileName);
f_attrs = ccall(:GetFileAttributesA, UInt32, (Cstring,), rp)
f_attrs = ccall(:GetFileAttributesA, UInt32, (Cstring,), ps.realpath)

# https://stackoverflow.com/a/1343643/12069968
# https://stackoverflow.com/a/14063074/12069968
Expand All @@ -187,29 +188,11 @@ else
end


# Each OS branch defines its own _ishidden function. In the main ishidden function, we check that the path exists, expand
# the real path out, and apply the branch's _ishidden function to that path to get a final result
# Each OS branch defines its own _ishidden function. In the main ishidden function, we check construct
# our PathStruct object to pass around to the branch's _ishidden function to use as the function necessitates
function ishidden(f::AbstractString)
# If path does not exist, `realpath` will error™
local rp::String
try
rp = realpath(f)
catch e
err_prexif = "ishidden($(repr(f)))"
# Julia < 1.3 throws a SystemError when `realpath` fails
isa(e, SystemError) && throw(SystemError(err_prexif, e.errnum))
# Julia ≥ 1.3 throws an IOError, constructed from UV Error codes
isa(e, Base.IOError) && throw(Base.uv_error(err_prexif, e.code))
# If this fails for some other reason, rethrow
rethrow()
end

# Julia < 1.2 on Windows does not error on `realpath` if path does not exist, so we
# must do so manually here
ispath(rp) || throw(Base.uv_error("ishidden($(repr(f)))", Base.UV_ENOENT))

# If we got here, the path exists, and we can continue safely with our _ishidden checks
return _ishidden(f, rp)
ps = PathStruct(f; err_prefix = :ishidden)
return _ishidden(ps)
end


Expand Down
52 changes: 52 additions & 0 deletions src/path.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
struct InvalidRealPathError <: Exception
msg::String
expected::AbstractString
actual::AbstractString
end

function Base.showerror(io::IO, e::InvalidRealPathError)
print(io, typeof(e), ": ", e.msg, ": ")
print(io, "Invalid real path: expected ", '"', e.expected, '"', ", ")
print(io, "found ", '"', e.actual, '"')
end

struct PathStruct
path::AbstractString
realpath::AbstractString

function PathStruct(path::AbstractString, rp::AbstractString)
ispath(rp) || throw(Base.uv_error("PathStruct($(repr(path)))", Base.UV_ENOENT))
# TODO: this will fail if path is not valid
realpath(path) == rp || throw(InvalidRealPathError("PathStruct($(repr(path)))", realpath(path), rp))
return new(path, rp)
end

# Each OS branch defines its own _ishidden functions, some of which require the user-provided path, and some of
# which require a real path. To easily maintain both of these, we pass around a PathStruct containing both
# information. If PathStruct is constructed with one positional argument, it attempts to construct the real path
# of the file (and will error with an IOError or SystemError if it fails).
function PathStruct(path::AbstractString; err_prefix::Symbol = :ishidden)
# If path does not exist, `realpath` will error™
local rp::String
try
rp = realpath(path)
catch e
err_prexif = "$(err_prefix)(PathStruct($(repr(path))))"
# Julia < 1.3 throws a SystemError when `realpath` fails
isa(e, SystemError) && throw(SystemError(err_prexif, e.errnum))
# Julia ≥ 1.3 throws an IOError, constructed from UV Error codes
isa(e, Base.IOError) && throw(Base.uv_error(err_prexif, e.code))
# If this fails for some other reason, rethrow
rethrow()
end

# Julia < 1.2 on Windows does not error on `realpath` if path does not exist, so we
# must do so manually here
ispath(rp) || throw(Base.uv_error("$(err_prefix)(PathStruct($(repr(path))))", Base.UV_ENOENT))

# If we got here, the path exists, and we can continue safely construct our PathStruct
# for our _ishidden tests
return new(path, rp)
end
end

20 changes: 19 additions & 1 deletion test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -105,9 +105,27 @@ using Test
end


@testset "HiddenFiles.jl—Path Handling" begin
@testset "HiddenFiles.jl—Path Handling (PathStruct)" begin
@static if Sys.isunix()
@test HiddenFiles.PathStruct("/bin", "/bin") isa HiddenFiles.PathStruct
@test HiddenFiles.PathStruct("/../bin", "/bin") isa HiddenFiles.PathStruct
@test_throws HiddenFiles.InvalidRealPathError HiddenFiles.PathStruct("/bin", "/../bin")


elseif Sys.iswindows()
@test HiddenFiles.PathStruct("C:\\", "C:\\") isa HiddenFiles.PathStruct
@test HiddenFiles.PathStruct("C:\\..\\", "C:\\") isa HiddenFiles.PathStruct
@test_throws HiddenFiles.InvalidRealPathError HiddenFiles.PathStruct("C:\\", "C:\\..\\")
else
# TODO
@test false
end

f = randpath()
# Julia < 1.3 throws a SystemError when `realpath` fails
@test_throws Union{Base.IOError, SystemError} HiddenFiles.PathStruct(f)
@test_throws Union{Base.IOError, SystemError} HiddenFiles.PathStruct(f, "")
# ishidden calls to PathStruct
@test_throws Union{Base.IOError, SystemError} ishidden(f)
end
end

0 comments on commit d3fa718

Please sign in to comment.