ffi-libfuse 0.0.1.rctest12 → 0.3.3

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 (63) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +3 -1
  3. data/CHANGELOG.md +60 -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/flock.rb +7 -5
  11. data/lib/ffi/gnu_extensions.rb +1 -1
  12. data/lib/ffi/libfuse/ackbar.rb +3 -3
  13. data/lib/ffi/libfuse/adapter/context.rb +12 -10
  14. data/lib/ffi/libfuse/adapter/fuse2_compat.rb +52 -51
  15. data/lib/ffi/libfuse/adapter/fuse3_support.rb +7 -4
  16. data/lib/ffi/libfuse/adapter/interrupt.rb +1 -1
  17. data/lib/ffi/libfuse/adapter/ruby.rb +499 -148
  18. data/lib/ffi/libfuse/adapter/safe.rb +12 -11
  19. data/lib/ffi/libfuse/adapter.rb +1 -2
  20. data/lib/ffi/libfuse/callbacks.rb +1 -1
  21. data/lib/ffi/libfuse/filesystem/accounting.rb +116 -0
  22. data/lib/ffi/libfuse/filesystem/mapped_dir.rb +74 -0
  23. data/lib/ffi/libfuse/filesystem/mapped_files.rb +141 -0
  24. data/lib/ffi/libfuse/filesystem/pass_through_dir.rb +55 -0
  25. data/lib/ffi/libfuse/filesystem/pass_through_file.rb +45 -0
  26. data/lib/ffi/libfuse/filesystem/utils.rb +102 -0
  27. data/lib/ffi/libfuse/filesystem/virtual_dir.rb +306 -0
  28. data/lib/ffi/libfuse/filesystem/virtual_file.rb +94 -0
  29. data/lib/ffi/libfuse/filesystem/virtual_fs.rb +196 -0
  30. data/lib/ffi/libfuse/filesystem/virtual_node.rb +101 -0
  31. data/lib/ffi/libfuse/filesystem.rb +25 -0
  32. data/lib/ffi/libfuse/fuse2.rb +32 -24
  33. data/lib/ffi/libfuse/fuse3.rb +28 -18
  34. data/lib/ffi/libfuse/fuse_args.rb +71 -34
  35. data/lib/ffi/libfuse/fuse_buffer.rb +128 -26
  36. data/lib/ffi/libfuse/fuse_callbacks.rb +1 -5
  37. data/lib/ffi/libfuse/fuse_common.rb +60 -61
  38. data/lib/ffi/libfuse/fuse_config.rb +134 -143
  39. data/lib/ffi/libfuse/fuse_conn_info.rb +310 -134
  40. data/lib/ffi/libfuse/fuse_context.rb +45 -3
  41. data/lib/ffi/libfuse/fuse_operations.rb +57 -21
  42. data/lib/ffi/libfuse/fuse_opt.rb +1 -1
  43. data/lib/ffi/libfuse/fuse_version.rb +10 -6
  44. data/lib/ffi/libfuse/gem_version.rb +54 -0
  45. data/lib/ffi/libfuse/main.rb +96 -48
  46. data/lib/ffi/libfuse/test_helper.rb +145 -0
  47. data/lib/ffi/libfuse/version.rb +1 -1
  48. data/lib/ffi/libfuse.rb +13 -4
  49. data/lib/ffi/ruby_object.rb +4 -1
  50. data/lib/ffi/stat/constants.rb +9 -0
  51. data/lib/ffi/stat/native.rb +36 -6
  52. data/lib/ffi/stat/time_spec.rb +26 -10
  53. data/lib/ffi/stat.rb +111 -22
  54. data/lib/ffi/stat_vfs.rb +59 -1
  55. data/lib/ffi/struct_wrapper.rb +22 -1
  56. data/sample/hello_fs.rb +54 -0
  57. data/sample/memory_fs.rb +5 -181
  58. data/sample/no_fs.rb +20 -21
  59. data/sample/pass_through_fs.rb +30 -0
  60. metadata +83 -10
  61. data/lib/ffi/libfuse/adapter/thread_local_context.rb +0 -36
  62. data/lib/ffi/libfuse/test/operations.rb +0 -56
  63. data/lib/ffi/libfuse/test.rb +0 -3
