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
@@ -2,178 +2,379 @@
2
2
 
3
3
  require_relative 'safe'
4
4
  require_relative 'debug'
5
+ require 'set'
5
6
 
6
7
  module FFI
7
8
  module Libfuse
8
9
  module Adapter
9
- # Wrapper module to give more natural ruby signatures to the fuse callbacks, in particular to avoid dealing with
10
- # FFI Pointers
10
+ # This module assists with converting native C libfuse into idiomatic Ruby
11
+ #
12
+ # Class Method helpers
13
+ # ===
14
+ # These functions deal with the native fuse FFI::Pointers, Buffers etc
15
+ #
16
+ # The class {ReaddirFiller} assists with the complexity of the readdir callback
17
+ #
18
+ # FUSE Callbacks
19
+ # ===
20
+ #
21
+ # Including this module in a Filesystem module changes the method signatures for callbacks to be more idiomatic
22
+ # ruby (via prepending {Prepend}). It also includes the type 1 adapters {Context}, {Debug} and {Safe}
23
+ #
24
+ # Filesystems that return {::IO} like objects from #{open} need not implement other file io operations
25
+ # Similarly Filesystems that return {::Dir} like objects from #{opendir} need not implement #{readdir}
11
26
  #
12
- # @note includes {Debug} and {Safe}
13
27
  module Ruby
14
- # adapter module prepended to the including module
15
- # @!visibility private
28
+ # Helper class for {FuseOperations#readdir}
29
+ # @example
30
+ # def readdir(path, buf, filler, _offset, _ffi, *flags)
31
+ # rdf = FFI::Adapter::Ruby::ReaddirFiller.new(buf, filler)
32
+ # %w[. ..].each { |dot_entry| rdf.fill(dot_entry) }
33
+ # entries.each { |entry| rdf.fill(entry) }
34
+ # end
35
+ # @example
36
+ # # short version
37
+ # def readdir(path, buf, filler, _offset, _ffi, *flags)
38
+ # %w[. ..].concat(entries).each(&FFI::Adapter::Ruby::ReaddirFiller.new(buf,filler))
39
+ # end
40
+ class ReaddirFiller
41
+ # @param [FFI::Pointer] buf from #{FuseOperations#readdir}
42
+ # @param [FFI::Function] filler from #{FuseOperations#readdir}
43
+ # @param [Boolean] fuse3 does the filler function expect fuse 3 compatibility
44
+ def initialize(buf, filler, fuse3: FUSE_MAJOR_VERSION >= 3)
45
+ @buf = buf
46
+ @filler = filler
47
+ @stat_buf = nil
48
+ @fuse3 = fuse3
49
+ end
50
+
51
+ # Fill readdir from a directory handle
52
+ # @param [#seek, #read, #tell] dir_handle
53
+ # @param [Integer] offset
54
+ # @raise [Errno::ENOTSUP] unless dir_handle quacks like ::Dir
55
+ def readdir_fh(dir_handle, offset = 0)
56
+ raise Errno::ENOTSUP unless %i[seek read tell].all? { |m| dir_handle.respond_to?(m) }
57
+
58
+ dir_handle.seek(offset)
59
+ loop while (name = dir_handle.read) && fill(name, offset: dir_handle.tell)
60
+ end
61
+
62
+ # @param [String] name a directory entry
63
+ # @param [FFI::Stat|Hash<Symbol,Integer>|nil] stat or stat fields to fill a {::FFI::Stat}
64
+ #
65
+ # Note sending nil values will cause Fuse to issue #getattr operations for each entry
66
+ #
67
+ # It is safe to reuse the same {FFI::Stat} object between calls
68
+ # @param [Integer] offset
69
+ # @param [Boolean] fill_dir_plus true if stat has full attributes for inode caching
70
+ # @return [Boolean] true if the buffer accepted the entry
71
+ # @raise [StopIteration] if called after a previous call returned false
72
+ def fill(name, stat: nil, offset: 0, fill_dir_plus: false)
73
+ raise StopIteration unless @buf
74
+
75
+ fill_flags = fill_flags(fill_dir_plus: fill_dir_plus)
76
+ fill_stat = fill_stat(stat)
77
+ return true if @filler.call(@buf, name, fill_stat, offset, *fill_flags).zero?
78
+
79
+ @buf = nil
80
+ end
81
+
82
+ # @return [Proc] a proc to pass to something that yields like #{fill}
83
+ def to_proc
84
+ proc do |name, stat: nil, offset: 0, fill_dir_plus: false|
85
+ fill(name, stat: stat, offset: offset, fill_dir_plus: fill_dir_plus)
86
+ end
87
+ end
88
+
89
+ private
90
+
91
+ def fill_flags(fill_dir_plus:)
92
+ return [] unless @fuse3
93
+
94
+ [fill_dir_plus ? :fuse_fill_dir_plus : 0]
95
+ end
96
+
97
+ def fill_stat(from)
98
+ return from if !from || from.is_a?(::FFI::Stat)
99
+
100
+ (@stat_buf ||= ::FFI::Stat.new).fill(from)
101
+ end
102
+ end
103
+
16
104
  # rubocop:disable Metrics/ModuleLength
