ffi-libfuse 0.0.1.rctest12 → 0.1.0.rc20220550

Sign up to get free protection for your applications and to get access to all the features.
Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +3 -1
  3. data/CHANGES.md +14 -0
  4. data/LICENSE +21 -0
  5. data/README.md +127 -44
  6. data/lib/ffi/accessors.rb +6 -6
  7. data/lib/ffi/boolean_int.rb +27 -0
  8. data/lib/ffi/devt.rb +23 -0
  9. data/lib/ffi/encoding.rb +38 -0
  10. data/lib/ffi/gnu_extensions.rb +1 -1
  11. data/lib/ffi/libfuse/ackbar.rb +3 -3
  12. data/lib/ffi/libfuse/adapter/context.rb +12 -10
  13. data/lib/ffi/libfuse/adapter/fuse2_compat.rb +52 -51
  14. data/lib/ffi/libfuse/adapter/fuse3_support.rb +0 -1
  15. data/lib/ffi/libfuse/adapter/ruby.rb +499 -148
  16. data/lib/ffi/libfuse/adapter/safe.rb +1 -1
  17. data/lib/ffi/libfuse/adapter.rb +1 -2
  18. data/lib/ffi/libfuse/callbacks.rb +1 -1
  19. data/lib/ffi/libfuse/filesystem/accounting.rb +116 -0
  20. data/lib/ffi/libfuse/filesystem/mapped_dir.rb +74 -0
  21. data/lib/ffi/libfuse/filesystem/mapped_files.rb +141 -0
  22. data/lib/ffi/libfuse/filesystem/pass_through_dir.rb +55 -0
  23. data/lib/ffi/libfuse/filesystem/pass_through_file.rb +45 -0
  24. data/lib/ffi/libfuse/filesystem/utils.rb +102 -0
  25. data/lib/ffi/libfuse/filesystem/virtual_dir.rb +306 -0
  26. data/lib/ffi/libfuse/filesystem/virtual_file.rb +94 -0
  27. data/lib/ffi/libfuse/filesystem/virtual_fs.rb +188 -0
  28. data/lib/ffi/libfuse/filesystem/virtual_node.rb +101 -0
  29. data/lib/ffi/libfuse/filesystem.rb +25 -0
  30. data/lib/ffi/libfuse/fuse2.rb +21 -21
  31. data/lib/ffi/libfuse/fuse3.rb +12 -12
  32. data/lib/ffi/libfuse/fuse_args.rb +69 -34
  33. data/lib/ffi/libfuse/fuse_buffer.rb +128 -26
  34. data/lib/ffi/libfuse/fuse_callbacks.rb +1 -5
  35. data/lib/ffi/libfuse/fuse_common.rb +55 -61
  36. data/lib/ffi/libfuse/fuse_config.rb +134 -143
  37. data/lib/ffi/libfuse/fuse_conn_info.rb +310 -134
  38. data/lib/ffi/libfuse/fuse_context.rb +45 -3
  39. data/lib/ffi/libfuse/fuse_operations.rb +43 -19
  40. data/lib/ffi/libfuse/fuse_version.rb +10 -6
  41. data/lib/ffi/libfuse/main.rb +80 -37
  42. data/lib/ffi/libfuse/version.rb +1 -1
  43. data/lib/ffi/libfuse.rb +13 -4
  44. data/lib/ffi/ruby_object.rb +1 -1
  45. data/lib/ffi/stat/constants.rb +9 -0
  46. data/lib/ffi/stat/native.rb +36 -6
  47. data/lib/ffi/stat/time_spec.rb +26 -10
  48. data/lib/ffi/stat.rb +111 -22
  49. data/lib/ffi/stat_vfs.rb +59 -1
  50. data/lib/ffi/struct_wrapper.rb +22 -1
  51. data/sample/hello_fs.rb +54 -0
  52. data/sample/memory_fs.rb +5 -181
  53. data/sample/no_fs.rb +20 -21
  54. data/sample/pass_through_fs.rb +30 -0
  55. metadata +77 -4
  56. data/lib/ffi/libfuse/adapter/thread_local_context.rb +0 -36
@@ -48,7 +48,7 @@ module FFI
48
48
  -e.errno
49
49
  rescue StandardError, ScriptError => e