@@ -12,28 +12,36 @@ require_relative '../flock'
12
12
  require_relative 'thread_pool'
13
13
  require_relative '../stat'
14
14
  require_relative '../struct_array'
15
+ require_relative '../encoding'
15
16
  require_relative 'fuse_callbacks'
16
17
 
17
18
  module FFI
18
19
  # Ruby FFI Binding for [libfuse](https://github.com/libfuse/libfuse)
19
20
  module Libfuse
21
+ # All paths are encoded in ruby's view of the filesystem encoding
22
+ typedef Encoding.for('filesystem'), :fs_string
23
+
20
24
  # typedef int (*fuse_fill_dir_t) (void *buf, const char *name, const struct stat *stbuf, off_t off);
21
- fill_dir_t_args = [:pointer, :string, Stat.by_ref, :off_t]
25
+ fill_dir_t_args = [:pointer, :fs_string, Stat.by_ref, :off_t]
22
26
  if FUSE_MAJOR_VERSION > 2
23
27
  enum :fuse_readdir_flags, [:fuse_readdir_plus, (1 << 0)]
24
28
  enum :fuse_fill_dir_flags, [:fuse_fill_dir_plus, (1 << 1)]
25
29
  fill_dir_t_args << :fuse_fill_dir_flags
26
30
  end
27
31
 
32
+ bitmask :fuse_ioctl_flags, %i[compat unrestricted retry dir]
28
33
  bitmask :lock_op, [:lock_sh, 0, :lock_ex, 2, :lock_nb, 4, :lock_un, 8]
29
34
  bitmask :falloc_mode, %i[keep_size punch_hole no_hide_stale collapse_range zero_range insert_range unshare_range]
30
35
  bitmask :flags_mask, %i[nullpath_ok nopath utime_omit_ok] if FUSE_MAJOR_VERSION < 3
31
- enum :xattr, [:xattr_create, 1, :xattr_replace]
36
+
37
+ # @!visibility private
38
+ XAttr = enum :xattr, [:xattr_create, 1, :xattr_replace]
39
+
32
40
  callback :fill_dir_t, fill_dir_t_args, :int
33
41
 
34
42
  # The file system operations as specified in libfuse.
35
43
  #
36
- # All Callback and Configuration methods are optional, but some are essential for a useful filesystem
44
+ # All Callback methods are optional, but some are essential for a useful filesystem
37
45
  # e.g. {getattr},{readdir}
38
46
  #
39
47
  # Almost all callback operations take a path which can be of any length and will return 0 for success, or raise a
@@ -42,6 +50,14 @@ module FFI
42
50
  class FuseOperations < FFI::Struct
43
51
  include FuseCallbacks
44
52
 
53
+ # Callbacks that are expected to return meaningful positive integers
54
+ MEANINGFUL_RETURN = %i[read write write_buf lseek copy_file_range getxattr listxattr].freeze
55
+
56
+ # @return [Boolean] true if fuse_callback expects a meaningful integer return
57
+ def self.meaningful_return?(fuse_callback)
58
+ MEANINGFUL_RETURN.include?(fuse_callback)
59
+ end
60
+
45
61
  # Container to dynamically build up the operations layout which is dependent on the loaded libfuse version
46
62
  op = {}
47
63
 
@@ -144,7 +160,7 @@ module FFI
144
160
  # @return [Integer] 0 for success or -ve errno
145
161
 
146
162
  # int (*symlink) (const char *, const char *);
147
- op[:symlink] = [:string]
163
+ op[:symlink] = [:fs_string]
148
164
 
149
165
  # @!method rename(from_path,to_path)
150
166
  # @abstract
@@ -155,7 +171,7 @@ module FFI
155
171
  # @return [Integer] 0 for success or -ve errno
