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
@@ -12,19 +12,24 @@ 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
@@ -33,7 +38,7 @@ module FFI
33
38
 
34
39
  # The file system operations as specified in libfuse.
35
40
  #
36
- # All Callback and Configuration methods are optional, but some are essential for a useful filesystem
41
+ # All Callback methods are optional, but some are essential for a useful filesystem
37
42
  # e.g. {getattr},{readdir}
38
43
  #
39
44
  # Almost all callback operations take a path which can be of any length and will return 0 for success, or raise a
@@ -144,7 +149,7 @@ module FFI
144
149
  # @return [Integer] 0 for success or -ve errno
145
150
 
146
151
  # int (*symlink) (const char *, const char *);
147
- op[:symlink] = [:string]
152
+ op[:symlink] = [:fs_string]
148
153
 
149
154
  # @!method rename(from_path,to_path)
150
155
  # @abstract
@@ -155,7 +160,7 @@ module FFI
155
160
  # @return [Integer] 0 for success or -ve errno
156
161
 
157
162
  # int (*rename) (const char *, const char *);
158
- op[:rename] = [:string]
163
+ op[:rename] = [:fs_string]
159
164
 
160
165
  # @!method link(path,target)
161
166
  # @abstract
@@ -166,7 +171,7 @@ module FFI
166
171
  # @return [Integer] 0 for success or -ve errno
167
172
 
168
173
  # int (*link) (const char *, const char *);
169
- op[:link] = [:string]
174
+ op[:link] = [:fs_string]
170
175
 
171
176
  # @!method chmod(path,mode,fuse_file_info=nil)
172
177
  # @abstract
@@ -660,18 +665,21 @@ module FFI
660
665
  # @param [Integer] cmd
661
666
  # @param [FFI::Pointer] arg
662
667
  # @param [FuseFileInfo] fuse_file_info
663
- # @param [Integer] flags
664
- # @param [FFI::Pointer] data
668
+ # @param [Array<Symbol>] flags
665
669
  #
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.
670
+ # - :compat 32bit compat ioctl on 64bit machine
671
+ # - :unrestricted not restricted to well-formed ioctls, retry allowed (lowlevel fuse)
672
+ # - :retry retry with new iovecs (lowlevel fuse)
673
+ # - :dir is a directory file handle
670
674
  #
671
- # If flags has FUSE_IOCTL_DIR then the fuse_file_info refers to a directory file handle.
675
+ # @param [FFI::Pointer] data
676
+ #
677
+ # The size and direction of data is determined by _IOC_*() decoding of cmd. For _IOC_NONE, data will be NULL,
678
+ # for _IOC_WRITE data is out area, for _IOC_READ in area and if both are set in/out area. In all non-NULL
679
+ # cases, the area is of _IOC_SIZE(cmd) bytes.
672
680
 
673
681
  # 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]
682
+ op[:ioctl] = [:int, :pointer, FuseFileInfo.by_ref, :fuse_ioctl_flags, :pointer]
675
683
 
676
684
  # @!method poll(path,fuse_file_info,ph,reventsp)
677
685
  # @abstract
@@ -697,8 +705,9 @@ module FFI
697
705
  # @abstract
698
706
  # Write contents of buffer to an open file
699
707
  #
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.
708
+ # Similar to the write() method, but data is supplied in a generic buffer.
709
+ # Use {FuseBufVec#copy_to_fd} to copy data to an open file descriptor, or {FuseBufVec#copy_to_str} to extract
710
+ # string data from the buffer
702
711
  #
703
712
  # @param [String] path
704
713
  # @param [FuseBufVec] buf
@@ -804,8 +813,8 @@ module FFI
804
813
  # );
805
814
  op[:copy_file_range] =
806
815
  callback [
807
- :string, FuseFileInfo.by_ref, :off_t,
808
- :string, FuseFileInfo.by_ref, :off_t,
816
+ :fs_string, FuseFileInfo.by_ref, :off_t,
817
+ :fs_string, FuseFileInfo.by_ref, :off_t,
809
818
  :size_t, :int
810
819
  ], :ssize_t
811
820
 
@@ -820,7 +829,7 @@ module FFI
820
829
  # @see lseek(2)
821
830
 
822
831
  # 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
832
+ op[:lseek] = callback [:fs_string, :off_t, Flock::Enums::SeekWhence, FuseFileInfo.by_ref], :off_t
824
833
  end
