ffi-libfuse 0.0.1.rctest12 → 0.3.3

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