17
- module Shim
105
+
106
+ # Can be prepended to concrete filesystem implementations to skip duplicate handling of {Debug}, {Safe}
107
+ #
108
+ # @note callbacks still expect to be ultimately handled by {Safe}, ie they raise SystemCallError and can
109
+ # return non Integer results
110
+ module Prepend
18
111
  include Adapter
19
112
 
20
- # Fuse3 support outer module can override this
113
+ # Returns true if our we can support fuse_method callback
114
+ #
115
+ # ie. when
116
+ #
117
+ # * fuse_method is implemented directly by our superclass
118
+ # * the adapter can handle the callback via
119
+ # * file or directory handle returned from {#open} or {#opendir}
120
+ # * fallback to an alternate implementation (eg {#read_buf} -> {#read}, {#write_buf} -> {#write})
121
+ def fuse_respond_to?(fuse_method)
122
+ fuse_methods =
123
+ case fuse_method
124
+ when :read, :write, :flush, :release
125
+ %i[open]
126
+ when :read_buf
127
+ %i[open read]
128
+ when :write_buf
129
+ %i[open write]
130
+ when :readdir, :releasedir
131
+ %i[opendir]
132
+ else
133
+ []
134
+ end
135
+ fuse_methods << fuse_method
136
+
137
+ fuse_methods.any? { |m| super(m) }
138
+ end
139
+
140
+ # Helper to test if path is root
141
+ def root?(path)
142
+ path.respond_to?(:root?) ? path.root? : path.to_s == '/'
143
+ end
144
+
145
+ # @!visibility private
146
+ # Fuse 3 compatibility
147
+ # @return [Boolean] true if this filesystem is receiving Fuse3 compatible arguments
148
+ # @see Fuse3Support
21
149
  def fuse3_compat?
22
150
  FUSE_MAJOR_VERSION >= 3
23
151
  end
24
152
 
25
- def write(*args)
26
- *pre, path, buf, size, offset, info = args
27
- super(*pre, path, buf.read_bytes(size), offset, info)
28
- size
153
+ # @!group FUSE Callbacks
154
+
155
+ # Writes data to path via
156
+ #
157
+ # * super as per {Ruby#write} if defined
158
+ # * {Ruby.write_fh} on ffi.fh
159
+ def write(path, buf, size = buf.size, offset = 0, ffi = nil)
160
+ return Ruby.write_fh(buf, size, offset, ffi&.fh) unless defined?(super)
161
+
162
+ Ruby.write_data(buf, size) { |data| super(path, data, offset, ffi) }
29
163
  end
30
164
 
31
- def read(*args)
32
- *pre, path, buf, size, offset, info = args
33
- res = super(*pre, path, size, offset, info)
165
+ # Writes data to path with {FuseBuf}s via
166
+ #
167
+ # * super directly if defined
168
+ # * {FuseBufVec#copy_to_fd} if ffi.fh has non-nil :fileno
169
+ # * {FuseBufVec#copy_to_str} with the result of {Ruby#write}
170
+ def write_buf(path, bufv, offset, ffi)
171
+ return super if defined?(super)
34
172
 
35
- return -Errno::ERANGE::Errno unless res.size <= size
173
+ fd = ffi&.fh&.fileno
174
+ return bufv.copy_to_fd(fd, offset) if fd
36
175
 
37
- buf.write_bytes(res)
38
- res.size
176
+ data = bufv.copy_to_str
177
+ write(path, data, data.size, offset, ffi)
39
178
  end