825
834
  end
826
835
 
@@ -829,7 +838,7 @@ module FFI
829
838
  layout_data = op.transform_values do |v|
830
839
  if v.is_a?(Array) && !v.last.is_a?(Integer)
831
840
  # A typical fuse callback
832
- callback([:string] + v, :int)
841
+ callback([:fs_string] + v, :int)
833
842
  else
834
843
  v
835
844
  end
@@ -865,6 +874,21 @@ module FFI
865
874
  fuse_flags.concat(delegate.fuse_flags) if delegate.respond_to?(:fuse_flags)
866
875
  send(:[]=, :flags, fuse_flags.uniq)
867
876
  end
877
+
878
+ # @!visibility private
879
+ def fuse_callbacks
880
+ self.class.fuse_callbacks
881
+ end
882
+
883
+ # @return [Set<Symbol>] list of callback methods
884
+ def self.fuse_callbacks
885
+ @fuse_callbacks ||= Set.new(members - [:flags])
886
+ end
887
+
888
+ # @return [Set<Symbol>] list of path callback methods
889
+ def self.path_callbacks
890
+ @path_callbacks ||= fuse_callbacks - %i[init destroy]
891
+ end
868
892
  end
869
893
  end
870
894
  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
@@ -14,7 +14,8 @@ module FFI
14
14
  # This function:
15
15
  #
16
16
  # - parses command line options - see {fuse_parse_cmdline}
17
- # and exits immediately if help or version options were processed
17
+ # exiting immediately if help or version options were processed
18
+ # - calls {#fuse_debug}, {#fuse_options}, {#fuse_configure} if implemented by operations
18
19
  # - installs signal handlers for INT, HUP, TERM to unmount and exit filesystem
19
20
  # - installs custom signal handlers if operations implements {fuse_traps}
20
21
  # - creates a fuse handle mounted with registered operations - see {fuse_create}
@@ -22,7 +23,7 @@ module FFI
22
23
  #
23
24
  # @param [Array<String>] argv mount.fuse arguments
24
25
  # expects progname, mountpoint, options....
25
- # @param [FuseArgs] args
26
+ # @param [FuseArgs|Array<String>] args
26
27
  # alternatively constructed args
27
28
  # @param [Object|FuseOperations] operations
28
29
  # something that responds to the fuse callbacks and optionally our abstract configuration methods
@@ -30,13 +31,18 @@ module FFI
30
31
  # any data to be made available to the {FuseOperations#init} callback
31
32
  #
32
33
  # @return [Integer] suitable for process exit code
33
- def fuse_main(*argv, operations:, args: argv, private_data: nil)
34
+ def fuse_main(*argv, operations:, args: argv.any? ? argv : [$0, *ARGV], private_data: nil)
34
35
  run_args = fuse_parse_cmdline(args: args, handler: operations)
35
36
  return 2 unless run_args
36
37
 
37
38
  fuse_args = run_args.delete(:args)
38
39
  mountpoint = run_args.delete(:mountpoint)
39
40
 
41
+ return 3 unless fuse_configure(operations: operations, **run_args)
42
+
43
+ warn "FuseCreate: mountpoint: #{mountpoint}, args: [#{fuse_args.argv.join(' ')}]" if run_args[:debug]
44
+ warn "FuseRun: #{run_args}" if run_args[:debug]
45
+
40
46
  fuse = fuse_create(mountpoint, args: fuse_args, operations: operations, private_data: private_data)
41
47
 
42
48
  return 0 if run_args[:show_help] || run_args[:show_version]
@@ -44,16 +50,15 @@ module FFI
44
50
 
45
51
  return unless fuse
46
52
 
47
- warn run_args.to_s if run_args[:debug]
48
-
49
53
  fuse.run(**run_args)
50
54
  end
55
+ alias main fuse_main
51
56
 
52
57
  # Parse command line arguments
53
58
  #
54
59
  # - parses standard command line options (-d -s -h -V)
55
60
  # 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}
61
+ # - calls {fuse_options} for custom option processing if implemented by handler
57
62
  # - records signal handlers if operations implements {fuse_traps}
58
63
  # - parses standard fuse mount options
59
64
  #
@@ -63,33 +68,25 @@ module FFI
63
68
  # alternatively constructed args
