# frozen_string_literal: true
#
# Copyright (c) 2006-2024 Hal Brodigan (postmodern.mod3 at gmail.com)
#
# ronin-support 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-support 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-support. If not, see .
#
require 'ronin/support/text/homoglyph/exceptions'
module Ronin
module Support
module Text
module Homoglyph
#
# Loads a table of characters and their homoglyph characters.
#
# @since 1.0.0
#
# @api private
#
class Table
# The list of all homoglyph characters in the table.
#
# @return [Array]
attr_reader :homoglyphs
# The table of ASCII characters and their homoglyph counterparts.
#
# @return [Hash{String => Array}]
attr_reader :table
#
# Initializes an empty homoglyph table.
#
def initialize
@homoglyphs = []
@table = {}
end
#
# Loads a table of homoglyphs from the `.txt` file.
#
# @param [String] path
# The path to the `.txt` file.
#
# @return [Table]
# The newly loaded homoglyph table.
#
# @api private
#
def self.load_file(path)
table = new
File.open(path) do |file|
file.each_line(chomp: true) do |line|
char, substitute = line.split(' ',2)
table[char] = substitute
end
end
return table
end
#
# Looks up the substitute characters for the given original character.
#
# @param [String] char
# The ASCII character to lookup in the table.
#
# @return [Array, nil]
# The homoglyphic equivalent characters for the given ASCII
# character.
#
# @api public
#
def [](char)
@table[char]
end
#
# Adds a homoglyph character for the character.
#
# @param [String] char
# The ASCII character.
#
# @param [String] substitute
# The ASCII character's homoglyph counterpart.
#
# @return [Array]
# All previously added homoglyph substitution characters.
#
# @api private
#
def []=(char,substitute)
@homoglyphs << substitute
(@table[char] ||= []) << substitute
end
#
# Enumerates over all characters and their substitutions in the table.
#
# @yield [char,substitutions]
# If a block is given, it will be passed each ASCII character and a
# homoglyphic equivalent character from the table.
#
# @yieldparam [String] char
# An ASCII character.
#
# @yieldparam [String] substitution
# A homoglyphic equivalent for the character.
#
# @return [Enumerator]
# If no block is given, an Enumerator will be returned.
#
def each(&block)
return enum_for(__method__) unless block
@table.each do |char,substitutions|
substitutions.each do |substitute_char|
yield char, substitute_char
end
end
end
#
# Combines the table with another table.
#
# @param [Table] other_table
# The other table to merge together.
#
# @return [Table]
# The new merged table.
#
def merge(other_table)
new_table = self.class.new
each do |char,substitute|
new_table[char] = substitute
end
other_table.each do |char,other_substitute|
new_table[char] = other_substitute
end
return new_table
end
alias + merge
#
# Performs a random homoglyphic substitution on the given String.
#
# @param [String] string
# The given String.
#
# @return [String]
# The random homoglyph string derived from the given String.
#
# @raise [NotViable]
# No homoglyph replaceable characters were found in the String.
#
def substitute(string)
replaceable_chars = string.chars & @table.keys
if replaceable_chars.empty?
raise(NotViable,"no homoglyph replaceable characters found in String (#{string.inspect})")
end
replaceable_char = replaceable_chars.sample
substitute_char = @table[replaceable_char].sample
return string.sub(replaceable_char,substitute_char)
end
#
# Enumerates over every possible homoglyphic substitution of the
# given String.
#
# @param [String] string
# The original to perform substitutions on.
#
# @yield [homoglyph]
# If a block is given, it will be passed each homoglyphic
# substitution of the given String.
#
# @yieldparam [String] homoglyph
# A copy of the given String with one character replaced with it's
# homoglyph equivalent from the table.
#
# @return [Enumerator]
# If no block is given, an Enumerator will be returned.
#
def each_substitution(string)
return enum_for(__method__,string) unless block_given?
(string.chars & @table.keys).each do |replaceable_char|
@table[replaceable_char].each do |substitute_char|
offset = 0
while (index = string.index(replaceable_char,offset))
homoglyph = string.dup
homoglyph[index] = substitute_char
yield homoglyph
offset = index + 1
end
end
end
end
end
end
end
end
end