ffi-libfuse 0.3.4 → 0.4.0

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 (41) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +22 -0
  3. data/README.md +1 -1
  4. data/lib/ffi/accessors.rb +21 -7
  5. data/lib/ffi/boolean_int.rb +1 -1
  6. data/lib/ffi/devt.rb +3 -3
  7. data/lib/ffi/libfuse/adapter/debug.rb +53 -15
  8. data/lib/ffi/libfuse/adapter/fuse2_compat.rb +38 -21
  9. data/lib/ffi/libfuse/adapter/fuse3_support.rb +0 -1
  10. data/lib/ffi/libfuse/adapter/ruby.rb +210 -159
  11. data/lib/ffi/libfuse/adapter/safe.rb +69 -21
  12. data/lib/ffi/libfuse/callbacks.rb +2 -1
  13. data/lib/ffi/libfuse/filesystem/accounting.rb +1 -1
  14. data/lib/ffi/libfuse/filesystem/mapped_files.rb +33 -7
  15. data/lib/ffi/libfuse/filesystem/pass_through_dir.rb +0 -1
  16. data/lib/ffi/libfuse/filesystem/virtual_dir.rb +293 -126
  17. data/lib/ffi/libfuse/filesystem/virtual_file.rb +85 -79
  18. data/lib/ffi/libfuse/filesystem/virtual_fs.rb +34 -15
  19. data/lib/ffi/libfuse/filesystem/virtual_link.rb +60 -0
  20. data/lib/ffi/libfuse/filesystem/virtual_node.rb +104 -87
  21. data/lib/ffi/libfuse/filesystem.rb +1 -1
  22. data/lib/ffi/libfuse/fuse2.rb +3 -2
  23. data/lib/ffi/libfuse/fuse3.rb +1 -1
  24. data/lib/ffi/libfuse/fuse_args.rb +5 -2
  25. data/lib/ffi/libfuse/fuse_buf.rb +112 -0
  26. data/lib/ffi/libfuse/fuse_buf_vec.rb +228 -0
  27. data/lib/ffi/libfuse/fuse_common.rb +10 -4
  28. data/lib/ffi/libfuse/fuse_config.rb +16 -7
  29. data/lib/ffi/libfuse/fuse_operations.rb +86 -41
  30. data/lib/ffi/libfuse/gem_helper.rb +2 -9
  31. data/lib/ffi/libfuse/io.rb +56 -0
  32. data/lib/ffi/libfuse/main.rb +27 -24
  33. data/lib/ffi/libfuse/test_helper.rb +68 -60
  34. data/lib/ffi/libfuse/version.rb +1 -1
  35. data/lib/ffi/libfuse.rb +1 -1
  36. data/lib/ffi/stat/native.rb +4 -4
  37. data/lib/ffi/stat.rb +19 -3
  38. data/lib/ffi/struct_array.rb +2 -1
  39. data/sample/hello_fs.rb +1 -1
  40. metadata +6 -3
  41. data/lib/ffi/libfuse/fuse_buffer.rb +0 -257
@@ -5,13 +5,6 @@ module FFI
5
5
  module Libfuse
6
6
  # @!visibility private
7
7
  class GemHelper
8
- SEMVER_TAG_REGEX = /^v\d+\.\d+\.\d+/.freeze
9
-
10
- # branches the format is refs/heads/<branch_name>,
11
- # tags it is refs/tags/<tag_name>.
12
- # for pull requests it is refs/pull/<pr_number>/merge,
13
- GIT_REF_TYPES = { 'heads' => :branch, 'tags' => :tag, 'pull' => :pull }.freeze
14
-
15
8
  class << self
16
9
  # set when install'd.
17
10
  attr_accessor :instance
@@ -33,7 +26,7 @@ module FFI
33
26
  return [ref, nil] unless ref&.start_with?('refs/')
34
27
 
35
28
  _refs, ref_type, ref_name = ref.split('/', 3)
36
- [ref_name, GIT_REF_TYPES[ref_type]]
29
+ [ref_name, { 'heads' => :branch, 'tags' => :tag, 'pull' => :pull }[ref_type]]
37
30
  end
38
31
 
39
32
  def gem_version(main_branch:, version:, env: ENV)