64
69
  # @param [Object] handler
65
70
  # something that responds to our abstract configuration methods
66
- # @param [Object] private_data passed to handler.fuse_opt_proc
67
- #
68
71
  # @return [Hash<Symbol,Object>]
69
- # * fsname [String]: the fsspec from /etc/fstab
70
72
  # * mountpoint [String]: the mountpoint argument
71
73
  # * args [FuseArgs]: remaining fuse_args to pass to {fuse_create}
72
74
  # * show_help [Boolean]: -h or --help
73
75
  # * show_version [Boolean]: -v or --version
74
76
  # * debug [Boolean]: -d
75
77
  # * others are options to pass to {FuseCommon#run}
76
- def fuse_parse_cmdline(*argv, args: argv, handler: nil, private_data: nil)
78
+ def fuse_parse_cmdline(*argv, args: argv, handler: nil)
77
79
  args = fuse_init_args(args)
78
80
 
79
81
  # Parse args and print cmdline help
80
82
  run_args = Fuse.parse_cmdline(args, handler: handler)
83
+ return nil unless run_args
81
84
 
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
85
+ return nil if handler.respond_to?(:fuse_options) && !handler.fuse_options(args)
89
86
 
90
87
  run_args[:traps] = handler.fuse_traps if handler.respond_to?(:fuse_traps)
91
88
 
92
- args.parse!(RUN_OPTIONS, run_args) { |*opt_args| hash_opt_proc(*opt_args, discard: %i[native max_threads]) }
89
+ return nil unless parse_run_options(args, run_args)
93
90
 
94
91
  run_args[:args] = args
95
92
  run_args
@@ -105,18 +102,24 @@ module FFI
105
102
  fuse if fuse.mounted?
106
103
  end
107
104
 
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)
105
+ # @!visibility private
113
106
 
114
- hash[key] = arg =~ /=/ ? arg.split('=', 2).last : true
115
- discard.include?(key) ? :discard : :keep
107
+ def fuse_configure(operations:, show_help: false, show_version: false, **_)
108
+ return true unless operations.respond_to?(:fuse_configure) && !show_help && !show_version
109
+
110
+ # Provide sensible values for FuseContext in case this is referenced during configure
111
+ FFI::Libfuse::FuseContext.overrides do
112
+ operations.fuse_configure
113
+ true
114
+ rescue Error => e
115
+ warn e.message
116
+ false
117
+ rescue StandardError, ScriptError => e
118
+ warn "#{e.class.name}: #{e.message}\n\t#{e.backtrace.join("\n\t")}"
119
+ false
120
+ end
116
121
  end
117
122
 
118
- # @!visibility private
119
-
120
123
  # Version text
121
124
  def version
122
125
  "#{name}: #{VERSION}"
@@ -131,6 +134,7 @@ module FFI
131
134
  unless args.size <= 2 || args[1]&.start_with?('-') || args[2]&.start_with?('-')
132
135
  args[1] = "-ofsname=#{args[1]}"
133
136
  end
137
+ warn "FuseArgs: #{args.join(' ')}" if args.include?('-d')
134
138
  args = FuseArgs.create(*args)
135
139
  end
136
140
 
@@ -138,23 +142,54 @@ module FFI
138
142
 
139
143
  raise ArgumentError "fuse main args: must be Array<String> or #{FuseArgs.class.name}"
140
144
  end
145
+
146
+ private
147
+
148
+ def parse_run_options(args, run_args)
149
+ args.parse!(RUN_OPTIONS) do |key:, value:, **|
150
+ run_args[key] = value
151
+ next :keep if (STANDARD_OPTIONS.values + %i[remember]).include?(key)
152
+
153
+ :discard
154
+ end
155
+ end
141
156
  end
142
157
 
143
158
  # @!group Abstract Configuration
144
159
 
145
- # @!method fuse_options
160
+ # @!method fuse_options(args)
146
161
  # @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
162
+ # Called to allow filesystem to handle custom options and observe standard mount options #
163
+ # @param [FuseArgs] args
164
+ # @return [Boolean] true if args parsed successfully
153
165
  # @see FuseArgs#parse!