40
179
 
41
- def readlink(*args)
42
- *pre, path, buf, size = args
43
- link = super(*pre, path)
44
- buf.write_bytes(link, [0..size])
45
- 0
180
+ # Flush data to path via
181
+ #
182
+ # * super if defined
183
+ # * :flush on ffi.fh if defined
184
+ def flush(path, ffi)
185
+ return super if defined?(super)
186
+
187
+ fh = ffi&.fh
188
+ fh.flush if fh.respond_to?(:flush)
46
189
  end
47
190
 
48
- # rubocop:disable Metrics/AbcSize
49
-
50
- # changes args (removes buf and filler), processes return, changes block
51
- # *pre, path, buf, filler, offset, fuse_file_info, flag = nil
52
- def readdir(*args)
53
- flag_arg = args.pop if fuse3_compat?
54
- *pre, buf, filler, offset, fuse_file_info = args
55
- args = pre + [offset, fuse_file_info]
56
- args << flag_arg if fuse3_compat?
57
- buffer_available = true
58
- super(*args) do |name, stat, buffered = false, flag = 0|
59
- raise StopIteration unless buffer_available
60
-
61
- offset = buffered if buffered.is_a?(Integer)
62
- offset += 1 if buffered && !buffered.is_a?(Integer) # auto-track offsets
63
- stat = Stat.new.fill(stat) if stat && !stat.is_a?(Stat)
64
- filler_args = [buf, name, stat, offset]
65
- filler_args << flag if fuse3_compat?
66
- buffer_available = filler.call(*filler_args).zero?
191
+ # Sync data to path via
192
+ #
193
+ # * super as per {Ruby#fsync} if defined
194
+ # * :datasync on ffi.fh if defined and datasync is non-zero
195
+ # * :fysnc on ffi.fh if defined
196
+ def fsync(path, datasync, ffi)
197
+ return super(path, datasync != 0, ffi) if defined?(super)
198
+
199
+ fh = ffi&.fh
200
+ return fh.datasync if datasync && fh.respond_to?(:datasync)
201
+
202
+ fh.fsync if fh.respond_to?(:fsync)
203
+ end
204
+
205
+ # Read data from path via
206
+ #
207
+ # * super as per {Ruby#read} if defined
208
+ # * ffi.fh as per {Ruby.read}
209
+ def read(path, buf, size, offset, ffi)
210
+ Ruby.read(buf, size, offset) do
211
+ defined?(super) ? super(path, size, offset, ffi) : ffi&.fh
67
212
  end
68
213
  end
69
- # rubocop:enable Metrics/AbcSize
70
214
 
71
- def setxattr(*args)
72
- # fuse converts the void* data buffer to a const char* null terminated string
73
- # which libfuse reads directly, so size is irrelevant
74
- *pre, path, name, data, _size, flags = args
75
- super(*pre, path, name, data, flags)
215
+ # Read data with {FuseBuf}s via
216
+ #
217
+ # * super if defined
218
+ # * ffi.fh.fileno if defined and not nil
219
+ # * result of {#read}
220
+ def read_buf(path, bufp, size, offset, ffi)
221
+ return super if defined?(super)
222
+
223
+ Ruby.read_buf(bufp, size, offset) do
224
+ fh = ffi&.fh
225
+ fd = fh.fileno if fh.respond_to?(:fileno)
226
+ next fd if fd
227
+
228
+ read(path, nil, size, offset, ffi)
229
+ end
76
230
  end
77
231
 
78
- def getxattr(*args)
79
- *pre, path, name, buf, size = args
80
- res = super(*pre, path, name)
232
+ # Read link name from path via super as per {Ruby#readlink}
233
+ def readlink(path, buf, size)
234
+ raise Errno::ENOTSUP unless defined?(super)
81
235
 
82
- return -Errno::ENODATA::Errno unless res
236
+ Ruby.readlink(buf, size) { super(path, size) }
237
+ end
83
238
 
84
- res = res.to_s
239
+ # Read directory entries via
240
+ #
241
+ # * super as per {Ruby#readdir} if defined
242
+ # * ffi.fh using {ReaddirFiller#readdir_fh}
243
+ def readdir(path, buf, filler, offset, ffi, flag_arg = nil)
244
+ rd_filler = ReaddirFiller.new(buf, filler, fuse3: fuse3_compat?)
85
245
 
