# -*- coding: binary -*-

# Concerns loading executables from a directory as modules
class Msf::Modules::Loader::Executable < Msf::Modules::Loader::Base
  # Returns true if the path is a directory
  #
  # @param (see Msf::Modules::Loader::Base#loadable?)
  # @return [true] if path is a directory
  # @return [false] otherwise
  def loadable?(path)
    File.directory?(path)
  end

  def loadable_module?(parent_path, type, module_reference_name)
    full_path = module_path(parent_path, type, module_reference_name)
    script_path?(full_path)
  end

  protected

  # Yields the module_reference_name for each module file found under the directory path.
  #
  # @param [String] path The path to the directory.
  # @param [Hash] opts Input Hash.
  # @yield (see Msf::Modules::Loader::Base#each_module_reference_name)
  # @yieldparam [String] path The path to the directory.
  # @yieldparam [String] type The type correlated with the directory under path.
  # @yieldparam module_reference_name (see Msf::Modules::Loader::Base#each_module_reference_name)
  # @return (see Msf::Modules::Loader::Base#each_module_reference_name)
  def each_module_reference_name(path, opts={})
    whitelist = opts[:whitelist] || []
    ::Dir.foreach(path) do |entry|
      full_entry_path = ::File.join(path, entry)
      type = entry.singularize

      unless ::File.directory?(full_entry_path) && module_manager.type_enabled?(type)
        next
      end

      full_entry_pathname = Pathname.new(full_entry_path)

      # Try to load modules from all the files in the supplied path
      Rex::Find.find(full_entry_path) do |entry_descendant_path|
        # Assume that all modules are scripts for now, workaround
        # filesystems where all files are labeled as executable.
        if script_path?(entry_descendant_path)
          entry_descendant_pathname = Pathname.new(entry_descendant_path)
          relative_entry_descendant_pathname = entry_descendant_pathname.relative_path_from(full_entry_pathname)
          relative_entry_descendant_path = relative_entry_descendant_pathname.to_s
          next if File::basename(relative_entry_descendant_path).start_with?('example')
          # The module_reference_name doesn't have a file extension
          module_reference_name = File.join(File.dirname(relative_entry_descendant_path), File.basename(relative_entry_descendant_path, '.*'))

          yield path, type, module_reference_name
        end
      end
    end
  end

  # Returns the full path to the module file on disk.
  #
  # @param (see Msf::Modules::Loader::Base#module_path)
  # @return [String] Path to module file on disk.
  def module_path(parent_path, type, module_reference_name)
    # The extension is lost on loading, hit the disk to recover :(
    partial_path = File.join(DIRECTORY_BY_TYPE[type], module_reference_name)
    full_path = File.join(parent_path, partial_path)

    Rex::Find.find(File.dirname(full_path)) do |mod|
      if File.basename(full_path, '.*') == File.basename(mod, '.*')
        return File.join(File.dirname(full_path), File.basename(mod))
      end
    end

    ''
  end

  # Loads the module content from the on disk file.
  #
  # @param (see Msf::Modules::Loader::Base#read_module_content)
  # @return (see Msf::Modules::Loader::Base#read_module_content)
  def read_module_content(parent_path, type, module_reference_name)
    full_path = module_path(parent_path, type, module_reference_name)

    read_module_content_from_path(full_path)
  end

  # Loads the module content from the on disk file.
  #
  # @param (see Msf::Modules::Loader::Base#read_module_content_from_path)
  # @return (see Msf::Modules::Loader::Base#read_module_content_from_path)
  def read_module_content_from_path(full_path)
    unless File.executable?(full_path)
      load_error(full_path, Errno::ENOENT.new)
      return ''
    end
    begin
      content = Msf::Modules::External::Shim.generate(full_path, @module_manager.framework)
      if content
        return content
      else
        elog "Unable to load module #{full_path}, unknown module type"
        return ''
      end
    rescue LoadError => e
      load_error(full_path, e)
      return ''
    rescue ::Exception => e
      elog("Unable to load module #{full_path}", error: e)
      # XXX migrate this to a full load_error when we can tell the user why the
      # module did not load and/or how to resolve it.
      # load_error(full_path, e)
      ''
    end
  end
end