156
172
 
157
173
  # int (*rename) (const char *, const char *);
158
- op[:rename] = [:string]
174
+ op[:rename] = [:fs_string]
159
175
 
160
176
  # @!method link(path,target)
161
177
  # @abstract
@@ -166,7 +182,7 @@ module FFI
166
182
  # @return [Integer] 0 for success or -ve errno
167
183
 
168
184
  # int (*link) (const char *, const char *);
169
- op[:link] = [:string]
185
+ op[:link] = [:fs_string]
170
186
 
171
187
  # @!method chmod(path,mode,fuse_file_info=nil)
172
188
  # @abstract
@@ -208,6 +224,7 @@ module FFI
208
224
  op[:truncate] = [:off_t]
209
225
  op[:truncate] << FuseFileInfo.by_ref if FUSE_MAJOR_VERSION >= 3
210
226
 
227
+ # Not directly implemented see utimens
211
228
  # int (*utime) (const char *, struct utimbuf *);
212
229
  op[:utime] = [:pointer] if FUSE_MAJOR_VERSION < 3
213
230
 
@@ -563,7 +580,7 @@ module FFI
563
580
  # @param [String] path
564
581
  # @param [FuseFileInfo] fuse_file_info
565
582
  # For checking lock ownership, the 'fuse_file_info->owner' argument must be used.
566
- # @param [Symbol] cmd either :f_getlck, :f_setlck or :f_setlkw.
583
+ # @param [Symbol] cmd either :getlck, :setlck or :setlkw.
567
584
  # @param [Flock] flock
568
585
  # For the meaning of fields in 'struct flock' see the man page for fcntl(2). The whence field will always
569
586
  # be set to :seek_set.
@@ -660,18 +677,21 @@ module FFI
660
677
  # @param [Integer] cmd
661
678
  # @param [FFI::Pointer] arg
662
679
  # @param [FuseFileInfo] fuse_file_info
663
- # @param [Integer] flags
664
- # @param [FFI::Pointer] data
680
+ # @param [Array<Symbol>] flags
665
681
  #
666
- # flags will have FUSE_IOCTL_COMPAT set for 32bit ioctls in 64bit environment. The size and direction of data
667
- # is determined by _IOC_*() decoding of cmd. For _IOC_NONE, data will be NULL, for _IOC_WRITE data is out
668
- # area, for _IOC_READ in area and if both are set in/out area. In all non-NULL cases, the area is of
669
- # _IOC_SIZE(cmd) bytes.
682
+ # - :compat 32bit compat ioctl on 64bit machine
683
+ # - :unrestricted not restricted to well-formed ioctls, retry allowed (lowlevel fuse)
684
+ # - :retry retry with new iovecs (lowlevel fuse)
685
+ # - :dir is a directory file handle
670
686
  #
671
- # If flags has FUSE_IOCTL_DIR then the fuse_file_info refers to a directory file handle.
687
+ # @param [FFI::Pointer] data
688
+ #
689
+ # The size and direction of data is determined by _IOC_*() decoding of cmd. For _IOC_NONE, data will be NULL,
690
+ # for _IOC_WRITE data is out area, for _IOC_READ in area and if both are set in/out area. In all non-NULL
691
+ # cases, the area is of _IOC_SIZE(cmd) bytes.
672
692
 
673
693
  # int (*ioctl) (const char *, int cmd, void *arg, struct fuse_file_info *, unsigned int flags, void *data);
674
- op[:ioctl] = [:int, :pointer, FuseFileInfo.by_ref, :uint, :pointer]
694
+ op[:ioctl] = [:int, :pointer, FuseFileInfo.by_ref, :fuse_ioctl_flags, :pointer]
675
695
 
676
696
  # @!method poll(path,fuse_file_info,ph,reventsp)
677
697
  # @abstract
@@ -697,8 +717,9 @@ module FFI
697
717
  # @abstract
698
718
  # Write contents of buffer to an open file
699
719
  #
