ffi-libfuse 0.0.1.pre

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 (53) hide show
  1. checksums.yaml +7 -0
  2. data/.yardopts +1 -0
  3. data/README.md +100 -0
  4. data/lib/ffi/accessors.rb +145 -0
  5. data/lib/ffi/devt.rb +30 -0
  6. data/lib/ffi/flock.rb +47 -0
  7. data/lib/ffi/gnu_extensions.rb +115 -0
  8. data/lib/ffi/libfuse/ackbar.rb +112 -0
  9. data/lib/ffi/libfuse/adapter/context.rb +37 -0
  10. data/lib/ffi/libfuse/adapter/debug.rb +89 -0
  11. data/lib/ffi/libfuse/adapter/fuse2_compat.rb +91 -0
  12. data/lib/ffi/libfuse/adapter/fuse3_support.rb +87 -0
  13. data/lib/ffi/libfuse/adapter/interrupt.rb +37 -0
  14. data/lib/ffi/libfuse/adapter/pathname.rb +23 -0
  15. data/lib/ffi/libfuse/adapter/ruby.rb +334 -0
  16. data/lib/ffi/libfuse/adapter/safe.rb +58 -0
  17. data/lib/ffi/libfuse/adapter/thread_local_context.rb +36 -0
  18. data/lib/ffi/libfuse/adapter.rb +79 -0
  19. data/lib/ffi/libfuse/callbacks.rb +61 -0
  20. data/lib/ffi/libfuse/fuse2.rb +159 -0
  21. data/lib/ffi/libfuse/fuse3.rb +162 -0
  22. data/lib/ffi/libfuse/fuse_args.rb +166 -0
  23. data/lib/ffi/libfuse/fuse_buffer.rb +155 -0
  24. data/lib/ffi/libfuse/fuse_callbacks.rb +48 -0
  25. data/lib/ffi/libfuse/fuse_cmdline_opts.rb +44 -0
  26. data/lib/ffi/libfuse/fuse_common.rb +249 -0
  27. data/lib/ffi/libfuse/fuse_config.rb +205 -0
  28. data/lib/ffi/libfuse/fuse_conn_info.rb +211 -0
  29. data/lib/ffi/libfuse/fuse_context.rb +79 -0
  30. data/lib/ffi/libfuse/fuse_file_info.rb +100 -0
  31. data/lib/ffi/libfuse/fuse_loop_config.rb +43 -0
  32. data/lib/ffi/libfuse/fuse_operations.rb +870 -0
  33. data/lib/ffi/libfuse/fuse_opt.rb +54 -0
  34. data/lib/ffi/libfuse/fuse_poll_handle.rb +59 -0
  35. data/lib/ffi/libfuse/fuse_version.rb +43 -0
  36. data/lib/ffi/libfuse/job_pool.rb +53 -0
  37. data/lib/ffi/libfuse/main.rb +200 -0
  38. data/lib/ffi/libfuse/test/operations.rb +56 -0
  39. data/lib/ffi/libfuse/test.rb +3 -0
  40. data/lib/ffi/libfuse/thread_pool.rb +147 -0
  41. data/lib/ffi/libfuse/version.rb +8 -0
  42. data/lib/ffi/libfuse.rb +24 -0
  43. data/lib/ffi/ruby_object.rb +95 -0
  44. data/lib/ffi/stat/constants.rb +29 -0
  45. data/lib/ffi/stat/native.rb +50 -0
  46. data/lib/ffi/stat/time_spec.rb +137 -0
  47. data/lib/ffi/stat.rb +96 -0
  48. data/lib/ffi/stat_vfs.rb +81 -0
  49. data/lib/ffi/struct_array.rb +39 -0
  50. data/lib/ffi/struct_wrapper.rb +100 -0
  51. data/sample/memory_fs.rb +189 -0
  52. data/sample/no_fs.rb +69 -0
  53. metadata +165 -0