86
- return res.size if size.zero?
87
- return -Errno::ERANGE::Errno if res.size > size
246
+ flag_args = {}
247
+ flag_args[:readdir_plus] = (flag_arg == :fuse_readdir_plus) if fuse3_compat?
248
+ return super(path, offset, ffi, **flag_args, &rd_filler) if defined?(super)
88
249
 
89
- buf.write_bytes(res)
90
- res.size
250
+ rd_filler.readdir_fh(ffi.fh, offset)
251
+ rescue StopIteration
252
+ # do nothing
91
253
  end
92
254
 
93
- def listxattr(*args)
94
- *pre, path, buf, size = args
95
- res = super(*pre, path)
96
- res.reduce(0) do |offset, name|
97
- name = name.to_s
98
- unless size.zero?
99
- return -Errno::ERANGE::Errno if offset + name.size >= size
255
+ # Set extended attributes via super as per {Ruby#setxattr}
256
+ def setxattr(path, name, data, _size, flags)
257
+ raise Errno::ENOTSUP unless defined?(super)
100
258
 
101
- buf.put_string(offset, name) # put string includes the NUL terminator
102
- end
103
- offset + name.size + 1
104
- end
259
+ # fuse converts the void* data buffer to a const char* null terminated string
260
+ # which libfuse reads directly, so size is irrelevant
261
+ super(path, name, data, flags)
105
262
  end
106
263
 
107
- def read_buf(*args)
108
- *pre, path, bufp, size, offset, fuse_file_info = args
109
- buf = FuseBufVec.new
110
- super(*pre, path, buf, size, offset, fuse_file_info)
111
- bufp.put_pointer(0, buf)
112
- 0
264
+ # Get extended attributes via super as per {Ruby#getxattr}
265
+ def getxattr(path, name, buf, size)
266
+ raise Errno::ENOTSUP unless defined?(super)
267
+
268
+ Ruby.getxattr(buf, size) { super(path, name) }
113
269
  end
114
270
 
115
- # extract atime, mtime from times array, convert from FFI::Stat::TimeSpec to ruby Time
116
- def utimens(*args)
117
- ffi = args.pop if fuse3_compat?
118
- *pre, path, times = args
271
+ # List extended attributes via super as per {Ruby#listxattr}
272
+ def listxattr(*args)
273
+ raise Errno::ENOTSUP unless defined?(super)
119
274
 
120
- # Empty times means set both to current time
121
- times = [Stat::TimeSpec.now, Stat::TimeSpec.now] unless times&.size == 2
275
+ path, buf, size = args
276
+ Ruby.listxattr(buf, size) { super(path) }
277
+ end
122
278
 
123
- # If both times are set to UTIME_NOW, make sure they get the same value!
124
- now = times.any?(&:now?) && Time.now
125
- atime, mtime = times.map { |t| t.time(now) }
279
+ # Set file atime, mtime via super as per {Ruby#utimens}
280
+ def utimens(path, times, *fuse3_args)
281
+ raise Errno::ENOTSUP unless defined?(super)
126
282
 
127
- args = pre + [path, atime, mtime]
128
- args << ffi if fuse3_compat?
129
- super(*args)
283
+ atime, mtime = Stat::TimeSpec.fill_times(times[0, 2], 2).map(&:time)
284
+ super(path, atime, mtime, *fuse3_args)
130
285
  0
131
286
  end
132
287
 
133
- # accept a filehandle object as result of these methods
134
- # keep a reference to the filehandle until corresponding release
288
+ # @!method create(path, mode, ffi)
289
+ # Calls super if defined as per {Ruby#create} storing result in ffi.fh and protecting it from GC
290
+ # until {#release}
291
+
292
+ # @!method open(path, ffi)
293
+ # Calls super if defined as per {Ruby#open} storing result in ffi.fh and protecting it from GC
294
+ # until {#release}
295
+
296
+ # @!method opendir(path, ffi)
297
+ # Calls super if defined as per {Ruby#opendir} storing result in ffi.fh and protecting it from GC
298
+ # until {#releasedir}
299
+
135
300
  %i[create open opendir].each do |fuse_method|
136
301
  define_method(fuse_method) do |*args|
