lib/ffi/libfuse/adapter/ruby.rb in ffi-libfuse-0.0.1.rctest12 vs lib/ffi/libfuse/adapter/ruby.rb in ffi-libfuse-0.1.0.rc20220550
- old
+ new
@@ -1,181 +1,382 @@
# frozen_string_literal: true
require_relative 'safe'
require_relative 'debug'
+require 'set'
module FFI
module Libfuse
module Adapter
- # Wrapper module to give more natural ruby signatures to the fuse callbacks, in particular to avoid dealing with
- # FFI Pointers
+ # This module assists with converting native C libfuse into idiomatic Ruby
#
- # @note includes {Debug} and {Safe}
+ # Class Method helpers
+ # ===
+ # These functions deal with the native fuse FFI::Pointers, Buffers etc
+ #
+ # The class {ReaddirFiller} assists with the complexity of the readdir callback
+ #
+ # FUSE Callbacks
+ # ===
+ #
+ # Including this module in a Filesystem module changes the method signatures for callbacks to be more idiomatic
+ # ruby (via prepending {Prepend}). It also includes the type 1 adapters {Context}, {Debug} and {Safe}
+ #
+ # Filesystems that return {::IO} like objects from #{open} need not implement other file io operations
+ # Similarly Filesystems that return {::Dir} like objects from #{opendir} need not implement #{readdir}
+ #
module Ruby
- # adapter module prepended to the including module
- # @!visibility private
+ # Helper class for {FuseOperations#readdir}
+ # @example
+ # def readdir(path, buf, filler, _offset, _ffi, *flags)
+ # rdf = FFI::Adapter::Ruby::ReaddirFiller.new(buf, filler)
+ # %w[. ..].each { |dot_entry| rdf.fill(dot_entry) }
+ # entries.each { |entry| rdf.fill(entry) }
+ # end
+ # @example
+ # # short version
+ # def readdir(path, buf, filler, _offset, _ffi, *flags)
+ # %w[. ..].concat(entries).each(&FFI::Adapter::Ruby::ReaddirFiller.new(buf,filler))
+ # end
+ class ReaddirFiller
+ # @param [FFI::Pointer] buf from #{FuseOperations#readdir}
+ # @param [FFI::Function] filler from #{FuseOperations#readdir}
+ # @param [Boolean] fuse3 does the filler function expect fuse 3 compatibility
+ def initialize(buf, filler, fuse3: FUSE_MAJOR_VERSION >= 3)
+ @buf = buf
+ @filler = filler
+ @stat_buf = nil
+ @fuse3 = fuse3
+ end
+
+ # Fill readdir from a directory handle
+ # @param [#seek, #read, #tell] dir_handle
+ # @param [Integer] offset
+ # @raise [Errno::ENOTSUP] unless dir_handle quacks like ::Dir
+ def readdir_fh(dir_handle, offset = 0)
+ raise Errno::ENOTSUP unless %i[seek read tell].all? { |m| dir_handle.respond_to?(m) }
+
+ dir_handle.seek(offset)
+ loop while (name = dir_handle.read) && fill(name, offset: dir_handle.tell)
+ end
+
+ # @param [String] name a directory entry
+ # @param [FFI::Stat|Hash<Symbol,Integer>|nil] stat or stat fields to fill a {::FFI::Stat}
+ #
+ # Note sending nil values will cause Fuse to issue #getattr operations for each entry
+ #
+ # It is safe to reuse the same {FFI::Stat} object between calls
+ # @param [Integer] offset
+ # @param [Boolean] fill_dir_plus true if stat has full attributes for inode caching
+ # @return [Boolean] true if the buffer accepted the entry
+ # @raise [StopIteration] if called after a previous call returned false
+ def fill(name, stat: nil, offset: 0, fill_dir_plus: false)
+ raise StopIteration unless @buf
+
+ fill_flags = fill_flags(fill_dir_plus: fill_dir_plus)
+ fill_stat = fill_stat(stat)
+ return true if @filler.call(@buf, name, fill_stat, offset, *fill_flags).zero?
+
+ @buf = nil
+ end
+
+ # @return [Proc] a proc to pass to something that yields like #{fill}
+ def to_proc
+ proc do |name, stat: nil, offset: 0, fill_dir_plus: false|
+ fill(name, stat: stat, offset: offset, fill_dir_plus: fill_dir_plus)
+ end
+ end
+
+ private
+
+ def fill_flags(fill_dir_plus:)
+ return [] unless @fuse3
+
+ [fill_dir_plus ? :fuse_fill_dir_plus : 0]
+ end
+
+ def fill_stat(from)
+ return from if !from || from.is_a?(::FFI::Stat)
+
+ (@stat_buf ||= ::FFI::Stat.new).fill(from)
+ end
+ end
+
# rubocop:disable Metrics/ModuleLength
- module Shim
+
+ # Can be prepended to concrete filesystem implementations to skip duplicate handling of {Debug}, {Safe}
+ #
+ # @note callbacks still expect to be ultimately handled by {Safe}, ie they raise SystemCallError and can
+ # return non Integer results
+ module Prepend
include Adapter
- # Fuse3 support outer module can override this
+ # Returns true if our we can support fuse_method callback
+ #
+ # ie. when
+ #
+ # * fuse_method is implemented directly by our superclass
+ # * the adapter can handle the callback via
+ # * file or directory handle returned from {#open} or {#opendir}
+ # * fallback to an alternate implementation (eg {#read_buf} -> {#read}, {#write_buf} -> {#write})
+ def fuse_respond_to?(fuse_method)
+ fuse_methods =
+ case fuse_method
+ when :read, :write, :flush, :release
+ %i[open]
+ when :read_buf
+ %i[open read]
+ when :write_buf
+ %i[open write]
+ when :readdir, :releasedir
+ %i[opendir]
+ else
+ []
+ end
+ fuse_methods << fuse_method
+
+ fuse_methods.any? { |m| super(m) }
+ end
+
+ # Helper to test if path is root
+ def root?(path)
+ path.respond_to?(:root?) ? path.root? : path.to_s == '/'
+ end
+
+ # @!visibility private
+ # Fuse 3 compatibility
+ # @return [Boolean] true if this filesystem is receiving Fuse3 compatible arguments
+ # @see Fuse3Support
def fuse3_compat?
FUSE_MAJOR_VERSION >= 3
end
- def write(*args)
- *pre, path, buf, size, offset, info = args
- super(*pre, path, buf.read_bytes(size), offset, info)
- size
+ # @!group FUSE Callbacks
+
+ # Writes data to path via
+ #
+ # * super as per {Ruby#write} if defined
+ # * {Ruby.write_fh} on ffi.fh
+ def write(path, buf, size = buf.size, offset = 0, ffi = nil)
+ return Ruby.write_fh(buf, size, offset, ffi&.fh) unless defined?(super)
+
+ Ruby.write_data(buf, size) { |data| super(path, data, offset, ffi) }
end
- def read(*args)
- *pre, path, buf, size, offset, info = args
- res = super(*pre, path, size, offset, info)
+ # Writes data to path with {FuseBuf}s via
+ #
+ # * super directly if defined
+ # * {FuseBufVec#copy_to_fd} if ffi.fh has non-nil :fileno
+ # * {FuseBufVec#copy_to_str} with the result of {Ruby#write}
+ def write_buf(path, bufv, offset, ffi)
+ return super if defined?(super)
- return -Errno::ERANGE::Errno unless res.size <= size
+ fd = ffi&.fh&.fileno
+ return bufv.copy_to_fd(fd, offset) if fd
- buf.write_bytes(res)
- res.size
+ data = bufv.copy_to_str
+ write(path, data, data.size, offset, ffi)
end
- def readlink(*args)
- *pre, path, buf, size = args
- link = super(*pre, path)
- buf.write_bytes(link, [0..size])
- 0
+ # Flush data to path via
+ #
+ # * super if defined
+ # * :flush on ffi.fh if defined
+ def flush(path, ffi)
+ return super if defined?(super)
+
+ fh = ffi&.fh
+ fh.flush if fh.respond_to?(:flush)
end
- # rubocop:disable Metrics/AbcSize
+ # Sync data to path via
+ #
+ # * super as per {Ruby#fsync} if defined
+ # * :datasync on ffi.fh if defined and datasync is non-zero
+ # * :fysnc on ffi.fh if defined
+ def fsync(path, datasync, ffi)
+ return super(path, datasync != 0, ffi) if defined?(super)
- # changes args (removes buf and filler), processes return, changes block
- # *pre, path, buf, filler, offset, fuse_file_info, flag = nil
- def readdir(*args)
- flag_arg = args.pop if fuse3_compat?
- *pre, buf, filler, offset, fuse_file_info = args
- args = pre + [offset, fuse_file_info]
- args << flag_arg if fuse3_compat?
- buffer_available = true
- super(*args) do |name, stat, buffered = false, flag = 0|
- raise StopIteration unless buffer_available
+ fh = ffi&.fh
+ return fh.datasync if datasync && fh.respond_to?(:datasync)
- offset = buffered if buffered.is_a?(Integer)
- offset += 1 if buffered && !buffered.is_a?(Integer) # auto-track offsets
- stat = Stat.new.fill(stat) if stat && !stat.is_a?(Stat)
- filler_args = [buf, name, stat, offset]
- filler_args << flag if fuse3_compat?
- buffer_available = filler.call(*filler_args).zero?
+ fh.fsync if fh.respond_to?(:fsync)
+ end
+
+ # Read data from path via
+ #
+ # * super as per {Ruby#read} if defined
+ # * ffi.fh as per {Ruby.read}
+ def read(path, buf, size, offset, ffi)
+ Ruby.read(buf, size, offset) do
+ defined?(super) ? super(path, size, offset, ffi) : ffi&.fh
end
end
- # rubocop:enable Metrics/AbcSize
- def setxattr(*args)
- # fuse converts the void* data buffer to a const char* null terminated string
- # which libfuse reads directly, so size is irrelevant
- *pre, path, name, data, _size, flags = args
- super(*pre, path, name, data, flags)
+ # Read data with {FuseBuf}s via
+ #
+ # * super if defined
+ # * ffi.fh.fileno if defined and not nil
+ # * result of {#read}
+ def read_buf(path, bufp, size, offset, ffi)
+ return super if defined?(super)
+
+ Ruby.read_buf(bufp, size, offset) do
+ fh = ffi&.fh
+ fd = fh.fileno if fh.respond_to?(:fileno)
+ next fd if fd
+
+ read(path, nil, size, offset, ffi)
+ end
end
- def getxattr(*args)
- *pre, path, name, buf, size = args
- res = super(*pre, path, name)
+ # Read link name from path via super as per {Ruby#readlink}
+ def readlink(path, buf, size)
+ raise Errno::ENOTSUP unless defined?(super)
- return -Errno::ENODATA::Errno unless res
+ Ruby.readlink(buf, size) { super(path, size) }
+ end
- res = res.to_s
+ # Read directory entries via
+ #
+ # * super as per {Ruby#readdir} if defined
+ # * ffi.fh using {ReaddirFiller#readdir_fh}
+ def readdir(path, buf, filler, offset, ffi, flag_arg = nil)
+ rd_filler = ReaddirFiller.new(buf, filler, fuse3: fuse3_compat?)
- return res.size if size.zero?
- return -Errno::ERANGE::Errno if res.size > size
+ flag_args = {}
+ flag_args[:readdir_plus] = (flag_arg == :fuse_readdir_plus) if fuse3_compat?
+ return super(path, offset, ffi, **flag_args, &rd_filler) if defined?(super)
- buf.write_bytes(res)
- res.size
+ rd_filler.readdir_fh(ffi.fh, offset)
+ rescue StopIteration
+ # do nothing
end
- def listxattr(*args)
- *pre, path, buf, size = args
- res = super(*pre, path)
- res.reduce(0) do |offset, name|
- name = name.to_s
- unless size.zero?
- return -Errno::ERANGE::Errno if offset + name.size >= size
+ # Set extended attributes via super as per {Ruby#setxattr}
+ def setxattr(path, name, data, _size, flags)
+ raise Errno::ENOTSUP unless defined?(super)
- buf.put_string(offset, name) # put string includes the NUL terminator
- end
- offset + name.size + 1
- end
+ # fuse converts the void* data buffer to a const char* null terminated string
+ # which libfuse reads directly, so size is irrelevant
+ super(path, name, data, flags)
end
- def read_buf(*args)
- *pre, path, bufp, size, offset, fuse_file_info = args
- buf = FuseBufVec.new
- super(*pre, path, buf, size, offset, fuse_file_info)
- bufp.put_pointer(0, buf)
- 0
+ # Get extended attributes via super as per {Ruby#getxattr}
+ def getxattr(path, name, buf, size)
+ raise Errno::ENOTSUP unless defined?(super)
+
+ Ruby.getxattr(buf, size) { super(path, name) }
end
- # extract atime, mtime from times array, convert from FFI::Stat::TimeSpec to ruby Time
- def utimens(*args)
- ffi = args.pop if fuse3_compat?
- *pre, path, times = args
+ # List extended attributes via super as per {Ruby#listxattr}
+ def listxattr(*args)
+ raise Errno::ENOTSUP unless defined?(super)
- # Empty times means set both to current time
- times = [Stat::TimeSpec.now, Stat::TimeSpec.now] unless times&.size == 2
+ path, buf, size = args
+ Ruby.listxattr(buf, size) { super(path) }
+ end
- # If both times are set to UTIME_NOW, make sure they get the same value!
- now = times.any?(&:now?) && Time.now
- atime, mtime = times.map { |t| t.time(now) }
+ # Set file atime, mtime via super as per {Ruby#utimens}
+ def utimens(path, times, *fuse3_args)
+ raise Errno::ENOTSUP unless defined?(super)
- args = pre + [path, atime, mtime]
- args << ffi if fuse3_compat?
- super(*args)
+ atime, mtime = Stat::TimeSpec.fill_times(times[0, 2], 2).map(&:time)
+ super(path, atime, mtime, *fuse3_args)
0
end
- # accept a filehandle object as result of these methods
- # keep a reference to the filehandle until corresponding release
+ # @!method create(path, mode, ffi)
+ # Calls super if defined as per {Ruby#create} storing result in ffi.fh and protecting it from GC
+ # until {#release}
+
+ # @!method open(path, ffi)
+ # Calls super if defined as per {Ruby#open} storing result in ffi.fh and protecting it from GC
+ # until {#release}
+
+ # @!method opendir(path, ffi)
+ # Calls super if defined as per {Ruby#opendir} storing result in ffi.fh and protecting it from GC
+ # until {#releasedir}
+
%i[create open opendir].each do |fuse_method|
define_method(fuse_method) do |*args|
- fh = super(*args)
- store_filehandle(args.last, fh)
+ fh = super(*args) if fuse_super_respond_to?(fuse_method)
+ store_handle(args.last, fh)
0
end
end
- %i[release release_dir].each do |fuse_method|
- define_method(fuse_method) do |*args|
- super(*args)
+ # @!method release(path, ffi)
+ # Calls super if defined and allows ffi.fh to be GC'd
+
+ # @!method releasedir(path, ffi)
+ # Calls super if defined and allows ffi.fh to be GC'd
+
+ %i[release releasedir].each do |fuse_method|
+ define_method(fuse_method) do |path, ffi|
+ super(path, ffi) if fuse_super_respond_to?(fuse_method)
ensure
- release_filehandle(args.last)
+ release_handle(ffi)
end
end
+ # Calls super if defined and storing result to protect from GC until {#destroy}
+ def init(*args)
+ o = super(*args) if fuse_super_respond_to?(:init)
+ handles << o if o
+ end
+
+ # Calls super if defined and allows init_obj to be GC'd
+ def destroy(init_obj)
+ super if fuse_super_respond_to?(:destroy)
+ handles.delete(init_obj) if init_obj
+ end
+
+ # @!endgroup
private
- def store_filehandle(ffi, filehandle)
- return unless filehandle
+ def handles
+ @handles ||= Set.new.compare_by_identity
+ end
- @filehandles ||= {}
- @filehandles[ffi]
- ffi.fh = filehandle
+ def store_handle(ffi, file_handle)
+ return unless file_handle
+
+ handles << file_handle
+ ffi.fh = file_handle
end
- def release_filehandle(ffi)
+ def release_handle(ffi)
return unless ffi.fh
- @filehandles.delete(ffi.fh.object_id)
+ handles.delete(ffi.fh)
end
end
# rubocop:enable Metrics/ModuleLength
# @!group FUSE Callbacks
+ # @!method create(path, mode, fuse_file_info)
+ # Create file
+ # @abstract
+ # @param [String] path
+ # @param [FuseFileInfo] fuse_file_info
+ # @return [Object] file handle available to future operations in fuse_file_info.fh
+
# @!method open(path,fuse_file_info)
# File open
# @abstract
# @param [String] path
# @param [FuseFileInfo] fuse_file_info
- # @return [Object] file handle (available to future operations in fuse_file_info.fh)
+ # @return [Object] file handle available to future operations in fuse_file_info.fh
+ #
+ # File handles are kept from being GC'd until {FuseOperations#release}
+ #
+ # If the file handle quacks like {::IO} then the file io operations
+ # :read, :write, :flush, :fsync, :release will be invoked on the file handle if not implemented
+ # by the filesystem
+ #
# @see FuseOperations#open
# @!method create(path,mode,fuse_file_info)
# File creation
# @abstract
@@ -192,92 +393,93 @@
# @param [FuseFileInfo] fuse_file_info
# @return [Object] directory handle (available to future operations in fuse_file_info.fh)
# @see FuseOperations#opendir
# @!method write(path,data,offset,info)
- # Write file data
# @abstract
+ # Write file data. If not implemented will attempt to use info.fh as an IO via :pwrite, or :seek + :write
# @param [String] path
# @param [String] data
# @param [Integer] offset
# @param [FuseFileInfo] info
# @return [void]
+ # @raise [Errno::ENOTSUP] if not implemented and info.fh does not quack like IO
# @see FuseOperations#write
+ # @!method write_buf(path,buffers,offset,info)
+ # @abstract
+ # Write file data from buffers
+ # If not implemented, will try to use info.fh,fileno to perform libfuse' file descriptor io, otherwise
+ # the string data is extracted from buffers and passed to #{write}
+
# @!method read(path,size,offset,info)
# @abstract
- # Read file data
- #
+ # Read file data. If not implemented will attempt to use info.fh to perform the read.
# @param [String] path
# @param [Integer] size
# @param [Integer] offset
# @param [FuseFileInfo] info
- #
# @return [String] the data, expected to be exactly size bytes, except if EOF
+ # @return [#pread, #read] something that supports :pread, or :seek and :read
+ # @raise [Errno::ENOTSUP] if not implemented, and info.fh does not quack like IO
# @see FuseOperations#read
+ # @see FuseOperations#read_buf
- # @!method readlink(path)
+ # @!method read_buf(path,buffers,size,offset,info)
# @abstract
+ # If not implemented and info.fh has :fileno then libfuse' file descriptor io will be used,
+ # otherwise will use {read} to populate buffers
+
+ # @!method readlink(path, size)
+ # @abstract
# Resolve target of a symbolic link
# @param [String] path
- # @return [String] the link target
+ # @param [Integer] size
+ # @return [String] the link target, truncated to size if necessary
# @see FuseOperations#readlink
- # @!method readdir(path,offset,fuse_file_info,&filler)
+ # @!method readdir(path,offset,fuse_file_info, readdir_plus:, &filler)
# @abstract
# List directory entries
#
- # The filesystem may choose between two modes of operation:
+ # The filesystem may choose between three modes of operation:
#
- # 1) The readdir implementation ignores the offset parameter yielding only name, stat pairs for each entry.
+ # 1) The readdir implementation ignores the offset parameter yielding only name, and optional stat
# The yield will always return true so the whole directory is read in a single readdir operation.
#
# 2) The readdir implementation keeps track of the offsets of the directory entries. It uses the offset
- # parameter and always yields buffered=true to the filler function. When the buffer is full, or an error
- # occurs, yielding to the filler function will return false. Subsequent yields will raise StopIteration
+ # parameter to restart the iteration and yields non-zero offsets for each entry. When the buffer is full, or
+ # an error occurs, yielding to the filler function will return false. Subsequent yields will raise
+ # StopIteration
#
+ # 3) Return a Dir like object from {opendir} and do not implement this method. The directory
+ # will be enumerated from offset via calling :seek, :read and :tell on fuse_file_info.fh
+ #
# @param [String] path
# @param [Integer] offset the starting offset (inclusive!)
#
# this is either 0 for a new operation, or the last value previously yielded to the filler function
- # (when it returned false to indicate the buffer was full). Type 2 implementations should therefore include
+ # (when it returned false to indicate the buffer was full).
#
# @param [FuseFileInfo] fuse_file_info
- #
+ # @param [Boolean] readdir_plus true if extended readdir is supported (Fuse3 only)
# @raise [SystemCallError] an appropriate Errno value
# @return [void]
- #
- # @yieldparam [String] name the name of a directory entry
- # @yieldparam [Stat|nil] stat the directory entry stat
- #
- # Note sending nil values will cause Fuse to issue #getattr operations for each entry
- #
- # @yieldparam [Integer|Boolean] offset (optional - default false)
- #
- # integer value will be used as offset. The last value yielded (with false return) will be used
- # for the next readdir call
- #
- # otherwise truthy to indicate support for restart from monotonically increasing offset
- #
- # false to indicate type 1 operation - full listing
- #
- # @yieldreturn [Boolean]
- #
- # * true if buffer accepted the directory entry
- # * false on first time buffer is full. StopIteration will be raised on subsequent yields
- #
+ # @yield [name,stat:,offset:,fill_dir_plus:]
+ # See {ReaddirFiller#fill}
# @see FuseOperations#readdir
+ # @see ReaddirFiller#fill
# @!method getxattr(path,name)
# @abstract
# Get extended attribute
# @param [String] path
# @param [String] name the attribute name
# @return [nil|String] the attribute value or nil if it does not exist
# @see FuseOperations#getxattr
- # @!method listxattr(path,name)
+ # @!method listxattr(path)
# @abstract
# List extended attributes
# @param [String] path
# @return [Array<String>] list of xattribute names
# @see FuseOperations#listxattr
@@ -292,21 +494,10 @@
# @return [void]
# @raise [Errno::EEXIST] for :xattr_create and name already exists
# @raise [Errno::ENODATA] for :xattr_replace and name does not already exist
# @see FuseOperations#setxattr
- # @!method read_buf(path,buffers,size,offset,fuse_file_info)
- # @abstract
- # Read through fuse data buffers
- # @param [String] path
- # @param [FuseBufVec] buffers
- # @param [Integer] size
- # @param [Integer] offset
- # @param [FuseFileInfo] fuse_file_info
- # @return [void]
- # @see FuseOperations#read_buf
-
# @!method utimens(path,atime,mtime,fuse_file_info=nil)
# @abstract
# Change the access and/or modification times of a file with nanosecond resolution
#
# @param [String] path
@@ -318,16 +509,176 @@
#
# @note
# Either atime or mtime can be nil corresponding to utimensat(2) receiving UTIME_OMIT.
# The special value UTIME_NOW passed from Fuse is automatically set to the current time
+ # @!method fsync(path, datasync, fuse_file_info)
+ # @abstract
+ # @param [String] path
+ # @param [Boolean] datasync if true only the user data should be flushed, not the meta data.
+ # @param [FuseFileInfo] fuse_file_info
+
# @!endgroup
# @!visibility private
def self.included(mod)
- mod.prepend(Shim)
- mod.include(Safe)
+ mod.prepend(Prepend)
+ mod.include(Context)
mod.include(Debug)
+ mod.include(Safe)
+ end
+
+ class << self
+ # Helper for implementing {FuseOperations#readlink}
+ # @param [FFI::Pointer] buf
+ # @param [Integer] size
+ # @yield []
+ # @yieldreturn [String] the link name
+ # @raise [Errno::ENOTSUP] if no data is returned
+ # @raise [Errno::ENAMETOOLONG] if data returned is larger than size
+ # @return [void]
+ def readlink(buf, size)
+ link = yield
+ raise Errno::ENOTSUP unless link
+ raise Errno::ENAMETOOLONG unless link.size < size # includes terminating NUL
+
+ buf.put_string(link)
+ 0
+ end
+
+ # Helper for implementing {FuseOperations#read}
+ # @param [FFI::Pointer] buf
+ # @param [Integer] size
+ # @param [Integer] offset
+ # @return [Integer] size of data read
+ # @yield []
+ # @yieldreturn [String, #pread, #pwrite] the resulting data or IO like object
+ # @raise [Errno::ENOTSUP] if no data is returned
+ # @raise [Errno::ERANGE] if data return is larger than size
+ # @see data_to_str
+ def read(buf, size, offset = 0)
+ data = yield
+ raise Errno::ENOTSUP unless data
+
+ return data unless buf # called from read_buf
+
+ data = data_to_str(data, size, offset)
+ raise Errno::ERANGE unless data.size <= size
+
+ buf.write_bytes(data)
+ data.size
+ end
+
+ # Helper for implementing {FuseOperations#read_buf}
+ # @param [FFI::Pointer] bufp
+ # @param [Integer] size
+ # @param [Integer] offset
+ # @yield []
+ # @yieldreturn [Integer|:fileno|String,:pread,:pwrite] a file descriptor, String or io like object
+ # @see data_to_bufvec
+ def read_buf(bufp, size, offset)
+ data = yield
+ raise Errno::ENOTSUP unless data
+
+ bufp.write_pointer(data_to_bufvec(data, size, offset).to_ptr)
+ 0
+ end
+
+ # Helper to convert input data to a string for use with {FuseOperations#read}
+ # @param [String|:pread|:read] io input data that is a String or quacks like {::IO}
+ # @param [Integer] size
+ # @param [Integer] offset
+ # @return [String] extracted data
+ def data_to_str(io, size, offset)
+ return io if io.is_a?(String)
+ return io.pread(size, offset) if io.respond_to?(:pread)
+ return io.read(size) if io.respond_to?(:read)
+
+ io.to_s
+ end
+
+ # Helper to convert string or IO to {FuseBufVec} for {FuseOperations#read_buf}
+ # @param [Integer|:fileno|String|:pread|:read] data the io like input data or an integer file descriptor
+ # @param [Integer] size
+ # @param [Integer] offset
+ # @return [FuseBufVec]
+ def data_to_bufvec(data, size, offset)
+ data = data.fileno if data.respond_to?(:fileno)
+ return FuseBufVec.init(autorelease: false, size: size, fd: data, pos: offset) if data.is_a?(Integer)
+
+ str = data_to_str(data, size, offset)
+ FuseBufVec.init(autorelease: false, size: str.size, mem: FFI::MemoryPointer.from_string(str))
+ end
+
+ # Helper to implement #{FuseOperations#write}
+ # @param [FFI::Pointer|:to_s] buf
+ # @param [Integer] size
+ # @return [Integer] size
+ # @yield [data]
+ # @yieldparam [String] data extracted from buf
+ # @yieldreturn [void]
+ def write_data(buf, size)
+ data = buf.read_bytes(size) if buf.respond_to?(:read_bytes)
+ data ||= buf.to_s
+ data = data[0..size] if data.size > size
+ yield data
+ size
+ end
+
+ # Helper to write a data buffer to an open file
+ # @param [FFI::Pointer] buf
+ # @param [Integer] size
+ # @param [Integer] offset
+ # @param [:pwrite,:seek,:write] handle an IO like file handle
+ # @return [Integer] size
+ # @raise [Errno::ENOTSUP] if handle is does not quack like an open file
+ def write_fh(buf, size, offset, handle)
+ write_data(buf, size) do |data|
+ if handle.respond_to?(:pwrite)
+ handle.pwrite(data, offset)
+ elsif handle.respond_to?(:write)
+ handle.seek(offset) if handle.respond_to?(:seek)
+ handle.write(data)
+ else
+ raise Errno::ENOTSUP
+ end
+ end
+ end
+
+ # Helper for implementing {FuseOperations#getxattr}
+ #
+ # @param [FFI::Pointer] buf
+ # @param [Integer] size
+ # @yieldreturn [String] the xattr name
+ def getxattr(buf, size)
+ res = yield
+ raise Errno::ENODATA unless res
+
+ res = res.to_s
+
+ return res.size if size.zero?
+ raise Errno::ERANGE if res.size > size
+
+ buf.write_bytes(res)
+ res.size
+ end
+
+ # Helper for implementing {FuseOperations#listxattr}
+ # @param [FFI::Pointer] buf
+ # @param [Integer] size
+ # @yieldreturn [Array<String>] a list of extended attribute names
+ def listxattr(buf, size)
+ res = yield
+ res.reduce(0) do |offset, name|
+ name = name.to_s
+ unless size.zero?
+ raise Errno::ERANGE if offset + name.size >= size
+
+ buf.put_string(offset, name) # put string includes the NUL terminator
+ end
+ offset + name.size + 1
+ end
+ end
end
end
end
end
end