700
- # Similar to the write() method, but data is supplied in a generic buffer. Use fuse_buf_copy() to transfer
701
- # data to the destination.
720
+ # Similar to the write() method, but data is supplied in a generic buffer.
721
+ # Use {FuseBufVec#copy_to_fd} to copy data to an open file descriptor, or {FuseBufVec#copy_to_str} to extract
722
+ # string data from the buffer
702
723
  #
703
724
  # @param [String] path
704
725
  # @param [FuseBufVec] buf
@@ -804,8 +825,8 @@ module FFI
804
825
  # );
805
826
  op[:copy_file_range] =
806
827
  callback [
807
- :string, FuseFileInfo.by_ref, :off_t,
808
- :string, FuseFileInfo.by_ref, :off_t,
828
+ :fs_string, FuseFileInfo.by_ref, :off_t,
829
+ :fs_string, FuseFileInfo.by_ref, :off_t,
809
830
  :size_t, :int
810
831
  ], :ssize_t
811
832
 
@@ -820,7 +841,7 @@ module FFI
820
841
  # @see lseek(2)
821
842
 
822
843
  # off_t (*lseek) (const char *, off_t off, int whence, struct fuse_file_info *);
823
- op[:lseek] = callback [:string, :off_t, Flock::Enums::SeekWhence, FuseFileInfo.by_ref], :off_t
844
+ op[:lseek] = callback [:fs_string, :off_t, Flock::Enums::SeekWhence, FuseFileInfo.by_ref], :off_t
824
845
  end
825
846
  end
826
847
 
@@ -829,7 +850,7 @@ module FFI
829
850
  layout_data = op.transform_values do |v|
830
851
  if v.is_a?(Array) && !v.last.is_a?(Integer)
831
852
  # A typical fuse callback
832
- callback([:string] + v, :int)
853
+ callback([:fs_string] + v, :int)
833
854
  else
834
855
  v
835
856
  end
@@ -865,6 +886,21 @@ module FFI
865
886
  fuse_flags.concat(delegate.fuse_flags) if delegate.respond_to?(:fuse_flags)
866
887
  send(:[]=, :flags, fuse_flags.uniq)
867
888
  end
889
+
890
+ # @!visibility private
891
+ def fuse_callbacks
892
+ self.class.fuse_callbacks
893
+ end
894
+
895
+ # @return [Set<Symbol>] list of callback methods
896
+ def self.fuse_callbacks
897
+ @fuse_callbacks ||= Set.new(members - [:flags])
898
+ end
899
+
900
+ # @return [Set<Symbol>] list of path callback methods
901
+ def self.path_callbacks
902
+ @path_callbacks ||= fuse_callbacks - %i[init destroy]
903
+ end
868
904
  end
869
905
  end
870
906
  end
@@ -19,7 +19,7 @@ module FFI
19
19
  def fill(template, value)
20
20
  str_ptr = FFI::MemoryPointer.from_string(template)
21
21
  self[:template] = str_ptr
22
- self[:offset] = (2**(8 * FFI::Type::INT.size)) - 1 # -(1U) in a LONG!!
22
+ self[:offset] = (2**(FFI::Type::INT.size * 8)) - 1 # -(1U) in a LONG!!
23
23
  self[:value] = value.to_i
24
24
  self
25
25
  end
@@ -9,15 +9,18 @@ module FFI
9
9
  module Libfuse
10
10
  extend FFI::Library
11
11
 
12
+ libs =
13
+ case FFI::Platform::NAME
14
+ when 'x86_64-darwin'
15
+ %w[libfuse.2.dylib]
16
+ else
17
+ %w[libfuse3.so.3 libfuse.so.2]
18
+ end
12
19
  # The fuse library to load from 'LIBFUSE' environment variable if set, otherwise prefer Fuse3 over Fuse2
13
- LIBFUSE = ENV['LIBFUSE'] || %w[libfuse3.so.3 libfuse.so.2]
20
+ LIBFUSE = ENV.fetch('LIBFUSE', libs)
14
21
  ffi_lib(LIBFUSE)