@@ -44,7 +37,7 @@ module FFI
44
37
  when :branch
45
38
  ref_name == main_branch ? [version] : [version, ref_name]
46
39
  when :tag
47
- SEMVER_TAG_REGEX.match?(ref_name) ? [ref_name[1..]] : [version, ref_name]
40
+ /^v\d+\.\d+\.\d+/.match?(ref_name) ? [ref_name[1..]] : [version, ref_name]
48
41
  when :pull
49
42
  pr_number, merge, _rest = ref_name.split('/')
50
43
  # GITHUB_BASE_REF The name of the base ref or target branch of the pull request in a workflow run
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FFI
4
+ module Libfuse
5
+ # Helpers for reading/writing to io like objects
6
+ module IO
7
+ class << self
8
+ # Helper to convert a ruby object to size bytes required by {FuseOperations#read}
9
+ # @param [#pread, #seek & #read, #to_s] io the source to read from, either...
10
+ #
11
+ # * an {::IO} like object via :pread(size, offset) or :seek(offset) then :read(size)
12
+ # * or the String from :to_s
13
+ #
14
+ # @param [Integer] size
15
+ # @param [Integer, nil] offset
16
+ # if nil the io is assumed to be already positioned
17
+ # @return [String] the extracted data
18
+ def read(io, size, offset = nil)
19
+ return io.pread(size, offset) if offset && io.respond_to?(:pread)
20
+
21
+ if (offset ? %i[seek read] : %i[read]).all? { |m| io.respond_to?(m) }
22
+ io.seek(offset) if offset
23
+ return io.read(size)
24
+ end
25
+
26
+ io.to_s[offset || 0, size] || ''
27
+ end
28
+
29
+ # Helper to write date to {::IO} or {::String} like objects for use with #{FuseOperations#write}
30
+ # @param [#pwrite, #seek & #write, #[]=] io an object that accepts String data via...
31
+ #
32
+ # * ```ruby io.pwrite(data, offset)```
33
+ # * ```ruby io.seek(offset) ; io.write(data)```
34
+ # * ```ruby io[offset, data.size] = data```
35
+ # @param [String] data
36
+ # @param [nil, Integer] offset
37
+ # if not nil start start writing at this position in io
38
+ # @return [Integer] number of bytes written
39
+ # @raise [Errno::EBADF] if io does not support the requisite methods
40
+ def write(io, data, offset = nil)
41
+ if offset && io.respond_to?(:pwrite)
42
+ io.pwrite(data, offset)
43
+ elsif (offset ? %i[seek write] : %i[write]).all? { |m| io.respond_to?(m) }
44
+ io.seek(offset) if offset
45
+ io.write(data)
46
+ elsif io.respond_to?(:[]=) # eg String
47
+ io[offset || 0, data.size] = data
48
+ data.size
49
+ else
50
+ raise "cannot :pwrite or :write to #{io}", Errno::EBADF
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -24,15 +24,13 @@ module FFI
24
24
 
25
25
  # Main function of FUSE
26
26
  #
27
- # This function:
28
- #
29
27
  # - parses command line options - see {fuse_parse_cmdline}
30
- # exiting immediately if help or version options were processed
31
- # - calls {#fuse_debug}, {#fuse_options}, {#fuse_configure} if implemented by operations
32
- # - installs signal handlers for INT, HUP, TERM to unmount and exit filesystem
33
- # - installs custom signal handlers if operations implements {fuse_traps}
34
- # - creates a fuse handle mounted with registered operations - see {fuse_create}
35
- # - calls either the single-threaded (option -s) or the multi-threaded event loop - see {FuseCommon#run}
28
+ # - calls {#fuse_configure} if implemented by operations
29
+ # - creates a fuse handle see {fuse_create}
30
+ # - returns 0 if help or version options were processed (ie after all messages have been printed by libfuse)
31
+ # - returns 2 if fuse handle is not successfully mounted
32
+ # - calls {#fuse_traps} if implemented by operations
33
+ # - calls run on the fuse handle with options from previous steps- see {FuseCommon#run}
36
34
  #
37
35
  # @param [Array<String>] argv mount.fuse arguments
38
36
  # expects progname, mountpoint, options....
