rapid7/ruby_smb

View on GitHub
lib/ruby_smb/server/share/provider/virtual_disk/virtual_pathname.rb

Summary

Maintainability
B
4 hrs
Test Coverage
module RubySMB
  class Server
    module Share
      module Provider
        class VirtualDisk < Disk
          # This object emulates Ruby's builtin Pathname object but uses a virtual file system instead of the real local
          # one.
          class VirtualPathname
            SEPARATOR = File::SEPARATOR
            # see: https://ruby-doc.org/stdlib-3.1.1/libdoc/pathname/rdoc/Pathname.html
            STAT_METHODS = %i[
              atime
              birthtime
              blockdev?
              chardev?
              ctime
              directory?
              executable?
              file?
              ftype
              grpowned?
              mtime
              owned?
              pipe?
              readable?
              setgid?
              setuid?
              size
              socket?
              sticky?
              symlink?
              world_readable?
              world_writable?
              writable?
              zero?
            ]
            private_constant :STAT_METHODS

            attr_accessor :virtual_disk

            # @param [Hash] disk The mapping of paths to objects representing the virtual file system.
            # @param [String] path The path of this entry.
            # @param [Boolean] exist? Whether or not this entry represents an existing entry in the virtual file system.
            # @param [File::Stat] stat An explicit stat that represents this object. A default VirtualStat will be
            #   created unless specified.
            def initialize(disk, path, **kwargs)
              @virtual_disk = disk
              @path = path

              if kwargs.fetch(:exist?, true)
                if kwargs[:stat]
                  if kwargs[:stat].is_a?(Hash)
                    @stat = VirtualStat.new(**kwargs[:stat])
                  else
                    @stat = kwargs[:stat]
                  end
                else
                  @stat = VirtualStat.new
                end
              else
                raise ArgumentError.new('can not specify a stat object when exist? is false') if kwargs[:stat]
                @stat = nil
              end
            end

            def ==(other)
              other.is_a?(self.class) && other.to_s == to_s
            end

            def <=>(other)
              to_s <=> other.to_s
            end

            def exist?
              !@stat.nil?
            rescue Errno::ENOENT
              false
            end

            def stat
              raise Errno::ENOENT.new('No such file or directory') unless exist? && (@stat.file? || @stat.directory?)

              @stat
            end

            def join(other)
              # per the docs this Pathname#join doesn't touch the file system
              # see: https://ruby-doc.org/stdlib-3.1.1/libdoc/pathname/rdoc/Pathname.html#class-Pathname-label-Core+methods
              lookup_or_create(Pathname.new(to_s).join(other).to_s)
            end

            alias :+ :join
            alias :/ :join

            def to_s
              @path
            end

            def absolute?
              to_s.start_with?(SEPARATOR)
            end

            def relative?
              !absolute?
            end

            def basename
              lookup_or_create(self.class.basename(to_s))
            end

            def self.basename(*args)
              File.basename(*args)
            end

            def dirname
              lookup_or_create(self.class.dirname(to_s))
            end

            def self.dirname(*args)
              File.dirname(*args)
            end

            def extname
              File.extname(to_s)
            end

            def split
              [dirname, basename]
            end

            alias :parent :dirname

            def children(with_directory=true)
              raise Errno::ENOTDIR.new("Not a directory @ dir_initialize - #{to_s}") unless directory?

              @virtual_disk.each_value.select { |dent| dent.dirname == self && dent != self }.map { |dent| with_directory ? dent : dent.basename }
            end

            def entries
              children(false)
            end

            def cleanpath(consider_symlink=false)
              lookup_or_create(self.class.cleanpath(to_s), stat: (exist? ? stat : nil))
            end

            def self.cleanpath(path)
              # per the docs this Pathname#cleanpath doesn't touch the file system
              # see: https://ruby-doc.org/stdlib-3.1.1/libdoc/pathname/rdoc/Pathname.html#class-Pathname-label-Core+methods
              Pathname.new(path).cleanpath.to_s
            end

            private

            # Check the virtual file system to see if the entry exists. Return it if it does, otherwise create a new
            # entry representing a non-existent path.
            #
            # @param [String] path The path to lookup in the virtual file system. It will be normalized using #cleanpath.
            # @return [Pathname] The path object representing the specified string.
            def lookup_or_create(path, **kwargs)
              existing = @virtual_disk[self.class.cleanpath(path)]
              return existing if existing

              kwargs[:exist?] = false
              VirtualPathname.new(@virtual_disk, path, **kwargs)
            end

            def method_missing(symbol, *args)
              # should we forward to one of the stat methods
              if STAT_METHODS.include?(symbol)
                # if we have a stat object then forward it
                return stat.send(symbol, *args) if exist?
                # if we don't have a stat object, emulate what Pathname does when it does not exist

                # these two methods return nil
                return nil if %i[ world_readable? world_writable? ].include?(symbol)

                # any of the other ?-suffixed methods return false
                return false if symbol.to_s.end_with?('?')

                # any other method raises a Errno::ENOENT exception
                raise Errno::ENOENT.new('No such file or directory')
              end

              raise NoMethodError, "undefined method `#{symbol}' for #{self.class}"
            end

            def respond_to_missing?(symbol, include_private = false)
              STAT_METHODS.include?(symbol)
            end
          end
        end
      end
    end
  end
end