Skip to content

Commit

Permalink
Merge pull request #23 from jakewilliami/path-struct
Browse files Browse the repository at this point in the history
Implement PathStruct and pass to _ishidden functions
  • Loading branch information
jakewilliami authored Apr 25, 2023
2 parents 2d4a64e + c8a300a commit 12ff92e
Show file tree
Hide file tree
Showing 5 changed files with 194 additions and 98 deletions.
161 changes: 79 additions & 82 deletions src/HiddenFiles.jl
Original file line number Diff line number Diff line change
Expand Up @@ -25,91 +25,104 @@ 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
# 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
_ishidden_bsd_related(f::AbstractString, rp::AbstractString) = _ishidden_unix(f, rp) || _isinvisible(rp)
_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
Expand All @@ -131,36 +144,38 @@ include("docs.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(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 @@ -171,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(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
return !iszero(f_attrs & (FILE_ATTRIBUTE_HIDDEN | FILE_ATTRIBUTE_SYSTEM))
Expand All @@ -187,31 +202,13 @@ 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


end # end module

3 changes: 1 addition & 2 deletions src/docs.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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)
"""
Expand Down Expand Up @@ -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

80 changes: 80 additions & 0 deletions src/path.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
"""
```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
actual::S2
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

"""
```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

function PathStruct(path::S1, rp::S2) where {S1 <: AbstractString, S2 <: AbstractString}
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)
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::S; err_prefix::Symbol = :ishidden) where {S <: AbstractString}
# 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{S, typeof(rp)}(path, rp)
end
end
5 changes: 2 additions & 3 deletions src/utils/darwin.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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))")
Expand Down Expand Up @@ -195,4 +195,3 @@ function _string_from_cf_string(cfstr::Cstring, encoding::Unsigned = CF_STRING_E
end
return String(take!(cfio))
end

Loading

0 comments on commit 12ff92e

Please sign in to comment.