# frozen_string_literal: true
#
# Copyright (c) 2021-2023 Hal Brodigan (postmodern.mod3 at gmail.com)
#
# ronin-repos 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-repos 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-repos. If not, see .
#
require 'ronin/repos/exceptions'
require 'ronin/repos/repository'
require 'ronin/core/home'
require 'set'
module Ronin
module Repos
#
# Manages the `~/.cache/ronin-repos/` directory and the repositories
# contained within.
#
# @api private
#
class CacheDir
include Enumerable
# The ~/.cache/ronin-repos/ directory where all repos are stored.
PATH = Core::Home.cache_dir('ronin-repos')
# The path to the cache directory.
#
# @return [String]
attr_reader :path
#
# Initializes the repository cache.
#
# @param [String] path
# The path to the repository cache directory.
#
def initialize(path=PATH)
@path = path
end
#
# Accesses a repository from the cache directory.
#
# @param [String] name
# The name of the repository.
#
# @return [Repository]
# The repository from the cache.
#
# @raise [RepositoryNotFound]
# No repository exists with the given name in the cache directory.
#
def [](name)
path = File.join(@path,name.to_s)
unless File.directory?(path)
raise(RepositoryNotFound,"repository not found: #{name.inspect}")
end
return Repository.new(path)
end
#
# Enumerates through every repository in the cache directory.
#
# @yield [repo]
# The given block will be passed each repository.
#
# @yieldparam [Repository] repo
# A repository from the cache directory.
#
# @return [Enumerator]
# If no block is given, an Enumerator will be returned.
#
def each
return enum_for unless block_given?
each_child_directory do |path|
yield Repository.new(path)
end
end
#
# Clones and installs a repository into the cache directory.
#
# @param [String, URI::HTTPS] uri
# The URI to clone the repository from.
#
# @param [String, nil] name
# The explicit name of the repository to use. Defaults to the base-name
# of the URI's path, sans any `.git` extension.
#
# @return [Repository]
# The newly installed repository.
#
# @raise [CommandFailed]
# One of the `git` commands failed.
#
# @raise [CommandNotInstalled]
# The `git` command is not installed.
#
def install(uri,name=nil)
uri = uri.to_s
name ||= File.basename(uri,File.extname(uri))
path = File.join(@path,name)
return Repository.install(uri,path)
end
#
# Updates all repositories in the cache directory.
#
# @raise [CommandNotInstalled]
# The `git` command is not installed.
#
def update
each do |repo|
begin
repo.update
rescue CommandFailed
end
end
end
#
# Removes a repository from the cache directory.
#
# @param [String] name
# The repository name to find and delete.
#
# @raise [RepositoryNotFound]
# The repository with the given name does not exist in the cache
# directory.
#
def remove(name)
self[name].delete
end
#
# Deletes all repositories in the cache directory.
#
def purge
each(&:delete)
end
#
# Finds the first matching file.
#
# @param [String] path
# The relative path of the file.
#
# @return [String, nil]
# The absolute path of the matching file or `nil` if no matching file
# could be found.
#
# @example
# repos.find_file("wordlists/wordlist.txt")
# # => "/home/user/.cache/ronin-repos/foo-repo/wordlists/wordlist.txt"
#
def find_file(path)
each do |repo|
if (file = repo.find_file(path))
return file
end
end
end
#
# Finds all files in all repos that matches the glob pattern.
#
# @param [String] pattern
# The file glob pattern to search for.
#
# @return [Array]
# The absolute paths to the files that match the glob pattern.
#
# @example
# repos.glob("wordlists/*.txt")
# # => ["/home/user/.cache/ronin-repos/foo-repo/wordlists/cities.txt",
# # "/home/user/.cache/ronin-repos/foo-repo/wordlists/states.txt",
# # "/home/user/.cache/ronin-repos/bar-repo/wordlists/bands.txt",
# # "/home/user/.cache/ronin-repos/bar-repo/wordlists/beers.txt"]
#
def glob(pattern,&block)
return enum_for(__method__,pattern).to_a unless block_given?
each do |repo|
repo.glob(pattern,&block)
end
end
#
# Lists all files across all repos installed in the cache directory.
#
# @param [String] pattern
# The optional glob pattern to use to list specific files.
#
# @return [Set]
# The matching paths within the repository.
#
# @example
# repos.list_files('exploits/{**/}*.rb')
# # => #
#
def list_files(pattern='{**/}*.*')
each_with_object(Set.new) do |repo,files|
files.merge(repo.list_files(pattern))
end
end
#
# Converts the cache directory to a String.
#
# @return [String]
# The path to the cache directory.
#
def to_s
@path
end
private
#
# Enumerates over each directory in the cache directory.
#
# @yield [dir]
# The given block will be passed each repository's directory path.
#
# @yieldparam [String] dir
# A path to a repository directory within the cache directory.
#
# @return [Enumerator]
# If no block is given, an Enumerator will be returned.
#
def each_child_directory
return enum_for(__method__) unless block_given?
if File.directory?(@path)
Dir.children(@path).sort.each do |name|
path = File.join(@path,name)
if File.directory?(path)
yield path
end
end
end
return nil
end
end
end
end