50
50
  # rubocop:disable Layout/LineLength
51
- warn "FFI::libfuse error in callback #{fuse_method}: #{e.class.name}: #{e.message}\n\t#{e.backtrace.join("\n\t")}"
51
+ warn ["FFI::Libfuse error in #{fuse_method}", *e.backtrace.reverse, "#{e.class.name}:#{e.message}"].join("\n\t")
52
52
  # rubocop:enable Layout/LineLength
53
53
  -Errno::ENOTRECOVERABLE::Errno
54
54
  end
@@ -11,7 +11,7 @@ module FFI
11
11
  #
12
12
  # These will implement {FuseOperations#fuse_wrappers} to add the proc which can then...
13
13
  #
14
- # * prepend additional arguments - eg. ({Context})
14
+ # * populate thread local information - eg. ({Context})
15
15
  # * wrap common arguments - eg. ({Pathname})
16
16
  # * handle return values/exceptions - eg. ({Safe})
17
17
  # * or just wrap the underlying block - eg. ({Debug})
@@ -70,7 +70,6 @@ module FFI
70
70
  end
71
71
 
72
72
  require_relative 'adapter/context'
73
- require_relative 'adapter/thread_local_context'
74
73
  require_relative 'adapter/debug'
75
74
  require_relative 'adapter/ruby'
76
75
  require_relative 'adapter/interrupt'
@@ -37,7 +37,7 @@ module FFI
37
37
 
38
38
  def initialize_callbacks(callbacks, delegate:, wrappers: [])
39
39
  callbacks.select { |m| respond_to_callback?(m, delegate) }.each do |m|
40
- register(m, wrappers) { |*f_args| delegate.send(m, *f_args) }
40
+ register(m, wrappers) { |*f_args| delegate.public_send(m, *f_args) }
41
41
  end
42
42
  end