137
- fh = super(*args)
138
- store_filehandle(args.last, fh)
302
+ fh = super(*args) if fuse_super_respond_to?(fuse_method)
303
+ store_handle(args.last, fh)
139
304
  0
140
305
  end
141
306
  end
142
307
 
143
- %i[release release_dir].each do |fuse_method|
144
- define_method(fuse_method) do |*args|
145
- super(*args)
308
+ # @!method release(path, ffi)
309
+ # Calls super if defined and allows ffi.fh to be GC'd
310
+
311
+ # @!method releasedir(path, ffi)
312
+ # Calls super if defined and allows ffi.fh to be GC'd
313
+
314
+ %i[release releasedir].each do |fuse_method|
315
+ define_method(fuse_method) do |path, ffi|
316
+ super(path, ffi) if fuse_super_respond_to?(fuse_method)
146
317
  ensure
147
- release_filehandle(args.last)
318
+ release_handle(ffi)
148
319
  end
149
320
  end
150
321
 
322
+ # Calls super if defined and storing result to protect from GC until {#destroy}
323
+ def init(*args)
324
+ o = super(*args) if fuse_super_respond_to?(:init)
325
+ handles << o if o
326
+ end
327
+
328
+ # Calls super if defined and allows init_obj to be GC'd
329
+ def destroy(init_obj)
330
+ super if fuse_super_respond_to?(:destroy)
331
+ handles.delete(init_obj) if init_obj
332
+ end
333
+
334
+ # @!endgroup
151
335
  private
152
336
 
153
- def store_filehandle(ffi, filehandle)
154
- return unless filehandle
337
+ def handles
338
+ @handles ||= Set.new.compare_by_identity
339
+ end
340
+
341
+ def store_handle(ffi, file_handle)
342
+ return unless file_handle
155
343
 
156
- @filehandles ||= {}
157
- @filehandles[ffi]
158
- ffi.fh = filehandle
344
+ handles << file_handle
345
+ ffi.fh = file_handle
159
346
  end
160
347
 
161
- def release_filehandle(ffi)
348
+ def release_handle(ffi)
162
349
  return unless ffi.fh
163
350
 
164
- @filehandles.delete(ffi.fh.object_id)
351
+ handles.delete(ffi.fh)
165
352
  end
166
353
  end
167
354
  # rubocop:enable Metrics/ModuleLength
168
355
 
169
356
  # @!group FUSE Callbacks
170
357
 
358
+ # @!method create(path, mode, fuse_file_info)
359
+ # Create file
360
+ # @abstract
361
+ # @param [String] path
362
+ # @param [FuseFileInfo] fuse_file_info
363
+ # @return [Object] file handle available to future operations in fuse_file_info.fh
364
+
171
365
  # @!method open(path,fuse_file_info)
172
366
  # File open
173
367
  # @abstract
174
368
  # @param [String] path
175
369
  # @param [FuseFileInfo] fuse_file_info
176
- # @return [Object] file handle (available to future operations in fuse_file_info.fh)
370
+ # @return [Object] file handle available to future operations in fuse_file_info.fh
371
+ #
372
+ # File handles are kept from being GC'd until {FuseOperations#release}
373
+ #
374
+ # If the file handle quacks like {::IO} then the file io operations
375
+ # :read, :write, :flush, :fsync, :release will be invoked on the file handle if not implemented
376
+ # by the filesystem
377
+ #
177
378
  # @see FuseOperations#open
178
379
 
179
380
  # @!method create(path,mode,fuse_file_info)
@@ -194,78 +395,79 @@ module FFI
194
395
  # @see FuseOperations#opendir
195
396
 
196
397
  # @!method write(path,data,offset,info)
197
- # Write file data
198
398
  # @abstract
399
+ # Write file data. If not implemented will attempt to use info.fh as an IO via :pwrite, or :seek + :write
199
400
  # @param [String] path
200
401
  # @param [String] data
201
402
  # @param [Integer] offset
202
403
  # @param [FuseFileInfo] info
203
404
  # @return [void]
405
+ # @raise [Errno::ENOTSUP] if not implemented and info.fh does not quack like IO
204
406
  # @see FuseOperations#write
205
407
 
408
+ # @!method write_buf(path,buffers,offset,info)
409
+ # @abstract
410
+ # Write file data from buffers
411
+ # If not implemented, will try to use info.fh,fileno to perform libfuse' file descriptor io, otherwise
412
+ # the string data is extracted from buffers and passed to #{write}
413
+
206
414
  # @!method read(path,size,offset,info)