15
22
 
16
- # @!scope class
17
- # @!method fuse_version()
18
- # @return [Integer] the fuse version
19
- # See {FUSE_VERSION} which captures this result in a constant
20
-
23
+ # @!visibility private
21
24
  attach_function :fuse_version, [], :int
22
25
 
23
26
  # prior to 3.10 this is Major * 10 + Minor, after 3.10 and later is Major * 100 + Minor
@@ -36,6 +39,7 @@ module FFI
36
39
  require_relative '../gnu_extensions'
37
40
 
38
41
  extend(GNUExtensions)
42
+
39
43
  # libfuse2 has busted symbols
40
44
  ffi_lib_versions(%w[FUSE_2.9.1 FUSE_2.9 FUSE_2.8 FUSE_2.7 FUSE_2.6 FUSE_2.5 FUSE_2.4 FUSE_2.3 FUSE_2.2])
41
45
  end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'version'
4
+
5
+ module FFI
6
+ # Ruby FFI Binding for [libfuse](https://github.com/libfuse/libfuse)
7
+ module Libfuse
8
+ # @!visibility private
9
+
10
+ SEMVER_TAG_REGEX = /^v\d+\.\d+\.\d+/.freeze
11
+
12
+ # branches the format is refs/heads/<branch_name>,
13
+ # tags it is refs/tags/<tag_name>.
14
+ # for pull requests it is refs/pull/<pr_number>/merge,
15
+ GIT_REF_TYPES = { 'heads' => :branch, 'tags' => :tag, 'pull' => :pull }.freeze
16
+
17
+ def self.git_ref(env: ENV)
18
+ ref = env.fetch('GIT_REF') do
19
+ # get branch ref, or detached head ref to tag
20
+ `git symbolic-ref HEAD 2>/dev/null || git name-rev HEAD | awk '{ gsub(/[\\^~@].*$/,"",$2); printf("refs/%s\\n",$2)}'`.strip # rubocop:disable Layout/LineLength
21
+ rescue StandardError
22
+ nil
23
+ end
24
+
25
+ return [ref, nil] unless ref&.start_with?('refs/')
26
+
27
+ _refs, ref_type, ref_name = ref.split('/', 3)
28
+ [ref_name, GIT_REF_TYPES[ref_type]]
29
+ end
30
+
31
+ def self.gem_version(main_branch: 'main', version: VERSION, env: ENV)
32
+ ref_name, ref_type = git_ref(env: env)
33
+
34
+ version =
35
+ case ref_type
36
+ when :branch
37
+ ref_name == main_branch ? [version] : [version, ref_name]
38
+ when :tag
39
+ SEMVER_TAG_REGEX.match?(ref_name) ? [ref_name[1..]] : [version, ref_name]
40
+ when :pull
41
+ pr_number, merge, _rest = ref_name.split('/')
42
+ # GITHUB_BASE_REF The name of the base ref or target branch of the pull request in a workflow run
43
+ base_ref = env.fetch('GIT_BASE_REF', 'undefined')
44
+ [version, base_ref, "#{merge}#{pr_number}"]
45
+ else
46
+ [version, 'pre', ref_name]
47
+ end.select { |p| p && !p.empty? }.join('.').tr('//_-', '')
48
+
49
+ [version, ref_name, ref_type]
50
+ end
51
+
52
+ GEM_VERSION, GIT_REF_NAME, GIT_REF_TYPE = gem_version
53
+ end
54
+ end
@@ -9,12 +9,26 @@ module FFI
9
9
  # Controls the main run loop for a FUSE filesystem
10
10
  module Main
11
11
  class << self
