lib/pwnlib/util/hexdump.rb
# encoding: ASCII-8BIT
# frozen_string_literal: true
require 'rainbow'
module Pwnlib
module Util
# Method for output a pretty hexdump.
# Since this may be used in log module, to avoid cyclic dependency, it is put in a separate module as {Fiddling}
#
# @todo Control coloring by context.
#
# @example Call by specifying full module path.
# require 'pwnlib/util/hexdump'
# Pwnlib::Util::HexDump.hexdump('217')
# #=> "00000000 32 31 37 │217│\n00000003"
# @example require 'pwn' and have all methods.
# require 'pwn'
# hexdump('217')
# #=> "00000000 32 31 37 │217│\n00000003"
module HexDump
MARKER = "\u2502"
HIGHLIGHT_STYLE = ->(s) { Rainbow(s).bg(:red) }
DEFAULT_STYLE = {
0x00 => ->(s) { Rainbow(s).red },
0x0a => ->(s) { Rainbow(s).red },
0xff => ->(s) { Rainbow(s).green },
marker: ->(s) { Rainbow(s).dimgray },
printable: ->(s) { s },
unprintable: ->(s) { Rainbow(s).dimgray }
}.freeze
module_function
# @!macro [new] hexdump_params
# Color is provided using +rainbow+ gem and only when output is a tty.
# To force enable/disable coloring, call <tt>Rainbow.enabled = true / false</tt>.
# @param [Integer] width
# The max number of characters per line.
# @param [Boolean] skip
# Whether repeated lines should be replaced by a +"*"+.
# @param [Integer] offset
# Offset of the first byte to print in the left column.
# @param [Hash{Integer, Symbol => Proc}] style
# Color scheme to use.
#
# Possible keys are:
# * <tt>0x00..0xFF</tt>, for specified byte.
# * +:marker+, for the separator in right column.
# * +:printable+, for printable bytes that don't have style specified.
# * +:unprintable+, for unprintable bytes that don't have style specified.
# The proc is called with a single argument, the string to be formatted.
# @param [String] highlight
# Convenient argument to highlight (red background) some bytes in style.
# Yields lines of a hexdump-dump of a string.
# Unless you have massive amounts of data you probably want to use {.hexdump}.
# Returns an Enumerator if no block given.
#
# @param [#read] io
# The object to be dumped.
# @!macro hexdump_params
#
# @return [Enumerator<String>]
# The resulting hexdump, line by line.
def hexdump_iter(io, width: 16, skip: true, offset: 0, style: {}, highlight: '')
Enumerator.new do |y|
style = DEFAULT_STYLE.merge(style)
highlight.bytes.each { |b| style[b] = HIGHLIGHT_STYLE }
(0..255).each do |b|
next if style.key?(b)
style[b] = (b.chr =~ /[[:print:]]/ ? style[:printable] : style[:unprintable])
end
styled_bytes = (0..255).map do |b|
left_hex = format('%02x', b)
c = b.chr
right_char = (c =~ /[[:print:]]/ ? c : "\u00b7")
[style[b].call(left_hex), style[b].call(right_char)]
end
marker = style[:marker].call(MARKER)
spacer = ' '
byte_index = offset
skipping = false
last_chunk = +''
loop do
# We assume that chunk is in ASCII-8BIT encoding.
chunk = io.read(width)
break unless chunk
chunk_bytes = chunk.bytes
start_byte_index = byte_index
byte_index += chunk_bytes.size
# Yield * once for repeated lines.
if skip && last_chunk == chunk
y << '*' unless skipping
skipping = true
next
end
skipping = false
last_chunk = chunk
hex_bytes = +''
printable = +''
chunk_bytes.each_with_index do |b, i|
left_hex, right_char = styled_bytes[b]
hex_bytes << left_hex
printable << right_char
if i % 4 == 3 && i != chunk_bytes.size - 1
hex_bytes << spacer
printable << marker
end
hex_bytes << ' '
end
if chunk_bytes.size < width
padded_hex_length = 3 * width + (width - 1) / 4
hex_length = 3 * chunk_bytes.size + (chunk_bytes.size - 1) / 4
hex_bytes << ' ' * (padded_hex_length - hex_length)
end
y << format("%08x %s #{MARKER}%s#{MARKER}", start_byte_index, hex_bytes, printable)
end
y << format('%08x', byte_index)
end
end
# Returns a hexdump-dump of a string.
#
# @param [String] str
# The string to be hexdumped.
# @!macro hexdump_params
#
# @return [String]
# The resulting hexdump.
def hexdump(str, width: 16, skip: true, offset: 0, style: {}, highlight: '')
hexdump_iter(StringIO.new(str),
width: width, skip: skip, offset: offset, style: style,
highlight: highlight).to_a.join("\n")
end
end
end
end