207
415
  # @abstract
208
- # Read file data
209
- #
416
+ # Read file data. If not implemented will attempt to use info.fh to perform the read.
210
417
  # @param [String] path
211
418
  # @param [Integer] size
212
419
  # @param [Integer] offset
213
420
  # @param [FuseFileInfo] info
214
- #
215
421
  # @return [String] the data, expected to be exactly size bytes, except if EOF
422
+ # @return [#pread, #read] something that supports :pread, or :seek and :read
423
+ # @raise [Errno::ENOTSUP] if not implemented, and info.fh does not quack like IO
216
424
  # @see FuseOperations#read
425
+ # @see FuseOperations#read_buf
426
+
427
+ # @!method read_buf(path,buffers,size,offset,info)
428
+ # @abstract
429
+ # If not implemented and info.fh has :fileno then libfuse' file descriptor io will be used,
430
+ # otherwise will use {read} to populate buffers
217
431
 
218
- # @!method readlink(path)
432
+ # @!method readlink(path, size)
219
433
  # @abstract
220
434
  # Resolve target of a symbolic link
221
435
  # @param [String] path
222
- # @return [String] the link target
436
+ # @param [Integer] size
437
+ # @return [String] the link target, truncated to size if necessary
223
438
  # @see FuseOperations#readlink
224
439
 
225
- # @!method readdir(path,offset,fuse_file_info,&filler)
440
+ # @!method readdir(path,offset,fuse_file_info, readdir_plus:, &filler)
226
441
  # @abstract
227
442
  # List directory entries
228
443
  #
229
- # The filesystem may choose between two modes of operation:
444
+ # The filesystem may choose between three modes of operation:
230
445
  #
231
- # 1) The readdir implementation ignores the offset parameter yielding only name, stat pairs for each entry.
446
+ # 1) The readdir implementation ignores the offset parameter yielding only name, and optional stat
232
447
  # The yield will always return true so the whole directory is read in a single readdir operation.
233
448
  #
234
449
  # 2) The readdir implementation keeps track of the offsets of the directory entries. It uses the offset
235
- # parameter and always yields buffered=true to the filler function. When the buffer is full, or an error
236
- # occurs, yielding to the filler function will return false. Subsequent yields will raise StopIteration
450
+ # parameter to restart the iteration and yields non-zero offsets for each entry. When the buffer is full, or
451
+ # an error occurs, yielding to the filler function will return false. Subsequent yields will raise
452
+ # StopIteration
453
+ #
454
+ # 3) Return a Dir like object from {opendir} and do not implement this method. The directory
455
+ # will be enumerated from offset via calling :seek, :read and :tell on fuse_file_info.fh
237
456
  #
238
457
  # @param [String] path
239
458
  # @param [Integer] offset the starting offset (inclusive!)
240
459
  #
241
460
  # this is either 0 for a new operation, or the last value previously yielded to the filler function
242
- # (when it returned false to indicate the buffer was full). Type 2 implementations should therefore include
461
+ # (when it returned false to indicate the buffer was full).
243
462
  #
244
463
  # @param [FuseFileInfo] fuse_file_info
245
- #
464
+ # @param [Boolean] readdir_plus true if extended readdir is supported (Fuse3 only)
246
465
  # @raise [SystemCallError] an appropriate Errno value
247
466
  # @return [void]
248
- #
249
- # @yieldparam [String] name the name of a directory entry
250
- # @yieldparam [Stat|nil] stat the directory entry stat
251
- #
252
- # Note sending nil values will cause Fuse to issue #getattr operations for each entry
253
- #
254
- # @yieldparam [Integer|Boolean] offset (optional - default false)
255
- #
256
- # integer value will be used as offset. The last value yielded (with false return) will be used
257
- # for the next readdir call
258
- #
259
- # otherwise truthy to indicate support for restart from monotonically increasing offset
260
- #
261
- # false to indicate type 1 operation - full listing
262
- #
263
- # @yieldreturn [Boolean]
264
- #
265
- # * true if buffer accepted the directory entry
266
- # * false on first time buffer is full. StopIteration will be raised on subsequent yields
267
- #
467
+ # @yield [name,stat:,offset:,fill_dir_plus:]
468
+ # See {ReaddirFiller#fill}
268
469
  # @see FuseOperations#readdir
