From d3fa718ec2b91f21a346d385dcd66bf708c973e1 Mon Sep 17 00:00:00 2001 From: Jake Ireland Date: Wed, 28 Sep 2022 00:20:02 +1300 Subject: [PATCH 1/8] Implement PathStruct and pass to _ishidden functions 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) --- src/HiddenFiles.jl | 41 +++++++++++------------------------- src/path.jl | 52 ++++++++++++++++++++++++++++++++++++++++++++++ test/runtests.jl | 20 +++++++++++++++++- 3 files changed, 83 insertions(+), 30 deletions(-) create mode 100644 src/path.jl diff --git a/src/HiddenFiles.jl b/src/HiddenFiles.jl index e6ecfc9..95a5f5b 100644 --- a/src/HiddenFiles.jl +++ b/src/HiddenFiles.jl @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 diff --git a/src/path.jl b/src/path.jl new file mode 100644 index 0000000..1704f3f --- /dev/null +++ b/src/path.jl @@ -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 + diff --git a/test/runtests.jl b/test/runtests.jl index 07eb306..861bc91 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -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 From d466e36295cd65a3f149e23c16a2c94a72ab6dbf Mon Sep 17 00:00:00 2001 From: Jake Ireland Date: Tue, 25 Apr 2023 13:51:12 +1200 Subject: [PATCH 2/8] Formatting changes with better line length in comments --- src/HiddenFiles.jl | 124 +++++++++++++++++++++++++-------------------- src/path.jl | 21 ++++---- 2 files changed, 80 insertions(+), 65 deletions(-) diff --git a/src/HiddenFiles.jl b/src/HiddenFiles.jl index 95a5f5b..ba0505e 100644 --- a/src/HiddenFiles.jl +++ b/src/HiddenFiles.jl @@ -34,83 +34,95 @@ include("path.jl") _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(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 # https://opensource.apple.com/source/xnu/xnu-4570.41.2/bsd/sys/stat.h.auto.html # https://www.freebsd.org/cgi/man.cgi?query=chflags&sektion=2 const UF_HIDDEN = 0x00008000 - + # https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man2/stat.2.html # http://docs.libuv.org/en/v1.x/fs.html#c.uv_stat_t const ST_FLAGS_STAT_OFFSET = 0x15 function _st_flags(f::AbstractString) statbuf = Vector{UInt32}(undef, ccall(:jl_sizeof_stat, Int32, ())) - + # int stat(const char *restrict path, struct stat *restrict buf); # int stat(const char * restrict path, struct stat * restrict sb); i = ccall(:jl_stat, Int32, (Cstring, Ptr{Cvoid}), f, statbuf) iszero(i) || Base.uv_error("_st_flags($(repr(f)))", i) - + # st_flags offset is at index 11, or 21 in 32-bit return statbuf[ST_FLAGS_STAT_OFFSET] end - + # https://github.com/dotnet/runtime/blob/5992145db2cb57956ee444aa0f0c2f3f85ee3673/src/native/libs/System.Native/pal_io.c#L219 # 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 - + _isinvisible(f::AbstractString) = (_st_flags(f) & UF_HIDDEN) == UF_HIDDEN + _ishidden_bsd_related(ps::PathStruct) = _ishidden_unix(ps) || _isinvisible(ps.realpath) end - + @static if Sys.isapple() # macOS/Darwin include("utils/darwin.jl") - + ###=== Hidden Files and Directories: Simplifying the User Experience ===## ##=== https://developer.apple.com/library/archive/documentation/FileManagement/Conceptual/FileSystemProgrammingGuide/FileSystemOverview/FileSystemOverview.html#//apple_ref/doc/uid/TP40010672-CH2-SW7 ===# - + #=== Case 1: Dot directories and files ===# - # Any file or directory whose name starts with a period (`.`) character is hidden automatically. This convention is taken from UNIX, - # which used it to hide system scripts and other special types of files and directories. Two special directories in this category - # are the `.` and `..` directories, which are references to the current and parent directories respectively. This case is handled by - # _ishidden_unix - - + # Any file or directory whose name starts with a period (`.`) character is hidden + # automatically. This convention is taken from UNIX, which used it to hide system + # scripts and other special types of files and directories. Two special directories + # in this category are the `.` and `..` directories, which are references to the + # current and parent directories respectively. This case is handled by _ishidden_unix + + #=== Case 2: UNIX-specific directories ===# - # The directories in this category are inherited from traditional UNIX installations. They are an important part of the system’s - # BSD layer but are more useful to software developers than end users. Some of the more important directories that are hidden include: - # - `/bin`—Contains essential command-line binaries. Typically, you execute these binaries from command-line scripts. - # - `/dev`—Contains essential device files, such as mount points for attached hardware. + # The directories in this category are inherited from traditional UNIX installations. + # They are an important part of the system’s BSD layer but are more useful to + # software developers than end users. Some of the more important directories that + # are hidden include: + # - `/bin`—Contains essential command-line binaries. Typically, you execute + # these binaries from command-line scripts. + # - `/dev`—Contains essential device files, such as mount points for attached + # hardware. # - `/etc`—Contains host-specific configuration files. # - `/sbin`—Contains essential system binaries. # - `/tmp`—Contains temporary files created by apps and the system. - # - `/usr`—Contains non-essential command-line binaries, libraries, header files, and other data. - # - `/var`—Contains log files and other files whose content is variable. (Log files are typically viewed using the Console app.) + # - `/usr`—Contains non-essential command-line binaries, libraries, header files, + # and other data. + # - `/var`—Contains log files and other files whose content is variable. (Log + # files are typically viewed using the Console app.) # TODO _issystemfile(f::AbstractString) = false - - + + #=== Case 3: Explicitly hidden files and directories ===# - # The Finder may hide specific files or directories that should not be accessed directly by the user. The most notable example of - # this is the /Volumes directory, which contains a subdirectory for each mounted disk in the local file system from the command line. - # (The Finder provides a different user interface for accessing local disks.) In macOS 10.7 and later, the Finder also hides the - # `~/Library` directory—that is, the `Library` directory located in the user’s home directory. This case is handled by `_isinvisible`. - - + # The Finder may hide specific files or directories that should not be accessed + # directly by the user. The most notable example of this is the /Volumes directory, + # which contains a subdirectory for each mounted disk in the local file system + # from the command line. (The Finder provides a different user interface for + # accessing local disks.) In macOS 10.7 and later, the Finder also hides the + # `~/Library` directory—that is, the `Library` directory located in the user’s + # home directory. This case is handled by `_isinvisible`. + + #=== Case 4: Packages and bundles ===# - # Packages and bundles are directories that the Finder presents to the user as if they were files. Bundles hide the internal workings - # of executables such as apps and just present a single entity that can be moved around the file system easily. Similarly, packages - # allow apps to implement complex document formats consisting of multiple individual files while still presenting what appears to be a - # single document to the user. - + # Packages and bundles are directories that the Finder presents to the user as if + # they were files. Bundles hide the internal workings of executables such as apps + # and just present a single entity that can be moved around the file system easily. + # Similarly, packages allow apps to implement complex document formats consisting + # of multiple individual files while still presenting what appears to be a single + # document to the user. + # https://developer.apple.com/documentation/coreservices/kmditemcontenttypetree const K_MDITEM_CONTENT_TYPE_TREE = _cfstring_create_with_cstring("kMDItemContentTypeTree") - + # https://superuser.com/questions/1739420/ # https://stackoverflow.com/a/9858910/12069968 # https://github.com/osquery/osquery/blob/598983db97459f858e7a9cc5c731409ffc089b48/osquery/tables/system/darwin/extended_attributes.cpp#L111-L144 @@ -132,31 +144,33 @@ include("path.jl") return content_types # TODO: release/free mdattrs end - + # https://stackoverflow.com/a/12233785 # Bundles: https://developer.apple.com/library/archive/documentation/CoreFoundation/Conceptual/CFBundles/AboutBundles/AboutBundles.html # Packages: https://developer.apple.com/library/archive/documentation/CoreFoundation/Conceptual/CFBundles/DocumentPackages/DocumentPackages.html const PKG_BUNDLE_TYPES = ("com.apple.package", "com.apple.bundle", "com.apple.application-bundle") _ispackage_or_bundle(f::AbstractString) = any(t ∈ PKG_BUNDLE_TYPES for t in _k_mditem_content_type_tree(f)) - - # If a file or directory exists inside a package or bundle, then it is hidden. Packages or bundles themselves - # are not necessarily hidden. + + # 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 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 + # 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. + + # 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) - end + end return false end - - + + #=== All macOS cases ===# _ishidden_macos(ps::PathStruct) = _ishidden_bsd_related(ps) || _issystemfile(ps.path) || _exists_inside_package_or_bundle(ps.realpath) _ishidden = _ishidden_macos @@ -172,12 +186,12 @@ elseif Sys.iswindows() # https://github.com/retep998/winapi-rs/blob/5b1829956ef645f3c2f8236ba18bb198ca4c2468/src/um/winnt.rs#L4081-L4082 const FILE_ATTRIBUTE_HIDDEN = 0x2 const FILE_ATTRIBUTE_SYSTEM = 0x4 - + 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,), ps.realpath) - + # https://stackoverflow.com/a/1343643/12069968 # https://stackoverflow.com/a/14063074/12069968 return !iszero(f_attrs & (FILE_ATTRIBUTE_HIDDEN | FILE_ATTRIBUTE_SYSTEM)) @@ -188,8 +202,9 @@ else end -# 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 +# 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) ps = PathStruct(f; err_prefix = :ishidden) return _ishidden(ps) @@ -197,4 +212,3 @@ end end # end module - diff --git a/src/path.jl b/src/path.jl index 1704f3f..14204b7 100644 --- a/src/path.jl +++ b/src/path.jl @@ -13,18 +13,20 @@ 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)) + 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). + + # 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 @@ -39,14 +41,13 @@ struct PathStruct # 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 - From 559c1a4e3d3788ef8e9750a0514ab5d3ac7cf05a Mon Sep 17 00:00:00 2001 From: Jake Ireland Date: Tue, 25 Apr 2023 13:59:52 +1200 Subject: [PATCH 3/8] Use parametric (rather than abstract) types in PathStruct --- src/path.jl | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/src/path.jl b/src/path.jl index 14204b7..3cdd58d 100644 --- a/src/path.jl +++ b/src/path.jl @@ -1,7 +1,7 @@ -struct InvalidRealPathError <: Exception +struct InvalidRealPathError{S1, S2} <: Exception msg::String - expected::AbstractString - actual::AbstractString + expected::S1 + actual::S2 end function Base.showerror(io::IO, e::InvalidRealPathError) @@ -10,16 +10,16 @@ function Base.showerror(io::IO, e::InvalidRealPathError) print(io, "found ", '"', e.actual, '"') end -struct PathStruct - path::AbstractString - realpath::AbstractString +struct PathStruct{S1, S2} + path::S1 + realpath::S2 - function PathStruct(path::AbstractString, rp::AbstractString) + function PathStruct(path::S1, rp::S2) where {S1 <: AbstractString, S2 <: 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) + return new{S1, S2}(path, rp) end # Each OS branch defines its own _ishidden functions, some of which require the @@ -27,7 +27,7 @@ struct PathStruct # 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) + function PathStruct(path::S; err_prefix::Symbol = :ishidden) where {S <: AbstractString} # If path does not exist, `realpath` will error™ local rp::String try @@ -44,10 +44,11 @@ struct PathStruct # 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)) + 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) + return new{S, typeof(rp)}(path, rp) end end From 9c45c3019edba268ae7b0b5bd3304e50a43df5e2 Mon Sep 17 00:00:00 2001 From: Jake Ireland Date: Tue, 25 Apr 2023 14:09:16 +1200 Subject: [PATCH 4/8] Write docstrings for PathStruct and related objects --- src/path.jl | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/path.jl b/src/path.jl index 3cdd58d..7756a84 100644 --- a/src/path.jl +++ b/src/path.jl @@ -1,3 +1,14 @@ +""" +```julia +struct InvalidRealPathError{S1, S2} <: Exception + msg::String + expected::S1 + actual::S2 +end +```julia + +Custom exception used by [`PathStruct`](@ref) for when real path given to the struct is not the actual real path. +""" struct InvalidRealPathError{S1, S2} <: Exception msg::String expected::S1 @@ -10,6 +21,18 @@ function Base.showerror(io::IO, e::InvalidRealPathError) print(io, "found ", '"', e.actual, '"') end +""" +```julia +struct PathStruct{S1, S2} + path::S1 + realpath::S2 +end +PathStruct(path::S1, rp::S2) where {S1 <: AbstractString, S2 <: AbstractString} +PathStruct(path::S; err_prefix::Symbol = :ishidden) where {S <: AbstractString} +``` + +Convenient path object to pass around to various functions used by HiddenFiles.jl. +""" struct PathStruct{S1, S2} path::S1 realpath::S2 @@ -34,10 +57,13 @@ struct PathStruct{S1, S2} 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 From e2762c75aae35e11a8ef93c20c338e115c79149e Mon Sep 17 00:00:00 2001 From: Jake Ireland Date: Tue, 25 Apr 2023 14:13:14 +1200 Subject: [PATCH 5/8] Add more PathStruct tests --- test/runtests.jl | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/test/runtests.jl b/test/runtests.jl index 861bc91..c119951 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -3,41 +3,41 @@ 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())) touch(tmp_hidden) return tmp_hidden end - + p, p′ = mk_temp_dot_file(), mk_temp_dot_file(homedir()) - + @testset "HiddenFiles.jl—General UNIX" begin @test ishidden(p) @test !ishidden(homedir()) @test_throws Union{Base.IOError, SystemError} HiddenFiles.ishidden("~/$(basename(p′))") @test HiddenFiles.ishidden(expanduser("~/$(basename(p′))")) end - + @static if Sys.isapple() @testset "HiddenFiles.jl—macOS" begin # Case 1: Dot directories and files @test ishidden(p) @test !ishidden(homedir()) - + # Case 2: UNIX-specific directories # TODO: complete this case @test HiddenFiles.ishidden("/bin/") @test HiddenFiles.ishidden("/dev/") @test HiddenFiles.ishidden("/usr/") @test !HiddenFiles.ishidden("/tmp/") - + # Case 3: Explicitly hidden files and directories @test HiddenFiles._isinvisible("/Volumes") @test ishidden("/Volumes") @test !HiddenFiles._isinvisible(p′) - + # Case 4: Packages and bundles @test !ishidden("/System/Applications/Utilities/Terminal.app") @test ishidden("/System/Applications/Utilities/Terminal.app/Contents") @@ -85,7 +85,7 @@ using Test @test !ishidden("/tmp/") end end - + rm(p); rm(p′) elseif Sys.iswindows() @testset "HiddenFiles.jl—Windows" begin @@ -103,24 +103,25 @@ using Test @test false end end - - + + @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") - + @test_throws HiddenFiles.InvalidRealPathError HiddenFiles.PathStruct("/bin", "/../bin") + @test HiddenFiles.PathStruct("/../bin").realpath == "/bin" + @test HiddenFiles.PathStruct(".").path == "." 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:\\..\\") + @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) From 6b7f2b22b9ae5074f5c6c1eff126552f3573313a Mon Sep 17 00:00:00 2001 From: Jake Ireland Date: Tue, 25 Apr 2023 14:14:30 +1200 Subject: [PATCH 6/8] Small formatting changes to whitespace --- src/docs.jl | 3 +-- src/utils/darwin.jl | 5 ++--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/docs.jl b/src/docs.jl index af41ab6..c3542f0 100644 --- a/src/docs.jl +++ b/src/docs.jl @@ -59,7 +59,7 @@ const UF_HIDDEN = 0x00008000 The flag on macOS or BSD systems specifying whether the file may be hidden from directory. -See `chflags`: +See `chflags`: - [macOS](https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man2/chflags.2.html) - [BSD](https://www.freebsd.org/cgi/man.cgi?query=chflags&sektion=2) """ @@ -242,4 +242,3 @@ Determine if the specified file or directory is hidden from ordinary directory l This function necessitates/expects that the file given to it is its real path. """ _ishidden_windows - diff --git a/src/utils/darwin.jl b/src/utils/darwin.jl index 3a2c940..8e8c272 100644 --- a/src/utils/darwin.jl +++ b/src/utils/darwin.jl @@ -25,7 +25,7 @@ Default string encoding for working with paths in macOS. !!! note You can reassign this variable so that other Core Foundation string functions implemented in this package uses your non-default string encoding. See `K_CF_STRING_ENCODING_*` values for more string encoding options. - + [1]: https://developer.apple.com/documentation/corefoundation/cfstringbuiltinencodings """ CF_STRING_ENCODING = K_CF_STRING_ENCODING_MAC_ROMAN # K_CF_STRING_ENCODING_UTF8 or UTF16 doesn't seem to work @@ -44,7 +44,7 @@ See also: [`_string_from_cf_string`](@ref). function _cfstring_create_with_cstring(s::AbstractString, encoding::Unsigned = CF_STRING_ENCODING) # https://developer.apple.com/documentation/corefoundation/1542942-cfstringcreatewithcstring # CFStringRef CFStringCreateWithCString(CFAllocatorRef alloc, const char *cStr, CFStringEncoding encoding); - cfstr = ccall(:CFStringCreateWithCString, Cstring, + cfstr = ccall(:CFStringCreateWithCString, Cstring, (Ptr{Cvoid}, Cstring, UInt32), C_NULL, s, encoding) cfstr == C_NULL && error("Cannot create CF String for $(repr(s)) using encoding $(repr(encoding))") @@ -195,4 +195,3 @@ function _string_from_cf_string(cfstr::Cstring, encoding::Unsigned = CF_STRING_E end return String(take!(cfio)) end - From 34d226945ad01f5ac588059283a02c1e6dfc8c59 Mon Sep 17 00:00:00 2001 From: Jake Ireland Date: Tue, 25 Apr 2023 14:21:00 +1200 Subject: [PATCH 7/8] Fix tests for /bin real path in Linux --- test/runtests.jl | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/test/runtests.jl b/test/runtests.jl index c119951..4469e04 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -107,10 +107,12 @@ using Test @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 + bin_rp = Sys.islinux() ? "/usr/bin" : "/bin" + + @test HiddenFiles.PathStruct("/bin", bin_rp) isa HiddenFiles.PathStruct + @test HiddenFiles.PathStruct("/../bin", bin_rp) isa HiddenFiles.PathStruct @test_throws HiddenFiles.InvalidRealPathError HiddenFiles.PathStruct("/bin", "/../bin") - @test HiddenFiles.PathStruct("/../bin").realpath == "/bin" + @test HiddenFiles.PathStruct("/../bin").realpath == bin_rp @test HiddenFiles.PathStruct(".").path == "." elseif Sys.iswindows() From c8a300af0682a7bfade36883e7067d417606b3f3 Mon Sep 17 00:00:00 2001 From: Jake Ireland Date: Tue, 25 Apr 2023 14:26:06 +1200 Subject: [PATCH 8/8] Add error handling for invalid path as well as invalid real path --- src/path.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/path.jl b/src/path.jl index 7756a84..aee04eb 100644 --- a/src/path.jl +++ b/src/path.jl @@ -38,8 +38,8 @@ struct PathStruct{S1, S2} realpath::S2 function PathStruct(path::S1, rp::S2) where {S1 <: AbstractString, S2 <: AbstractString} - ispath(rp) || throw(Base.uv_error("PathStruct($(repr(path)))", Base.UV_ENOENT)) - # TODO: this will fail if path is not valid + ispath(path) || throw(Base.uv_error("PathStruct($(repr(path)))", Base.UV_ENOENT)) + ispath(rp) || throw(Base.uv_error("PathStruct($(repr(rp)))", Base.UV_ENOENT)) realpath(path) == rp || throw(InvalidRealPathError("PathStruct($(repr(path)))", realpath(path), rp)) return new{S1, S2}(path, rp)