ffi-libfuse 0.0.1.pre → 0.1.0.rc20220550

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) 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 +6 -8
  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/thread_pool.rb +1 -1
  43. data/lib/ffi/libfuse/version.rb +1 -1
  44. data/lib/ffi/libfuse.rb +13 -4
  45. data/lib/ffi/ruby_object.rb +1 -1
  46. data/lib/ffi/stat/constants.rb +9 -0
  47. data/lib/ffi/stat/native.rb +36 -6
  48. data/lib/ffi/stat/time_spec.rb +28 -12
  49. data/lib/ffi/stat.rb +111 -22
  50. data/lib/ffi/stat_vfs.rb +59 -1
  51. data/lib/ffi/struct_wrapper.rb +22 -1
  52. data/sample/hello_fs.rb +54 -0
  53. data/sample/memory_fs.rb +5 -181
  54. data/sample/no_fs.rb +20 -21
  55. data/sample/pass_through_fs.rb +30 -0
  56. metadata +66 -7
  57. 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