# frozen_string_literal: true # # Copyright (c) 2021-2024 Hal Brodigan (postmodern.mod3 at gmail.com) # # ronin-core is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published # by the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # ronin-core is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License # along with ronin-core. If not, see . # module Ronin module Core # # A mixin that adds a class registry to a library: # # ### Example # # `lib/ronin/exploits.rb`: # # require 'ronin/core/class_registry' # # module Ronin # module Exploits # include Ronin::Core::ClassRegistry # # class_dir "#{__dir__}/classes" # end # end # # `lib/ronin/exploits/exploit.rb`: # # module Ronin # module Exploits # class Exploit # # def self.register(name) # Exploits.register(name,self) # end # # end # end # end # # `lib/ronin/exploits/my_exploit.rb`: # # require 'ronin/exploits/exploit' # # module Ronin # module Exploits # class MyExploit < Exploit # # register 'my_exploit' # # end # end # end # # @api semipublic # module ClassRegistry class ClassNotFound < RuntimeError end # # Extends {ClassMethods}. # # @param [Module] namespace # The module that is including {ClassRegistry}. # def self.included(namespace) namespace.extend ClassMethods end # # Class-methods. # module ClassMethods # # Gets or sets the class directory path. # # @param [String, nil] new_dir # The new class directory path. # # @return [String] # The class directory path. # # @raise [NotImplementedError] # The `class_dir` method was not defined in the module. # # @example # class_dir "#{__dir__}/classes" # def class_dir(new_dir=nil) if new_dir @class_dir = new_dir else @class_dir || raise(NotImplementedError,"#{self} did not define a class_dir") end end # # Lists all class files within {#class_dir}. # # @return [Array] # The list of class paths within {#class_dir}. # def list_files paths = Dir.glob('{**/}*.rb', base: class_dir) paths.each { |path| path.chomp!('.rb') } return paths end # # The class registry. # # @return [Hash{String => Class}] # The mapping of class `id` and classes. # def registry @registry ||= {} end # # Registers a class with the registry. # # @param [String] id # The class `id` to be registered. # # @param [Class] mod # The class to be registered. # # @example # Exploits.register('myexploit',MyExploit) # def register(id,mod) registry[id] = mod end # # Finds the path for the class `id`. # # @param [String] id # The class `id`. # # @return [String, nil] # The path for the module. If the module file does not exist in # {#class_dir} then `nil` will be returned. # # @example # Exploits.path_for('my_exploit') # # => "/path/to/lib/ronin/exploits/classes/my_exploit.rb" # def path_for(id) path = File.join(class_dir,"#{id}.rb") if File.file?(path) return path end end # # Loads a class from a file. # # @param [String] file # The file to load. # # @return [Class] # The loaded class. # # @raise [ClassNotFound] # The file does not exist or the class `id` was not found within the # file. # # @raise [LoadError] # A load error occurred while requiring the other files required by # the class file. # def load_class_from_file(file) file = File.expand_path(file) unless File.file?(file) raise(ClassNotFound,"no such file or directory: #{file.inspect}") end require(file) registry.each_value do |worker_class| class_file, = worker_class.const_source_location(worker_class.name) return worker_class if class_file == file end raise(ClassNotFound,"file did not register a class: #{file.inspect}") end # # Loads a class from the {#class_dir}. # # @param [String] id # The class `id` to load. # # @return [Class] # The loaded class. # # @raise [ClassNotFound] # The class file could not be found within {#class_dir}.or has # a file/registered-name mismatch. # # @raise [LoadError] # A load error occurred while requiring the other files required by # the class file. # def load_class(id) # short-circuit if the module is already loaded if (klass = registry[id]) return klass end unless (path = path_for(id)) raise(ClassNotFound,"could not find file for #{id.inspect}") end previous_entries = registry.keys require(path) unless (klass = registry[id]) new_entries = registry.keys - previous_entries if new_entries.empty? raise(ClassNotFound,"file did not register a class: #{path.inspect}") else raise(ClassNotFound,"file registered a class with a different id (#{new_entries.map(&:inspect).join(', ')}): #{path.inspect}") end end return klass end end end end end