@@ -46,23 +44,23 @@ module FFI
46
44
  # @return [Integer] suitable for process exit code
47
45
  def fuse_main(*argv, operations:, args: argv.any? ? argv : default_args, private_data: nil)
48
46
  run_args = fuse_parse_cmdline(args: args, handler: operations)
49
- return 2 unless run_args
50
47
 
51
48
  fuse_args = run_args.delete(:args)
52
49
  mountpoint = run_args.delete(:mountpoint)
53
50
 
54
- return 3 unless fuse_configure(operations: operations, **run_args)
51
+ show_only = run_args[:show_help] || run_args[:show_version]
52
+
53
+ return 3 if !show_only && !fuse_configure(operations)
55
54
 
56
55
  warn "FuseCreate: mountpoint: #{mountpoint}, args: [#{fuse_args.argv.join(' ')}]" if run_args[:debug]
57
56
  warn "FuseRun: #{run_args}" if run_args[:debug]
58
57
 
59
58
  fuse = fuse_create(mountpoint, args: fuse_args, operations: operations, private_data: private_data)
60
59
 
61
- return 0 if run_args[:show_help] || run_args[:show_version]
60
+ return 0 if show_only
62
61
  return 2 if !fuse || !mountpoint
63
62
 
64
- return unless fuse
65
-
63
+ run_args[:traps] = operations.fuse_traps if operations.respond_to?(:fuse_traps)
66
64
  fuse.run(**run_args)
67
65
  end
68
66
  alias main fuse_main
@@ -72,7 +70,6 @@ module FFI
72
70
  # - parses standard command line options (-d -s -h -V)
73
71
  # will call {fuse_debug}, {fuse_version}, {fuse_help} if implemented by handler
74
72
  # - calls {fuse_options} for custom option processing if implemented by handler
75
- # - records signal handlers if operations implements {fuse_traps}
76
73
  # - parses standard fuse mount options
77
74
  #
78
75
  # @param [Array<String>] argv mount.fuse arguments
@@ -88,24 +85,25 @@ module FFI
88
85
  # * show_version [Boolean]: -v or --version
89
86
  # * debug [Boolean]: -d
90
87
  # * others are options to pass to {FuseCommon#run}
88
+ # @return [nil] if args are not parsed successfully
91
89
  def fuse_parse_cmdline(*argv, args: argv.any? ? argv : default_args, handler: nil)
92
90
  args = fuse_init_args(args)
93
91
 
94
92
  # Parse args and print cmdline help
95
93
  run_args = Fuse.parse_cmdline(args, handler: handler)
96
- return nil unless run_args
97
94
 
98
- return nil if handler.respond_to?(:fuse_options) && !handler.fuse_options(args)
95
+ handler.fuse_options(args) if handler.respond_to?(:fuse_options)
99
96
 
100
- run_args[:traps] = handler.fuse_traps if handler.respond_to?(:fuse_traps)
101
-
102
- return nil unless parse_run_options(args, run_args)
97
+ parse_run_options(args, run_args)
103
98
 
104
99
  run_args[:args] = args
105
100
  run_args
101
+ rescue Error
102
+ nil
106
103
  end
107
104
 
108
- # @return [FuseCommon|nil] the mounted filesystem or nil if not mounted
105
+ # @return [FuseCommon] the mounted filesystem handle
106
+ # @return [nil] if not mounted (eg due to --help or --version, or an error)
109
107
  def fuse_create(mountpoint, *argv, operations:, args: nil, private_data: nil)
110
108
  args = fuse_init_args(args || argv)
111
109
 
@@ -116,8 +114,8 @@ module FFI
116
114
  end
117
115
 
118
116
  # @!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
117
+ def fuse_configure(operations)
118
+ return true unless operations.respond_to?(:fuse_configure)
121
119
 
122
120
  # Provide sensible values for FuseContext in case this is referenced during configure
123
121
  FFI::Libfuse::FuseContext.overrides do
@@ -166,7 +164,8 @@ module FFI
166
164
  # @abstract
167
165
  # Called to allow filesystem to handle custom options and observe standard mount options #
168
166
  # @param [FuseArgs] args