12
+ # Builds default argument list for #{fuse_main} regardless of being called directly or from mount.fuse3
13
+ #
14
+ # @param [Array<String>] extra_args additional arguments to add to $0 and *ARGV
15
+ # @return [Array<String>]
16
+ # @see https://github.com/libfuse/libfuse/issues/621
17
+ def default_args(*extra_args)
18
+ args = ARGV.dup
19
+
20
+ # If called from mount.fuse3 we already have a 'source' argument which should go at args[0]
21
+ args.unshift($0) unless args.size >= 2 && args[0..1].all? { |a| !a.start_with?('-') }
22
+ args.concat(extra_args)
23
+ end
24
+
12
25
  # Main function of FUSE
13
26
  #
14
27
  # This function:
15
28
  #
16
29
  # - parses command line options - see {fuse_parse_cmdline}
17
- # and exits immediately if help or version options were processed
30
+ # exiting immediately if help or version options were processed
31
+ # - calls {#fuse_debug}, {#fuse_options}, {#fuse_configure} if implemented by operations
18
32
  # - installs signal handlers for INT, HUP, TERM to unmount and exit filesystem
19
33
  # - installs custom signal handlers if operations implements {fuse_traps}
20
34
  # - creates a fuse handle mounted with registered operations - see {fuse_create}
@@ -22,7 +36,7 @@ module FFI
22
36
  #
23
37
  # @param [Array<String>] argv mount.fuse arguments
24
38
  # expects progname, mountpoint, options....
25
- # @param [FuseArgs] args
39
+ # @param [FuseArgs|Array<String>] args
26
40
  # alternatively constructed args
27
41
  # @param [Object|FuseOperations] operations
28
42
  # something that responds to the fuse callbacks and optionally our abstract configuration methods
@@ -30,13 +44,18 @@ module FFI
30
44
  # any data to be made available to the {FuseOperations#init} callback
31
45
  #
32
46
  # @return [Integer] suitable for process exit code
33
- def fuse_main(*argv, operations:, args: argv, private_data: nil)
47
+ def fuse_main(*argv, operations:, args: argv.any? ? argv : default_args, private_data: nil)
34
48
  run_args = fuse_parse_cmdline(args: args, handler: operations)
35
49
  return 2 unless run_args
36
50
 
37
51
  fuse_args = run_args.delete(:args)
38
52
  mountpoint = run_args.delete(:mountpoint)
39
53
 
54
+ return 3 unless fuse_configure(operations: operations, **run_args)
55
+
56
+ warn "FuseCreate: mountpoint: #{mountpoint}, args: [#{fuse_args.argv.join(' ')}]" if run_args[:debug]
57
+ warn "FuseRun: #{run_args}" if run_args[:debug]
58
+
40
59
  fuse = fuse_create(mountpoint, args: fuse_args, operations: operations, private_data: private_data)
41
60
 
42
61
  return 0 if run_args[:show_help] || run_args[:show_version]
@@ -44,52 +63,43 @@ module FFI
44
63
 
45
64
  return unless fuse
46
65
 
47
- warn run_args.to_s if run_args[:debug]
48
-
49
66
  fuse.run(**run_args)
50
67
  end
68
+ alias main fuse_main
51
69
 
52
70
  # Parse command line arguments
53
71
  #
54
72
  # - parses standard command line options (-d -s -h -V)
55
73
  # will call {fuse_debug}, {fuse_version}, {fuse_help} if implemented by handler
56
- # - parses custom options if handler implements {fuse_options} and {fuse_opt_proc}
74
+ # - calls {fuse_options} for custom option processing if implemented by handler
57
75
  # - records signal handlers if operations implements {fuse_traps}
58
76
  # - parses standard fuse mount options
59
77
  #
60
78
  # @param [Array<String>] argv mount.fuse arguments
61
- # expects progname, [fsname,] mountpoint, options.... from mount.fuse3
79
+ # expects progname, mountpoint, options....
62
80
  # @param [FuseArgs] args
63
81
  # alternatively constructed args
64
82
  # @param [Object] handler
65
83
  # something that responds to our abstract configuration methods
66
- # @param [Object] private_data passed to handler.fuse_opt_proc
67
- #
68
84
  # @return [Hash<Symbol,Object>]
