Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refine real path expansion logic #19

Merged
merged 12 commits into from
Sep 27, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name = "HiddenFiles"
uuid = "d01c2003-3cc0-4d61-912c-b250feb01c5b"
authors = ["Jake Ireland <[email protected]> and contributors"]
version = "0.1.0"
version = "0.1.1"

[compat]
julia = "1"
Expand Down
52 changes: 39 additions & 13 deletions src/HiddenFiles.jl
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,13 @@ Check if a file or directory is hidden.
On Unix-like systems, a file or directory is hidden if it starts with a full stop/period (`U+002e`). On Windows systems, this function will parse file attributes to determine if the given file or directory is hidden.

!!! note
On macOS and BSD, this function will also check the `st_flags` field from `stat` to check if the `UF_HIDDEN` flag has been set.
On Unix-like systems, in order to correctly determine if the file begins with a full stop, we must first expand the path to its real path.

!!! note
On macOS, any file or directory within a [package](https://developer.apple.com/library/archive/documentation/CoreFoundation/Conceptual/CFBundles/DocumentPackages/DocumentPackages.html) or a [bundle](https://developer.apple.com/library/archive/documentation/CoreFoundation/Conceptual/CFBundles/AboutBundles/AboutBundles.html) will be considered hidden.
On operating systems deriving from BSD (i.e., *BSD, macOS), this function will also check the `st_flags` field from `stat` to check if the `UF_HIDDEN` flag has been set.

!!! note
On macOS, any file or directory within a [package](https://developer.apple.com/library/archive/documentation/CoreFoundation/Conceptual/CFBundles/DocumentPackages/DocumentPackages.html) or a [bundle](https://developer.apple.com/library/archive/documentation/CoreFoundation/Conceptual/CFBundles/AboutBundles/AboutBundles.html) will also be considered hidden.
"""
ishidden

Expand All @@ -27,13 +30,14 @@ include("docs.jl")
include("utils/zfs.jl")
if iszfs() # @static breaks here # ZFS
error("not yet implemented")
_ishidden_zfs(f::AbstractString) = error("not yet implemented")
_ishidden_zfs(f::AbstractString, rp::AbstractString) = error("not yet implemented")
_ishidden = _ishidden_zfs
end

# Trivial Unix check
_isdotfile(f::AbstractString) = startswith(basename(f), '.')
# Account for ZFS
_ishidden_unix(f::AbstractString) = _isdotfile(f) || (iszfs() && _ishidden_zfs())
# Check dotfiles, but also account for ZFS
_ishidden_unix(f::AbstractString, rp::AbstractString) = _isdotfile(rp) || (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 @@ -60,7 +64,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) = _ishidden_unix(f) || _isinvisible(f)
_ishidden_bsd_related(f::AbstractString, rp::AbstractString) = _ishidden_unix(f, rp) || _isinvisible(rp)
end

@static if Sys.isapple() # macOS/Darwin
Expand Down Expand Up @@ -137,9 +141,13 @@ include("docs.jl")
# If a file or directory exists inside a package or bundle, then it is hidden. Packages or bundles themselves
# are not necessarily hidden.
function _exists_inside_package_or_bundle(f::AbstractString)
# This assumes that f has already been modified with the realpath function, as if it hasn't,
# This function necessitates that f has is modified with the realpath function, as if it hasn't,
# it is possible that f has a trailing slash, meaning its dirname is still itself
f = dirname(f)

# We can't check the root directory, as this doesn't have any metadata information, so
# _k_mditem_content_type_tree will fail. Otherwise, we start at the parent directory of the
# given file, and check if any of its parents are packages or bundles.
while f != "/"
_ispackage_or_bundle(f) && return true
f = dirname(f)
Expand All @@ -149,10 +157,10 @@ include("docs.jl")


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

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

# https://stackoverflow.com/a/1343643/12069968
# https://stackoverflow.com/a/14063074/12069968
Expand All @@ -182,8 +190,26 @@ 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
function ishidden(f::AbstractString)
ispath(f) || throw(Base.uv_error("ishidden($(repr(f)))", Base.UV_ENOENT))
return _ishidden(realpath(f))
# 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)
end


Expand Down
29 changes: 23 additions & 6 deletions src/docs.jl
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
"""
```julia
_ishidden(f::AbstractString) -> Bool
_ishidden(f::AbstractString, rp::AbstractString) -> Bool
```

An alias for your system's internal `_ishidden_*` function.

This function also takes an expanded real path, as some internal functions neccesitate a real path.

The reason this is still an internal function is because the main [`ishidden`](@ref) function also checks the validity of the path, so that all internal functions can assume that the path exists.

See also: [`_ishidden_unix`](@ref), [`_ishidden_windows`](@ref), [`_ishidden_macos`](@ref), [`_ishidden_bsd`](@ref), [`_ishidden_zfs`](@ref).
Expand All @@ -27,17 +29,23 @@ _isdotfile(f::AbstractString) -> Bool
```

Determines if a file or directory is hidden from ordinary directory listings by checking if it starts with a full stop/period (`U+002E`).

!!! note
This function expects the path given to be a normalised/real path, so that the base name of the path can be correctly assessed.
"""
_isdotfile

"""
```julia
_ishidden_unix(f::AbstractString) -> Bool
_ishidden_unix(f::AbstractString, rp::AbstractString) -> Bool
```

Determines if a file or directory is hidden from ordinary directory listings by checking if it starts with a full stop/period, or if it is a ZFS mount point on operating systems with a Unix-like interface.

See also: [`_isdotfile`](@ref), [`_ishidden_zfs`](@ref).

!!! note
This function expects the path given to be a normalised/real path, so that the base name of the path can be correctly assessed.
"""
_ishidden_unix

Expand Down Expand Up @@ -88,12 +96,15 @@ _isinvisible(f::AbstractString) -> Bool
Determines if the specified file or directory is invisible/hidden, as defined by the Finder flags for the path.

See also: [`_st_flags`](@ref), [`UF_HIDDEN`](@ref).

!!! note
This function expects that the file given to it is its real path.
"""
_isinvisible

"""
```julia
_ishidden_bsd_related(f::AbstractString) -> Bool
_ishidden_bsd_related(f::AbstractString, rp::AbstractString) -> Bool
```

Determines if a file or directory on a BSD-related system (i.e., macOS or BSD) is hidden from ordinary directory listings, as defined either by the Unix standard, or by user-defined flags.
Expand Down Expand Up @@ -154,12 +165,15 @@ _exists_inside_package_or_bundle(f::AbstractString) -> Bool
Determines whether the given path exists inside a package or bundle on macOS. If it does, the path will be considered hidden.

See also: [`_ispackage_or_bundle`](@ref), [`_ishidden_macos`](@ref)

!!! note
This function necessitates/expects that the file given to it is its real path, as it is possible that the file provided has a trailing slash, meaning the first "parent" this function will check is itself. This also makes relative paths much simpler to work with.
"""
_exists_inside_package_or_bundle

"""
```julia
_ishidden_macos(f::AbstractString) -> Bool
_ishidden_macos(f::AbstractString, rp::AbstractString) -> Bool
```

Determines if the specified file or directory on macOS is hidden from ordinary directory listings. There are a few conditions this function needs to check:
Expand All @@ -182,7 +196,7 @@ _ishidden_macos

"""
```julia
_ishidden_bsd(f::AbstractString) -> Bool
_ishidden_bsd(f::AbstractString, rp::AbstractString) -> Bool
```

Determines if the specified file or directory is hidden from ordinary directory listings by checking the following conditions:
Expand Down Expand Up @@ -219,10 +233,13 @@ FILE_ATTRIBUTE_SYSTEM

"""
```julia
_ishidden_windows(f::AbstractString) -> Bool
_ishidden_windows(f::AbstractString, rp::AbstractString) -> Bool
```

Determine if the specified file or directory is hidden from ordinary directory listings for operating systems that are derivations of Microsoft Windows NT.

!!! note
This function necessitates/expects that the file given to it is its real path.
"""
_ishidden_windows

18 changes: 16 additions & 2 deletions test/runtests.jl
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
using HiddenFiles
using Test

@testset "HiddenFiles.jl" begin
randpath(path_len::Integer = 64) = String(rand(Char, path_len)) # this path shouldn't exist

@static if Sys.isunix()
function mk_temp_dot_file(parent::String = tempdir())
tmp_hidden = joinpath(parent, '.' * basename(tempname()))
Expand All @@ -13,7 +16,7 @@ using Test
@testset "HiddenFiles.jl—General UNIX" begin
@test ishidden(p)
@test !ishidden(homedir())
@test_throws Base.IOError HiddenFiles.ishidden("~/$(basename(p′))")
@test_throws Union{Base.IOError, SystemError} HiddenFiles.ishidden("~/$(basename(p′))")
@test HiddenFiles.ishidden(expanduser("~/$(basename(p′))"))
end

Expand All @@ -38,11 +41,15 @@ using Test
# Case 4: Packages and bundles
@test !ishidden("/System/Applications/Utilities/Terminal.app")
@test ishidden("/System/Applications/Utilities/Terminal.app/Contents")
@test ishidden("/System/Applications/Utilities/Terminal.app/Contents/../../Terminal.app/Contents")
@test ishidden("/////System/Applications/Utilities/Terminal.app/Contents/../Contents")
@test ishidden("/System/Applications/Utilities/Terminal.app/Contents/../Contents///MacOS////../MacOS/../../Contents/MacOS/Terminal///")
@test !ishidden("/")
@test ishidden("/System/Applications/Utilities/Terminal.app/Contents/") # This should be the same as above, as we expand all paths using realpath
@test !HiddenFiles._ispackage_or_bundle("/System/Applications/Utilities/Terminal.app/Contents/")
@test HiddenFiles._exists_inside_package_or_bundle("/System/Applications/Utilities/Terminal.app/Contents/")
@test !HiddenFiles._exists_inside_package_or_bundle("/bin/")
f = String(rand(Char, 32)) # this path shouldn't exist
f = randpath()
cfstr_nonexistent = HiddenFiles._cfstring_create_with_cstring(f)
@test_throws Exception HiddenFiles._mditem_create(cfstr_nonexistent)
encoding_mode_nonexistent = 0x1c000101 # this encoding mode should not exist
Expand Down Expand Up @@ -96,4 +103,11 @@ using Test
@test false
end
end


@testset "HiddenFiles.jl—Path Handling" begin
f = randpath()
# Julia < 1.3 throws a SystemError when `realpath` fails
@test_throws Union{Base.IOError, SystemError} ishidden(f)
end
end