169
- # @return [Boolean] true if args parsed successfully
167
+ # @raise [Error] if there is an error parsing the options
168
+ # @return [void]
170
169
  # @see FuseArgs#parse!
171
170
  # @example
172
171
  # OPTIONS = { 'config=' => :config, '-c ' => :config }
@@ -174,7 +173,7 @@ module FFI
174
173
  # args.parse!(OPTIONS) do |key:, value:, out:, **opts|
175
174
  #
176
175
  # # raise errors for invalid config
177
- # raise FFI::Libfuse::FuseArgs::Error, "Invalid config" unless valid_config?(key,value)
176
+ # raise FFI::Libfuse::Error, "Invalid config" unless valid_config?(key,value)
178
177
  #
179
178
  # # Configure the file system
180
179
  # @config = value if key == :config
@@ -189,6 +188,8 @@ module FFI
189
188
 
190
189
  # @!method fuse_traps
191
190
  # @abstract
191
+ # Passed to {FuseCommon#run} to allow filesystem to handle custom signal traps. These traps
192
+ # are merged over those from {FuseCommon#default_traps}
192
193
  # @return [Hash<String|Symbol|Integer,String|Proc>]
193
194
  # map of signal name or number to signal handler as per Signal.trap
194
195
  # @example
@@ -198,6 +199,7 @@ module FFI
198
199
 
199
200
  # @!method fuse_version
200
201
  # @abstract
202
+ # Called as part of generating output for the -V option
201
203
  # @return [String] a custom version string to output with -V option
202
204
 
203
205
  # @!method fuse_help
@@ -214,6 +216,7 @@ module FFI
214
216
  # @!method fuse_configure
215
217
  # @abstract
216
218
  # Called immediately before the filesystem is mounted, after options have been parsed
219
+ # (eg to validate required options)
217
220
  #
218
221
  # @raise [Error] to prevent the mount from proceeding
219
222
  # @return [void]
@@ -9,9 +9,6 @@ module FFI
9
9
  module Libfuse
10
10
  # Can be included test classes to assist with running/debugging filesystems
11
11
  module TestHelper
12
- # rubocop:disable Metrics/AbcSize
13
- # rubocop:disable Metrics/MethodLength
14
-
15
12
  # Runs the fuse loop on a pre configured fuse filesystem
16
13
  # @param [FuseOperations] operations
17
14
  # @param [Array<String>] args to pass to {FFI::Libfuse::Main.fuse_create}
@@ -19,7 +16,7 @@ module FFI
19
16
  # @yield [mnt]
20
17
  # caller can execute and test file operations using mnt and ruby File/Dir etc
21
18
  # the block is run in a forked process and is successful unless an exception is raised
22
- # @yieldparam [String] mnt the temporary direct used as the mount point
19
+ # @yieldparam [String] mnt the temporary directory used as the mount point
23
20
  # @raise [Error] if unexpected state is found during operations
24
21
  # @return [void]
25
22
  def with_fuse(operations, *args, **options)
@@ -34,34 +31,14 @@ module FFI
34
31
  yield mnt
35
32
  end
36
33
 
37
- fuse = FFI::Libfuse::Main.fuse_create(mnt, *args, operations: operations)
38
- raise FFI::Libfuse::Error, 'No fuse object returned from fuse_create' unless fuse
39
-
40
- # Rake owns INT
41
- fuse.default_traps.delete(:TERM)
42
- fuse.default_traps.delete(:INT)
43
-
44
- raise FFI::Libfuse::Error, 'fuse object is not mounted?' unless fuse.mounted?
34
+ run_fuse(mnt, *args, operations: operations, **options) do
35
+ # TODO: Work out why waitpid2 hangs on mac unless the process has already finished
36
+ sleep 10 if mac_fuse?
45
37
 