69
- # * fsname [String]: the fsspec from /etc/fstab
70
85
  # * mountpoint [String]: the mountpoint argument
71
86
  # * args [FuseArgs]: remaining fuse_args to pass to {fuse_create}
72
87
  # * show_help [Boolean]: -h or --help
73
88
  # * show_version [Boolean]: -v or --version
74
89
  # * debug [Boolean]: -d
75
90
  # * others are options to pass to {FuseCommon#run}
76
- def fuse_parse_cmdline(*argv, args: argv, handler: nil, private_data: nil)
91
+ def fuse_parse_cmdline(*argv, args: argv.any? ? argv : default_args, handler: nil)
77
92
  args = fuse_init_args(args)
78
93
 
79
94
  # Parse args and print cmdline help
80
95
  run_args = Fuse.parse_cmdline(args, handler: handler)
96
+ return nil unless run_args
81
97
 
82
- # process custom options
83
- if %i[fuse_options fuse_opt_proc].all? { |m| handler.respond_to?(m) }
84
- parse_ok = args.parse!(handler.fuse_options, private_data) do |*p_args|
85
- handler.fuse_opt_proc(*p_args)
86
- end
87
- return unless parse_ok
88
- end
98
+ return nil if handler.respond_to?(:fuse_options) && !handler.fuse_options(args)
89
99
 
90
100
  run_args[:traps] = handler.fuse_traps if handler.respond_to?(:fuse_traps)
91
101
 
92
- args.parse!(RUN_OPTIONS, run_args) { |*opt_args| hash_opt_proc(*opt_args, discard: %i[native max_threads]) }
102
+ return nil unless parse_run_options(args, run_args)
93
103
 
94
104
  run_args[:args] = args
95
105
  run_args
@@ -97,40 +107,41 @@ module FFI
97
107
 
98
108
  # @return [FuseCommon|nil] the mounted filesystem or nil if not mounted
99
109
  def fuse_create(mountpoint, *argv, operations:, args: nil, private_data: nil)
100
- args = fuse_init_args(args || argv.unshift(mountpoint))
110
+ args = fuse_init_args(args || argv)
101
111
 
102
112
  operations = FuseOperations.new(delegate: operations) unless operations.is_a?(FuseOperations)
103
113
 
104
- fuse = Fuse.new(mountpoint, args, operations, private_data)
114
+ fuse = Fuse.new(mountpoint.to_s, args, operations, private_data)
105
115
  fuse if fuse.mounted?
106
116
  end
107
117
 
108
- # Helper fuse_opt_proc function to capture options into a hash
109
- #
110
- # See {FuseArgs.parse!}
111
- def hash_opt_proc(hash, arg, key, _out, discard: [])
112
- return :keep if %i[unmatched non_option].include?(key)
113
-
114
- hash[key] = arg =~ /=/ ? arg.split('=', 2).last : true
115
- discard.include?(key) ? :discard : :keep
118
+ # @!visibility private
119
+ def fuse_configure(operations:, show_help: false, show_version: false, **_)
120
+ return true unless operations.respond_to?(:fuse_configure) && !show_help && !show_version
121
+
122
+ # Provide sensible values for FuseContext in case this is referenced during configure
123
+ FFI::Libfuse::FuseContext.overrides do
124
+ operations.fuse_configure
125
+ true
126
+ rescue Error => e
127
+ warn e.message
128
+ false
129
+ rescue StandardError, ScriptError => e
130
+ warn "#{e.class.name}: #{e.message}\n\t#{e.backtrace.join("\n\t")}"
131
+ false
132
+ end
116
133
  end
117
134
 
118
135
  # @!visibility private
119
-
120
- # Version text
121
136
  def version
122
137
  "#{name}: #{VERSION}"
123
138
  end
124
139
 
140
+ private
141
+
125
142
  def fuse_init_args(args)
126
143
  if args.is_a?(Array)