166
+ # @example
167
+ # OPTIONS = { 'config=' => :config, '-c ' => :config }
168
+ # def fuse_options(args)
169
+ # args.parse!(OPTIONS) do |key:, value:, out:, **opts|
170
+ #
171
+ # # raise errors for invalid config
172
+ # raise FFI::Libfuse::FuseArgs::Error, "Invalid config" unless valid_config?(key,value)
173
+ #
174
+ # # Configure the file system
175
+ # @config = value if key == :config
176
+ #
177
+ # #Optionally manipulate other arguments for fuse_mount() based on the current argument and state
178
+ # out.add('-oopt=val')
179
+ #
180
+ # # Custom options must be marked :handled otherwise fuse_mount() will fail with unknown arguments
181
+ # :handled
182
+ # end
183
+ # end
154
184
 
155
185
  # @!method fuse_traps
156
186
  # @abstract
157
- # @return [Hash] map of signal name or number to signal handler as per Signal.trap
187
+ # @return [Hash<String|Symbol|Integer,String|Proc>]
188
+ # map of signal name or number to signal handler as per Signal.trap
189
+ # @example
190
+ # def fuse_traps
191
+ # { HUP: ->() { reload() }}
192
+ # end
158
193
 
159
194
  # @!method fuse_version
160
195
  # @abstract
@@ -162,14 +197,22 @@ module FFI
162
197
 
163
198
  # @!method fuse_help
164
199
  # @abstract
165
- # @return [String] help text to explain custom options to show with -h option
200
+ # Called as part of generating output for the -h option
201
+ # @return [String] help text to explain custom options
166
202
 
167
203
  # @!method fuse_debug(enabled)
168
204
  # @abstract
169
- # Indicate to the filesystem whether debugging option is in use.
205
+ # Called to indicate to the filesystem whether debugging option is in use.
170
206
  # @param [Boolean] enabled if -d option is in use
171
207
  # @return [void]
172
208
 
209
+ # @!method fuse_configure
210
+ # @abstract
211
+ # Called immediately before the filesystem is mounted, after options have been parsed
212
+ #
213
+ # @raise [Error] to prevent the mount from proceeding
214
+ # @return [void]
215
+
173
216
  # @!endgroup
174
217
 
175
218
  # @!visibility private
@@ -136,7 +136,7 @@ module FFI
136
136
  def idle_limit_exceeded?
137
137
  return false unless @max_idle
138
138
 
139
- synchronize { (@size - @busy - @idle_death.size - 1) > @max_idle && @idle_death << Thread.current }
139
+ synchronize { (@size - @busy - @idle_death.size - 1) > @max_idle && (@idle_death << Thread.current) }
140
140
  end
141
141
 
142
142
  def synchronize(&block)
@@ -3,6 +3,6 @@
3
3
  module FFI
4
4
  # Ruby FFI Binding for [libfuse](https://github.com/libfuse/libfuse)
5
5
  module Libfuse
6
- VERSION = '0.0.1'
6
+ VERSION = '0.1.0'
7
7
  end
8
8
  end
data/lib/ffi/libfuse.rb CHANGED
@@ -5,20 +5,29 @@ require_relative 'libfuse/fuse2' if FFI::Libfuse::FUSE_MAJOR_VERSION == 2
5
5
  require_relative 'libfuse/fuse3' if FFI::Libfuse::FUSE_MAJOR_VERSION == 3
6
6
  require_relative 'libfuse/main'
7
7
  require_relative 'libfuse/adapter'
8
+ require_relative 'libfuse/filesystem'
8
9
  require_relative 'devt'
9
10
 
10
11
  module FFI
11
12
  # Ruby FFI Binding for [libfuse](https://github.com/libfuse/libfuse)
12
13
  module Libfuse
14
+ # Filesystems can raise this error to indicate errors from filesystem users
15
+ class Error < StandardError; end
16
+
17
+ # Opinionated default args for {.main}.
18
+ #
19
+ # Filesystems that want full control (eg to take advantage of multi-threaded operations) should call
20
+ # {Main.fuse_main} instead
21
+ # @note These may change between major versions
22
+ DEFAULT_ARGS = [$0, '-s', '-odefault_permissions', *ARGV].freeze
23
+
13
24
  class << self
14
25
  # Filesystem entry point
15
- # @note This main function defaults to single-threaded operation by injecting the '-s' option. Pass `$0,*ARGV`
16
- # if your filesystem can usefully support multi-threaded operation.
17
- #
18
26
  # @see Main.fuse_main