46
- t = Thread.new { fuse.run(foreground: true, **options) }
47
-
48
- # TODO: Work out why waitpid2 hangs on mac unless the process has already finished
49
- sleep 10 if mac_fuse?
50
-
51
- _pid, block_status = Process.waitpid2(fpid)
52
- block_exit = block_status.exitstatus
53
- fuse.exit('fuse_helper')&.join
54
- run_result = t.value
55
-
56
- raise FFI::Libfuse::Error, 'fuse is still mounted after fuse.exit' if fuse.mounted?
57
- raise FFI::Libfuse::Error, "forked file operations failed with #{block_exit}" unless block_exit.zero?
58
- raise FFI::Libfuse::Error, "fuse run failed #{run_result}" unless run_result.zero?
59
-
60
- if !mac_fuse? && mounted?(mnt)
61
- raise FFI::Libfuse::Error, "OS reports fuse is still mounted at #{mnt} after fuse.exit"
38
+ _pid, block_status = Process.waitpid2(fpid)
39
+ block_exit = block_status.exitstatus
40
+ raise FFI::Libfuse::Error, "forked file operations failed with #{block_exit}" unless block_exit.zero?
62
41
  end
63
-
64
- true
65
42
  end
66
43
  end
67
44
 
@@ -77,42 +54,28 @@ module FFI
77
54
  # @note if the filesystem is configured to daemonize then no output will be captured
78
55
  def run_filesystem(filesystem, *args, env: {})
79
56
  fsname = File.basename(filesystem)
80
- safe_fuse do |mnt|
81
- t = Thread.new do
82
- if defined?(Bundler)
83
- Bundler.with_unbundled_env do
84
- Open3.capture3(env, 'bundle', 'exec', filesystem.to_s, mnt, "-ofsname=#{fsname}", *args, binmode: true)
85
- end
86
- else
87
- Open3.capture3(env, filesystem.to_s, mnt, "-ofsname=#{fsname}", *args, binmode: true)
88
- end
89
- end
57
+
58
+ t, err = safe_fuse do |mnt|
59
+ t = Thread.new { open3_filesystem(args, env, filesystem, fsname, mnt) }
90
60
  sleep 1
91
61
 
92
62
  begin
93
- if block_given?
94
- raise Error, "#{fsname} not mounted at #{mnt}" unless mounted?(mnt, fsname)
95
-
96
- yield mnt
97
- end
98
- # rubocop:disable Lint/RescueException
99
- # Minitest::Assertion and other test assertion classes are not derived from StandardError
100
- rescue Exception => _err
101
- # rubocop:enable Lint/RescueException
102
- unmount(mnt) if mounted?(mnt)
103
- o, e, _s = t.value
104
- warn "Errors\n#{e}" unless e.empty?
105
- warn "Output\n#{o}" unless o.empty?
106
- raise
107
- end
63
+ raise Error, "#{fsname} not mounted at #{mnt}" if block_given? && !mounted?(mnt, fsname)
108
64
 
109
- unmount(mnt) if mounted?(mnt)
110
- o, e, s = t.value
111
- [o, e, s.exitstatus]
65
+ yield mnt if block_given?
66
+ [t, nil]
67
+ rescue Minitest::Assertion, StandardError => e
68
+ [t, e]
69
+ end
112
70
  end
71
+
72
+ o, e, s = t.value
73
+ return [o, e, s.exitstatus] unless err
74
+
75
+ warn "Errors\n#{e}" unless e.empty?
76
+ warn "Output\n#{o}" unless o.empty? Minitest::Assertion, StandardError => e
77
+ raise err
113
78
  end
114
- # rubocop:enable Metrics/AbcSize
115
- # rubocop:enable Metrics/MethodLength
116
79
 
117
80
  def mounted?(mnt, _filesystem = '.*')
118
81
  type, prefix = mac_fuse? ? %w[macfuse /private] : %w[fuse]
@@ -140,6 +103,51 @@ module FFI
140
103
  def mac_fuse?
141
104
  FFI::Platform::IS_MAC
142
105
  end