data/lib/ffi/stat.rb ADDED
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'struct_wrapper'
4
+ require_relative 'stat/native'
5
+ require_relative 'stat/constants'
6
+
7
+ module FFI
8
+ # Ruby representation of stat.h struct
9
+ 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
24
+ include StructWrapper
25
+
26
+ native_struct(Native)
27
+
28
+ # @!attribute [rw] mode
29
+ # @return [Integer] file mode (type | perms)
30
+
31
+ # @!attribute [rw] size
32
+ # @return [Integer] size of file in bytes
33
+
34
+ # @!attribute [rw] nlink
35
+ # @return [Integer] number of links
36
+
37
+ # @!attribute [rw] uid
38
+ # @return [Integer] owner user id
39
+
40
+ # @!attribute [rw] gid
41
+ # @return [Integer] owner group id
42
+
43
+ int_members = Native
44
+ .members
45
+ .select { |m| m.to_s.start_with?('st_') && !m.to_s.end_with?('timespec') }
46
+ .map { |m| m[3..].to_sym }
47
+
48
+ ffi_attr_accessor(*int_members, format: 'st_%s')
49
+
50
+ # @!attribute [rw] atime
51
+ # @return [Time] time of last access
52
+
53
+ # @!attribute [rw] mtime
54
+ # @return [Time] time of last modification
55
+
56
+ # @!attribute [rw] ctime
57
+ # @return [Time] time of last status change
58
+
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 }
63
+
64
+ ffi_attr_reader(*time_members, format: 'st_%sspec', &:time)
65
+
66
+ 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)
69
+ end
70
+
71
+ # Fill content for a regular file
72
+ # @param [Integer] mode
73
+ # @param [Integer] size
74
+ # @param [Integer] uid
75
+ # @param [Integer] gid
76
+ # @param [Hash] args additional system specific stat fields
77
+ # @return [self]
78
+ def file(mode:, size:, uid: Process.uid, gid: Process.gid, **args)
79
+ mode = ((S_IFREG & S_IFMT) | (mode & 0o777))
80
+ fill(mode: mode, size: size, uid: uid, gid: gid, **args)
81
+ end
82
+
83
+ # Fill content for a directory
84
+ # @param [Integer] mode
85
+ # @param [Integer] nlink
86
+ # @param [Integer] uid
87
+ # @param [Integer] gid
88
+ # @param [Hash] args additional system specific stat fields
89
+ # @return [self]
90
+ def dir(mode:, nlink: 1, uid: Process.uid, gid: Process.gid, **args)
91
+ mode = ((S_IFDIR & S_IFMT) | (mode & 0o777))
92
+ fill(mode: mode, uid: uid, gid: gid, nlink: nlink, **args)
93
+ end
94
+ alias directory dir
95
+ end
96
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ffi'
4
+ require_relative('accessors')
5
+
6
+ module FFI
7
+ # Represents Statvfs for use with {Libfuse::FuseOperations#statfs} callback
8
+ class StatVfs < Struct
9
+ include Accessors
10
+
11
+ case Platform::NAME
12
+ when /darwin/
13
+ layout(
14
+ f_bsize: :ulong,
15
+ f_frsize: :ulong,
16
+ f_blocks: :uint,
17
+ f_bfree: :uint,
18
+ f_bavail: :uint,
19
+ f_files: :uint,
20
+ f_ffree: :uint,
21
+ f_favail: :uint,
22
+ f_fsid: :ulong,
23
+ f_flag: :ulong,
24
+ f_namemax: :ulong
25
+ )
26
+ when /linux/
27
+ layout(
28
+ :f_bsize, :ulong,
29
+ :f_frsize, :ulong,
30
+ :f_blocks, :uint64,
31
+ :f_bfree, :uint64,
32
+ :f_bavail, :uint64,
33
+ :f_files, :uint64,
34
+ :f_ffree, :uint64,
35
+ :f_favail, :uint64,
36
+ :f_fsid, :ulong,
37
+ :f_flag, :ulong,
38
+ :f_namemax, :ulong,
39
+ :f_spare, [:int, 6]
40
+ )
41
+ else
42
+ raise NotImplementedError, "FFI::StatVfs not implemented for FFI::Platform #{Platform::NAME}"
43
+ end
44
+
45
+ # @!attribute [rw] bsize
46
+ # @return [Integer] Filesystem block size
47
+
48
+ # @!attribute [rw] frsize
49
+ # @return [Integer] Fragment size
50
+
51
+ # @!attribute [rw] blocks
52
+ # @return [Integer] Size of fs in frsize units
53
+
54
+ # @!attribute [rw] bfree
55
+ # @return [Integer] Number of free blocks
56
+
57
+ # @!attribute [rw] bavail
58
+ # @return [Integer] Number of free blocks for unprivileged users
59
+
60
+ # @!attribute [rw] files
61
+ # @return [Integer] Number of inodes
62
+
63
+ # @!attribute [rw] ffree
64
+ # @return [Integer] Number of free inodes
65
+
66
+ # @!attribute [rw] favail
67
+ # @return [Integer] Number of free inodes for unprivileged users
68
+
69
+ # @!attribute [rw] fsid
70
+ # @return [Integer] Filesystem ID
71
+
72
+ # @!attribute [rw] flag
73
+ # @return [Integer] Mount flags
74
+
75
+ # @!attribute [rw] namemax
76
+ # @return [Integer] Maximum filename length
77
+
78
+ int_members = members.select { |m| m =~ /^f_/ }.map { |m| m[2..].to_sym }
79
+ ffi_attr_accessor(*int_members, format: 'f_%s')
80
+ end
81
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ffi'
4
+
5
+ module FFI
6
+ # Module to extend struct classes that are present in callbacks as fixed length arrays
7
+ module StructArray
8
+ # Generate a one way converter for a fixed size array of struct
9
+ # @return [DataConverter]
10
+ def array(size)
11
+ ArrayConverter.new(self, size)
12
+ end
13
+
14
+ # @!visibility private
15
+ # Helper to handle callbacks containing fixed length array of struct
16
+ class ArrayConverter
17
+ include DataConverter
18
+
19
+ def initialize(type, size)
20
+ @type = type
21
+ @size = size
22
+ end
23
+
24
+ def native_type(_type = nil)
25
+ FFI::Type::POINTER
26
+ end
27
+
28
+ def from_native(ptr, _ctx)
29
+ return [] if ptr.null?
30
+
31
+ Array.new(@size) { |i| @type.new(ptr + (i * @type.size)) }
32
+ end
33
+
34
+ def to_native(ary, _ctx)
35
+ raise NotImplementedError, "#{self.class.name} Cannot convert #{ary} to pointer"
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ffi'
4
+ require_relative 'accessors'
5
+
6
+ module FFI
7
+ # Helper to wrap structs with ugly names and attribute clashes with FFI::Struct (eg size)
8
+ module StructWrapper
9
+ # @!visibility private
10
+ class ByReference < StructByReference
11
+ def initialize(wrapper_class)
12
+ super(wrapper_class.native_struct)
13
+ @wrapper_class = wrapper_class
14
+ end
15
+
16
+ def to_native(value, ctx)
17
+ return Pointer::NULL if value.nil?
18
+
19
+ value = value.native if value.is_a?(StructWrapper)
20
+ super(value, ctx)
21
+ end
22
+
23
+ def from_native(value, ctx)
24
+ return nil if value.null?
25
+
26
+ native = super(value, ctx)
27
+ @wrapper_class.new(native)
28
+ end
29
+ end
30
+
31
+ # Additional class methods for StructWrapper
32
+ module ClassMethods
33
+ # @!visibility private
34
+ def from_native(value, _)
35
+ new(value)
36
+ end
37
+
38
+ # @!visibility private
39
+ def to_native(value, _)
40
+ case value
41
+ when native_type
42
+ value
43
+ when self
44
+ value.native
45
+ else
46
+ new.fill(value).native
47
+ end
48
+ end
49
+
50
+ # @!visibility private
51
+ def native_struct(struct_class = nil)
52
+ if struct_class
53
+ native_type(struct_class.by_value)
54
+ @struct_class = struct_class
55
+ end
56
+ @struct_class
57
+ end
58
+
59
+ # @return [Type] represents a pointer to the wrapped struct
60
+ def by_ref
61
+ @by_ref ||= ByReference.new(self)
62
+ end
63
+
64
+ # @return [Type] represents passing wrapped struct by value
65
+ def by_value
66
+ self
67
+ end
68
+ end
69
+
70
+ class << self
71
+ # @!visibility private
72
+ def included(mod)
73
+ mod.extend(DataConverter)
74
+ mod.extend(ClassMethods)
75
+ mod.include(Accessors)
76
+ end
77
+ end
78
+
79
+ # @!parse extend ClassMethods
80
+ # @!parse include Accessors
81
+
82
+ # @!visibility private
83
+ attr_reader :native
84
+
85
+ # @!visibility private
86
+ def initialize(native = self.class.native_struct.new)
87
+ @native = native
88
+ end
89
+
90
+ # Get attribute
91
+ def [](member_or_attr)
92
+ @native[self.class.ffi_attr_readers.fetch(member_or_attr, member_or_attr)]
93
+ end
94
+
95
+ # Set attribute
96
+ def []=(member_or_attr, val)
97
+ @native[self.class.ffi_attr_writers.fetch(member_or_attr, member_or_attr)] = val
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,189 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'ffi/libfuse'
5
+
6
+ # A simple in-memory filesystem defined with hashes.
7
+ #
8
+ # It is writable to the user that mounted it may create and edit files within it
9
+ #
10
+ # === Usage
11
+ # root = Memory.new(files: { 'hello' => { 'world.txt' => 'Hello World'}})
12
+ # root.mkdir("/hello")
13
+ # root.("/hello/world","Hello World!\n")
14
+ # root.write("/hello/everybody","Hello Everyone!\n")
15
+ #
16
+ # Libfuse::fuse_main($0,ARGV,operations: root)
17
+ #
18
+ #
19
+ class MemoryFS
20
+ # @return [Hash<String,Object>] list of file objects by path
21
+ attr_reader :root
22
+
23
+ include FFI::Libfuse::Adapter::Fuse3Support
24
+ include FFI::Libfuse::Adapter::Ruby
25
+ include FFI::Libfuse::Adapter::Pathname
26
+
27
+ File = Struct.new(:mode, :content, :ctime, :atime, :mtime) do
28
+ def dig(*_args)
29
+ raise Errno::ENOTDIR
30
+ end
31
+
32
+ def fill_stat(stat = FFI::Stat.new)
33
+ stat.file(mode: mode, ctime: ctime, atime: atime, mtime: mtime, size: content.size)
34
+ end
35
+ end
36
+
37
+ # rubocop:disable Lint/StructNewOverride
38
+ Dir = Struct.new(:mode, :entries, :ctime, :atime, :mtime) do
39
+ def dig(*args)
40
+ entries.dig(*args)
41
+ end
42
+
43
+ def fill_stat(stat = FFI::Stat.new)
44
+ stat.directory(mode: mode, ctime: ctime, atime: atime, mtime: mtime)
45
+ end
46
+ end
47
+ # rubocop:enable Lint/StructNewOverride
48
+
49
+ def initialize(files: {}, max_size: 100_000, max_files: 1_000)
50
+ now = Time.now
51
+ @root = Dir.new(0x755, {}, now, now, now)
52
+ @total_size = 0
53
+ @total_files = 1
54
+ @max_size = max_size
55
+ @max_files = max_files
56
+
57
+ build(files)
58
+ end
59
+
60
+ def build(files, path = ::Pathname.new('/'))
61
+ files.each_pair do |basename, content|
62
+ raise 'Initial file keys must be String' unless basename.is_a?(String)
63
+ raise 'Initial file keys must not contain path separators' if basename =~ %r{[/\\]}
64
+
65
+ entry_path = path + basename
66
+ case content
67
+ when String
68
+ create(entry_path, 0x644)
69
+ write(entry_path, content, 0)
70
+ when Hash
71
+ mkdir(entry_path, 0x755)
72
+ build(content, entry_path)
73
+ else
74
+ raise 'Initial files must be String or Hash'
75
+ end
76
+ end
77
+ end
78
+
79
+ def fuse_version
80
+ 'MemoryFS: Version x.y.z'
81
+ end
82
+
83
+ def fuse_traps
84
+ {
85
+ HUP: -> { reload }
86
+ }
87
+ end
88
+
89
+ def statfs(_path, statfs_buf)
90
+ blocks = @total_size / 1_000
91
+ statfs_buf.bsize = 1 # block size (in Kb)
92
+ statfs_buf.frsize = 1 # fragment size pretty much always bsize
93
+ statfs_buf.blocks = @max_size
94
+ statfs_buf.bfree = @max_size - blocks
95
+ statfs_buf.bavail = @max_size - blocks
96
+ statfs_buf.files = @max_files
97
+ statfs_buf.ffree = @max_files - @total_files
98
+ statfs_buf.favail = @max_files - @total_files
99
+ 0
100
+ end
101
+
102
+ def getattr(path, stat_buf)
103
+ entry = find(path)
104
+ return -Errno::ENOENT::Errno unless entry
105
+
106
+ entry.fill_stat(stat_buf)
107
+ 0
108
+ end
109
+
110
+ def readdir(path, _offset, _ffi)
111
+ %w[. ..].each { |d| yield(d, nil) }
112
+ dir = find(path)
113
+ dir.entries.each_pair { |k, e| yield(k, e.fill_stat) }
114
+ end
115
+
116
+ def create(path, mode, _ffi)
117
+ dir_entries = find(path.dirname).entries
118
+ now = Time.now
119
+ dir_entries[path.basename.to_s] = File.new(mode, String.new, now, now, now)
120
+ @total_files += 1
121
+ 0
122
+ end
123
+
124
+ # op[:read] = [:pointer, :size_t, :off_t, FuseFileInfo.by_ref]
125
+ def read(path, len, off, _ffi)
126
+ file = find(path)
127
+ file.atime = Time.now.utc
128
+ FFI::Libfuse::ThreadPool.busy
129
+ sleep 0.5
130
+ file.content[off, len]
131
+ end
132
+
133
+ # write(const char* path, char *buf, size_t size, off_t offset, struct fuse_file_info* fi)
134
+ def write(path, data, offset, _ffi)
135
+ file = find(path)
136
+ content = file.content
137
+ @total_size -= content.size
138
+ content[offset, data.length] = data
139
+ @total_size += content.size
140
+ file.mtime = Time.now.utc
141
+ end
142
+
143
+ def truncate(path, size)
144
+ file = find(path)
145
+ @total_size -= file.content.size
146
+ file.content[size..-1] = ''
147
+ file.mtime = Time.now.utc
148
+ @total_size += file.content.size
149
+ 0
150
+ end
151
+
152
+ def unlink(path)
153
+ dir = find(path.dirname)
154
+ deleted = dir.entries.delete(path.basename.to_s)
155
+ @total_files -= 1
156
+ @total_size -= deleted.content.size if deleted.is_a?(File)
157
+ 0
158
+ end
159
+
160
+ def mkdir(path, mode)
161
+ entries = find(path.dirname).entries
162
+ now = Time.now
163
+ entries[path.basename.to_s] = Dir.new(mode, {}, now, now, now)
164
+ end
165
+
166
+ def rmdir(path)
167
+ dir = find(path)
168
+ raise Errno::ENOTDIR unless dir.is_a?(Dir)
169
+ raise Errno::ENOTEMPTY unless dir.entries.empty?
170
+
171
+ find(path.dirname).entries.delete(path.basename.to_s)
172
+ 0
173
+ end
174
+
175
+ def utimens(path, atime, mtime)
176
+ entry = find(path)
177
+ entry.atime = atime if atime
178
+ entry.mtime = mtime if mtime
179
+ 0
180
+ end
181
+
182
+ private
183
+
184
+ def find(path)
185
+ path.root? ? root : root.dig(*path.to_s.split('/')[1..])
186
+ end
187
+ end
188
+
189
+ exit(FFI::Libfuse.fuse_main($0, *ARGV, operations: MemoryFS.new)) if __FILE__ == $0