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
@@ -0,0 +1,306 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'accounting'
4
+ require_relative 'virtual_node'
5
+ require_relative 'virtual_file'
6
+ require_relative 'pass_through_file'
7
+ require_relative 'pass_through_dir'
8
+ require_relative 'mapped_dir'
9
+
10
+ module FFI
11
+ module Libfuse
12
+ module Filesystem
13
+ # A Filesystem of Filesystems
14
+ #
15
+ # Implements a recursive Hash based directory of sub filesystems.
16
+ #
17
+ # FUSE Callbacks
18
+ # ===
19
+ #
20
+ # If path is root ('/') then the operation applies to this directory itself
21
+ #
22
+ # If the path is a simple basename (with leading slash and no others) then the operation applies to an entry in
23
+ # this directory. The operation is handled by the directory and then passed on to the entry itself
24
+ # (with path = '/')
25
+ #
26
+ # Otherwise it is passed on to the next entry via {#path_method}
27
+ #
28
+ # Constraints
29
+ # ===
30
+ #
31
+ # * Expects to be wrapped by {Adapter::Safe}
32
+ # * Passes on FUSE Callbacks to sub filesystems agnostic of {FUSE_MAJOR_VERSION}. Sub-filesystems should use
33
+ # {Adapter::Fuse2Compat} or {Adapter::Fuse3Support} as required
34
+ #
35
+ class VirtualDir < VirtualNode
36
+ include Utils
37
+
38
+ # @return [Hash<String,FuseOperations>] our directory entries
39
+ attr_reader :entries
40
+
41
+ def initialize(accounting: Accounting.new)
42
+ @entries = {}
43
+ @mounted = false
44
+ super(accounting: accounting)
45
+ end
46
+
47
+ # @return [Boolean] true if this dir been mounted
48
+ def mounted?
49
+ @mounted
50
+ end
51
+
52
+ # @!endgroup
53
+
54
+ # @!group FUSE Callbacks
55
+
56
+ # For the root path provides this directory's stat information, otherwise passes on to the next filesystem
57
+ def getattr(path, stat_buf = nil, _ffi = nil)
58
+ return path_method(__method__, path, stat_buf) unless root?(path)
59
+
60
+ stat_buf&.directory(**virtual_stat.merge({ nlink: entries.size + 2 }))
61
+ end
62
+
63
+ # Safely passes on file open to next filesystem
64
+ #
65
+ # @raise [Errno::EISDIR] for the root path since we are a directory rather than a file
66
+ # @return [Object] the result of {#path_method} for the sub filesystem
67
+ # @return [nil] for sub-filesystems that do not implement this callback or raise ENOTSUP or ENOSYS
68
+ def open(path, *args)
69
+ raise Errno::EISDIR if root?(path)
70
+
71
+ path_method(__method__, path, *args, notsup: nil)
72
+ rescue Errno::ENOTSUP, Errno::ENOSYS
73
+ nil
74
+ end
75
+
76
+ # Safely handle file release
77
+ #
78
+ # Passes on to next filesystem, rescuing ENOTSUP or ENOSYS
79
+ # @raise [Errno::EISDIR] for the root path since we are a directory rather than a file
80
+ def release(path, *args)
81
+ raise Errno::EISDIR if root?(path)
82
+
83
+ path_method(__method__, path, *args, notsup: nil)
84
+ rescue Errno::ENOTSUP, Errno::ENOSYS
85
+ # do nothing
86
+ end
87
+
88
+ # Safely handles directory open to next filesystem
89
+ #
90
+ # @return [self] for the root path, which helps shortcut future operations. See {#readdir}
91
+ # @return [Object] the result of {#path_method} for all other paths
92
+ # @return [nil] for sub-filesystems that do not implement this callback or raise ENOTSUP or ENOSYS
93
+ def opendir(path, ffi)
94
+ return path_method(__method__, path, ffi, notsup: nil) unless root?(path)
95
+
96
+ ffi.fh = self
97
+ rescue Errno::ENOTSUP, Errno::ENOSYS
98
+ nil
99
+ end
100
+
101
+ # Safely handles directory release
102
+ #
103
+ # Does nothing for the root path
104
+ #
105
+ # Otherwise safely passes on to next filesystem, rescuing ENOTSUP or ENOSYS
106
+ def releasedir(path, *args)
107
+ path_method(__method__, path, *args, notsup: nil) unless root?(path)
108
+ rescue Errno::ENOTSUP, Errno::ENOSYS
109
+ # do nothing
110
+ end
111
+
112
+ # If path is root fills the directory from the keys in {#entries}
113
+ #
114
+ # If ffi.fh is itself a filesystem then try to call its :readdir directly
115
+ #
116
+ # Otherwise passes to the next filesystem in path
117
+ def readdir(path, buf, filler, offset, ffi, *flag)
118
+ return %w[. ..].concat(entries.keys).each(&Adapter::Ruby::ReaddirFiller.new(buf, filler)) if root?(path)
119
+
120
+ return ffi.fh.readdir('/', buf, filler, offset, ffi, *flag) if entry_fuse_respond_to?(ffi.fh, :readdir)
121
+
122
+ path_method(:readdir, path, buf, filler, offset, ffi, *flag) unless root?(path)
123
+ end
124
+
125
+ # For root path validates we are empty and removes a node link from {#accounting}
126
+ # For our entries, passes on the call to the entry (with path='/') and then removes the entry. If available
127
+ # :destroy will be called on the deleted entry
128
+ # @raise [Errno::ENOTEMPTY] if path is root and our entries list is not empty
129
+ # @raise [Errno::ENOENT] if the entry does not exist
130
+ # @raise [Errno::ENOTDIR] if the entry does not respond to :readdir (ie: is not a directory)
131
+ def rmdir(path)
132
+ if root?(path)
133
+ raise Errno::ENOTEMPTY unless entries.empty?
134
+
135
+ accounting.adjust(0, -1)
136
+ return
137
+ end
138
+
139
+ entry_key = entry_key(path)
140
+ return path_method(__method__, path) unless entry_key
141
+
142
+ dir = entries[entry_key]
143
+ raise Errno::ENOENT unless dir
144
+ raise Errno::ENOTDIR unless entry_fuse_respond_to?(dir, :readdir)
145
+
146
+ entry_send(dir, :rmdir, '/')
147
+
148
+ dir = entries.delete(entry_key)
149
+ entry_send(dir, :destroy, init_results.delete(entry_key)) if dir && mounted?
150
+ end
151
+
152
+ # For our entries, validates the entry exists and is not a directory, then passes on unlink (with path = '/')
153
+ # and finally deletes.
154
+ # @raise [Errno:EISDIR] if the request entry responds to :readdir
155
+ def unlink(path)
156
+ entry_key = entry_key(path)
157
+ return path_method(__method__, path) unless entry_key
158
+
159
+ entry = entries[entry_key]
160
+ raise Errno::ENOENT unless entry
161
+ raise Errno::EISDIR if entry_fuse_respond_to?(entry, :readdir)
162
+
163
+ entry_send(entry, :unlink, '/')
164
+ entries.delete(entry_key) && true
165
+ end
166
+
167
+ # For our entries, creates a new file
168
+ # @raise [Errno::EISDIR] if the entry exists and responds_to?(:readdir)
169
+ # @raise [Errno::EEXIST] if the entry exists
170
+ # @yield []
171
+ # @yieldreturn [Object] something that quacks with the FUSE Callbacks of a regular file
172
+ #
173
+ # :create or :mknod + :open will be attempted with path = '/' on this file
174
+ # @return [Object] the result of the supplied block, or if not given a new {VirtualFile}
175
+ def create(path, mode = FuseContext.get.mask(0o644), ffi = nil, &file)
176
+ file_name = entry_key(path)
177
+
178
+ # fuselib will fallback to mknod on ENOSYS on a case by case basis
179
+ return path_method(__method__, path, mode, ffi, notsup: Errno::ENOSYS, &file) unless file_name
180
+
181
+ existing = entries[file_name]
182
+ raise Errno::EISDIR if entry_fuse_respond_to?(existing, :readdir)
183
+ raise Errno::EEXIST if existing
184
+
185
+ # TODO: Strictly should understand setgid and sticky bits of this dir's mode when creating new files
186
+ new_file = file ? file.call(name) : VirtualFile.new(accounting: accounting)
187
+ if entry_fuse_respond_to?(new_file, :create)
188
+ new_file.public_send(:create, '/', mode, ffi)
189
+ else
190
+ # TODO: generate a sensible device number
191
+ entry_send(new_file, :mknod, '/', mode, 0)
192
+ entry_send(new_file, :open, '/', ffi)
193
+ end
194
+ entries[file_name] = new_file
195
+ end
196
+
197
+ # Creates a new directory entry in this directory
198
+ # @param [String] path
199
+ # @param [Integer] mode
200
+ # @yield []
201
+ # @yieldreturn [Object] something that quacks with the FUSE Callbacks representing a directory
202
+ # @return [Object] the result of the block if given, otherwise the newly created sub {VirtualDir}
203
+ # @raise [Errno::EEXIST] if the entry already exists at path
204
+ def mkdir(path, mode = FuseContext.get.mask(0o777), &dir)
205
+ return init_node(mode) if root?(path)
206
+
207
+ dir_name = entry_key(path)
208
+ return path_method(__method__, path, mode, &dir) unless dir_name
209
+
210
+ existing = entries[dir_name]
211
+ raise Errno::EEXIST if existing
212
+
213
+ new_dir = dir ? dir.call : VirtualDir.new(accounting: accounting)
214
+ init_dir(dir_name, new_dir) if mounted?
215
+ entry_send(new_dir, :mkdir, '/', mode)
216
+ entries[dir_name] = new_dir
217
+ end
218
+
219
+ # Calls init on all current entries, keeping track of their init objects for use with {destroy}
220
+ def init(*args)
221
+ @mounted = true
222
+ @init_args = args
223
+ @init_results = {}
224
+ entries.each_pair { |name, d| init_dir(name, d) }
225
+ nil
226
+ end
227
+
228
+ # Calls destroy on all current entries
229
+ def destroy(*_args)
230
+ entries.each_pair { |name, d| entry_send(d, :destroy, init_results[name]) }
231
+ nil
232
+ end
233
+
234
+ # @!endgroup
235
+
236
+ # Looks up the first path component in {#entries} and then sends the remainder of the path to the callback
237
+ # on that entry
238
+ # @param [Symbol] callback a FUSE Callback
239
+ # @param [String] path
240
+ # @param [Array] args callback arguments
241
+ # @param [Proc] invoke optional block to keep passing down. See {#mkdir}, {#create}
242
+ # @param [Class<SystemCallError>] notsup
243
+ # @raise [Errno:ENOENT] if the next entry does not exist
244
+ # @raise [SystemCallError] error from notsup if the next entry does not respond to ths callback
245
+ def path_method(callback, path, *args, notsup: Errno::ENOTSUP, &invoke)
246
+ path = path.to_s
247
+ # Fuse paths always start with a leading slash and never have a trailing slash
248
+ sep_index = path.index('/', 1)
249
+ entry_key = sep_index ? path[1..sep_index - 1] : path[1..]
250
+ entry = entries[entry_key]
251
+
252
+ raise Errno::ENOENT unless entry
253
+
254
+ responds = entry_fuse_respond_to?(entry, callback)
255
+ return unless responds || notsup
256
+ raise notsup unless responds
257
+
258
+ if mounted? && (init_obj = init_results[entry_key])
259
+ FuseContext.get.overrides.merge!(private_data: init_obj)
260
+ end
261
+
262
+ # Pass remaining path components to the next filesystem
263
+ next_path = sep_index ? path[sep_index..] : '/'
264
+ entry.public_send(callback, next_path, *args, &invoke)
265
+ end
266
+
267
+ private
268
+
269
+ attr_reader :init_args, :init_results
270
+
271
+ def method_missing(method, *args, &invoke)
272
+ return super unless FuseOperations.path_callbacks.include?(method)
273
+
274
+ raise Errno::ENOTSUP if root?(args.first)
275
+
276
+ path_method(method, *args, &invoke)
277
+ end
278
+
279
+ def respond_to_missing?(method, inc_private = false)
280
+ return true if FuseOperations.path_callbacks.include?(method)
281
+
282
+ super
283
+ end
284
+
285
+ def entry_key(path)
286
+ path[1..] unless path.index('/', 1)
287
+ end
288
+
289
+ def init_dir(name, dir)
290
+ init_result = entry_fuse_respond_to?(dir, :init) ? dir.init(*init_args) : nil
291
+ init_results[name] = init_result if init_result
292
+ end
293
+
294
+ def entry_fuse_respond_to?(entry_fs, method)
295
+ entry_fs.respond_to?(:fuse_respond_to?) ? entry_fs.fuse_respond_to?(method) : entry_fs.respond_to?(method)
296
+ end
297
+
298
+ def entry_send(entry, callback, *args)
299
+ return unless entry_fuse_respond_to?(entry, callback)
300
+
301
+ entry.public_send(callback, *args)
302
+ end
303
+ end
304
+ end
305
+ end
306
+ end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'accounting'
4
+ require 'stringio'
5
+
6
+ module FFI
7
+ module Libfuse
8
+ module Filesystem
9
+ # A Filesystem representing a single synthetic file at the root
10
+ class VirtualFile < VirtualNode
11
+ prepend Adapter::Ruby::Prepend
12
+ include Fuse2Compat
13
+
14
+ # @return [String] the (binary) content of the synthetic file
15
+ attr_reader :content
16
+
17
+ # Create an empty synthetic file
18
+ def initialize(accounting: nil)
19
+ super(accounting: accounting)
20
+ end
21
+
22
+ # @!visibility private
23
+ def path_method(_method, *_args)
24
+ raise Errno::ENOENT
25
+ end
26
+
27
+ # @!group FUSE Callbacks
28
+
29
+ def getattr(path, stat, ffi = nil)
30
+ # We don't exist until create or otherwise or virtual stat exists
31
+ raise Errno::ENOENT unless root?(path) && virtual_stat
32
+
33
+ stat.file(size: (ffi&.fh || content).size, **virtual_stat)
34
+ self
35
+ end
36
+
37
+ # @param [String] _path ignored, expected to be '/'
38
+ # @param [Integer] mode
39
+ # @param [FuseFileInfo] ffi
40
+ # @return [Object] a file handled (captured by {Adapter::Ruby::Prepend})
41
+ def create(_path, mode, ffi = nil)
42
+ init_node(mode)
43
+ @content = String.new(encoding: 'binary')
44
+ sio(ffi) if ffi
45
+ end
46
+
47
+ def open(_path, ffi)
48
+ virtual_stat[:atime] = Time.now.utc
49
+ sio(ffi)
50
+ end
51
+
52
+ # op[:read] = [:pointer, :size_t, :off_t, FuseFileInfo.by_ref]
53
+ def read(path, size, off, ffi)
54
+ raise Errno::ENOENT unless root?(path)
55
+
56
+ io = sio(ffi)
57
+ io.seek(off)
58
+ io.read(size)
59
+ end
60
+
61
+ # write(const char* path, char *buf, size_t size, off_t offset, struct fuse_file_info* fi)
62
+ def write(path, data, offset = 0, ffi = nil)
63
+ raise Errno::ENOENT unless root?(path)
64
+
65
+ accounting&.write(content.size, data.size, offset)
66
+ io = sio(ffi)
67
+ io.seek(offset)
68
+ io.write(data)
69
+ virtual_stat[:mtime] = Time.now.utc
70
+ end
71
+
72
+ def truncate(path, size, ffi = nil)
73
+ raise Errno::ENOENT unless root?(path)
74
+
75
+ accounting&.truncate(content.size, size)
76
+ sio(ffi).truncate(size)
77
+ virtual_stat[:mtime] = Time.now.utc
78
+ end
79
+
80
+ def unlink(path)
81
+ raise Errno::ENOENT unless root?(path)
82
+
83
+ accounting&.adjust(-content.size, -1)
84
+ end
85
+
86
+ private
87
+
88
+ def sio(ffi)
89
+ ffi&.fh || StringIO.new(content, ffi&.flags)
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,188 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'utils'
4
+ require_relative 'virtual_dir'
5
+ require_relative 'accounting'
6
+ require 'pathname'
7
+
8
+ module FFI
9
+ module Libfuse
10
+ module Filesystem
11
+ # A configurable main Filesystem that delegates FUSE Callbacks to another filesystem
12
+ #
13
+ # This class registers support for all available fuse callbacks, subject to the options below.
14
+ #
15
+ # Delegate filesystems like {VirtualDir} may raise ENOTSUP to indicate a callback is not handled at runtime
16
+ # although the behaviour of C libfuse varies in this regard.
17
+ #
18
+ # Filesystem options
19
+ # ===
20
+ #
21
+ # Passed by -o options to {Libfuse.main}
22
+ #
23
+ # * :max_space used for #{FuseOperations#statfs}. See {#accounting}
24
+ # * :max_nodes used for #{FuseOperations#statfs}. See {#accounting}
25
+ # * :no_buf do not register :read_buf, :write_buf
26
+ #
27
+ # when set all filesystems must implement :read/:write
28
+ #
29
+ # when not set all filesystems must implement :read_buf, :write_buf which enables C libfuse to handle
30
+ # file descriptor based io, eg. {MappedFiles}, but means Ruby FFI is doing memory allocations for string
31
+ # based io, eg. {VirtualFile}.
32
+ #
33
+ # Note that {VirtualFile} and {MappedFiles} both prepend {Adapter::Ruby::Prepend} which implements
34
+ # the logic to fallback from :read/:write_buf to plain :read/:write as necessary to support this option.
35
+ #
36
+ # It is writable to the user that mounted it may create and edit files within it
37
+ #
38
+ # @example
39
+ # class MyFS < FFI::Libfuse::Filesystem::VirtualFS
40
+ # def fuse_configure
41
+ # build({ 'hello' => { 'world.txt' => 'Hello World'}})
42
+ # mkdir("/hello")
43
+ # create("/hello/world").write("Hello World!\n")
44
+ # create("/hello/everybody").write("Hello Everyone!\n")
45
+ # end
46
+ # end
47
+ #
48
+ # exit(FFI::Libfuse.fuse_main(operations: MyFS.new))
49
+ #
50
+ class VirtualFS
51
+ include Utils
52
+ include Adapter::Context
53
+ include Adapter::Debug
54
+ include Adapter::Safe
55
+
56
+ # @return [Object] the root filesystem that quacks like a {FuseOperations}
57
+ attr_reader :root
58
+
59
+ # @return [Hash{Symbol => String,Boolean}] custom options captured as defined by {fuse_options}
60
+ attr_reader :options
61
+
62
+ # @return [Accounting|:max_space=,:max_nodes=] an accumulator of filesystem statistics used to consume the
63
+ # max_space and max_nodes options
64
+ def accounting
65
+ @accounting ||= Accounting.new
66
+ end
67
+
68
+ # @param [FuseOperations] root the root filesystem
69
+ # Subclasses can override the no-arg method and call super to pass in a different root.
70
+ def fuse_configure(root = VirtualDir.new(accounting: accounting).mkdir('/'))
71
+ @root = root
72
+ end
73
+
74
+ # @overload build(files)
75
+ # Adds files directly to the filesystem
76
+ # @param [Hash] files map of paths to content responding to
77
+ #
78
+ # * :each_pair is treated as a subdir of files
79
+ # * :readdir (eg {PassThroughDir}) is treated as a directory- sent via mkdir
80
+ # * :getattr (eg {PassThroughFile}) is treated as a file - sent via create
81
+ # * :to_str (eg {::String} ) is created as a {VirtualFile}
82
+ def build(files, base_path = Pathname.new('/'))
83
+ files.each_pair do |path, content|
84
+ path = (base_path + path).cleanpath
85
+ @root.mkdir_p(path.dirname) unless path.dirname == base_path
86
+
87
+ rt = %i[each_pair readdir getattr to_str].detect { |m| content.respond_to?(m) }
88
+ raise "Unsupported initial content for #{self.class.name}: #{content.class.name}- #{content}" unless rt
89
+
90
+ send("build_#{rt}", content, path)
91
+ end
92
+ end
93
+
94
+ # @!group Fuse Configuration
95
+
96
+ # TODO: Raise bug on libfuse (or fuse kernel module - ouch!) create to also fallback to mknod on ENOTSUP
97
+
98
+ # Respond to all FUSE Callbacks except deprecated, noting that ..
99
+ #
100
+ # * :read_buf, :write_buf can be excluded by the 'no_buf' mount option
101
+ # * :access already has a libfuse mount option (default_permissions)
102
+ # * :create falls back to :mknod on ENOSYS (as raised by {VirtualDir})
103
+ # * :copy_file_range can raise ENOTSUP to trigger glibc to fallback to inefficient copy
104
+ def fuse_respond_to?(method)
105
+ case method
106
+ when :getdir, :fgetattr
107
+ # TODO: Find out if fgetattr works on linux, something wrong with stat values on OSX.
108
+ # https://github.com/osxfuse/osxfuse/issues/887
109
+ false
110
+ when :read_buf, :write_buf
111
+ !no_buf
112
+ else
113
+ true
114
+ end
115
+ end
116
+
117
+ # Default fuse options
118
+ # Subclasses can override this method and call super with the additional options:
119
+ # @param [Hash] opts additional options to parse into the {#options} attribute
120
+ def fuse_options(args, opts = {})
121
+ @options = {}
122
+ opts = opts.merge({ 'no_buf' => :no_buf }).merge(Accounting::OPTIONS)
123
+ args.parse!(opts) do |key:, value:, **|
124
+ case key
125
+ when *Accounting::OPTIONS.values.uniq
126
+ next accounting.fuse_opt_proc(key: key, value: value)
127
+ when :no_buf
128
+ @no_buf = true
129
+ else
130
+ options[key] = value
131
+ end
132
+ :handled
133
+ end
134
+ end
135
+
136
+ # Subclasses can override this method to add descriptions for additional options
137
+ def fuse_help
138
+ <<~END_HELP
139
+ #{Accounting::HELP}
140
+ #{self.class.name} options:
141
+ -o no_buf always use read, write instead of read_buf, write_buf
142
+ END_HELP
143
+ end
144
+
145
+ # Subclasses can override to produce a nice version string for -V
146
+ def fuse_version
147
+ self.class.name
148
+ end
149
+
150
+ # @!endgroup
151
+
152
+ private
153
+
154
+ def build_each_pair(content, path)
155
+ build(content, path)
156
+ end
157
+
158
+ def build_readdir(content, path)
159
+ @root.mkdir(path.to_s) { content }
160
+ end
161
+
162
+ def build_getattr(content, path)
163
+ @root.create(path.to_s) { content }
164
+ end
165
+
166
+ def build_to_str(content, path)
167
+ @root.create(path.to_s) { content }
168
+ end
169
+
170
+ # Passes FUSE Callbacks on to the {#root} filesystem
171
+ def method_missing(method, *args, &block)
172
+ return @root.public_send(method, *args, &block) if @root.respond_to?(method)
173
+
174
+ # This is not always reliable but better than raising NoMethodError
175
+ return -Errno::ENOTSUP::Errno if FuseOperations.fuse_callbacks.include?(method)
176
+
177
+ super
178
+ end
179
+
180
+ def respond_to_missing?(method, private = false)
181
+ FuseOperations.fuse_callbacks.include?(method) || @root.respond_to?(method, private) || super
182
+ end
183
+
184
+ attr_reader :no_buf
185
+ end
186
+ end
187
+ end
188
+ end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'accounting'
4
+ require_relative '../adapter/ruby'
5
+
6
+ module FFI
7
+ module Libfuse
8
+ module Filesystem
9
+ # @abstract
10
+ # Common FUSE Callbacks for a virtual inode
11
+ #
12
+ # **Note** this class is used by both {VirtualFile} which is under {Adapter::Ruby::Prepend}
13
+ # and {VirtualDir} which passes on native {FuseOperations} calls
14
+ class VirtualNode
15
+ # @return [Hash<Symbol,Integer>] base file or directory stat information used for :getattr of this node
16
+ attr_reader :virtual_stat
17
+
18
+ # @return [Hash<String,String>] virtual extended attributes
19
+ attr_reader :virtual_xattr
20
+
21
+ # @return [Accounting|nil] file system statistcs accumulator
22
+ attr_reader :accounting
23
+
24
+ # @param [Accounting] accounting accumulator of filesystem statistics
25
+ def initialize(accounting: Accounting.new)
26
+ @accounting = accounting
27
+
28
+ @virtual_xattr = {}
29
+ end
30
+
31
+ # @!method path_method(callback, *args)
32
+ # @abstract
33
+ # called if this node cannot handle the callback (ie path is not root or an entry in this directory)
34
+
35
+ # @!group FUSE Callbacks
36
+
37
+ def utimens(path, *args)
38
+ return path_method(__method__, path, *args) unless root?(path)
39
+
40
+ atime, mtime, *_fuse3 = args
41
+ # if native fuse call atime will be Array<Stat::TimeSpec>
42
+ atime, mtime = Stat::TimeSpec.fill_times(atime[0, 2], 2).map(&:time) if atime.is_a?(Array)
43
+ virtual_stat[:atime] = atime if atime
44
+ virtual_stat[:mtime] = mtime if mtime
45
+ virtual_stat[:ctime] = mtime if mtime
46
+ end
47
+
48
+ def chmod(path, mode, *args)
49
+ return path_method(__method__, path, mode, *args) unless root?(path)
50
+
51
+ virtual_stat[:mode] = mode
52
+ virtual_stat[:ctime] = Time.now
53
+ end
54
+
55
+ def chown(path, uid, gid, *args)
56
+ return path_method(__method__, path, uid, gid, *args) unless root?(path)
57
+
58
+ virtual_stat[:uid] = uid
59
+ virtual_stat[:gid] = gid
60
+ virtual_stat[:ctime] = Time.now
61
+ end
62
+
63
+ def statfs(path, statfs_buf)
64
+ return path_method(__method__, path, statfs_buf) unless root?(path)
65
+ raise Errno::ENOTSUP unless accounting
66
+
67
+ accounting.to_statvfs(statfs_buf)
68
+ end
69
+
70
+ def getxattr(path, name, buf = nil, size = nil)
71
+ return path_method(__method__, path, name, buf, size) unless root?(path)
72
+ return virtual_xattr[name] unless buf
73
+
74
+ Adapter::Ruby.getxattr(buf, size) { virtual_xattr[name] }
75
+ end
76
+
77
+ def listxattr(path, buf = nil, size = nil)
78
+ return path_method(__method__, path) unless root?(path)
79
+ return virtual_xattr.keys unless buf
80
+
81
+ Adapter::Ruby.listxattr(buf, size) { virtual_xattr.keys }
82
+ end
83
+
84
+ # @!endgroup
85
+
86
+ # Initialise the stat information for the node - should only be called once (eg from create or mkdir)
87
+ def init_node(mode, ctx: FuseContext.get, now: Time.now)
88
+ @virtual_stat = { mode: mode & ~ctx.umask, uid: ctx.uid, gid: ctx.gid, ctime: now, mtime: now, atime: now }
89
+ accounting&.adjust(0, +1)
90
+ self
91
+ end
92
+
93
+ private
94
+
95
+ def root?(path)
96
+ path.to_s == '/'
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end