106
+
107
+ private
108
+
109
+ def open3_filesystem(args, env, filesystem, fsname, mnt)
110
+ if defined?(Bundler)
111
+ Bundler.with_unbundled_env do
112
+ Open3.capture3(env, 'bundle', 'exec', filesystem.to_s, mnt, "-ofsname=#{fsname}", *args, binmode: true)
113
+ end
114
+ else
115
+ Open3.capture3(env, filesystem.to_s, mnt, "-ofsname=#{fsname}", *args, binmode: true)
116
+ end
117
+ end
118
+
119
+ def run_fuse(mnt, *args, operations:, **options)
120
+ fuse = mount_fuse(mnt, *args, operations: operations)
121
+ begin
122
+ t = Thread.new do
123
+ Thread.current.name = 'Fuse Run'
124
+ # Rake owns INT
125
+ fuse.run(foreground: true, traps: { INT: nil, TERM: nil }, **options)
126
+ end
127
+
128
+ yield
129
+ ensure
130
+ fuse.exit('fuse_helper')&.join
131
+ run_result = t.value
132
+
133
+ raise FFI::Libfuse::Error, 'fuse is still mounted after fuse.exit' if fuse.mounted?
134
+ raise FFI::Libfuse::Error, "fuse run failed #{run_result}" unless run_result.zero?
135
+
136
+ if !mac_fuse? && mounted?(mnt)
137
+ raise FFI::Libfuse::Error, "OS reports fuse is still mounted at #{mnt} after fuse.exit"
138
+ end
139
+ end
140
+ end
141
+
142
+ def mount_fuse(mnt, *args, operations:)
143
+ operations.fuse_debug(args.include?('-d')) if operations.respond_to?(:fuse_debug)
144
+
145
+ fuse = FFI::Libfuse::Main.fuse_create(mnt, *args, operations: operations)
146
+ raise FFI::Libfuse::Error, 'No fuse object returned from fuse_create' unless fuse
147
+ raise FFI::Libfuse::Error, 'fuse object is not mounted?' unless fuse.mounted?
148
+
149
+ fuse
150
+ end
143
151
  end
144
152
  end
145
153
  end
@@ -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.3.4'
6
+ VERSION = '0.4.0'
7
7
  end
8
8
  end
data/lib/ffi/libfuse.rb CHANGED
@@ -11,7 +11,7 @@ require_relative 'devt'
11
11
  module FFI
12
12
  # Ruby FFI Binding for [libfuse](https://github.com/libfuse/libfuse)
13
13
  module Libfuse
14
- # Filesystems can raise this error to indicate errors from filesystem users
14
+ # Filesystems can raise this error to indicate misconfiguration issues etc...
15
15
  class Error < StandardError; end
16
16
 
17
17
  # Opinionated default args for {.main}.
@@ -27,14 +27,14 @@ module FFI
27
27
  :unused, [:long, 3]
28
28
 
29
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
30
+ native_func = :"native_#{prefix}stat"
31
+ lib_func = :"#{prefix}stat"
32
32
  begin
33
33
  ::FFI::Stat.attach_function native_func, lib_func, [ftype, by_ref], :int
34
34
  rescue FFI::NotFoundError
35
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
36
+ native_xfunc = :"native_#{prefix}xstat"
37
+ lib_xfunc = :"__#{prefix}xstat"
38
38
  ::FFI::Stat.attach_function native_xfunc, lib_xfunc, [:int, ftype, by_ref], :int
39
39
  # 1 is 64 bit versions of struct stat, 3 is 32 bit
40
40
  ::FFI::Stat.define_singleton_method(native_func) { |file, buf| send(native_xfunc, 1, file, buf) }
data/lib/ffi/stat.rb CHANGED
@@ -62,9 +62,9 @@ module FFI
62
62
  # @param [Integer] gid
63
63
  # @param [Hash] args additional system specific stat fields
64
64
  # @return [self]
65
- def file(mode:, size:, uid: Process.uid, gid: Process.gid, **args)
65
+ def file(mode:, size:, nlink: 1, uid: Process.uid, gid: Process.gid, **args)
66
66
  mode = ((S_IFREG & S_IFMT) | (mode & 0o777))
67
- fill(mode: mode, size: size, uid: uid, gid: gid, **args)
67
+ fill(mode: mode, size: size, nlink: nlink, uid: uid, gid: gid, **args)
68
68
  end
69
69
 
70
70
  # Fill content for a directory
@@ -80,6 +80,18 @@ module FFI
80
80
  end
81
81
  alias directory dir
82
82
 
