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.
- checksums.yaml +4 -4
- data/.yardopts +3 -1
- data/CHANGELOG.md +60 -0
- data/LICENSE +21 -0
- data/README.md +127 -44
- data/lib/ffi/accessors.rb +6 -6
- data/lib/ffi/boolean_int.rb +27 -0
- data/lib/ffi/devt.rb +23 -0
- data/lib/ffi/encoding.rb +38 -0
- data/lib/ffi/flock.rb +7 -5
- data/lib/ffi/gnu_extensions.rb +1 -1
- data/lib/ffi/libfuse/ackbar.rb +3 -3
- data/lib/ffi/libfuse/adapter/context.rb +12 -10
- data/lib/ffi/libfuse/adapter/fuse2_compat.rb +52 -51
- data/lib/ffi/libfuse/adapter/fuse3_support.rb +7 -4
- data/lib/ffi/libfuse/adapter/interrupt.rb +1 -1
- data/lib/ffi/libfuse/adapter/ruby.rb +499 -148
- data/lib/ffi/libfuse/adapter/safe.rb +12 -11
- data/lib/ffi/libfuse/adapter.rb +1 -2
- data/lib/ffi/libfuse/callbacks.rb +1 -1
- data/lib/ffi/libfuse/filesystem/accounting.rb +116 -0
- data/lib/ffi/libfuse/filesystem/mapped_dir.rb +74 -0
- data/lib/ffi/libfuse/filesystem/mapped_files.rb +141 -0
- data/lib/ffi/libfuse/filesystem/pass_through_dir.rb +55 -0
- data/lib/ffi/libfuse/filesystem/pass_through_file.rb +45 -0
- data/lib/ffi/libfuse/filesystem/utils.rb +102 -0
- data/lib/ffi/libfuse/filesystem/virtual_dir.rb +306 -0
- data/lib/ffi/libfuse/filesystem/virtual_file.rb +94 -0
- data/lib/ffi/libfuse/filesystem/virtual_fs.rb +196 -0
- data/lib/ffi/libfuse/filesystem/virtual_node.rb +101 -0
- data/lib/ffi/libfuse/filesystem.rb +25 -0
- data/lib/ffi/libfuse/fuse2.rb +32 -24
- data/lib/ffi/libfuse/fuse3.rb +28 -18
- data/lib/ffi/libfuse/fuse_args.rb +71 -34
- data/lib/ffi/libfuse/fuse_buffer.rb +128 -26
- data/lib/ffi/libfuse/fuse_callbacks.rb +1 -5
- data/lib/ffi/libfuse/fuse_common.rb +60 -61
- data/lib/ffi/libfuse/fuse_config.rb +134 -143
- data/lib/ffi/libfuse/fuse_conn_info.rb +310 -134
- data/lib/ffi/libfuse/fuse_context.rb +45 -3
- data/lib/ffi/libfuse/fuse_operations.rb +57 -21
- data/lib/ffi/libfuse/fuse_opt.rb +1 -1
- data/lib/ffi/libfuse/fuse_version.rb +10 -6
- data/lib/ffi/libfuse/gem_version.rb +54 -0
- data/lib/ffi/libfuse/main.rb +96 -48
- data/lib/ffi/libfuse/test_helper.rb +145 -0
- data/lib/ffi/libfuse/version.rb +1 -1
- data/lib/ffi/libfuse.rb +13 -4
- data/lib/ffi/ruby_object.rb +4 -1
- data/lib/ffi/stat/constants.rb +9 -0
- data/lib/ffi/stat/native.rb +36 -6
- data/lib/ffi/stat/time_spec.rb +26 -10
- data/lib/ffi/stat.rb +111 -22
- data/lib/ffi/stat_vfs.rb +59 -1
- data/lib/ffi/struct_wrapper.rb +22 -1
- data/sample/hello_fs.rb +54 -0
- data/sample/memory_fs.rb +5 -181
- data/sample/no_fs.rb +20 -21
- data/sample/pass_through_fs.rb +30 -0
- metadata +83 -10
- data/lib/ffi/libfuse/adapter/thread_local_context.rb +0 -36
- data/lib/ffi/libfuse/test/operations.rb +0 -56
- 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
|
data/lib/ffi/libfuse/version.rb
CHANGED
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 :
|
|
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
|
data/lib/ffi/ruby_object.rb
CHANGED
|
@@ -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(
|
|
80
|
+
proc { keys.each { |k| cache.delete(k) } }
|
|
78
81
|
end
|
|
79
82
|
|
|
80
83
|
def store(obj)
|
data/lib/ffi/stat/constants.rb
CHANGED
data/lib/ffi/stat/native.rb
CHANGED
|
@@ -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, :
|
|
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 '
|
|
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
|
data/lib/ffi/stat/time_spec.rb
CHANGED
|
@@ -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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
@
|
|
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 [
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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.
|
|
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
|
data/lib/ffi/struct_wrapper.rb
CHANGED
|
@@ -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
|
-
#
|
|
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
|