ffi-libfuse 0.0.1.pre

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