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
@@ -0,0 +1,145 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../libfuse'
4
+ require 'open3'
5
+ require 'sys-filesystem'
6
+
7
+ # utilities for running tests with fuse filesystems
8
+ module FFI
9
+ module Libfuse
10
+ # Can be included test classes to assist with running/debugging filesystems
11
+ module TestHelper
12
+ # rubocop:disable Metrics/AbcSize
13
+ # rubocop:disable Metrics/MethodLength
14
+
15
+ # Runs the fuse loop on a pre configured fuse filesystem
16
+ # @param [FuseOperations] operations
17
+ # @param [Array<String>] args to pass to {FFI::Libfuse::Main.fuse_create}
18
+ # @param [Hash] options to pass to {FFI::Libfuse::FuseCommon.run}
19
+ # @yield [mnt]
20
+ # caller can execute and test file operations using mnt and ruby File/Dir etc
21
+ # 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
23
+ # @raise [Error] if unexpected state is found during operations
24
+ # @return [void]
25
+ def with_fuse(operations, *args, **options)
26
+ raise ArgumentError, 'Needs block' unless block_given?
27
+
28
+ # ignore MacOS special files
29
+ args << '-onoappledouble,noapplexattr' if mac_fuse?
30
+ safe_fuse do |mnt|
31
+ # Start the fork before loading fuse (for MacOS)
32
+ fpid = Process.fork do
33
+ sleep 2.5 # Give fuse a chance to start
34
+ yield mnt
35
+ end
36
+
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?
45
+
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"
62
+ end
63
+
64
+ true
65
+ end
66
+ end
67
+
68
+ # Runs a filesystem in a separate process
69
+ # @param [String] filesystem path to filesystem executable
70
+ # @param [Array<String>] args to pass the filesystem
71
+ # @param [Hash<String,String>] env environment to run the filesystem under
72
+ # @yield [mnt]
73
+ # caller can execute and test file operations using mnt and ruby File/Dir etc
74
+ # @yieldparam [String] mnt the temporary direct used as the mount point
75
+ # @raise [Error] if unexpected state is found during operations
76
+ # @return [Array] stdout, stderr, exit code as captured by Open3.capture3
77
+ # @note if the filesystem is configured to daemonize then no output will be captured
78
+ def run_filesystem(filesystem, *args, env: {})
79
+ 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
90
+ sleep 1
91
+
92
+ 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
108
+
109
+ unmount(mnt) if mounted?(mnt)
110
+ o, e, s = t.value
111
+ [o, e, s.exitstatus]
112
+ end
113
+ end
114
+ # rubocop:enable Metrics/AbcSize
115
+ # rubocop:enable Metrics/MethodLength
116
+
117
+ def mounted?(mnt, _filesystem = '.*')
118
+ type, prefix = mac_fuse? ? %w[macfuse /private] : %w[fuse]
119
+ mounts = Sys::Filesystem.mounts.select { |m| m.mount_type == type }
120
+ mounts.detect { |m| m.mount_point == "#{prefix}#{mnt}" }
121
+ end
122
+
123
+ def unmount(mnt)
124
+ if mac_fuse?
125
+ system("diskutil unmount force #{mnt} >/dev/null 2>&1")
126
+ else
127
+ system("fusermount -zu #{mnt} >/dev/null 2>&1")
128
+ end
129
+ end
130
+
131
+ def safe_fuse
132
+ Dir.mktmpdir('ffi-libfuse-spec') do |mountpoint|
133
+ yield mountpoint
134
+ ensure
135
+ # Attempt to force unmount.
136
+ unmount(mountpoint) if mounted?(mountpoint)
137
+ end
138
+ end
139
+
140
+ def mac_fuse?
141
+ FFI::Platform::IS_MAC
142
+ end
143
+ end
144
+ end
145
+ 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.0.1'
6
+ VERSION = '0.3.3'
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 = %w[-s -odefault_permissions].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 : Main.default_args(*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
@@ -32,6 +32,7 @@ module FFI
32
32
  return nil if object_id.zero?
33
33
 
34
34
  _ptr, obj = RubyObject.cache[object_id]
35
+ obj = obj.__getobj__ if obj.is_a?(WeakRef)
35
36
  obj
36
37
  end
37
38
  end
@@ -66,6 +67,8 @@ module FFI
66
67
  raise TypeError, "No RubyObject stored at #{ptr.address}" unless cache.key?(ptr.address.object_id)
67
68
 
68
69
  _ptr, obj = cache[ptr.get(:long, 0)]
70
+ # unwrap as the object gets used
71
+ obj = obj.__getobj__ if obj.is_a?(WeakRef)
69
72
  obj
70
73
  end
71
74
 
@@ -74,7 +77,7 @@ module FFI
74
77
  end
75
78
 
76
79
  def finalizer(*keys)
77
- proc { keys.each { cache.delete(key) } }
80
+ proc { keys.each { |k| cache.delete(k) } }
78
81
  end
79
82
 
80
83
  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
data/lib/ffi/stat.rb CHANGED
@@ -1,28 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'struct_wrapper'
4
- require_relative 'stat/native'
5
4
  require_relative 'stat/constants'
6
5
 
7
6
  module FFI
8
7
  # Ruby representation of stat.h struct
9
8
  class Stat
10
- class << self
11
- # @return [Stat] Newly allocated stat representing a regular file - see {Stat#file}
12
- def file(**fields)
13
- new.file(**fields)
14
- end
15
-
16
- # @return [Stat] Newly allocated stat representing a directory - see {Stat#dir}
17
- def dir(**fields)
18
- new.dir(**fields)
19
- end
20
- alias directory dir
21
- end
22
-
23
- # We need to be a StructWrapper because of clash with #size
9
+ # Use a StructWrapper because of clash with #size and the ability to attach functions
24
10
  include StructWrapper
25
11
 
12
+ extend FFI::Library
13
+ ffi_lib FFI::Library::LIBC
14
+
15
+ # stat/native will attach functions to Stat
16
+ require_relative 'stat/native'
26
17
  native_struct(Native)
27
18
 
28
19
  # @!attribute [rw] mode
@@ -56,16 +47,12 @@ module FFI
56
47
  # @!attribute [rw] ctime
57
48
  # @return [Time] time of last status change
58
49
 
59
- time_members = Native
60
- .members
61
- .select { |m| m.to_s.start_with?('st_') && m.to_s.end_with?('timespec') }
62
- .map { |m| m[3..-5].to_sym }
50
+ time_members = Native.members.select { |m| m.to_s =~ /^st_.*timespec$/ }.map { |m| m[3..-5].to_sym }
63
51
 
64
52
  ffi_attr_reader(*time_members, format: 'st_%sspec', &:time)
65
53
 
66
54
  ffi_attr_writer(*time_members, format: 'st_%sspec', simple: false) do |sec, nsec = 0|
67
- t = self[__method__[0..-2].to_sym]
68
- t.set_time(sec, nsec)
55
+ self[__method__[0..-2].to_sym].set_time(sec, nsec)
69
56
  end
70
57
 
71
58
  # Fill content for a regular file
@@ -87,10 +74,112 @@ module FFI
87
74
  # @param [Integer] gid
88
75
  # @param [Hash] args additional system specific stat fields
89
76
  # @return [self]
90
- def dir(mode:, nlink: 1, uid: Process.uid, gid: Process.gid, **args)
77
+ def dir(mode:, nlink: 3, uid: Process.uid, gid: Process.gid, **args)
91
78
  mode = ((S_IFDIR & S_IFMT) | (mode & 0o777))
92
79
  fill(mode: mode, uid: uid, gid: gid, nlink: nlink, **args)
93
80
  end
94
81
  alias directory dir
82
+
83
+ # Fill attributes from file (using native LIBC calls)
84
+ # @param [Integer|:to_s] file descriptor or a file path
85
+ # @param [Boolean] follow links
86
+ # @return [self]
87
+ def from(file, follow: true)
88
+ return fstat(file) if file.is_a?(Integer)
89
+
90
+ return stat(file.to_s) if follow
91
+
92
+ lstat(file.to_s)
93
+ end
94
+
95
+ # @!method stat(path)
96
+ # Fill attributes from file, following links
97
+ # @param [:to_s] path a file path
98
+ # @raise [SystemCallError] on error
99
+ # @return [self]
100
+
101
+ # @!method lstat(path)
102
+ # Fill attributes from file path, without following links
103
+ # @param [:to_s] path
104
+ # @raise [SystemCallError] on error
105
+ # @return [self]
106
+
107
+ # @!method fstat(fileno)
108
+ # Fill attributes from file descriptor
109
+ # @param [:to_i] fileno file descriptor
110
+ # @raise [SystemCallError] on error
111
+ # @return [self]
112
+
113
+ %i[stat lstat fstat].each do |m|
114
+ define_method(m) do |file|
115
+ res = self.class.send("native_#{m}", (m == :fstat ? file.to_i : file.to_s), native)
116
+ raise SystemCallError.new('', FFI::LastError.error) unless res.zero?
117
+
118
+ self
119
+ end
120
+ end
121
+
122
+ # Apply permissions mask to mode
123
+ # @param [Integer] mask (see umask)
124
+ # @param [Hash] overrides see {fill}
125
+ # @return self
126
+ def mask(mask = 0o4000, **overrides)
127
+ fill(mode: mode & (~mask), **overrides)
128
+ end
129
+
130
+ def file?
131
+ mode & S_IFREG != 0
132
+ end
133
+
134
+ def directory?
135
+ mode & S_IFDIR != 0
136
+ end
137
+
138
+ def setuid?
139
+ mode & S_ISUID != 0
140
+ end
141
+
142
+ def setgid?
143
+ mode & S_ISGID != 0
144
+ end
145
+
146
+ def sticky?
147
+ mode & S_ISVTX != 0
148
+ end
149
+
150
+ class << self
151
+ # @!method file(stat,**fields)
152
+ # @return [Stat]
153
+ # @raise [SystemCallError]
154
+ # @see Stat#file
155
+
156
+ # @!method dir(stat,**fields)
157
+ # @return [Stat]
158
+ # @raise [SystemCallError]
159
+ # @see Stat#dir
160
+ %i[file dir].each { |m| define_method(m) { |stat = new, **args| stat.send(m, **args) } }
161
+ alias directory dir
162
+
163
+ # @!method from(file, stat = new(), follow: false)
164
+ # @return [Stat]
165
+ # @raise [SystemCallError]
166
+ # @see Stat#from
167
+
168
+ # @!method stat(file, stat = new())
169
+ # @return [Stat]
170
+ # @raise [SystemCallError]
171
+ # @see Stat#stat
172
+
173
+ # @!method lstat(file, stat = new())
174
+ # @return [Stat]
175
+ # @raise [SystemCallError]
176
+ # @see Stat#lstat
177
+
178
+ # @!method fstat(file, stat = new())
179
+ # @return [Stat]
180
+ # @raise [SystemCallError]
181
+ # @see Stat#fstat
182
+ %i[from stat lstat fstat].each { |m| define_method(m) { |file, stat = new, **args| stat.send(m, file, **args) } }
183
+ end
95
184
  end
96
185
  end
data/lib/ffi/stat_vfs.rb CHANGED
@@ -75,7 +75,65 @@ module FFI
75
75
  # @!attribute [rw] namemax
76
76
  # @return [Integer] Maximum filename length
77
77
 
78
- int_members = members.select { |m| m =~ /^f_/ }.map { |m| m[2..].to_sym }
78
+ int_members = members.grep(/^f_/).map { |m| m[2..].to_sym }
79
79
  ffi_attr_accessor(*int_members, format: 'f_%s')
80
+
81
+ extend FFI::Library
82
+ ffi_lib FFI::Library::LIBC
83
+
84
+ attach_function :native_statvfs, :statvfs, [:string, by_ref], :int
85
+ attach_function :native_fstatvfs, :fstatvfs, [:int, by_ref], :int
86
+
87
+ # Fill from native statvfs for path
88
+ # @param [:to_s] path
89
+ # @return [self]
90
+ def statvfs(path)
91
+ res = self.class.native_statvfs(path.to_s, self)
92
+ raise SystemCallError.new('', FFI::LastError.errno) unless res.zero?
93
+
94
+ self
95
+ end
96
+
97
+ # Fill from native fstatvfs for fileno
98
+ # @param [Integer] fileno
99
+ # @return [self]
100
+ def fstatvfs(fileno)
101
+ res = self.class.native_fstatvfs(fileno, self)
102
+ raise SystemCallError.new('', FFI::LastError.errno) unless res.zero?
103
+
104
+ self
105
+ end
106
+
107
+ # File from native LIBC calls for file
108
+ # @param [Integer|:to_s] file a file descriptor or a file path
109
+ # @return [self]
110
+ def from(file)
111
+ return fstatvfs(file) if file.is_a?(Integer)
112
+
113
+ statvfs(file)
114
+ end
115
+
116
+ class << self
117
+ # @!method from(file)
118
+ # @return [StatVfs]
119
+ # @raise [SystemCallError]
120
+ # @see StatVfs#from
121
+
122
+ # @!method statvfs(file)
123
+ # @return [StatVfs]
124
+ # @raise [SystemCallError]
125
+ # @see StatVfs#statvfs
126
+
127
+ # @!method fstatvfs(file)
128
+ # @return [StatVfs]
129
+ # @raise [SystemCallError]
130
+ # @see StatVfs#fstatvfs
131
+ %i[from statvfs fstatvfs].each { |m| define_method(m) { |file, stat = new, **args| stat.send(m, file, **args) } }
132
+
133
+ # @!visibility private
134
+
135
+ # @!method native_statvfs(path, statvfs_buf)
136
+ # @!method native_fstatvfs(fd, statvfs_buf)
137
+ end
80
138
  end
81
139
  end
@@ -5,6 +5,16 @@ require_relative 'accessors'
5
5
 
6
6
  module FFI
7
7
  # Helper to wrap structs with ugly names and attribute clashes with FFI::Struct (eg size)
8
+ #
9
+ # @example
10
+ # class MyStruct
11
+ # include FFI::StructWrapper
12
+ # native_struct(MyNativeStruct)
13
+ #
14
+ # #!@attribute [rw] field
15
+ # ffi_attr_accessor :field
16
+ # end
17
+ #
8
18
  module StructWrapper
9
19
  # @!visibility private
10
20
  class ByReference < StructByReference
@@ -78,8 +88,9 @@ module FFI
78
88
 
79
89
  # @!parse extend ClassMethods
80
90
  # @!parse include Accessors
91
+ # @!parse extend Accessors::ClassMethods
81
92
 
82
- # @!visibility private
93
+ # @return [FFI::Struct] the underlying native struct
83
94
  attr_reader :native
84
95
 
85
96
  # @!visibility private
@@ -96,5 +107,15 @@ module FFI
96
107
  def []=(member_or_attr, val)
97
108
  @native[self.class.ffi_attr_writers.fetch(member_or_attr, member_or_attr)] = val
98
109
  end
110
+
111
+ # Pass unimplemented methods on to {#native} underlying struct
112
+ def method_missing(method, *args)
113
+ @native.send(method, *args)
114
+ end
115
+
116
+ # @!visibility private
117
+ def respond_to_missing?(method, private = false)
118
+ @native.respond_to?(method, private)
119
+ end
99
120
  end
100
121
  end