19
- def fuse_main(*argv, operations:, args: argv.any? ? argv : [$0, '-s', *ARGV], private_data: nil)
27
+ def fuse_main(*argv, operations:, args: argv.any? ? argv : DEFAULT_ARGS, private_data: nil)
20
28
  Main.fuse_main(args: args, operations: operations, private_data: private_data) || -1
21
29
  end
30
+ alias main fuse_main
22
31
  end
23
32
  end
24
33
  end
@@ -74,7 +74,7 @@ module FFI
74
74
  end
75
75
 
76
76
  def finalizer(*keys)
77
- proc { keys.each { cache.delete(key) } }
77
+ proc { keys.each { |k| cache.delete(k) } }
78
78
  end
79
79
 
80
80
  def store(obj)
@@ -25,5 +25,14 @@ module FFI
25
25
 
26
26
  # Socket
27
27
  S_IFSOCK = 0o140000
28
+
29
+ # SetUID
30
+ S_ISUID = 0o004000
31
+
32
+ # SetGID
33
+ S_ISGID = 0o002000
34
+
35
+ # Sticky Bit
36
+ S_ISVTX = 0o001000
28
37
  end
29
38
  end
@@ -5,7 +5,6 @@ require_relative 'time_spec'
5
5
 
6
6
  module FFI
7
7
  class Stat
8
- # Native (and naked) stat from stat.h
9
8
  # @!visibility private
10
9
  class Native < Struct
11
10
  case Platform::NAME
@@ -17,31 +16,62 @@ module FFI
17
16
  :st_mode, :mode_t,
18
17
  :st_uid, :uid_t,
19
18
  :st_gid, :gid_t,
20
- :__pad0, :int,
19
+ :__pad0, :uint,
21
20
  :st_rdev, :dev_t,
22
21
  :st_size, :off_t,
23
22
  :st_blksize, :blksize_t,
24
23
  :st_blocks, :blkcnt_t,
25
24
  :st_atimespec, TimeSpec,
26
25
  :st_mtimespec, TimeSpec,
27
- :st_ctimespec, TimeSpec
26
+ :st_ctimespec, TimeSpec,
27
+ :unused, [:long, 3]
28
+
29
+ [['', :string], ['l', :string], ['f', :int]].each do |(prefix, ftype)|
30
+ native_func = "native_#{prefix}stat".to_sym
31
+ lib_func = "#{prefix}stat".to_sym
32
+ begin
33
+ ::FFI::Stat.attach_function native_func, lib_func, [ftype, by_ref], :int
34
+ rescue FFI::NotFoundError
35
+ # gLibc 2.31 (Ubuntu focal) does not export these functions, it maps them to __xstat variants
36
+ native_xfunc = "native_#{prefix}xstat".to_sym
37
+ lib_xfunc = "__#{prefix}xstat".to_sym
38
+ ::FFI::Stat.attach_function native_xfunc, lib_xfunc, [:int, ftype, by_ref], :int
39
+ # 1 is 64 bit versions of struct stat, 3 is 32 bit
40
+ ::FFI::Stat.define_singleton_method(native_func) { |file, buf| send(native_xfunc, 1, file, buf) }
41
+ end
42
+ end
28
43
 
29
- when 'x65_64-darwin'
44
+ when 'x86_64-darwin', 'aarch64-darwin'
45
+ # man stat - this is stat with 64 bit inodes.
30
46
  layout :st_dev, :dev_t,
31
- :st_ino, :uint32,
32
47
  :st_mode, :mode_t,
33
48
  :st_nlink, :nlink_t,
49
+ :st_ino, :ino_t,
34
50
  :st_uid, :uid_t,
35
51
  :st_gid, :gid_t,
36
52
  :st_rdev, :dev_t,
37
53
  :st_atimespec, TimeSpec,
38
54
  :st_mtimespec, TimeSpec,
39
55
  :st_ctimespec, TimeSpec,
56
+ :st_birthtimespec, TimeSpec,
40
57
  :st_size, :off_t,
41
58
  :st_blocks, :blkcnt_t,
42
59
  :st_blksize, :blksize_t,
43
60
  :st_flags, :uint32,