470
+ # @see ReaddirFiller#fill
269
471
 
270
472
  # @!method getxattr(path,name)
271
473
  # @abstract
@@ -275,7 +477,7 @@ module FFI
275
477
  # @return [nil|String] the attribute value or nil if it does not exist
276
478
  # @see FuseOperations#getxattr
277
479
 
278
- # @!method listxattr(path,name)
480
+ # @!method listxattr(path)
279
481
  # @abstract
280
482
  # List extended attributes
281
483
  # @param [String] path
@@ -294,17 +496,6 @@ module FFI
294
496
  # @raise [Errno::ENODATA] for :xattr_replace and name does not already exist
295
497
  # @see FuseOperations#setxattr
296
498
 
297
- # @!method read_buf(path,buffers,size,offset,fuse_file_info)
298
- # @abstract
299
- # Read through fuse data buffers
300
- # @param [String] path
301
- # @param [FuseBufVec] buffers
302
- # @param [Integer] size
303
- # @param [Integer] offset
304
- # @param [FuseFileInfo] fuse_file_info
305
- # @return [void]
306
- # @see FuseOperations#read_buf
307
-
308
499
  # @!method utimens(path,atime,mtime,fuse_file_info=nil)
309
500
  # @abstract
310
501
  # Change the access and/or modification times of a file with nanosecond resolution
@@ -320,13 +511,173 @@ module FFI
320
511
  # Either atime or mtime can be nil corresponding to utimensat(2) receiving UTIME_OMIT.
321
512
  # The special value UTIME_NOW passed from Fuse is automatically set to the current time
322
513
 
514
+ # @!method fsync(path, datasync, fuse_file_info)
515
+ # @abstract
516
+ # @param [String] path
517
+ # @param [Boolean] datasync if true only the user data should be flushed, not the meta data.
518
+ # @param [FuseFileInfo] fuse_file_info
519
+
323
520
  # @!endgroup
324
521
 
325
522
  # @!visibility private
326
523
  def self.included(mod)
327
- mod.prepend(Shim)
328
- mod.include(Safe)
524
+ mod.prepend(Prepend)
525
+ mod.include(Context)
329
526
  mod.include(Debug)
