ffi-libfuse 0.3.4 → 0.4.1
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/CHANGELOG.md +29 -0
- data/README.md +1 -1
- data/lib/ffi/accessors.rb +419 -93
- data/lib/ffi/boolean_int.rb +1 -1
- data/lib/ffi/devt.rb +36 -10
- data/lib/ffi/flock.rb +31 -27
- data/lib/ffi/libfuse/adapter/context.rb +1 -1
- data/lib/ffi/libfuse/adapter/debug.rb +54 -16
- data/lib/ffi/libfuse/adapter/fuse2_compat.rb +43 -26
- data/lib/ffi/libfuse/adapter/fuse3_support.rb +7 -8
- data/lib/ffi/libfuse/adapter/interrupt.rb +1 -1
- data/lib/ffi/libfuse/adapter/pathname.rb +1 -1
- data/lib/ffi/libfuse/adapter/ruby.rb +211 -160
- data/lib/ffi/libfuse/adapter/safe.rb +70 -22
- data/lib/ffi/libfuse/callbacks.rb +2 -1
- data/lib/ffi/libfuse/filesystem/accounting.rb +1 -1
- data/lib/ffi/libfuse/filesystem/mapped_files.rb +33 -7
- data/lib/ffi/libfuse/filesystem/pass_through_dir.rb +0 -1
- data/lib/ffi/libfuse/filesystem/virtual_dir.rb +294 -127
- data/lib/ffi/libfuse/filesystem/virtual_file.rb +85 -79
- data/lib/ffi/libfuse/filesystem/virtual_fs.rb +34 -15
- data/lib/ffi/libfuse/filesystem/virtual_link.rb +60 -0
- data/lib/ffi/libfuse/filesystem/virtual_node.rb +104 -87
- data/lib/ffi/libfuse/filesystem.rb +1 -1
- data/lib/ffi/libfuse/fuse2.rb +3 -2
- data/lib/ffi/libfuse/fuse3.rb +6 -6
- data/lib/ffi/libfuse/fuse_args.rb +14 -21
- data/lib/ffi/libfuse/fuse_buf.rb +112 -0
- data/lib/ffi/libfuse/fuse_buf_vec.rb +228 -0
- data/lib/ffi/libfuse/fuse_cmdline_opts.rb +19 -16
- data/lib/ffi/libfuse/fuse_common.rb +10 -4
- data/lib/ffi/libfuse/fuse_config.rb +35 -23
- data/lib/ffi/libfuse/fuse_conn_info.rb +1 -1
- data/lib/ffi/libfuse/fuse_context.rb +2 -1
- data/lib/ffi/libfuse/fuse_loop_config.rb +68 -20
- data/lib/ffi/libfuse/fuse_operations.rb +86 -41
- data/lib/ffi/libfuse/gem_helper.rb +2 -9
- data/lib/ffi/libfuse/io.rb +56 -0
- data/lib/ffi/libfuse/main.rb +33 -26
- data/lib/ffi/libfuse/test_helper.rb +67 -61
- data/lib/ffi/libfuse/version.rb +1 -1
- data/lib/ffi/libfuse.rb +1 -1
- data/lib/ffi/stat/native.rb +4 -4
- data/lib/ffi/stat.rb +35 -12
- data/lib/ffi/stat_vfs.rb +1 -2
- data/lib/ffi/struct_array.rb +2 -1
- data/lib/ffi/struct_wrapper.rb +6 -4
- data/sample/hello_fs.rb +1 -1
- metadata +6 -3
- data/lib/ffi/libfuse/fuse_buffer.rb +0 -257
| @@ -14,6 +14,11 @@ module FFI | |
| 14 14 | 
             
                  #
         | 
| 15 15 | 
             
                  # Implements callbacks satisfying {Adapter::Ruby} which is automatically included.
         | 
| 16 16 | 
             
                  module MappedFiles
         | 
| 17 | 
            +
                    # @!visibility private
         | 
| 18 | 
            +
                    def self.included(mod)
         | 
| 19 | 
            +
                      mod.prepend(Adapter::Ruby::Prepend)
         | 
| 20 | 
            +
                    end
         | 
| 21 | 
            +
             | 
| 17 22 | 
             
                    # Do we have ffi-xattr to handle extended attributes in real files
         | 
| 18 23 | 
             
                    HAS_XATTR =
         | 
| 19 24 | 
             
                      begin
         | 
| @@ -34,7 +39,7 @@ module FFI | |
| 34 39 | 
             
                    #  @return [String] mapped_path in an underlying filesystem
         | 
| 35 40 | 
             
                    #
         | 
| 36 41 | 
             
                    #    Fuse callbacks are fulfilled using Ruby's native File methods called on this path
         | 
| 37 | 
            -
                    #  @return [String, Adapter::Ruby | 
| 42 | 
            +
                    #  @return [String, Adapter::Ruby] mapped_path, filesystem
         | 
| 38 43 | 
             
                    #
         | 
| 39 44 | 
             
                    #    If an optional filesystem value is returned fuse callbacks will be passed on to this filesystem with the
         | 
| 40 45 | 
             
                    #    mapped_path and other callback args unchanged
         | 
| @@ -78,6 +83,26 @@ module FFI | |
| 78 83 | 
             
                      path_method(__method__, path, ffi) { |rp| File.open(rp, ffi.flags) }
         | 
| 79 84 | 
             
                    end
         | 
| 80 85 |  | 
| 86 | 
            +
                    # implemented to allow for virtual files within the mapped fs, but we just rely om the result of open
         | 
| 87 | 
            +
                    def read(path, size, offset, ffi)
         | 
| 88 | 
            +
                      path_method(__method__, path, size, offset, ffi) { |_rp| nil }
         | 
| 89 | 
            +
                    end
         | 
| 90 | 
            +
             | 
| 91 | 
            +
                    # implemented to allow for virtual files within the mapped fs, but we just rely om the result of open
         | 