43
43
 
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../stat_vfs'
4
+
5
+ module FFI
6
+ module Libfuse
7
+ module Filesystem
8
+ # Helper for filesystem accounting
9
+ class Accounting
10
+ OPTIONS = { 'max_space=' => :max_space, 'max_nodes=' => :max_nodes }.freeze
11
+
12
+ HELP =
13
+ <<~END_HELP
14
+ #{name} options:
15
+ -o max_space=<int> maximum space consumed by files, --ve will always show free space
16
+ -o max_nodes=<int> maximum number of files and directories, -ve will always show free nodes
17
+
18
+ END_HELP
19
+
20
+ def fuse_opt_proc(key:, value:, **)
21
+ return :keep unless OPTIONS.values.include?(key)
22
+
23
+ public_send("#{key}=", value.to_i)
24
+ :handled
25
+ end
26
+
27
+ # @return [Integer] maximum allowed space in bytes
28
+ #
29
+ # Positive values will limit values in {adjust} to stay below this value
30
+ #
31
+ # Negative or zero will simply report this amount of space as 'free' in {to_statvfs}
32
+ attr_accessor :max_space
33
+
34
+ # @return [Integer] maximum number of (virtual) inodes
35
+ #
36
+ # Positive values will limit {adjust} to stay below this value
37
+ #
38
+ # Negative or zero will simply report this number of inodes as 'free' in {to_statvfs}
39
+ attr_accessor :max_nodes
40
+
41
+ # @return [Integer] accumulated space in bytes
42
+ attr_reader :space
43
+
44
+ # @return [Integer] accumulated inodes (typically count of files and directories)
45
+ attr_reader :nodes
46
+
47
+ # @return [Integer] block size for statvfs
48
+ attr_accessor :block_size
49
+
50
+ def initialize(max_space: 0, max_nodes: 0, block_size: 1024)
51
+ @nodes = 0
52
+ @space = 0
53
+ @max_space = max_space
54
+ @max_nodes = max_nodes
55
+ @block_size = block_size
56
+ end
57
+
58
+ # Adjust accumlated statistics
59
+ # @param [Integer] delta_space change in {#space} usage
60
+ # @param [Integer] delta_nodes change in {#nodes} usage
61
+ # @return [self]
62
+ # @raise [Errno::ENOSPC] if adjustment {#space}/{#nodes} would exceed {#max_space} or {#max_nodes}
63
+ def adjust(delta_space, delta_nodes = 0, strict: true)
64
+ strict_space = strict && delta_space.positive? && max_space.positive?
65
+ raise Errno::ENOSPC if strict_space && space + delta_space > max_space
66
+
67
+ strict_nodes = strict && delta_nodes.positive? && max_nodes.positive?
68
+ raise Errno::ENOSPC if strict_nodes && nodes + delta_nodes > max_nodes
69
+
70
+ @nodes += delta_nodes
71
+ @space += delta_space
72
+ self
73
+ end
74
+
75
+ # Adjust for incremental write
76
+ # @param [Integer] current_size
77
+ # @param [Integer] data_size size of new data
78
+ # @param [Integer] offset offset of new data
79
+ # @return [self]
80
+ def write(current_size, data_size, offset, strict: true)
81
+ adjust(offset + data_size - current_size, strict: strict) if current_size < offset + data_size
82
+ self
83
+ end
84
+
85
+ # Adjust for truncate
86
+ # @param [Integer] current_size
87
+ # @param [Integer] new_size the size being truncated to
88
+ # @return [self]
89
+ def truncate(current_size, new_size)
90
+ adjust(new_size - current_size, strict: false) if new_size < current_size
91
+ self
92
+ end
93
+
94
+ # rubocop:disable Metrics/AbcSize
95
+
96
+ # @param [FFI::StatVfs] statvfs an existing statvfs buffer to fill
97
+ # @param [Integer] block_size
98
+ # @return [FFI::StatVfs] the filesystem statistics
99
+ def to_statvfs(statvfs = FFI::StatVfs.new, block_size: self.block_size || 1024)
100
+ used_blocks, max_blocks = [space, max_space].map { |s| s / block_size }
101
+ max_blocks = used_blocks - max_blocks unless max_blocks.positive?
102
+ max_files = max_nodes.positive? ? max_nodes : nodes - max_nodes
103
+ statvfs.bsize = block_size # block size (in Kb)
104
+ statvfs.frsize = block_size # fragment size pretty much always bsize
105
+ statvfs.blocks = max_blocks
106
+ statvfs.bfree = max_blocks - used_blocks
107
+ statvfs.bavail = max_blocks - used_blocks
108
+ statvfs.files = max_files
109
+ statvfs.ffree = max_files - nodes
110
+ statvfs
111
+ end
112
+ # rubocop:enable Metrics/AbcSize
113
+ end
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'mapped_files'
4
+
5
+ module FFI
6
+ module Libfuse
7
+ module Filesystem
8
+ # A read-only directory of {MappedFiles}
9
+ #
10
+ # Subclasses must implement {#entries} and {#map_path}
11
+ class MappedDir
12
+ include MappedFiles
13
+ include Utils
14
+ attr_accessor :stat
15
+
16
+ # @!method entries
17
+ # @abstract
18
+ # @return [Enumerable] set of entries in this directory (excluding '.' and '..')
19
+
20
+ def initialize(accounting: nil)
21
+ @accounting = accounting
22
+ @root = VirtualNode.new(accounting: accounting)
23
+ end
24
+
25
+ # @!group Fuse Callbacks
26
+
27
+ # For the root path provides this directory's stat information, otherwise passes on to the next filesystem
28
+ def getattr(path, stat = nil, _ffi = nil)
29
+ return super unless root?(path)
30
+
31
+ stat&.directory(@root.virtual_stat.merge({ nlink: entries.size + 2 }))
32
+
33
+ self
34
+ end
35
+
36
+ # For root path enumerates {#entries}
37
+ # @raise [Errno::ENOTDIR] unless root path
38
+ def readdir(path, *_args, &block)
39
+ raise Errno::ENOTDIR unless root?(path)
40
+
41
+ %w[. ..].concat(entries).each(&block)
42
+ end
43
+
44
+ def mkdir(path, mode, *_args)
45
+ raise Errno::EROFS unless root?(path)
46
+
47
+ @root.init_node(mode)
48
+ end
49
+
50
+ # @!endgroup
51
+
52
+ # Passes FUSE Callbacks on to the {#root} filesystem
53
+ def method_missing(method, path = nil, *args, &block)
54
+ return @root.public_send(method, path, *args, &block) if @root.respond_to?(method) && root?(path)
55
+
56
+ raise Errno::ENOTSUP if FuseOperations.path_callbacks.include?(method)
57
+
58
+ super
59
+ end
60
+
61
+ def respond_to_missing?(method, private = false)
62
+ (FuseOperations.fuse_callbacks.include?(method) && @root.respond_to?(method, false)) || super
63
+ end
64
+
65
+ # subclass only call super for root path
66
+ def map_path(path)
67
+ raise ArgumentError, "map_path received non root path #{path}" unless root?(path)
68
+
69
+ [path, @root]
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,141 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../adapter'
4
+ require_relative 'accounting'
5
+
6
+ module FFI
7
+ module Libfuse
8
+ module Filesystem
9
+ # An abstract filesystem mapping paths to either real files or an alternate filesystem based on outcome of
10
+ # {#map_path} as implemented by including class
11
+ #
12
+ # Real files permissions are made read-only by default. Including classes can override {#stat_mask} to
13
+ # change this behaviour
14
+ #
15
+ # Implements callbacks satisfying {Adapter::Ruby} which is automatically included.
16
+ module MappedFiles
17
+ # Do we have ffi-xattr to handle extended attributes in real files
18
+ HAS_XATTR =
19
+ begin
20
+ require 'ffi-xattr'
21
+ true
22
+ rescue LoadError
23
+ false
24
+ end
25
+
26
+ # @return [Accounting|nil]
27
+ # if set the accounting object will be used to provide {#statfs} for the root path
28
+ # @note the real LIBC statvfs is always used for non-root paths
29
+ attr_accessor :accounting
30
+
31
+ # @!method map_path(path)
32
+ # @abstract
33
+ # @param [String] path the path in the fuse filesystem
34
+ # @return [String] mapped_path in an underlying filesystem
35
+ #
36
+ # Fuse callbacks are fulfilled using Ruby's native File methods called on this path
37
+ # @return [String, Adapter::Ruby::Prepend] mapped_path, filesystem
38
+ #
39
+ # If an optional filesystem value is returned fuse callbacks will be passed on to this filesystem with the
40
+ # mapped_path and other callback args unchanged
41
+ # @return [nil]
42
+ #
43
+ # eg on create to indicate the path does not exist
44
+
45
+ # Manipulate file attributes
46
+ #
47
+ # Default implementation forces read-only permissions
48
+ # @overload stat_mask(path,stat)
49
+ # @param [String] path the path received by {#getattr}
50
+ # @param [FFI::Stat] stat loaded from the mapped file, can be filled, mapped as necessary
51
+ # @return [FFI::Stat] stat
52
+ def stat_mask(_path, stat)
53
+ stat.mask(0o0222)
54
+ end
55
+
56
+ # @!group FUSE Callbacks
57
+
58
+ # Pass to real stat and then {#stat_mask}
59
+ def getattr(path, stat, ffi = nil)
60
+ if (fd = ffi&.fh&.fileno)
61
+ stat.fstat(fd)
62
+ else
63
+ path_method(__method__, path, stat, ffi) { |rp| stat.stat(rp) }
64
+ end
65
+
66
+ stat_mask(path, stat)
67
+ end
68
+
69
+ # Create real file - assuming the path can be mapped before it exists
70
+ def create(path, perms, ffi)
71
+ path_method(__method__, path, perms, ffi, error: Errno::EROFS) do |rp|
72
+ File.open(rp, ffi.flags, perms)
73
+ end
74
+ end
75
+
76
+ # @return [File] the newly opened file at {#map_path}(path)
77
+ def open(path, ffi)
78
+ path_method(__method__, path, ffi) { |rp| File.open(rp, ffi.flags) }
79
+ end
80
+
81
+ # Truncates the file handle (or the real file)
82
+ def truncate(path, size, ffi = nil)
83
+ return ffi.fh.truncate(size) if ffi&.fh
84
+
85
+ path_method(__method__, path, size, ffi) { |rp| File.truncate(rp, size) }
86
+ end
87
+
88
+ # Delete the real file
89
+ def unlink(path)
90
+ path_method(__method__, path) { |rp| File.unlink(rp) }
91
+ end
92
+
93
+ # Calls File.utime on an Integer file handle or the real file
94
+ def utimens(_path, atime, mtime, ffi = nil)
95
+ return File.utime(atime, mtime, ffi.fh) if ffi&.fh.is_a?(Integer)
96
+
97
+ path_method(__method__, atime, mtime, ffi) { |rp| File.utime(atime, mtime, rp) }
98
+ end
99
+
100
+ # @return [String] the value of the extended attribute name from the real file
101
+ def getxattr(path, name)
102
+ return nil unless HAS_XATTR
103
+
104
+ path_method(__method__, path, name) { |rp| Xattr.new(rp)[name] }
105
+ end
106
+
107
+ # @return [Array<String>] the list of extended attributes from the real file
108
+ def listxattr(path)
109
+ return [] unless HAS_XATTR
110
+
111
+ path_method(__method__, path) { |rp| Xattr.new(rp).list }
112
+ end
113
+
114
+ # TODO: Set xattr
115
+ # TODO: chmod, change the stat[:mode]
116
+ # TODO: chown, change the stat[:uid,:gid]
117
+
118
+ def statfs(path, statvfs)
119
+ return accounting&.to_statvfs(statvfs) if root?(path)
120
+
121
+ path_method(__method__, path, statvfs) { |rp| statvfs.from(rp) }
122
+ end
123
+ # @!endgroup
124
+
125
+ # @!visibility private
126
+ def self.included(mod)
127
+ mod.prepend(Adapter::Ruby::Prepend)
128
+ end
129
+
130
+ private
131
+
132
+ def path_method(callback, path, *args, error: Errno::ENOENT, block: nil)
133
+ rp, fs = map_path(path)
134
+ raise error if error && !rp
135
+
136
+ fs ? fs.send(callback, rp, *args, &block) : yield(rp)
137
+ end
138
+ end
139
+ end
140
+ end
141
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'mapped_files'
4
+
5
+ module FFI
6
+ module Libfuse
7
+ module Filesystem
8
+ # A Filesystem that maps paths to an underlying directory
9
+ class PassThroughDir
10
+ include MappedFiles
11
+ include Adapter::Debug
12
+ include Adapter::Safe
13
+ include Utils
14
+
15
+ # @return [String] The base directory
16
+ attr_accessor :base_dir
17
+
18
+ # @!group FUSE Callbacks
19
+
20
+ # @return [Dir] the directory at {#map_path}(path)
21
+ def opendir(path, _ffi)
22
+ Dir.new(map_path(path))
23
+ end
24
+
25
+ # Removes the directory at {#map_path}(path)
26
+ def rmdir(path)
27
+ return Dir.rmdir(map_path(path)) unless root?(path)
28
+
29
+ accounting&.adjust(0, -1) if root?(path)
30
+ self
31
+ end
32
+
33
+ # Creates the directory at {#map_path}(path)
34
+ def mkdir(path, mode)
35
+ return Dir.mkdir(map_path(path), mode) unless root?(path)
36
+
37
+ accounting&.adjust(0, +1)
38
+ self
39
+ end
40
+
41
+ # Creates the File at {#map_path}(path)
42
+ def create(path, perms = 0o644, ffi = nil)
43
+ File.open(map_path(path), ffi&.flags, perms)
44
+ end
45
+
46
+ # @!endgroup
47
+
48
+ # @return [String] {#base_dir} + path
49
+ def map_path(path)
50
+ root?(path) ? @base_dir : "#{@base_dir}#{path}"
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'accounting'
4
+ require_relative 'mapped_files'
5
+
6
+ module FFI
7
+ module Libfuse
8
+ module Filesystem
9
+ # Represents a single regular file at a given underlying path
10
+ class PassThroughFile
11
+ include MappedFiles
12
+
13
+ # @param [String] real_path
14
+ def initialize(real_path)
15
+ @real_path = real_path
16
+ end
17
+
18
+ # @!visibility private
19
+ def map_path(path)
20
+ raise Errno::ENOENT unless root?(path)
21
+
22
+ @real_path
23
+ end
24
+
25
+ # @!group FUSE Callbacks
26
+
27
+ # Adjust accounting to add a node
28
+ def create(path, perms, ffi)
29
+ raise Errno::ENOENT unless root?(path)
30
+
31
+ accounting&.adjust(0, +1)
32
+ super
33
+ end
34
+
35
+ # Adjust accounting to remove a node
36
+ def unlink(path)
37
+ raise Errno::ENOENT unless root?(path)
38
+
39
+ accounting&.adjust(0, -1)
40
+ super
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../fuse_context'
4
+
5
+ module FFI
6
+ module Libfuse
7
+ module Filesystem
8
+ # RubySpace File Utilities
9
+ #
10
+ # This module provides utility methods for file operations in RubySpace which are useful for creating custom
11
+ # entries prior to mounting, or otherwise manipulating the filesystem from within the Ruby process that is
12
+ # running FUSE.
13
+ #
14
+ # **Note** You cannot generally call Ruby's {::File} and {::Dir} operations from within the same Ruby process
15
+ # as the mounted filesystem because MRI will not release the GVL to allow the Fuse callbacks to run.
16
+ module Utils
17
+ # Recursive mkdir
18
+ # @param [:to_s] path
19
+ # @param [Integer] mode permissions for any dirs that need to be created
20
+ # @yieldparam [String] the path component being created
21
+ # @yieldreturn [FuseOperations] optionally a filesystem to mount at path, if the path did not previously exist
22
+ def mkdir_p(path, mode = (0o0777 & ~FuseContext.get.umask), &mount_fs)
23
+ return if root?(path) # nothing to make
24
+
25
+ path.to_s.split('/')[1..].inject('') do |base_path, sub_dir|
26
+ full_path = "#{base_path}/#{sub_dir}"
27
+ err = Adapter::Safe.safe_callback(:mkdir) { mkdir(full_path, mode, &mount_fs) }
28
+ unless [0, -Errno::EEXIST::Errno].include?(err)
29
+ raise SystemCallError.new("Unexpected err #{err.abs} from mkdir #{full_path}", err.abs)
30
+ end
31
+
32
+ full_path
33
+ end
34
+ 0
35
+ end
36
+ alias mkpath mkdir_p
37
+
38
+ # @param [:to_s] path
39
+ # @return [FFI::Stat]
40
+ def stat(path)
41
+ path = path.to_s
42
+ stat_buf = FFI::Stat.new
43
+
44
+ err = Adapter::Safe.safe_callback(:getattr) { getattr(path, stat_buf) }
45
+
46
+ return nil if err == -Errno::ENOENT::Errno
47
+ raise SystemCallError, "Unexpected error from #{fs}.getattr #{path}", err unless err.zero?
48
+
49
+ stat_buf
50
+ end
51
+
52
+ # @param [:to_s] path
53
+ # @return [Boolean] true if file or directory exists at path
54
+ def exists?(path)
55
+ stat(path) && true
56
+ end
57
+
58
+ # @param [:to_s] path
59
+ # @return [Boolean] true if regular file exists at path
60
+ def file?(path)
61
+ stat(path)&.file? || false
62
+ end
63
+
64
+ # @param [:to_s] path
65
+ # @return [Boolean] true if directory exists at path
66
+ def directory?(path)
67
+ stat(path)&.directory? || false
68
+ end
69
+
70
+ # @param [:to_s] path
71
+ # @return [Boolean] File exists at path and has zero size
72
+ def empty_file?(path)
73
+ s = stat(path)
74
+ (s&.file? && s.size.zero?) || false
75
+ end
76
+
77
+ # Check if a directory is empty
78
+ # @param [String] path
79
+ # @return [Boolean] true if an empty directory exists at path
80
+ # @raise [Errno::ENOTDIR] if path does not point to a directory
81
+ def empty_dir?(path)
82
+ return false unless directory?(path)
83
+
84
+ empty = true
85
+ fake_filler = proc do |_buf, name, _stat = nil, _offset = 0, _fuse_flag = 0|
86
+ next 0 if %w[. ..].include?(name)
87
+
88
+ empty = false
89
+ -1 # buf full don't send more entries!
90
+ end
91
+ readdir(path.to_s, nil, fake_filler, 0, nil, *(fuse3_compat? ? [] : [0]))
92
+ empty
93
+ end
94
+
95
+ # @!visibility private
96
+ def fuse3_compat?
97
+ FUSE_MAJOR_VERSION >= 3
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end