127
- args = args.map(&:to_s) # handle mountpoint as Pathname etc..
128
-
129
- # https://github.com/libfuse/libfuse/issues/621 handle "source" field sent from /etc/fstab via mount.fuse3
130
- # if arg[1] and arg[2] are both non option fields then replace arg1 with -ofsname=<arg1>
131
- unless args.size <= 2 || args[1]&.start_with?('-') || args[2]&.start_with?('-')
132
- args[1] = "-ofsname=#{args[1]}"
133
- end
144
+ warn "FuseArgs: #{args.join(' ')}" if args.include?('-d')
134
145
  args = FuseArgs.create(*args)
135
146
  end
136
147
 
@@ -138,23 +149,52 @@ module FFI
138
149
 
139
150
  raise ArgumentError "fuse main args: must be Array<String> or #{FuseArgs.class.name}"
140
151
  end
152
+
153
+ def parse_run_options(args, run_args)
154
+ args.parse!(RUN_OPTIONS) do |key:, value:, **|
155
+ run_args[key] = value
156
+ next :keep if (STANDARD_OPTIONS.values + %i[remember]).include?(key)
157
+
158
+ :discard
159
+ end
160
+ end
141
161
  end
142
162
 
143
163
  # @!group Abstract Configuration
144
164
 
145
- # @!method fuse_options
165
+ # @!method fuse_options(args)
146
166
  # @abstract
147
- # @return [Hash] custom option schema
148
- # @see FuseArgs#parse!
149
-
150
- # @!method fuse_opt_proc(data,arg,key,out)
151
- # @abstract
152
- # Process custom options
167
+ # Called to allow filesystem to handle custom options and observe standard mount options #
168
+ # @param [FuseArgs] args
169
+ # @return [Boolean] true if args parsed successfully
153
170
  # @see FuseArgs#parse!
171
+ # @example
172
+ # OPTIONS = { 'config=' => :config, '-c ' => :config }
173
+ # def fuse_options(args)
174
+ # args.parse!(OPTIONS) do |key:, value:, out:, **opts|
175
+ #
176
+ # # raise errors for invalid config
177
+ # raise FFI::Libfuse::FuseArgs::Error, "Invalid config" unless valid_config?(key,value)
178
+ #
179
+ # # Configure the file system
180
+ # @config = value if key == :config
181
+ #
182
+ # #Optionally manipulate other arguments for fuse_mount() based on the current argument and state
183
+ # out.add('-oopt=val')
184
+ #
185
+ # # Custom options must be marked :handled otherwise fuse_mount() will fail with unknown arguments
186
+ # :handled
187
+ # end
188
+ # end
154
189
 
155
190
  # @!method fuse_traps
156
191
  # @abstract
157
- # @return [Hash] map of signal name or number to signal handler as per Signal.trap
192
+ # @return [Hash<String|Symbol|Integer,String|Proc>]
193
+ # map of signal name or number to signal handler as per Signal.trap
194
+ # @example
195
+ # def fuse_traps
196
+ # { HUP: ->() { reload() }}
197
+ # end
158
198
 
159
199
  # @!method fuse_version
160
200
  # @abstract
@@ -162,14 +202,22 @@ module FFI
162
202
 
163
203
  # @!method fuse_help
164
204
  # @abstract
165
- # @return [String] help text to explain custom options to show with -h option
205
+ # Called as part of generating output for the -h option
206
+ # @return [String] help text to explain custom options
166
207
 
167
208
  # @!method fuse_debug(enabled)
168
209
  # @abstract
169
- # Indicate to the filesystem whether debugging option is in use.
210
+ # Called to indicate to the filesystem whether debugging option is in use.
170
211
  # @param [Boolean] enabled if -d option is in use
171
212
  # @return [void]
172
213
 
214
+ # @!method fuse_configure
215
+ # @abstract
216
+ # Called immediately before the filesystem is mounted, after options have been parsed
217
+ #
218
+ # @raise [Error] to prevent the mount from proceeding
219
+ # @return [void]
220
+
173
221
  # @!endgroup
174
222
 
175
223
  # @!visibility private