83
+ # Fill content for a symbolic link
84
+ # @param [Integer] size length of the target name (including null terminator)
85
+ # @param [Integer] mode
86
+ # @param [Integer] uid
87
+ # @param [Integer] gid
88
+ # @param [Hash] args additional system specific stat fields
89
+ # @return [self]
90
+ def symlink(size:, mode: 0o777, nlink: 1, uid: Process.uid, gid: Process.gid, **args)
91
+ mode = ((S_IFLNK & S_IFMT) | (mode & 0o777))
92
+ fill(mode: mode, nlink: nlink, size: size, uid: uid, gid: gid, **args)
93
+ end
94
+
83
95
  # Fill attributes from file (using native LIBC calls)
84
96
  # @param [Integer|:to_s] file descriptor or a file path
85
97
  # @param [Boolean] follow links
@@ -123,7 +135,7 @@ module FFI
123
135
  # @param [Integer] mask (see umask)
124
136
  # @param [Hash] overrides see {fill}
125
137
  # @return self
126
- def mask(mask = 0o4000, **overrides)
138
+ def mask(mask = S_ISUID, **overrides)
127
139
  fill(mode: mode & (~mask), **overrides)
128
140
  end
129
141
 
@@ -147,6 +159,10 @@ module FFI
147
159
  mode & S_ISVTX != 0
148
160
  end
149
161
 
162
+ def symlink?
163
+ mode & S_IFLNK != 0
164
+ end
165
+
150
166
  class << self
151
167
  # @!method file(stat,**fields)
152
168
  # @return [Stat]
@@ -10,6 +10,7 @@ module FFI
10
10
  def array(size)
11
11
  ArrayConverter.new(self, size)
12
12
  end
13
+ alias [] array
13
14
 
14
15
  # @!visibility private
15
16
  # Helper to handle callbacks containing fixed length array of struct
@@ -28,7 +29,7 @@ module FFI
28
29
  def from_native(ptr, _ctx)
29
30
  return [] if ptr.null?
30
31
 
31
- Array.new(@size) { |i| @type.new(ptr + (i * @type.size)) }
32
+ Array.new(@size) { |i| @type.new(ptr + (i * @type.size)) }.freeze
32
33
  end
33
34
 
34
35
  def to_native(ary, _ctx)
data/sample/hello_fs.rb CHANGED
@@ -51,4 +51,4 @@ class HelloFS
51
51
  end
52
52
 
53
53
  # Start the file system
54
- FFI::Libfuse.fuse_main(operations: HelloFS.new) if __FILE__ == $0
54
+ exit(FFI::Libfuse.fuse_main(operations: HelloFS.new)) if __FILE__ == $0
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ffi-libfuse
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.4
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Grant Gardner
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-01-08 00:00:00.000000000 Z
11
+ date: 2024-01-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ffi
@@ -161,11 +161,13 @@ files:
161
161
  - lib/ffi/libfuse/filesystem/virtual_dir.rb
162
162
  - lib/ffi/libfuse/filesystem/virtual_file.rb
163
163
  - lib/ffi/libfuse/filesystem/virtual_fs.rb
164
+ - lib/ffi/libfuse/filesystem/virtual_link.rb
164
165
  - lib/ffi/libfuse/filesystem/virtual_node.rb
165
166
  - lib/ffi/libfuse/fuse2.rb
166
167
  - lib/ffi/libfuse/fuse3.rb
167
168
  - lib/ffi/libfuse/fuse_args.rb
168
- - lib/ffi/libfuse/fuse_buffer.rb
169
+ - lib/ffi/libfuse/fuse_buf.rb
170
+ - lib/ffi/libfuse/fuse_buf_vec.rb
169
171
  - lib/ffi/libfuse/fuse_callbacks.rb
170
172
  - lib/ffi/libfuse/fuse_cmdline_opts.rb
171
173
  - lib/ffi/libfuse/fuse_common.rb
@@ -180,6 +182,7 @@ files:
180
182
  - lib/ffi/libfuse/fuse_version.rb
181
183
  - lib/ffi/libfuse/gem_helper.rb
182
184
  - lib/ffi/libfuse/gem_version.rb
185
+ - lib/ffi/libfuse/io.rb
183
186
  - lib/ffi/libfuse/job_pool.rb
184
187
  - lib/ffi/libfuse/main.rb
185
188
  - lib/ffi/libfuse/test_helper.rb