44
- :st_gen, :uint32
61
+ :st_gen, :uint32,
62
+ :st_lspare, :int32,
63
+ :st_gspare, :int64
64
+
65
+ begin
66
+ # TODO: these functions are deprecated, but at least on Cataline -> Monterey the old stat functions
67
+ # use the stat struct *without* 64 bit inodes, but macfuse is compiled with 64 bit inodes
68
+ ::FFI::Stat.attach_function :native_stat, :stat64, [:string, by_ref], :int
69
+ ::FFI::Stat.attach_function :native_lstat, :lstat64, [:string, by_ref], :int
70
+ ::FFI::Stat.attach_function :native_fstat, :fstat64, [:int, by_ref], :int
71
+ rescue FFI::NotFoundError
72
+ # these are only used in testing
73
+ end
74
+
45
75
  else
46
76
  raise NotImplementedError, "FFI::Stat not implemented for FFI::Platform #{Platform::NAME}"
47
77
  end
@@ -14,14 +14,27 @@ module FFI
14
14
  # Special nsec value representing a request to omit setting this time - see utimensat(2)
15
15
  UTIME_OMIT = (1 << 30) - 2
16
16
 
17
- # A fixed TimeSpec representing the current time
18
- def self.now
19
- @now ||= new.set_time(0, UTIME_NOW)
20
- end
17
+ class << self
18
+ # A fixed TimeSpec representing the current time
19
+ def now
20
+ @now ||= new.set_time(0, UTIME_NOW)
21
+ end
22
+
23
+ # A fixed TimeSpec representing a request to omit setting this time
24
+ def omit
25
+ @omit ||= new.set_time(0, UTIME_OMIT)
26
+ end
21
27
 
22
- # A fixed TimeSpec representing a request to omit setting this time
23
- def self.omit
24
- @omit ||= new.set_time(0, UTIME_OMIT)
28
+ # @param [Array<TimeSpec>] times
29
+ # @param [Integer] size
30
+ # @return [Array<TimeSpec>] list of times filled out to size with TimeSpec.now if times was empty,
31
+ # otherwise with TimeSpec.omit
32
+ def fill_times(times, size = times.size)
33
+ return times unless times.size < size
34
+ return Array.new(size, now) if times.empty?
35
+
36
+ times.dup.fill(omit, times.size..size - times.size) if times.size < size
37
+ end
25
38
  end
26
39
 
27
40
  layout(
@@ -45,15 +58,14 @@ module FFI
45
58
 
46
59
  # @overload set_time(time)
47
60
  # @param [Time] time
48
- # @return [TimeSpec] self
61
+ # @return [self]
49
62
  # @overload set_time(sec,nsec=0)
50
63
  # @param [Integer] sec number of (nano/micro)seconds from epoch, precision depending on nsec
51
64
  # @param [Symbol|Integer] nsec
52
65
  # - :nsec to treat sec as number of nanoseconds since epoch
53
66
  # - :usec to treat sec as number of microseconds since epoch
54
67
  # - Integer to treat sec as number of seconds since epoch, and nsec as additional nanoseconds
55
- #
56
- # @return [TimeSpec] self
68
+ # @return [self]
57
69
  def set_time(sec, nsec = 0)
58
70
  return set_time(sec.to_i, sec.nsec) if sec.is_a?(Time)
59
71
 
@@ -95,6 +107,10 @@ module FFI
95
107
  Time.at(sec, nsec, :nsec, in: 0).utc
96
108
  end
97
109
 
110
+ def to_s(now = nil)
111
+ time(now).to_s
112
+ end
113
+
98
114
  # Convert to Integer
99
115
  # @param [Time|nil] now
100
116
  # optional value to use if {now?} is true. If not set then Time.now will be used
@@ -104,7 +120,7 @@ module FFI
104
120
  return nil if omit?
105
121
 
106
122
  t = now? ? (now || Time.now) : self
107
- t.tv_sec * 10**9 + t.tv_nsec
123
+ (t.tv_sec * (10**9)) + t.tv_nsec
108
124
  end
109
125
 
110
126
  # Convert to Float
@@ -116,7 +132,7 @@ module FFI
116
132
  return nil if omit?
117
133
 
118
134
  t = now? ? (now || Time.now) : self
119
- t.tv_sec.to_f + t.tv_nsec.to_f / (10**9)
135
+ t.tv_sec.to_f + (t.tv_nsec.to_f / (10**9))
120
136
  end
121
137
 
122
138
  # @!visibility private