| 92 | 
            +
                    def read_buf(path, size, offset, ffi)
         | 
| 93 | 
            +
                      path_method(__method__, path, size, offset, ffi, error: nil) { |_rp| nil }
         | 
| 94 | 
            +
                    end
         | 
| 95 | 
            +
             | 
| 96 | 
            +
                    # implemented to allow for virtual files within the mapped fs, but we just rely om the result of open
         | 
| 97 | 
            +
                    def write(path, size, offset, ffi)
         | 
| 98 | 
            +
                      path_method(__method__, path, size, offset, ffi) { |_rp| nil }
         | 
| 99 | 
            +
                    end
         | 
| 100 | 
            +
             | 
| 101 | 
            +
                    # implemented to allow for virtual files within the mapped fs, but we just rely om the result of open
         | 
| 102 | 
            +
                    def write_buf(path, offset, ffi, &buffer)
         | 
| 103 | 
            +
                      path_method(__method__, path, offset, ffi, block: buffer, error: nil) { |_rp| nil }
         | 
| 104 | 
            +
                    end
         | 
| 105 | 
            +
             | 
| 81 106 | 
             
                    # Truncates the file handle (or the real file)
         | 
| 82 107 | 
             
                    def truncate(path, size, ffi = nil)
         | 
| 83 108 | 
             
                      return ffi.fh.truncate(size) if ffi&.fh
         | 
| @@ -122,18 +147,19 @@ module FFI | |
| 122 147 | 
             
                    end
         | 
| 123 148 | 
             
                    # @!endgroup
         | 
| 124 149 |  | 
| 125 | 
            -
                    # @!visibility private
         | 
| 126 | 
            -
                    def self.included(mod)
         | 
| 127 | 
            -
                      mod.prepend(Adapter::Ruby::Prepend)
         | 
| 128 | 
            -
                    end
         | 
| 129 | 
            -
             | 
| 130 150 | 
             
                    private
         | 
| 131 151 |  | 
| 132 152 | 
             
                    def path_method(callback, path, *args, error: Errno::ENOENT, block: nil)
         | 
| 133 153 | 
             
                      rp, fs = map_path(path)
         | 
| 154 | 
            +
             | 
| 134 155 | 
             
                      raise error if error && !rp
         | 
| 156 | 
            +
                      return nil unless rp
         | 
| 157 | 
            +
                      return yield(rp) unless fs
         | 
| 158 | 
            +
             | 
| 159 | 
            +
                      return fs.send(callback, rp, *args, &block) if fs.respond_to?(callback)
         | 
| 160 | 
            +
                      raise error if error
         | 
| 135 161 |  | 
| 136 | 
            -
                       | 
| 162 | 
            +
                      nil
         | 
| 137 163 | 
             
                    end
         | 
| 138 164 | 
             
                  end
         | 
| 139 165 | 
             
                end
         | 
| @@ -3,6 +3,7 @@ | |
| 3 3 | 
             
            require_relative 'accounting'
         | 
| 4 4 | 
             
            require_relative 'virtual_node'
         | 
| 5 5 | 
             
            require_relative 'virtual_file'
         | 
| 6 | 
            +
            require_relative 'virtual_link'
         | 
| 6 7 | 
             
            require_relative 'pass_through_file'
         | 
| 7 8 | 
             
            require_relative 'pass_through_dir'
         | 
| 8 9 | 
             
            require_relative 'mapped_dir'
         | 
| @@ -12,7 +13,7 @@ module FFI | |
| 12 13 | 
             
                module Filesystem
         | 
| 13 14 | 
             
                  # A Filesystem of Filesystems
         | 
| 14 15 | 
             
                  #
         | 
| 15 | 
            -
                  # Implements a  | 
| 16 | 
            +
                  # Implements a simple Hash based directory of sub filesystems.
         | 
| 16 17 | 
             
                  #
         | 
| 17 18 | 
             
                  # FUSE Callbacks
         | 
| 18 19 | 
             
                  # ===
         | 
| @@ -40,13 +41,7 @@ module FFI | |
| 40 41 |  | 
| 41 42 | 
             
                    def initialize(accounting: Accounting.new)
         | 
| 42 43 | 
             
                      @entries = {}
         | 
| 43 | 
            -
                       | 
| 44 | 
            -
                      super(accounting: accounting)
         | 
| 45 | 
            -
                    end
         | 
| 46 | 
            -
             | 
| 47 | 
            -
                    # @return [Boolean] true if this dir been mounted
         | 
| 48 | 
            -
                    def mounted?
         | 
| 49 | 
            -
                      @mounted
         | 
| 44 | 
            +
                      super
         | 
| 50 45 | 
             
                    end
         | 
| 51 46 |  | 
| 52 47 | 
             
                    # @!endgroup
         | 
| @@ -54,10 +49,13 @@ module FFI | |
| 54 49 | 
             
                    # @!group FUSE Callbacks
         | 
| 55 50 |  | 
| 56 51 | 
             
                    # For the root path provides this directory's stat information, otherwise passes on to the next filesystem
         | 
