ffi-libfuse 0.0.1.pre → 0.1.0.rc20220550

Sign up to get free protection for your applications and to get access to all the features.
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