diff --git a/Project.toml b/Project.toml index b0ac001..55ba599 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "HiddenFiles" uuid = "d01c2003-3cc0-4d61-912c-b250feb01c5b" authors = ["Jake Ireland and contributors"] -version = "0.1.0" +version = "0.1.1" [compat] julia = "1" diff --git a/src/HiddenFiles.jl b/src/HiddenFiles.jl index 0bfe883..e6ecfc9 100644 --- a/src/HiddenFiles.jl +++ b/src/HiddenFiles.jl @@ -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 @@ -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 @@ -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 @@ -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) @@ -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 @@ -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 @@ -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 diff --git a/src/docs.jl b/src/docs.jl index 8a23c92..af41ab6 100644 --- a/src/docs.jl +++ b/src/docs.jl @@ -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). @@ -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 @@ -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. @@ -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: @@ -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: @@ -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 diff --git a/test/runtests.jl b/test/runtests.jl index 427de09..07eb306 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -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())) @@ -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 @@ -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 @@ -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