527
+ mod.include(Safe)
528
+ end
529
+
530
+ class << self
531
+ # Helper for implementing {FuseOperations#readlink}
532
+ # @param [FFI::Pointer] buf
533
+ # @param [Integer] size
534
+ # @yield []
535
+ # @yieldreturn [String] the link name
536
+ # @raise [Errno::ENOTSUP] if no data is returned
537
+ # @raise [Errno::ENAMETOOLONG] if data returned is larger than size
538
+ # @return [void]
539
+ def readlink(buf, size)
540
+ link = yield
541
+ raise Errno::ENOTSUP unless link
542
+ raise Errno::ENAMETOOLONG unless link.size < size # includes terminating NUL
543
+
544
+ buf.put_string(link)
545
+ 0
546
+ end
547
+
548
+ # Helper for implementing {FuseOperations#read}
549
+ # @param [FFI::Pointer] buf
550
+ # @param [Integer] size
551
+ # @param [Integer] offset
552
+ # @return [Integer] size of data read
553
+ # @yield []
554
+ # @yieldreturn [String, #pread, #pwrite] the resulting data or IO like object
555
+ # @raise [Errno::ENOTSUP] if no data is returned
556
+ # @raise [Errno::ERANGE] if data return is larger than size
557
+ # @see data_to_str
558
+ def read(buf, size, offset = 0)
559
+ data = yield
560
+ raise Errno::ENOTSUP unless data
561
+
562
+ return data unless buf # called from read_buf
563
+
564
+ data = data_to_str(data, size, offset)
565
+ raise Errno::ERANGE unless data.size <= size
566
+
567
+ buf.write_bytes(data)
568
+ data.size
569
+ end
570
+
571
+ # Helper for implementing {FuseOperations#read_buf}
572
+ # @param [FFI::Pointer] bufp
573
+ # @param [Integer] size
574
+ # @param [Integer] offset
575
+ # @yield []
576
+ # @yieldreturn [Integer|:fileno|String,:pread,:pwrite] a file descriptor, String or io like object
577
+ # @see data_to_bufvec
578
+ def read_buf(bufp, size, offset)
579
+ data = yield
580
+ raise Errno::ENOTSUP unless data
581
+
582
+ bufp.write_pointer(data_to_bufvec(data, size, offset).to_ptr)
583
+ 0
584
+ end
585
+
586
+ # Helper to convert input data to a string for use with {FuseOperations#read}
587
+ # @param [String|:pread|:read] io input data that is a String or quacks like {::IO}
588
+ # @param [Integer] size
589
+ # @param [Integer] offset
590
+ # @return [String] extracted data
591
+ def data_to_str(io, size, offset)
592
+ return io if io.is_a?(String)
593
+ return io.pread(size, offset) if io.respond_to?(:pread)
594
+ return io.read(size) if io.respond_to?(:read)
595
+
596
+ io.to_s
597
+ end
598
+
599
+ # Helper to convert string or IO to {FuseBufVec} for {FuseOperations#read_buf}
600
+ # @param [Integer|:fileno|String|:pread|:read] data the io like input data or an integer file descriptor
601
+ # @param [Integer] size
602
+ # @param [Integer] offset
603
+ # @return [FuseBufVec]
604
+ def data_to_bufvec(data, size, offset)
605
+ data = data.fileno if data.respond_to?(:fileno)
606
+ return FuseBufVec.init(autorelease: false, size: size, fd: data, pos: offset) if data.is_a?(Integer)
607
+
608
+ str = data_to_str(data, size, offset)
609
+ FuseBufVec.init(autorelease: false, size: str.size, mem: FFI::MemoryPointer.from_string(str))
610
+ end
611
+
612
+ # Helper to implement #{FuseOperations#write}
613
+ # @param [FFI::Pointer|:to_s] buf
614
+ # @param [Integer] size
615
+ # @return [Integer] size
616
+ # @yield [data]
617
+ # @yieldparam [String] data extracted from buf
618
+ # @yieldreturn [void]
619
+ def write_data(buf, size)
620
+ data = buf.read_bytes(size) if buf.respond_to?(:read_bytes)
621
+ data ||= buf.to_s
622
+ data = data[0..size] if data.size > size
623
+ yield data
624
+ size
625
+ end
626
+
627
+ # Helper to write a data buffer to an open file
628
+ # @param [FFI::Pointer] buf
629
+ # @param [Integer] size
630
+ # @param [Integer] offset
631
+ # @param [:pwrite,:seek,:write] handle an IO like file handle
632
+ # @return [Integer] size
633
+ # @raise [Errno::ENOTSUP] if handle is does not quack like an open file
634
+ def write_fh(buf, size, offset, handle)
635
+ write_data(buf, size) do |data|
636
+ if handle.respond_to?(:pwrite)
637
+ handle.pwrite(data, offset)
638
+ elsif handle.respond_to?(:write)
639
+ handle.seek(offset) if handle.respond_to?(:seek)
640
+ handle.write(data)
641
+ else
642
+ raise Errno::ENOTSUP
643
+ end
644
+ end
645
+ end
646
+
647
+ # Helper for implementing {FuseOperations#getxattr}
648
+ #
649
+ # @param [FFI::Pointer] buf
650
+ # @param [Integer] size
651
+ # @yieldreturn [String] the xattr name
652
+ def getxattr(buf, size)
653
+ res = yield
654
+ raise Errno::ENODATA unless res
655
+
656
+ res = res.to_s
657
+
658
+ return res.size if size.zero?
659
+ raise Errno::ERANGE if res.size > size
660
+
661
+ buf.write_bytes(res)
662
+ res.size
663
+ end
664
+
665
+ # Helper for implementing {FuseOperations#listxattr}
666
+ # @param [FFI::Pointer] buf
667
+ # @param [Integer] size
668
+ # @yieldreturn [Array<String>] a list of extended attribute names
669
+ def listxattr(buf, size)
670
+ res = yield
671
+ res.reduce(0) do |offset, name|
672
+ name = name.to_s
673
+ unless size.zero?
674
+ raise Errno::ERANGE if offset + name.size >= size
675
+
676
+ buf.put_string(offset, name) # put string includes the NUL terminator
677
+ end
678
+ offset + name.size + 1
679
+ end
680
+ end
330
681
  end
331
682
  end
332
683
  end