ronin-rb/ronin-support

View on GitHub
lib/ronin/support/encoding/hex.rb

Summary

Maintainability
A
1 hr
Test Coverage
# frozen_string_literal: true
#
# Copyright (c) 2006-2023 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 <https://www.gnu.org/licenses/>.
#

require 'strscan'

module Ronin
  module Support
    class Encoding < ::Encoding
      #
      # Contains methods for hex encoding/decoding and escaping/unescaping data.
      #
      # ## Core-Ext Methods
      #
      # * {Integer#hex_encode}
      # * {Integer#hex_escape}
      # * {Integer#hex_int}
      # * {String#hex_encode}
      # * {String#hex_decode}
      # * {String#hex_escape}
      # * {String#hex_unescape}
      # * {String#hex_string}
      # * {String#hex_unquote}
      #
      module Hex
        #
        # Hex encodes the given byte.
        #
        # @param [Integer] byte
        #   The byte to hex encode.
        #
        # @return [String]
        #   The hex encoded version of the byte.
        #
        # @example
        #   Encoding::Hex.encode_byte(0x41)
        #   # => "41"
        #
        def self.encode_byte(byte)
          "%.2x" % byte
        end

        #
        # Hex escapes the given byte.
        #
        # @param [Integer] byte
        #   The byte value to hex escape.
        #
        # @return [String]
        #   The hex escaped version of the Integer.
        #
        # @raise [RangeError]
        #   The byte value is negative.
        #
        # @example
        #   Encoding::Hex.escape_byte(42)
        #   # => "\\x2a"
        #
        def self.escape_byte(byte)
          if byte >= 0x00 && byte <= 0xff
            if    byte == 0x22
              "\\\""
            elsif byte == 0x5d
              "\\\\"
            elsif byte >= 0x20 && byte <= 0x7e
              byte.chr
            else
              "\\x%.2x" % byte
            end
          elsif byte > 0xff
            "\\x%x" % byte
          else
            raise(RangeError,"#{byte.inspect} out of char range")
          end
        end

        #
        # Hex encodes the given data.
        #
        # @param [String] data
        #   The given data to hex encode.
        #
        # @return [String]
        #   The hex encoded version of the String.
        #
        # @example
        #   Encoding::Hex.encode("hello")
        #   # => "68656C6C6F"
        #
        def self.encode(data)
          encoded = String.new

          data.each_byte do |byte|
            encoded << encode_byte(byte)
          end

          return encoded
        end

        #
        # Hex decodes the String.
        #
        # @param [String] data
        #   The given data to hex decode.
        #
        # @return [String]
        #   The hex decoded version of the String.
        #
        # @example
        #   Encoding::Hex.decode("68656C6C6F")
        #   # => "hello"
        #
        def self.decode(data)
          decoded = String.new

          data.scan(/../) do |hex|
            decoded << hex.to_i(16).chr
          end

          return decoded
        end

        #
        # Hex-escapes the given data.
        #
        # @param [String] data
        #   The given data to hex escape.
        #
        # @return [String]
        #   The hex escaped version of the String.
        #
        # @example
        #   Encoding::Hex.escape("hello")
        #   # => "\\x68\\x65\\x6c\\x6c\\x6f"
        #
        def self.escape(data)
          escaped = String.new

          if data.valid_encoding?
            data.each_codepoint do |codepoint|
              escaped << escape_byte(codepoint)
            end
          else
            data.each_byte do |byte|
              escaped << escape_byte(byte)
            end
          end

          return escaped
        end

        # Backslash escaped characters.
        BACKSLASHED_CHARS = {
          "\\" => '\\',
          '"'  => '"',
          '0'  => "\0",
          'a'  => "\a",
          'b'  => "\b",
          't'  => "\t",
          'n'  => "\n",
          'v'  => "\v",
          'f'  => "\f",
          'r'  => "\r"
        }

        #
        # Removes the quotes an unescapes a quoted hex string.
        #
        # @param [String] data
        #   The quoted and hex escaped hex string.
        #
        # @return [String]
        #   The un-quoted String if the String begins and ends with quotes, or
        #   the same String if it is not quoted.
        #
        # @example
        #   Encoding::Hex.unescape("hello\\nworld")
        #   # => "hello\nworld"
        #
        def self.unescape(data)
          buffer  = String.new(encoding: Encoding::UTF_8)
          scanner = StringScanner.new(data)

          until scanner.eos?
            buffer << case (char = scanner.getch)
                      when '\\'
                        if (hex_escape    = scanner.scan(/x[0-9a-fA-F]{4,8}/))
                          hex_escape[1..].to_i(16).chr(Encoding::UTF_8)
                        elsif (hex_escape = scanner.scan(/x[0-9a-fA-F]{1,2}/))
                          hex_escape[1..].to_i(16).chr
                        elsif (char       = scanner.getch)
                          BACKSLASHED_CHARS.fetch(char,char)
                        end
                      else
                        char
                      end
          end

          return buffer
        end

        #
        # Converts the given data into a double-quoted hex string.
        #
        # @param [String] data
        #   The given data to hex-escape and double-quote.
        #
        # @return [String]
        #   The double-quoted and hex escaped string.
        #
        # @example
        #   Encoding::Hex.quote("hello\nworld")
        #   # => "\"hello\\nworld\""
        #
        def self.quote(data)
          "\"#{escape(data)}\""
        end

        #
        # Removes the quotes and unescapes a hex string.
        #
        # @param [String] data
        #   The quoted hex String to unescape and unquote.
        #
        # @return [String]
        #   The un-quoted String if the String begins and ends with quotes, or
        #   the same String if it is not quoted.
        #
        # @example
        #   Encoding::Hex.unquote("\"hello\\nworld\"")
        #   # => "hello\nworld"
        #
        def self.unquote(data)
          if ((data[0] == '"' && data[-1] == '"') ||
              (data[0] == "'" && data[-1] == "'"))
            unescape(data[1..-2])
          else
            data
          end
        end
      end
    end
  end
end

require 'ronin/support/encoding/hex/core_ext'