lib/ffi/libfuse/adapter/ruby.rb in ffi-libfuse-0.3.4 vs lib/ffi/libfuse/adapter/ruby.rb in ffi-libfuse-0.4.0
- old
+ new
@@ -5,11 +5,11 @@
require 'set'
module FFI
module Libfuse
module Adapter
- # This module assists with converting native C libfuse into idiomatic Ruby
+ # This module assists with converting native C libfuse into idiomatic and duck-typed Ruby behaviour
#
# Class Method helpers
# ===
# These functions deal with the native fuse FFI::Pointers, Buffers etc
#
@@ -75,10 +75,11 @@
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
+ false
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|
@@ -99,12 +100,10 @@
(@stat_buf ||= ::FFI::Stat.new).fill(from)
end
end
- # rubocop:disable Metrics/ModuleLength
-
# 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
@@ -150,33 +149,101 @@
FUSE_MAJOR_VERSION >= 3
end
# @!group FUSE Callbacks
+ # 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?)
+
+ 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)
+
+ rd_filler.readdir_fh(ffi.fh, offset)
+ rescue StopIteration
+ # do nothing
+ end
+
+ # Read data from path via
+ #
+ # * super as per {Ruby#read} if defined (or returns nil|false)
+ # * ffi.fh as per {Ruby.read}
+ def read(path, buf, size, offset, ffi)
+ io, super_offset = defined?(super) && Ruby.rescue_not_implemented { super(path, size, offset, ffi) }
+ offset = super_offset if io
+ io ||= ffi.fh
+
+ return [io, offset] unless buf # nil buf as called from read_buf, just wants the io/data back
+
+ Ruby.read(buf, size, offset) { io }
+ end
+
+ # 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)
+ io, super_offset = defined?(super) && Ruby.rescue_not_implemented { super(path, size, offset, ffi) }
+ offset = super_offset if io
+
+ io ||= ffi.fh if ffi.fh.is_a?(Integer) || ffi.fh.respond_to?(:fileno)
+
+ io, offset = read(path, nil, size, offset, ffi) unless io
+
+ Ruby.read_buf(bufp, size, offset) { io }
+ end
+
+ # Read link name from path via super as per {Ruby#readlink}
+ def readlink(path, buf, size)
+ raise Errno::ENOTSUP unless defined?(super)
+
+ Ruby.readlink(buf, size) { super(path, size) }
+ end
+
# Writes data to path via
#
- # * super as per {Ruby#write} if defined
- # * {Ruby.write_fh} on ffi.fh
+ # * super if defined
+ # * ffi.fh if not null and quacks like IO (see {IO.write})
+ #
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) }
+ Ruby.write(buf, size) do |data|
+ (defined?(super) && Ruby.rescue_not_implemented { super(path, data, offset, ffi) }) || [ffi&.fh, offset]
+ end
end
- # Writes data to path with {FuseBuf}s via
+ # Writes data to path with {FuseBufVec} 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}
+ # * super directly if defined and returns truthy
+ # * ffi.fh if it represents a file descriptor
+ # * {#write}
+ #
def write_buf(path, bufv, offset, ffi)
- return super if defined?(super)
+ super_result =
+ if defined?(super)
+ Ruby.rescue_not_implemented do
+ super(path, offset, ffi) do |fh = nil, *flags|
+ Ruby.write_buf(bufv) { |data| data || [fh, *flags] }
+ end
+ end
+ end
- fd = ffi&.fh&.fileno
- return bufv.copy_to_fd(fd, offset) if fd
+ return super_result if super_result
- data = bufv.copy_to_str
- write(path, data, data.size, offset, ffi)
+ Ruby.write_buf(bufv) do |data|
+ # only handle fileno, otherwise fall back to write (which will try other kinds of IO)
+ if data
+ # fallback to #write
+ write(path, data, data.size, offset, ffi)
+ else
+ [ffi&.fh.respond_to?(:fileno) && ffi.fh.fileno, offset]
+ end
+ end
end
# Flush data to path via
#
# * super if defined
@@ -200,60 +267,10 @@
return fh.datasync if datasync && fh.respond_to?(:datasync)
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
-
- # 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
-
- # Read link name from path via super as per {Ruby#readlink}
- def readlink(path, buf, size)
- raise Errno::ENOTSUP unless defined?(super)
-
- Ruby.readlink(buf, size) { super(path, size) }
- end
-
- # 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?)
-
- 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)
-
- rd_filler.readdir_fh(ffi.fh, offset)
- rescue StopIteration
- # do nothing
- end
-
# Set extended attributes via super as per {Ruby#setxattr}
def setxattr(path, name, data, _size, flags)
raise Errno::ENOTSUP unless defined?(super)
# fuse converts the void* data buffer to a const char* null terminated string
@@ -349,12 +366,10 @@
return unless ffi.fh
handles.delete(ffi.fh)
end
end
- # rubocop:enable Metrics/ModuleLength
-
# @!group FUSE Callbacks
# @!method create(path, mode, fuse_file_info)
# Create file
# @abstract
@@ -392,44 +407,82 @@
# @param [String] path
# @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)
+ # @!method write(path,data,offset,ffi)
# @abstract
- # Write file data. If not implemented will attempt to use info.fh as an IO via :pwrite, or :seek + :write
+ # Write file data. If not implemented will pass ffi.fh (from {open}) to {IO.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
+ # @param [FuseFileInfo] ffi
+ # @return [Integer] number of bytes written (<= data.size)
+ # @return [IO] an IO object (data will be sent to {IO.write})
+ # @return [nil|false] treat as not implemented
+ # @raise [NotImplementedError, Errno::ENOTSUP] treat as not implemented
# @see FuseOperations#write
- # @!method write_buf(path,buffers,offset,info)
+ # @!method write_buf(path,offset,ffi,&buffer)
# @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}
+ # Write buffered data to a file via one of the following techniques
+ #
+ # 1. Yield object and flags to &buffer to write data directly to a {FuseBufVec}, File or IO
+ # and return the yield result (number of bytes written)
+ #
+ # 2. Yield with no params (or explicitly nil and flags) to retrieve String data (via {FuseBufVec#copy_to_str})
+ # to write to path@offset, returning the number of bytes written (eg data.size)
+ #
+ # 3. Return nil, false or do not implement to
+ # * try ffi.fh.fileno (from {#open}) as a file descriptor
+ # * or otherwise fallback to {#write}
+ #
+ # @param [String] path
+ # @param [Integer] offset
+ # @param [FuseFileInfo] ffi
+ # @param [Proc] buffer
+ # @yield [io = nil, *flags] Send data to io, or if not set, receive data as a string
+ # @yieldparam [FuseBufVec] io write directly into these buffers via {FuseBufVec.copy_to}
+ # @yieldparam [Integer|:fileno] io write to an open file descriptor via {FuseBufVec.copy_to_fd}
+ # @yieldparam [IO] io quacks like IO passed to {IO.write} to receive data
+ # @yieldparam [nil|false] io pull data from buffers into a String
+ # @yieldparam [Array<Symbol>] flags see {FuseBufVec}
+ # @yieldreturn [String] if io not supplied, the chunk of data to write is returned
+ # @yieldreturn [Integer] the number of bytes written to io
+ # @return [Integer] number of bytes written (<= data.size)
+ # @return [nil|false] treat as not implemented (do not yield AND return nil/false)
+ # @raise [NotImplementedError, Errno::ENOTSUP] treat as not implemented
+ # @see FuseOperations#write_buf
- # @!method read(path,size,offset,info)
+ # @!method read(path,size,offset,ffi)
# @abstract
- # Read file data. If not implemented will attempt to use info.fh to perform the read.
+ # Read file data.
+ #
+ # If not implemented will send ffi.fh (from {open}) to {IO.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
+ # @param [FuseFileInfo] ffi
+ # @return [Array<Object,Integer>] io, offset will be passed to {IO.read}(io, size, offset)
+ # @return [nil|false] treat as not implemented
+ # @raise [NotImplementedError, Errno::ENOTSUP] treat as not implemented
# @see FuseOperations#read
- # @see FuseOperations#read_buf
- # @!method read_buf(path,buffers,size,offset,info)
+ # @!method read_buf(path,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
+ # Read file data directly from a buffer
+ #
+ # If not implemented first tries ffi.fh.fileno (from {#open}) as a file descriptor before
+ # falling back to {#read}
+ # @param [String] path
+ # @param [Integer] size
+ # @param [Integer] offset
+ # @param [FuseFileInfo] info
+ # @return [Array<Object,Integer>] io, offset passed to {FuseBufVec#copy_to_io}(io, offset)
+ # @return [nil|false] treat as not implemented
+ # @raise [NotImplementedError, Errno::ENOTSUP] treat as not implemented
+ # @see FuseOperations#read_buf
# @!method readlink(path, size)
# @abstract
# Resolve target of a symbolic link
# @param [String] path
@@ -522,14 +575,22 @@
# @!visibility private
def self.included(mod)
mod.prepend(Prepend)
mod.include(Context)
mod.include(Debug)
- mod.include(Safe)
end
class << self
+ # Helper to rescue not implemented or not supported errors from sub-filesystems
+ # @return[Object] result of block
+ # @return[nil] if block raises NotImplementedError or Errno::ENOTSUP
+ def rescue_not_implemented
+ yield
+ rescue NotImplementedError, Errno::ENOTSUP
+ nil
+ end
+
# Helper for implementing {FuseOperations#readlink}
# @param [FFI::Pointer] buf
# @param [Integer] size
# @yield []
# @yieldreturn [String] the link name
@@ -539,110 +600,100 @@
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)
+ buf.put_string(0, link) # with NULL terminator.
0
end
# Helper for implementing {FuseOperations#read}
# @param [FFI::Pointer] buf
# @param [Integer] size
- # @param [Integer] offset
- # @return [Integer] size of data read
+ # @param [Integer, nil] offset
+ # @return [Integer] returns the size of data read into buf
# @yield []
- # @yieldreturn [String, #pread, #pwrite] the resulting data or IO like object
+ # @yieldreturn [String, IO] 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
+ # @see Libfuse::IO.read
+ def read(buf, size, offset = nil)
+ io = yield
+ raise Errno::ENOTSUP unless io
- return data unless buf # called from read_buf
-
- data = data_to_str(data, size, offset)
+ data = Libfuse::IO.read(io, 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
+ # @return [Integer] 0 for success
+ # @raise [Errno:ENOTSUP] if no data or io is returned
# @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
+ # @yieldreturn [FuseBufVec] list of buffers to read from
+ # @yieldreturn [String, IO, Integer, :fileno] String, IO or file_descriptor to read from
+ # (see {FuseBufVec.create})
+ def read_buf(bufp, size, offset = nil)
+ io = yield
+ raise Errno::ENOTSUP unless io
- bufp.write_pointer(data_to_bufvec(data, size, offset).to_ptr)
+ fbv = io.is_a?(FuseBufVec) ? io : FuseBufVec.create(io, size, offset)
+ fbv.store_to(bufp)
+
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}
+ # Helper to implement #{FuseOperations#write}
+ # yields the data and receives expects IO to write the data to
+ # @param [FFI::Pointer] buf
# @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)
+ # @yield(data)
+ # @yieldparam [String] data data to write
+ # @yieldreturn [nil|false] data has not been handled (raises Errno::ENOTSUP)
+ # @yieldreturn [Integer] number of bytes written (will not send to {IO.write})
+ # @yieldreturn [IO] to use with {IO.write}
+ # @return [Integer] number of bytes written
+ # @raise [Errno::ENOTSUP] if nothing is returned from yield
+ def write(buf, size)
+ data = buf.read_bytes(size) if buf.respond_to?(:read_bytes)
+ data ||= buf.to_s
- io.to_s
- end
+ io, offset = yield data
- # 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)
+ raise Errno::ENOSUP unless io
+ return io if io.is_a?(Integer)
- str = data_to_str(data, size, offset)
- FuseBufVec.init(autorelease: false, size: str.size, mem: FFI::MemoryPointer.from_string(str))
+ Libfuse::IO.write(io, data, offset)
end
- # Helper to implement #{FuseOperations#write}
- # @param [FFI::Pointer|:to_s] buf
- # @param [Integer] size
- # @return [Integer] size
+ # Helper to implement #{FuseOperations#write_buf}
+ #
+ # Yields firstly with data = nil
+ # A returned truthy object is sent to buvf.copy_to_io
+ # Otherwise yields again with string data from bufv.copy_to_str expecting the caller to write the data
+ # and return the number of bytes written
+ #
+ # @param [FuseBufVec] bufv
# @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
+ # @yieldparam [nil] data first yield is nil
+ # @yieldparam [String] data second yield is the data
+ # @yieldreturn [nil, Array<IO,Integer,Symbol...>] io, [offset,] *flags
+ # for first yield can return nil to indicate it wants the data as a string (via second yield)
+ # alternative an object to receive the data via {FuseBufVec#copy_to_io}(io, offset = nil, *flags)
+ # @yieldreturn [Integer] second yield must return number of bytes written
+ # @return [Integer] number of bytes written
+ # @raise [Errno::ENOTSUP] if nothing is returned from either yield
+ def write_buf(bufv)
+ fh, *flags = yield nil # what kind of result do we want
+ return bufv.copy_to_io(fh, offset, *flags) if fh
- # 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
+ data = bufv.copy_to_str(*flags)
+ yield data || (raise Errno::ENOTSUP)
end
# Helper for implementing {FuseOperations#getxattr}
#
# @param [FFI::Pointer] buf