| 57 | 
            -
                    def getattr(path, stat_buf = nil,  | 
| 58 | 
            -
                       | 
| 52 | 
            +
                    def getattr(path, stat_buf = nil, ffi = nil)
         | 
| 53 | 
            +
                      if root?(path)
         | 
| 54 | 
            +
                        stat_buf&.directory(nlink: entries.size + 2, **virtual_stat)
         | 
| 55 | 
            +
                        return self
         | 
| 56 | 
            +
                      end
         | 
| 59 57 |  | 
| 60 | 
            -
                       | 
| 58 | 
            +
                      path_method(__method__, path, stat_buf, ffi, notsup: Errno::ENOSYS)
         | 
| 61 59 | 
             
                    end
         | 
| 62 60 |  | 
| 63 61 | 
             
                    # Safely passes on file open to next filesystem
         | 
| @@ -69,8 +67,6 @@ module FFI | |
| 69 67 | 
             
                      raise Errno::EISDIR if root?(path)
         | 
| 70 68 |  | 
| 71 69 | 
             
                      path_method(__method__, path, *args, notsup: nil)
         | 
| 72 | 
            -
                    rescue Errno::ENOTSUP, Errno::ENOSYS
         | 
| 73 | 
            -
                      nil
         | 
| 74 70 | 
             
                    end
         | 
| 75 71 |  | 
| 76 72 | 
             
                    # Safely handle file release
         | 
| @@ -81,8 +77,6 @@ module FFI | |
| 81 77 | 
             
                      raise Errno::EISDIR if root?(path)
         | 
| 82 78 |  | 
| 83 79 | 
             
                      path_method(__method__, path, *args, notsup: nil)
         | 
| 84 | 
            -
                    rescue Errno::ENOTSUP, Errno::ENOSYS
         | 
| 85 | 
            -
                      # do nothing
         | 
| 86 80 | 
             
                    end
         | 
| 87 81 |  | 
| 88 82 | 
             
                    # Safely handles directory open to next filesystem
         | 
| @@ -91,11 +85,9 @@ module FFI | |
| 91 85 | 
             
                    # @return [Object] the result of {#path_method} for all other paths
         | 
| 92 86 | 
             
                    # @return [nil] for sub-filesystems that do not implement this callback or raise ENOTSUP or ENOSYS
         | 
| 93 87 | 
             
                    def opendir(path, ffi)
         | 
| 94 | 
            -
                      return  | 
| 88 | 
            +
                      return (ffi.fh = self) if root?(path)
         | 
| 95 89 |  | 
| 96 | 
            -
                      ffi | 
| 97 | 
            -
                    rescue Errno::ENOTSUP, Errno::ENOSYS
         | 
| 98 | 
            -
                      nil
         | 
| 90 | 
            +
                      path_method(__method__, path, ffi, notsup: nil)
         | 
| 99 91 | 
             
                    end
         | 
| 100 92 |  | 
| 101 93 | 
             
                    # Safely handles directory release
         | 
| @@ -104,9 +96,9 @@ module FFI | |
| 104 96 | 
             
                    #
         | 
| 105 97 | 
             
                    # Otherwise safely passes on to next filesystem, rescuing ENOTSUP or ENOSYS
         | 
| 106 98 | 
             
                    def releasedir(path, *args)
         | 
| 107 | 
            -
                       | 
| 108 | 
            -
             | 
| 109 | 
            -
                       | 
| 99 | 
            +
                      return if root?(path)
         | 
| 100 | 
            +
             | 
| 101 | 
            +
                      path_method(__method__, path, *args, notsup: nil)
         | 
| 110 102 | 
             
                    end
         | 
| 111 103 |  | 
| 112 104 | 
             
                    # If path is root fills the directory from the keys in {#entries}
         | 
| @@ -117,14 +109,13 @@ module FFI | |
| 117 109 | 
             
                    def readdir(path, buf, filler, offset, ffi, *flag)
         | 
| 118 110 | 
             
                      return %w[. ..].concat(entries.keys).each(&Adapter::Ruby::ReaddirFiller.new(buf, filler)) if root?(path)
         | 
| 119 111 |  | 
| 120 | 
            -
                      return ffi.fh.readdir('/', buf, filler, offset, ffi, *flag) if  | 
| 112 | 
            +
                      return ffi.fh.readdir('/', buf, filler, offset, ffi, *flag) if dir_entry?(ffi.fh)
         | 
| 121 113 |  | 
| 122 | 
            -
                      path_method(:readdir, path, buf, filler, offset, ffi, *flag | 
| 114 | 
            +
                      path_method(:readdir, path, buf, filler, offset, ffi, *flag, notsup: Errno::ENOTDIR)
         | 
| 123 115 | 
             
                    end
         | 
| 124 116 |  | 
| 125 117 | 
             
                    # For root path validates we are empty and removes a node link from {#accounting}
         | 
| 126 | 
            -
                    # For our entries, passes on the call to the entry (with path='/') and then removes the entry. | 
| 127 | 
            -
                    #  :destroy will be called on the deleted entry
         | 
| 118 | 
            +
                    # For our entries, passes on the call to the entry (with path='/') and then removes the entry.
         | 
| 128 119 | 
             
                    # @raise [Errno::ENOTEMPTY] if path is root and our entries list is not empty
         | 
| 129 120 | 
             
                    # @raise [Errno::ENOENT] if the entry does not exist
         | 
| 130 121 | 
             
                    # @raise [Errno::ENOTDIR] if the entry does not respond to :readdir (ie: is not a directory)
         | 
| @@ -136,144 +127,275 @@ module FFI | |
| 136 127 | 
             
                        return
         | 
| 137 128 | 
             
                      end
         | 
| 138 129 |  | 
| 139 | 
            -
                       | 
| 140 | 
            -
             | 
| 141 | 
            -
             | 
| 142 | 
            -
                      dir = entries[entry_key]
         | 
| 143 | 
            -
                      raise Errno::ENOENT unless dir
         | 
| 144 | 
            -
                      raise Errno::ENOTDIR unless entry_fuse_respond_to?(dir, :readdir)
         | 
| 145 | 
            -
             | 
| 146 | 
            -
                      entry_send(dir, :rmdir, '/')
         | 
| 147 | 
            -
             | 
| 148 | 
            -
                      dir = entries.delete(entry_key)
         | 
| 149 | 
            -
                      entry_send(dir, :destroy, init_results.delete(entry_key)) if dir && mounted?
         | 
| 150 | 
            -
                    end
         | 
| 151 | 
            -
             | 
| 152 | 
            -
                    # For our entries, validates the entry exists and is not a directory, then passes on unlink (with path = '/')
         | 
| 153 | 
            -
                    #  and finally deletes.
         | 
| 154 | 
            -
                    # @raise [Errno:EISDIR] if the request entry responds to :readdir
         | 
| 155 | 
            -
                    def unlink(path)
         | 
| 156 | 
            -
                      entry_key = entry_key(path)
         | 
| 157 | 
            -
                      return path_method(__method__, path) unless entry_key
         | 
| 130 | 
            +
                      path_method(__method__, path) do |entry_key, dir|
         | 
| 131 | 
            +
                        raise Errno::ENOENT unless dir
         | 
| 132 | 
            +
                        raise Errno::ENOTDIR unless dir_entry?(dir)
         | 
| 158 133 |  | 
| 159 | 
            -
             | 
| 160 | 
            -
                      raise Errno::ENOENT unless entry
         | 
| 161 | 
            -
                      raise Errno::EISDIR if entry_fuse_respond_to?(entry, :readdir)
         | 
| 134 | 
            +
                        entry_send(dir, :rmdir, '/')
         | 
| 162 135 |  | 
| 163 | 
            -
             | 
| 164 | 
            -
             | 
| 136 | 
            +
                        entries.delete(entry_key)
         | 
| 137 | 
            +
                        dir
         | 
| 138 | 
            +
                      end
         | 
| 165 139 | 
             
                    end
         | 
| 166 140 |  | 
| 167 141 | 
             
                    # For our entries, creates a new file
         | 
| 168 142 | 
             
                    # @raise [Errno::EISDIR] if the entry exists and responds_to?(:readdir)
         | 
| 169 143 | 
             
                    # @raise [Errno::EEXIST] if the entry exists
         | 
| 170 | 
            -
                    # @yield []
         | 
| 171 | 
            -
                    # @yieldreturn [ | 
| 144 | 
            +
                    # @yield [String] filename the name of the file in this directory
         | 
| 145 | 
            +
                    # @yieldreturn [:getattr] something that quacks with the FUSE Callbacks of a regular file
         | 
| 172 146 | 
             
                    #
         | 
| 173 147 | 
             
                    #   :create or :mknod + :open will be attempted with path = '/' on this file
         | 
| 174 | 
            -
                    # @return  | 
| 148 | 
            +
                    # @return the result of the supplied block, or if not given a new {VirtualFile}
         | 
| 175 149 | 
             
                    def create(path, mode = FuseContext.get.mask(0o644), ffi = nil, &file)
         | 
| 176 | 
            -
                       | 
| 150 | 
            +
                      raise Errno::EISDIR if root?(path)
         | 
| 177 151 |  | 
| 178 152 | 
             
                      # fuselib will fallback to mknod on ENOSYS on a case by case basis
         | 
| 179 | 
            -
                       | 
| 180 | 
            -
             | 
| 181 | 
            -
             | 
| 182 | 
            -
             | 
| 183 | 
            -
             | 
| 184 | 
            -
             | 
| 185 | 
            -
             | 
| 186 | 
            -
             | 
| 187 | 
            -
             | 
| 188 | 
            -
             | 
| 189 | 
            -
             | 
| 190 | 
            -
             | 
| 191 | 
            -
                         | 
| 192 | 
            -
                         | 
| 153 | 
            +
                      path_method(__method__, path, mode, ffi, notsup: Errno::ENOSYS, block: file) do |name, existing|
         | 
| 154 | 
            +
                        raise Errno::EISDIR if dir_entry?(existing)
         | 
| 155 | 
            +
                        raise Errno::EEXIST if existing
         | 
| 156 | 
            +
             | 
| 157 | 
            +
                        # TODO: Strictly should understand setgid and sticky bits of this dir's mode when creating new files
         | 
| 158 | 
            +
                        new_file = file ? file.call(name) : new_file(name)
         | 
| 159 | 
            +
                        if entry_fuse_respond_to?(new_file, :create)
         | 
| 160 | 
            +
                          new_file.create('/', mode, ffi)
         | 
| 161 | 
            +
                        else
         | 
| 162 | 
            +
                          # TODO: generate a sensible device number
         | 
| 163 | 
            +
                          entry_send(new_file, :mknod, '/', mode, 0)
         | 
| 164 | 
            +
                          entry_send(new_file, :open, '/', ffi)
         | 
| 165 | 
            +
                        end
         | 
| 166 | 
            +
                        entries[name] = new_file
         | 
| 193 167 | 
             
                      end
         | 
| 194 | 
            -
             | 
| 168 | 
            +
                    end
         | 
| 169 | 
            +
             | 
| 170 | 
            +
                    # Method for creating a new file
         | 
| 171 | 
            +
                    # @param  [String] _name
         | 
| 172 | 
            +
                    # @return [FuseOperations] something representing a regular file
         | 
| 173 | 
            +
                    def new_file(_name)
         | 
| 174 | 
            +
                      VirtualFile.new(accounting: accounting)
         | 
| 195 175 | 
             
                    end
         | 
| 196 176 |  | 
| 197 177 | 
             
                    # Creates a new directory entry in this directory
         | 
| 198 178 | 
             
                    # @param [String] path
         | 
| 199 179 | 
             
                    # @param [Integer] mode
         | 
| 200 | 
            -
                    # @yield []
         | 
| 180 | 
            +
                    # @yield [String] name the name of the directory in this filesystem
         | 
| 201 181 | 
             
                    # @yieldreturn [Object] something that quacks with the FUSE Callbacks representing a directory
         | 
| 202 | 
            -
                    # @return  | 
| 182 | 
            +
                    # @return the result of the block if given, otherwise the newly created sub {VirtualDir}
         | 
| 203 183 | 
             
                    # @raise [Errno::EEXIST] if the entry already exists at path
         | 
| 204 184 | 
             
                    def mkdir(path, mode = FuseContext.get.mask(0o777), &dir)
         | 
| 205 185 | 
             
                      return init_node(mode) if root?(path)
         | 
| 206 186 |  | 
| 207 | 
            -
                       | 
| 208 | 
            -
             | 
| 187 | 
            +
                      path_method(__method__, path, mode, block: dir) do |dir_name, existing|
         | 
| 188 | 
            +
                        raise Errno::EEXIST if existing
         | 
| 209 189 |  | 
| 210 | 
            -
             | 
| 211 | 
            -
             | 
| 190 | 
            +
                        new_dir = dir ? dir.call(dir_name) : new_dir(dir_name)
         | 
| 191 | 
            +
                        entry_send(new_dir, :mkdir, '/', mode)
         | 
| 192 | 
            +
                        entries[dir_name] = new_dir
         | 
| 193 | 
            +
                      end
         | 
| 194 | 
            +
                    end
         | 
| 212 195 |  | 
| 213 | 
            -
             | 
| 214 | 
            -
             | 
| 215 | 
            -
             | 
| 216 | 
            -
             | 
| 196 | 
            +
                    # Method for creating a new directory, called from mkdir
         | 
| 197 | 
            +
                    # @param [String] _name
         | 
| 198 | 
            +
                    # @return [FuseOperations] something representing a directory
         | 
| 199 | 
            +
                    def new_dir(_name)
         | 
| 200 | 
            +
                      VirtualDir.new(accounting: accounting)
         | 
| 217 201 | 
             
                    end
         | 
| 218 202 |  | 
| 219 | 
            -
                    #  | 
| 220 | 
            -
                     | 
| 221 | 
            -
             | 
| 222 | 
            -
             | 
| 223 | 
            -
             | 
| 224 | 
            -
             | 
| 225 | 
            -
             | 
| 203 | 
            +
                    # Create a new hard link in this filesystem
         | 
| 204 | 
            +
                    #
         | 
| 205 | 
            +
                    # @param [String, nil] from_path
         | 
| 206 | 
            +
                    # @param [String] to_path
         | 
| 207 | 
            +
                    # @yield [existing]
         | 
| 208 | 
            +
                    #   Used to retrieve the filesystem object at from_path to be linked at to_path
         | 
| 209 | 
            +
                    #
         | 
| 210 | 
            +
                    #   If not supplied, a proc wrapping #{new_link} is created and used or passed on to sub-filesystems
         | 
| 211 | 
            +
                    # @yieldparam [FuseOperations] existing the object currently at to_path
         | 
| 212 | 
            +
                    # @yieldreturn [FuseOperations] an object representing an inode to be linked at to_path
         | 
| 213 | 
            +
                    # @raise [Errno::EISDIR] if this object is trying to be added as a link (since you can't hard link directories)
         | 
| 214 | 
            +
                    # @see new_link
         | 
| 215 | 
            +
                    def link(from_path, to_path, &linker)
         | 
| 216 | 
            +
                      # Can't link to a directory
         | 
| 217 | 
            +
                      raise Errno::EISDIR if root?(to_path)
         | 
| 218 | 
            +
                      raise Errno::ENOSYS unless from_path || linker
         | 
| 219 | 
            +
             | 
| 220 | 
            +
                      same_filesystem_method(__method__, from_path, to_path) do
         | 
| 221 | 
            +
                        linker ||= proc { |replacing| new_link(from_path, replacing) }
         | 
| 222 | 
            +
                        path_method(__method__, from_path, to_path, block: linker) do |link_name, existing|
         | 
| 223 | 
            +
                          linked_entry = linker.call(existing)
         | 
| 224 | 
            +
                          entries[link_name] = linked_entry
         | 
| 225 | 
            +
                        end
         | 
| 226 | 
            +
                      end
         | 
| 226 227 | 
             
                    end
         | 
| 227 228 |  | 
| 228 | 
            -
                    #  | 
| 229 | 
            -
                     | 
| 230 | 
            -
             | 
| 231 | 
            -
             | 
| 229 | 
            +
                    # Called from within #{link}
         | 
| 230 | 
            +
                    #   Uses #{getattr}(from_path) to find the filesystem object at from_path.
         | 
| 231 | 
            +
                    #   Calls #{link}(nil, '/') on this object to signal that a new link has been created to it.
         | 
| 232 | 
            +
                    #   Filesystem objects that do not support linking should raise `Errno::EPERM` if the object should not be hard
         | 
| 233 | 
            +
                    #   linked (eg directories)
         | 
| 234 | 
            +
                    # @return [FuseOperations]
         | 
| 235 | 
            +
                    # @raise Errno::EXIST if there is an existing object to replace
         | 
| 236 | 
            +
                    # @raise Errno::EPERM if the object at from_path is not a filesystem (does not itself respond to #getattr)
         | 
| 237 | 
            +
                    def new_link(from_path, replacing)
         | 
| 238 | 
            +
                      raise Errno::EEXIST if replacing
         | 
| 239 | 
            +
             | 
| 240 | 
            +
                      linked_entry = getattr(from_path)
         | 
| 241 | 
            +
             | 
| 242 | 
            +
                      # the linked entry itself must represent a filesystem inode
         | 
| 243 | 
            +
                      raise Errno::EPERM unless entry_fuse_respond_to?(linked_entry, :getattr)
         | 
| 244 | 
            +
             | 
| 245 | 
            +
                      entry_send(linked_entry, :link, nil, '/')
         | 
| 246 | 
            +
                      linked_entry
         | 
| 247 | 
            +
                    end
         | 
| 248 | 
            +
             | 
| 249 | 
            +
                    # For our entries validates the entry exists and calls unlink('/') on it to do any cleanup
         | 
| 250 | 
            +
                    # before removing the entry from our entries list.
         | 
| 251 | 
            +
                    #
         | 
| 252 | 
            +
                    # If a block is supplied (eg #{rename}) it will be called before the entry is deleted
         | 
| 253 | 
            +
                    #
         | 
| 254 | 
            +
                    # @raise [Errno:EISDIR] if we are unlinking ourself (use rmdir instead)
         | 
| 255 | 
            +
                    # @raise [Errno::ENOENT] if the entry does not exist at path (and no block is provided)
         | 
| 256 | 
            +
                    # @return the unlinked filesystem object
         | 
| 257 | 
            +
                    # @yield(file_name, entry)
         | 
| 258 | 
            +
                    # @yieldparam [FuseOperations] entry a filesystem like object representing the file being unlinked
         | 
| 259 | 
            +
                    # @yieldreturn [void]
         | 
| 260 | 
            +
                    def unlink(path, &rename)
         | 
| 261 | 
            +
                      raise Errno::EISDIR if root?(path)
         | 
| 262 | 
            +
             | 
| 263 | 
            +
                      path_method(__method__, path, block: rename) do |entry_key, entry|
         | 
| 264 | 
            +
                        if rename
         | 
| 265 | 
            +
                          rename.call(entry)
         | 
| 266 | 
            +
                        elsif entry
         | 
| 267 | 
            +
                          entry_send(entry, :unlink, '/')
         | 
| 268 | 
            +
                        else
         | 
| 269 | 
            +
                          raise Errno::ENOENT
         | 
| 270 | 
            +
                        end
         | 
| 271 | 
            +
             | 
| 272 | 
            +
                        entries.delete(entry_key)
         | 
| 273 | 
            +
                      end
         | 
| 274 | 
            +
                    end
         | 
| 275 | 
            +
             | 
| 276 | 
            +
                    # Rename is handled via #{link} and #{unlink} using their respective block arguments to handle validation
         | 
| 277 | 
            +
                    # and retrieve the object at from_path. Intermediate directory filesystems are only required to pass on the
         | 
| 278 | 
            +
                    # block, while the final directory target of from_path and to_path must call these blocks as this class does.
         | 
| 279 | 
            +
                    #
         | 
| 280 | 
            +
                    # If to_path is being replaced the existing entry will be signaled via #{unlink}('/'), or #{rmdir}('/')
         | 
| 281 | 
            +
                    # @raise Errno::EINVAL if trying to rename the root object OR from_path is a directory prefix of to_path
         | 
| 282 | 
            +
                    # @raise Errno::ENOENT if the filesystem at from_path does not exist
         | 
| 283 | 
            +
                    # @raise Errno::ENOSYS if the filesystem at from_path or directory of to_path does not support rename
         | 
| 284 | 
            +
                    # @raise Errno::EEXIST if the filesystem at to_path already exists and is not a symlink
         | 
| 285 | 
            +
                    # @see POSIX rename(2)
         | 
| 286 | 
            +
                    # @note As per POSIX raname(2) silently succeeds if from_path and to_path are hard links to the
         | 
| 287 | 
            +
                    # same filesystem object (ie without unlinking from_path)
         | 
| 288 | 
            +
                    def rename(from_path, to_path)
         | 
| 289 | 
            +
                      return if from_path == to_path
         | 
| 290 | 
            +
                      raise Errno::EINVAL if root?(from_path)
         | 
| 291 | 
            +
             | 
| 292 | 
            +
                      same_filesystem_method(__method__, from_path, to_path, rescue_notsup: true) do
         | 
| 293 | 
            +
                        # Can't rename into a subdirectory of itself
         | 
| 294 | 
            +
                        raise Errno::EINVAL if to_path.start_with?("#{from_path}/")
         | 
| 295 | 
            +
             | 
| 296 | 
            +
                        # POSIX rename(2) requires to silently abandon, without unlinking from_path,
         | 
| 297 | 
            +
                        # if the inodes at from_path and to_path are the same object (ie hard linked to each other))
         | 
| 298 | 
            +
                        catch :same_hard_link do
         | 
| 299 | 
            +
                          link(nil, to_path) do |replacing|
         | 
| 300 | 
            +
                            check_rename_unlink(from_path)
         | 
| 301 | 
            +
                            unlink(from_path) do |source|
         | 
| 302 | 
            +
                              raise Errno::ENOENT unless source
         | 
| 303 | 
            +
             | 
| 304 | 
            +
                              throw :same_hard_link if source.equal?(replacing)
         | 
| 305 | 
            +
                              rename_cleanup_overwritten(replacing)
         | 
| 306 | 
            +
                            end
         | 
| 307 | 
            +
                          end
         | 
| 308 | 
            +
                        end
         | 
| 309 | 
            +
                      end
         | 
| 310 | 
            +
                    end
         | 
| 311 | 
            +
             | 
| 312 | 
            +
                    # Common between {#link} and {#rename} are callbacks that might have different semantics
         | 
| 313 | 
            +
                    # if called within the same sub-filesystem.
         | 
| 314 | 
            +
                    # While from_path and to_path have a common top level directory, we pass the callback on
         | 
| 315 | 
            +
                    # to the entry at that directory
         | 
| 316 | 
            +
                    def same_filesystem_method(callback, from_path, to_path, rescue_notsup: false)
         | 
| 317 | 
            +
                      return yield unless from_path # no from_path to traverse
         | 
| 318 | 
            +
             | 
| 319 | 
            +
                      to_dir, next_to_path = entry_path(to_path)
         | 
| 320 | 
            +
                      return yield if root?(next_to_path) # target is our entry, no more directories to traverse
         | 
| 321 | 
            +
             | 
| 322 | 
            +
                      from_dir, next_from_path = entry_path(from_path)
         | 
| 323 | 
            +
                      return yield if from_dir != to_dir # from and to in different directories, we need to handle it ourself
         | 
| 324 | 
            +
             | 
| 325 | 
            +
                      # try traverse into sub-fs, which must itself be a directory
         | 
| 326 | 
            +
                      begin
         | 
| 327 | 
            +
                        entry_send(
         | 
| 328 | 
            +
                          entries[to_dir], callback,
         | 
| 329 | 
            +
                          next_from_path, next_to_path,
         | 
| 330 | 
            +
                          notsup: Errno::ENOSYS, notdir: Errno::ENOTDIR, rescue_notsup: rescue_notsup
         | 
| 331 | 
            +
                        )
         | 
| 332 | 
            +
                      rescue Errno::ENOSYS, Errno::ENOTSUP
         | 
| 333 | 
            +
                        raise unless rescue_notsup
         | 
| 334 | 
            +
             | 
| 335 | 
            +
                        yield
         | 
| 336 | 
            +
                      end
         | 
| 337 | 
            +
                    end
         | 
| 338 | 
            +
             | 
| 339 | 
            +
                    # Creates a new symbolic link in this directory
         | 
| 340 | 
            +
                    # @param [String] target - an absolute path for the operating system or relative to path
         | 
| 341 | 
            +
                    # @param [String] path - the path to create the link at
         | 
| 342 | 
            +
                    def symlink(target, path)
         | 
| 343 | 
            +
                      path_method(__method__, target, path) do |link_name, existing|
         | 
| 344 | 
            +
                        raise Errno::EEXIST if existing
         | 
| 345 | 
            +
             | 
| 346 | 
            +
                        new_link = new_symlink(link_name)
         | 
| 347 | 
            +
                        entry_send(new_link, :symlink, target, '/')
         | 
| 348 | 
            +
                        entries[link_name] = new_link
         | 
| 349 | 
            +
                      end
         | 
| 350 | 
            +
                    end
         | 
| 351 | 
            +
             | 
| 352 | 
            +
                    def new_symlink(_name)
         | 
| 353 | 
            +
                      VirtualLink.new(accounting: accounting)
         | 
| 232 354 | 
             
                    end
         | 
| 233 355 |  | 
| 234 356 | 
             
                    # @!endgroup
         | 
| 235 357 |  | 
| 236 | 
            -
                    #  | 
| 237 | 
            -
                    # | 
| 358 | 
            +
                    # Finds the path argument of the callback and splits it into an entry in this directory and a remaining path
         | 
| 359 | 
            +
                    #
         | 
| 360 | 
            +
                    # If a block is given and there is no remaining path (ie our entry) the block is called and its value returned
         | 
| 361 | 
            +
                    #
         | 
| 362 | 
            +
                    # If the path is not our entry, the callback is passed on to the sub filesystem entry with the remaining path
         | 
| 363 | 
            +
                    #
         | 
| 364 | 
            +
                    # If the path is our entry, but not block is provided, the callback is passed to our entry with a path of '/'
         | 
| 365 | 
            +
                    #
         | 
| 238 366 | 
             
                    # @param [Symbol] callback a FUSE Callback
         | 
| 239 | 
            -
                    # @param [ | 
| 240 | 
            -
                    # @param [ | 
| 241 | 
            -
                    # @param [Proc]  | 
| 242 | 
            -
                    # @param [Class<SystemCallError>] notsup
         | 
| 367 | 
            +
                    # @param [Array] args callback arguments (first argument is typically 'path')
         | 
| 368 | 
            +
                    # @param [Errno] notsup an error to raise if this callback is not supported by our entry
         | 
| 369 | 
            +
                    # @param [Proc] block optional block to keep passing down. See {#mkdir}, {#create}, {#link}
         | 
| 243 370 | 
             
                    # @raise [Errno:ENOENT] if the next entry does not exist
         | 
| 371 | 
            +
                    # @raise [Errno::ENOTDIR] if the next entry must be a directory, but does not respond to :raaddir
         | 
| 244 372 | 
             
                    # @raise [SystemCallError] error from notsup if the next entry does not respond to ths callback
         | 
| 245 | 
            -
                     | 
| 246 | 
            -
             | 
| 247 | 
            -
             | 
| 248 | 
            -
             | 
| 249 | 
            -
                       | 
| 250 | 
            -
                       | 
| 373 | 
            +
                    # @yield(entry_key, entry)
         | 
| 374 | 
            +
                    # @yieldparam [String,nil] entry_key the name of the entry in this directory or nil, if path is '/'
         | 
| 375 | 
            +
                    # @yieldparam [FuseOperations,nil] entry the filesystem object currently stored at entry_key
         | 
| 376 | 
            +
                    def path_method(callback, *args, notsup: Errno::ENOTSUP, block: nil)
         | 
| 377 | 
            +
                      # Inside path_method
         | 
| 378 | 
            +
                      _read_arg_method, path_arg_method, next_arg_method = FuseOperations.path_arg_methods(callback)
         | 
| 379 | 
            +
                      path = args.send(path_arg_method)
         | 
| 251 380 |  | 
| 252 | 
            -
                       | 
| 381 | 
            +
                      entry_key, next_path = entry_path(path)
         | 
| 382 | 
            +
                      our_entry = root?(next_path)
         | 
| 253 383 |  | 
| 254 | 
            -
                       | 
| 255 | 
            -
                      return unless responds || notsup
         | 
| 256 | 
            -
                      raise notsup unless responds
         | 
| 384 | 
            +
                      return yield entry_key, entries[entry_key] if block_given? && our_entry
         | 
| 257 385 |  | 
| 258 | 
            -
                       | 
| 259 | 
            -
             | 
| 260 | 
            -
                      end
         | 
| 386 | 
            +
                      # Pass to our entry
         | 
| 387 | 
            +
                      args.send(next_arg_method, next_path)
         | 
| 261 388 |  | 
| 262 | 
            -
                       | 
| 263 | 
            -
                       | 
| 264 | 
            -
                      entry.public_send(callback, next_path, *args, &invoke)
         | 
| 389 | 
            +
                      notdir = Errno::ENOTDIR unless our_entry
         | 
| 390 | 
            +
                      entry_send(entries[entry_key], callback, *args, notsup: notsup, notdir: notdir, &block)
         | 
| 265 391 | 
             
                    end
         | 
| 266 392 |  | 
| 267 393 | 
             
                    private
         | 
| 268 394 |  | 
| 269 | 
            -
                     | 
| 270 | 
            -
             | 
| 271 | 
            -
                    def method_missing(method, *args, &invoke)
         | 
| 395 | 
            +
                    def method_missing(method, *args, &block)
         | 
| 272 396 | 
             
                      return super unless FuseOperations.path_callbacks.include?(method)
         | 
| 273 397 |  | 
| 274 | 
            -
                       | 
| 275 | 
            -
             | 
| 276 | 
            -
                      path_method(method, *args, &invoke)
         | 
| 398 | 
            +
                      path_method(method, *args, block: block)
         | 
| 277 399 | 
             
                    end
         | 
| 278 400 |  | 
| 279 401 | 
             
                    def respond_to_missing?(method, inc_private = false)
         | 
| @@ -282,23 +404,68 @@ module FFI | |
| 282 404 | 
             
                      super
         | 
| 283 405 | 
             
                    end
         | 
| 284 406 |  | 
| 285 | 
            -
                     | 
| 286 | 
            -
             | 
| 287 | 
            -
                     | 
| 407 | 
            +
                    # Split path into an entry key and remaining path
         | 
| 408 | 
            +
                    # @param [:to_s] path
         | 
| 409 | 
            +
                    # @return [nil] if path is root (or nil)
         | 
| 410 | 
            +
                    # @return [Array<String, String] entry_key and '/' if path refers to an entry in this directory
         | 
| 411 | 
            +
                    # @return [Array<String, String>] entry key and remaining path when path refers to an entry in a sub-directory
         | 
| 412 | 
            +
                    def entry_path(path)
         | 
| 413 | 
            +
                      return nil unless path
         | 
| 288 414 |  | 
| 289 | 
            -
             | 
| 290 | 
            -
                       | 
| 291 | 
            -
             | 
| 415 | 
            +
                      path = path.to_s
         | 
| 416 | 
            +
                      return nil if root?(path)
         | 
| 417 | 
            +
             | 
| 418 | 
            +
                      # Fuse paths always start with a leading slash and never have a trailing slash
         | 
| 419 | 
            +
                      sep_index = path.index('/', 1)
         | 
| 420 | 
            +
             | 
| 421 | 
            +
                      return [path[1..], '/'] unless sep_index
         | 
| 422 | 
            +
             | 
| 423 | 
            +
                      [path[1..sep_index - 1], path[sep_index..]]
         | 
| 292 424 | 
             
                    end
         | 
| 293 425 |  | 
| 294 426 | 
             
                    def entry_fuse_respond_to?(entry_fs, method)
         | 
| 295 427 | 
             
                      entry_fs.respond_to?(:fuse_respond_to?) ? entry_fs.fuse_respond_to?(method) : entry_fs.respond_to?(method)
         | 
| 296 428 | 
             
                    end
         | 
| 297 429 |  | 
| 298 | 
            -
                    def  | 
| 299 | 
            -
                       | 
| 430 | 
            +
                    def dir_entry?(entry)
         | 
| 431 | 
            +
                      entry_fuse_respond_to?(entry, :readdir)
         | 
| 432 | 
            +
                    end
         | 
| 433 | 
            +
             | 
| 434 | 
            +
                    def entry_send(entry, callback, *args, notsup: nil, notdir: nil, rescue_notsup: notsup.nil?, &blk)
         | 
| 435 | 
            +
                      raise Errno::ENOENT unless entry
         | 
| 436 | 
            +
                      raise notdir if notdir && !dir_entry?(entry)
         | 
| 437 | 
            +
             | 
| 438 | 
            +
                      responds = entry_fuse_respond_to?(entry, callback)
         | 
| 439 | 
            +
                      return unless responds || notsup
         | 
| 440 | 
            +
                      raise notsup unless responds
         | 
| 441 | 
            +
             | 
| 442 | 
            +
                      entry.public_send(callback, *args, &blk)
         | 
| 443 | 
            +
                    rescue Errno::ENOTSUP, Errno::ENOSYS
         | 
| 444 | 
            +
                      raise unless rescue_notsup
         | 
| 445 | 
            +
             | 
| 446 | 
            +
                      nil
         | 
| 447 | 
            +
                    end
         | 
| 448 | 
            +
             | 
| 449 | 
            +
                    def check_rename_unlink(from_path)
         | 
| 450 | 
            +
                      # Safety check that the unlink proc is passed through to the final directory
         | 
| 451 | 
            +
                      # to explicitly support our rename proc.
         | 
| 452 | 
            +
                      rename_support = false
         | 
| 453 | 
            +
                      unlink("#{from_path}.__unlink_rename__") do |source|
         | 
| 454 | 
            +
                        rename_support = source.nil?
         | 
| 455 | 
            +
                      end
         | 
| 456 | 
            +
                      raise Errno::ENOSYS, 'rename via unlink not supported' unless rename_support
         | 
| 457 | 
            +
                    end
         | 
| 458 | 
            +
             | 
| 459 | 
            +
                    # Cleanup the object being overwritten, including potentially raising SystemCallError
         | 
| 460 | 
            +
                    # to prevent the rename going ahead
         | 
| 461 | 
            +
                    def rename_cleanup_overwritten(replacing)
         | 
| 462 | 
            +
                      return unless replacing
         | 
| 463 | 
            +
             | 
| 464 | 
            +
                      entry_send(replacing, dir_entry?(replacing) ? :rmdir : :unlink, '/')
         | 
| 465 | 
            +
                    end
         | 
| 300 466 |  | 
| 301 | 
            -
             | 
| 467 | 
            +
                    def root?(path)
         | 
| 468 | 
            +
                      path ? super : true
         | 
| 302 469 | 
             
                    end
         | 
| 303 470 | 
             
                  end
         | 
| 304 471 | 
             
                end
         |