Add initial support for word wrapping rex tables
This commit is contained in:
parent
474640afec
commit
59e2b747a6
4
Gemfile
4
Gemfile
|
@ -2,3 +2,7 @@ source 'https://rubygems.org'
|
|||
|
||||
# Specify your gem's dependencies in rex-text.gemspec
|
||||
gemspec
|
||||
|
||||
group :development do
|
||||
gem 'pry-byebug'
|
||||
end
|
||||
|
|
|
@ -29,7 +29,7 @@ require 'rex/text/xor'
|
|||
|
||||
require 'rex/text/color'
|
||||
require 'rex/text/table'
|
||||
|
||||
require 'rex/text/wrapped_table'
|
||||
|
||||
module Rex
|
||||
|
||||
|
|
|
@ -12,6 +12,34 @@ module Text
|
|||
###
|
||||
class Table
|
||||
|
||||
# Temporary forking logic for using the prototype `WrappedTable` implementation.
|
||||
#
|
||||
# This method replaces the default `Table.new` with the ability to call the `WrappedTable` class instead,
|
||||
# to allow users to safely toggle between wrapped/unwrapped tables at a global level without changing
|
||||
# their existing codebases. This approach will reduce the risk of enabling wrapped table behavior by default.
|
||||
#
|
||||
# To enforce all tables to be wrapped to the terminal's current width, call `Table.wrap_tables!`
|
||||
# before invoking `Table.new` as normal.
|
||||
def self.new(*args, &block)
|
||||
if wrap_tables?
|
||||
table_options = args[0]
|
||||
return ::Rex::Text::WrappedTable.new(table_options)
|
||||
end
|
||||
return super(*args, &block)
|
||||
end
|
||||
|
||||
def self.wrap_tables?
|
||||
@@wrapped_tables_enabled ||= false
|
||||
end
|
||||
|
||||
def self.wrap_tables!
|
||||
@@wrapped_tables_enabled = true
|
||||
end
|
||||
|
||||
def self.unwrap_tables!
|
||||
@@wrapped_tables_enabled = false
|
||||
end
|
||||
|
||||
#
|
||||
# Initializes a text table instance using the supplied properties. The
|
||||
# Table class supports the following hash attributes:
|
||||
|
@ -441,7 +469,7 @@ protected
|
|||
|
||||
def format_table_field(str, idx)
|
||||
str_cp = str.clone
|
||||
|
||||
|
||||
colprops[idx]['Formatters'].each do |f|
|
||||
str_cp = f.format(str_cp)
|
||||
end
|
||||
|
@ -451,7 +479,7 @@ protected
|
|||
|
||||
def style_table_field(str, idx)
|
||||
str_cp = str.clone
|
||||
|
||||
|
||||
colprops[idx]['Stylers'].each do |s|
|
||||
str_cp = s.style(str_cp)
|
||||
end
|
||||
|
|
|
@ -0,0 +1,538 @@
|
|||
# -*- coding: binary -*-
|
||||
require 'ipaddr'
|
||||
require 'io/console'
|
||||
|
||||
module Rex
|
||||
module Text
|
||||
|
||||
###
|
||||
#
|
||||
# Prints text in a tablized format. Pretty lame at the moment, but
|
||||
# whatever.
|
||||
#
|
||||
###
|
||||
# private_constant
|
||||
class WrappedTable
|
||||
|
||||
#
|
||||
# Initializes a text table instance using the supplied properties. The
|
||||
# Table class supports the following hash attributes:
|
||||
#
|
||||
# Header
|
||||
#
|
||||
# The string to display as a heading above the table. If none is
|
||||
# specified, no header will be displayed.
|
||||
#
|
||||
# HeaderIndent
|
||||
#
|
||||
# The amount of space to indent the header. The default is zero.
|
||||
#
|
||||
# Columns
|
||||
#
|
||||
# The array of columns that will exist within the table.
|
||||
#
|
||||
# Rows
|
||||
#
|
||||
# The array of rows that will exist.
|
||||
#
|
||||
# Width
|
||||
#
|
||||
# The maximum width of the table in characters.
|
||||
#
|
||||
# Indent
|
||||
#
|
||||
# The number of characters to indent the table.
|
||||
#
|
||||
# CellPad
|
||||
#
|
||||
# The number of characters to put between each horizontal cell.
|
||||
#
|
||||
# Prefix
|
||||
#
|
||||
# The text to prefix before the table.
|
||||
#
|
||||
# Postfix
|
||||
#
|
||||
# The text to affix to the end of the table.
|
||||
#
|
||||
# Sortindex
|
||||
#
|
||||
# The column to sort the table on, -1 disables sorting.
|
||||
#
|
||||
# ColProps
|
||||
#
|
||||
# A hash specifying column MaxWidth, Stylers, and Formatters.
|
||||
#
|
||||
def initialize(opts = {})
|
||||
self.header = opts['Header']
|
||||
self.headeri = opts['HeaderIndent'] || 0
|
||||
self.columns = opts['Columns'] || []
|
||||
# updated below if we got a "Rows" option
|
||||
self.rows = []
|
||||
|
||||
# TODO: Discuss a cleaner way to handle this information
|
||||
self.width = opts['Width'] || ::IO.console.winsize[1]
|
||||
self.indent = opts['Indent'] || 0
|
||||
self.cellpad = opts['CellPad'] || 2
|
||||
self.prefix = opts['Prefix'] || ''
|
||||
self.postfix = opts['Postfix'] || ''
|
||||
self.colprops = []
|
||||
self.scterm = /#{opts['SearchTerm']}/mi if opts['SearchTerm']
|
||||
|
||||
self.sort_index = opts['SortIndex'] || 0
|
||||
self.sort_order = opts['SortOrder'] || :forward
|
||||
|
||||
# Default column properties
|
||||
self.columns.length.times { |idx|
|
||||
self.colprops[idx] = {}
|
||||
self.colprops[idx]['MaxWidth'] = self.columns[idx].length
|
||||
self.colprops[idx]['WordWrap'] = true
|
||||
self.colprops[idx]['Stylers'] = []
|
||||
self.colprops[idx]['Formatters'] = []
|
||||
}
|
||||
|
||||
# ensure all our internal state gets updated with the given rows by
|
||||
# using add_row instead of just adding them to self.rows. See #3825.
|
||||
opts['Rows'].each { |row| add_row(row) } if opts['Rows']
|
||||
|
||||
# Merge in options
|
||||
if (opts['ColProps'])
|
||||
opts['ColProps'].each_key { |col|
|
||||
idx = self.columns.index(col)
|
||||
|
||||
if (idx)
|
||||
self.colprops[idx].merge!(opts['ColProps'][col])
|
||||
end
|
||||
}
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
#
|
||||
# Converts table contents to a string.
|
||||
#
|
||||
def to_s
|
||||
str = prefix.dup
|
||||
str << header_to_s || ''
|
||||
str << columns_to_s || ''
|
||||
str << hr_to_s || ''
|
||||
|
||||
sort_rows
|
||||
rows.each { |row|
|
||||
if (is_hr(row))
|
||||
str << hr_to_s
|
||||
else
|
||||
str << row_to_s(row) if row_visible(row)
|
||||
end
|
||||
}
|
||||
|
||||
str << postfix
|
||||
|
||||
return str
|
||||
end
|
||||
|
||||
#
|
||||
# Converts table contents to a csv
|
||||
#
|
||||
def to_csv
|
||||
str = ''
|
||||
str << ( columns.join(",") + "\n" )
|
||||
rows.each { |row|
|
||||
next if is_hr(row) || !row_visible(row)
|
||||
str << ( row.map{|x|
|
||||
x = x.to_s
|
||||
x.gsub(/[\r\n]/, ' ').gsub(/\s+/, ' ').gsub('"', '""')
|
||||
}.map{|x| "\"#{x}\"" }.join(",") + "\n" )
|
||||
}
|
||||
str
|
||||
end
|
||||
|
||||
#
|
||||
#
|
||||
# Returns the header string.
|
||||
#
|
||||
def header_to_s # :nodoc:
|
||||
if (header)
|
||||
pad = " " * headeri
|
||||
|
||||
return pad + header + "\n" + pad + "=" * header.length + "\n\n"
|
||||
end
|
||||
|
||||
return ''
|
||||
end
|
||||
|
||||
#
|
||||
# Prints the contents of the table.
|
||||
#
|
||||
def print
|
||||
puts to_s
|
||||
end
|
||||
|
||||
#
|
||||
# Adds a row using the supplied fields.
|
||||
#
|
||||
def <<(fields)
|
||||
add_row(fields)
|
||||
end
|
||||
|
||||
#
|
||||
# Adds a row with the supplied fields.
|
||||
#
|
||||
def add_row(fields = [])
|
||||
if fields.length != self.columns.length
|
||||
raise RuntimeError, 'Invalid number of columns!'
|
||||
end
|
||||
formatted_fields = fields.map.with_index { |field, idx|
|
||||
# Remove whitespace and ensure String format
|
||||
field = format_table_field(field.to_s.strip, idx)
|
||||
|
||||
if (colprops[idx]['MaxWidth'] < display_width(field.to_s))
|
||||
old = colprops[idx]['MaxWidth']
|
||||
colprops[idx]['MaxWidth'] = display_width(field.to_s)
|
||||
end
|
||||
|
||||
field
|
||||
}
|
||||
|
||||
rows << formatted_fields
|
||||
end
|
||||
|
||||
def ip_cmp(a, b)
|
||||
begin
|
||||
a = IPAddr.new(a.to_s)
|
||||
b = IPAddr.new(b.to_s)
|
||||
return 1 if a.ipv6? && b.ipv4?
|
||||
return -1 if a.ipv4? && b.ipv6?
|
||||
a <=> b
|
||||
rescue IPAddr::Error
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
#
|
||||
# Sorts the rows based on the supplied index of sub-arrays
|
||||
# If the supplied index is an IPv4 address, handle it differently, but
|
||||
# avoid actually resolving domain names.
|
||||
#
|
||||
def sort_rows(index = sort_index, order = sort_order)
|
||||
return if index == -1
|
||||
return unless rows
|
||||
rows.sort! do |a,b|
|
||||
if a[index].nil?
|
||||
cmp = -1
|
||||
elsif b[index].nil?
|
||||
cmp = 1
|
||||
elsif a[index] =~ /^[0-9]+$/ and b[index] =~ /^[0-9]+$/
|
||||
cmp = a[index].to_i <=> b[index].to_i
|
||||
elsif (cmp = ip_cmp(a[index], b[index])) != nil
|
||||
else
|
||||
cmp = a[index] <=> b[index] # assumes otherwise comparable.
|
||||
end
|
||||
cmp ||= 0
|
||||
order == :forward ? cmp : -cmp
|
||||
end
|
||||
end
|
||||
|
||||
#
|
||||
# Adds a horizontal line.
|
||||
#
|
||||
def add_hr
|
||||
rows << '__hr__'
|
||||
end
|
||||
|
||||
#
|
||||
# Returns new sub-table with headers and rows maching column names submitted
|
||||
#
|
||||
#
|
||||
# Flips table 90 degrees left
|
||||
#
|
||||
def drop_left
|
||||
tbl = self.class.new(
|
||||
'Columns' => Array.new(self.rows.count+1,' '),
|
||||
'Header' => self.header,
|
||||
'Indent' => self.indent)
|
||||
(self.columns.count+1).times do |ti|
|
||||
row = self.rows.map {|r| r[ti]}.unshift(self.columns[ti]).flatten
|
||||
# insert our col|row break. kind of hackish
|
||||
row[1] = "| #{row[1]}" unless row.all? {|e| e.nil? || e.empty?}
|
||||
tbl << row
|
||||
end
|
||||
return tbl
|
||||
end
|
||||
|
||||
def valid_ip?(value)
|
||||
begin
|
||||
IPAddr.new value
|
||||
true
|
||||
rescue IPAddr::Error
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
#
|
||||
# Build table from CSV dump
|
||||
#
|
||||
def self.new_from_csv(csv)
|
||||
# Read in or keep data, get CSV or die
|
||||
if csv.is_a?(String)
|
||||
csv = File.file?(csv) ? CSV.read(csv) : CSV.parse(csv)
|
||||
end
|
||||
# Adjust for skew
|
||||
if csv.first == ["Keys", "Values"]
|
||||
csv.shift # drop marker
|
||||
cols = []
|
||||
rows = []
|
||||
csv.each do |row|
|
||||
cols << row.shift
|
||||
rows << row
|
||||
end
|
||||
tbl = self.new('Columns' => cols)
|
||||
rows.in_groups_of(cols.count) {|r| tbl << r.flatten}
|
||||
else
|
||||
tbl = self.new('Columns' => csv.shift)
|
||||
while !csv.empty? do
|
||||
tbl << csv.shift
|
||||
end
|
||||
end
|
||||
return tbl
|
||||
end
|
||||
|
||||
def [](*col_names)
|
||||
tbl = self.class.new('Indent' => self.indent,
|
||||
'Header' => self.header,
|
||||
'Columns' => col_names)
|
||||
indexes = []
|
||||
|
||||
col_names.each do |col_name|
|
||||
index = self.columns.index(col_name)
|
||||
raise RuntimeError, "Invalid column name #{col_name}" if index.nil?
|
||||
indexes << index
|
||||
end
|
||||
|
||||
self.rows.each do |old_row|
|
||||
new_row = []
|
||||
indexes.map {|i| new_row << old_row[i]}
|
||||
tbl << new_row
|
||||
end
|
||||
|
||||
return tbl
|
||||
end
|
||||
|
||||
|
||||
alias p print
|
||||
|
||||
attr_accessor :header, :headeri # :nodoc:
|
||||
attr_accessor :columns, :rows, :colprops # :nodoc:
|
||||
attr_accessor :width, :indent, :cellpad # :nodoc:
|
||||
attr_accessor :prefix, :postfix # :nodoc:
|
||||
attr_accessor :sort_index, :sort_order, :scterm # :nodoc:
|
||||
|
||||
protected
|
||||
|
||||
#
|
||||
# Returns if a row should be visible or not
|
||||
#
|
||||
def row_visible(row)
|
||||
return true if self.scterm.nil?
|
||||
row_to_s(row).match(self.scterm)
|
||||
end
|
||||
|
||||
#
|
||||
# Defaults cell widths and alignments.
|
||||
#
|
||||
def defaults # :nodoc:
|
||||
self.columns.length.times { |idx|
|
||||
}
|
||||
end
|
||||
|
||||
#
|
||||
# Checks to see if the row is an hr.
|
||||
#
|
||||
def is_hr(row) # :nodoc:
|
||||
return ((row.kind_of?(String)) && (row == '__hr__'))
|
||||
end
|
||||
|
||||
#
|
||||
# Converts the columns to a string.
|
||||
#
|
||||
def columns_to_s # :nodoc:
|
||||
optimal_widths = calculate_optimal_widths
|
||||
values_as_chunks = chunk_values(columns, optimal_widths)
|
||||
result = chunks_to_s(values_as_chunks, optimal_widths)
|
||||
|
||||
barline = ""
|
||||
columns.each.with_index do |_column, idx|
|
||||
bar_width = display_width(values_as_chunks[idx].first)
|
||||
column_width = optimal_widths[idx]
|
||||
|
||||
if idx == 0
|
||||
barline << ' ' * indent
|
||||
end
|
||||
|
||||
barline << '-' * bar_width
|
||||
is_last_column = (idx + 1) == columns.length
|
||||
unless is_last_column
|
||||
barline << ' ' * (column_width - bar_width)
|
||||
barline << ' ' * cellpad
|
||||
end
|
||||
end
|
||||
|
||||
result + barline
|
||||
end
|
||||
|
||||
#
|
||||
# Converts an hr to a string.
|
||||
#
|
||||
def hr_to_s # :nodoc:
|
||||
return "\n"
|
||||
end
|
||||
|
||||
#
|
||||
# Converts a row to a string.
|
||||
#
|
||||
def row_to_s(row) # :nodoc:
|
||||
optimal_widths = calculate_optimal_widths
|
||||
values_as_chunks = chunk_values(row, optimal_widths)
|
||||
chunks_to_s(values_as_chunks, optimal_widths)
|
||||
end
|
||||
|
||||
#
|
||||
# Placeholder function that aims to calculate the display width of the given string.
|
||||
# In the future this will be aware of East Asian characters having different display
|
||||
# widths. For now it simply returns the string's length.
|
||||
#
|
||||
def display_width(str)
|
||||
str.length
|
||||
end
|
||||
|
||||
def chunk_values(values, optimal_widths)
|
||||
# First split long strings into an array of chunks, where each chunk size is the calculated column width
|
||||
values_as_chunks = values.each_with_index.map do |value, idx|
|
||||
column_width = optimal_widths[idx]
|
||||
value
|
||||
.split('')
|
||||
.each_slice(column_width)
|
||||
.map(&:join)
|
||||
end
|
||||
|
||||
values_as_chunks
|
||||
end
|
||||
|
||||
def chunks_to_s(values_as_chunks, optimal_widths)
|
||||
result = ''
|
||||
|
||||
interleave(values_as_chunks).each do |row_chunks|
|
||||
line = ""
|
||||
row_chunks.each_with_index do |chunk, idx|
|
||||
column_width = optimal_widths[idx]
|
||||
|
||||
if idx == 0
|
||||
line << ' ' * indent
|
||||
end
|
||||
|
||||
line << chunk.to_s.ljust(column_width)
|
||||
line << ' ' * cellpad
|
||||
end
|
||||
|
||||
result << line.rstrip << "\n"
|
||||
end
|
||||
|
||||
result
|
||||
end
|
||||
|
||||
def interleave(arrays)
|
||||
max_length = arrays.map(&:size).max
|
||||
padding = [nil] * max_length
|
||||
with_left_extra_column = padding.zip(*arrays)
|
||||
without_extra_column = with_left_extra_column.map { |columns| columns.drop(1) }
|
||||
|
||||
without_extra_column
|
||||
end
|
||||
|
||||
def calculate_optimal_widths
|
||||
# Calculate the minimum width each column can be. This is dictated by the user.
|
||||
user_influenced_column_widths = colprops.map do |colprop|
|
||||
if colprop['WordWrap'] == false
|
||||
colprop['MaxWidth']
|
||||
raise 'Not implemented'
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
required_padding = indent + (colprops.length) * cellpad
|
||||
available_space = self.width - user_influenced_column_widths.sum(&:to_i) - required_padding
|
||||
remaining_column_calculations = user_influenced_column_widths.select(&:nil?).count
|
||||
|
||||
# Calculate the initial widths, which will need an additional refinement to reallocate surplus space
|
||||
naive_optimal_width_calculations = colprops.map.with_index do |colprop, index|
|
||||
shared_column_width = available_space / [remaining_column_calculations, 1].max
|
||||
remaining_column_calculations -= 1
|
||||
|
||||
if user_influenced_column_widths[index]
|
||||
{ width: user_influenced_column_widths[index], wrapped: false }
|
||||
elsif colprop['MaxWidth'] < shared_column_width
|
||||
available_space -= colprop['MaxWidth']
|
||||
{ width: colprop['MaxWidth'], wrapped: false }
|
||||
else
|
||||
available_space -= shared_column_width
|
||||
{ width: shared_column_width, wrapped: true }
|
||||
end
|
||||
end
|
||||
|
||||
# Naively redistribute any surplus space to columns that were wrapped, and try to fit the cell on one line still
|
||||
current_width = naive_optimal_width_calculations.sum { |width| width[:width] }
|
||||
surplus_width = self.width - current_width - required_padding
|
||||
# revisit all columns that were wrapped and add add additional characters
|
||||
revisiting_column_counts = naive_optimal_width_calculations.count { |width| width[:wrapped] }
|
||||
optimal_widths = naive_optimal_width_calculations.map.with_index do |naive_width, index|
|
||||
additional_column_width = surplus_width / [revisiting_column_counts, 1].max
|
||||
revisiting_column_counts -= 1
|
||||
|
||||
if naive_width[:wrapped]
|
||||
max_width = colprops[index]['MaxWidth']
|
||||
if max_width < (naive_width[:width] + additional_column_width)
|
||||
surplus_width -= max_width - naive_width[:width]
|
||||
max_width
|
||||
else
|
||||
surplus_width -= additional_column_width
|
||||
naive_width[:width] + additional_column_width
|
||||
end
|
||||
else
|
||||
naive_width[:width]
|
||||
end
|
||||
end
|
||||
|
||||
# In certain scenarios columns can be allocated 0 widths if it's completely impossible to fit the columns into the
|
||||
# given space. There's different ways to handle that, for instance truncating data in the table to the initial
|
||||
# columns that can fit. For now, we just ensure every width is at least 1 or more character wide, and in the future
|
||||
# it may have to truncate columns entirely.
|
||||
optimal_widths.map { |width| [1, width].max }
|
||||
end
|
||||
|
||||
def format_table_field(str, idx)
|
||||
str_cp = str.dup
|
||||
|
||||
colprops[idx]['Formatters'].each do |f|
|
||||
str_cp = f.format(str_cp)
|
||||
end
|
||||
|
||||
str_cp.dup.force_encoding('UTF-8')
|
||||
end
|
||||
|
||||
def style_table_field(str, _idx)
|
||||
str_cp = str.dup
|
||||
|
||||
# Not invoking as color currently conflicts with the wrapping of tables
|
||||
# colprops[idx]['Stylers'].each do |s|
|
||||
# str_cp = s.style(str_cp)
|
||||
# end
|
||||
|
||||
str_cp
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,644 @@
|
|||
# -*- coding: binary -*-
|
||||
|
||||
require 'spec_helper'
|
||||
require 'rex/text/wrapped_table'
|
||||
|
||||
RSpec::Matchers.define :have_maximum_width do |expected|
|
||||
match do |actual|
|
||||
actual.length <= expected
|
||||
end
|
||||
failure_message do |actual|
|
||||
"expected '#{actual}' to have a length than or equal to #{expected}, instead got #{actual.length}"
|
||||
end
|
||||
end
|
||||
|
||||
RSpec::Matchers.define :match_table do |expected|
|
||||
diffable
|
||||
|
||||
match do |actual|
|
||||
@actual = actual.to_s.strip
|
||||
@expected = expected.to_s.strip
|
||||
|
||||
@actual == @expected
|
||||
end
|
||||
|
||||
failure_message do |actual|
|
||||
<<~MSG
|
||||
Expected:
|
||||
#{with_whitespace_highlighted(expected.to_s.strip)}
|
||||
|
||||
Received:
|
||||
#{with_whitespace_highlighted(actual.to_s.strip)}
|
||||
|
||||
Raw Result:
|
||||
#{actual.to_s}
|
||||
MSG
|
||||
end
|
||||
|
||||
def with_whitespace_highlighted(string)
|
||||
string.lines.map { |line| "'#{line.gsub("\n", "")}'" }.join("\n")
|
||||
end
|
||||
end
|
||||
|
||||
describe Rex::Text::Table do
|
||||
let(:formatter) do
|
||||
Formatter = Class.new do
|
||||
def format(str)
|
||||
"IHAVEBEENFORMATTED#{str}"
|
||||
end
|
||||
end
|
||||
|
||||
Formatter.new
|
||||
end
|
||||
|
||||
let(:styler) do
|
||||
Styler = Class.new do
|
||||
def style(str)
|
||||
"IHAVEBEENSTYLED#{str}"
|
||||
end
|
||||
end
|
||||
|
||||
Styler.new
|
||||
end
|
||||
|
||||
let(:mock_window_size_rows) { 30 }
|
||||
let(:mock_window_size_columns) { 180 }
|
||||
let(:mock_window_size) { [mock_window_size_rows, mock_window_size_columns] }
|
||||
|
||||
before(:each) do
|
||||
allow(::IO.console).to receive(:winsize).and_return(mock_window_size)
|
||||
allow(Rex::Text::Table).to receive(:wrap_tables?).and_return(true)
|
||||
end
|
||||
|
||||
describe "#to_csv" do
|
||||
it "handles strings in different encodings" do
|
||||
options = {
|
||||
'Header' => 'Header',
|
||||
'Indent' => 2,
|
||||
'Width' => 80,
|
||||
'Columns' => [
|
||||
'Name',
|
||||
'Value'
|
||||
]
|
||||
}
|
||||
|
||||
tbl = Rex::Text::Table.new(options)
|
||||
tbl << [
|
||||
"hello world".force_encoding("ASCII-8BIT"),
|
||||
"hello world".force_encoding("ASCII-8BIT")
|
||||
]
|
||||
tbl << [
|
||||
"Administratör".force_encoding("UTF-8"),
|
||||
"Administratör".force_encoding("UTF-8")
|
||||
]
|
||||
tbl << [
|
||||
"Administrator’s Shares".force_encoding("UTF-16LE"),
|
||||
"Administrator’s Shares".force_encoding("UTF-16LE")
|
||||
]
|
||||
tbl << [
|
||||
"这是中文这是中文这是中文这是中文",
|
||||
"这是中文这是中文这是中文这是中文"
|
||||
]
|
||||
|
||||
expect(tbl.to_csv).to eql <<~TABLE.force_encoding("UTF-8")
|
||||
Name,Value
|
||||
"hello world","hello world"
|
||||
"Administratör","Administratör"
|
||||
"Administrator’s Shares","Administrator’s Shares"
|
||||
"这是中文这是中文这是中文这是中文","这是中文这是中文这是中文这是中文"
|
||||
TABLE
|
||||
expect(tbl.to_s.lines).to all(have_maximum_width(80))
|
||||
end
|
||||
end
|
||||
|
||||
describe "#to_s" do
|
||||
it 'should space columns correctly' do
|
||||
col_1_field = "A" * 5
|
||||
col_2_field = "B" * 50
|
||||
col_3_field = "C" * 15
|
||||
|
||||
options = {
|
||||
'Header' => 'Header',
|
||||
'SearchTerm' => ['jim', 'bob'],
|
||||
'Columns' => [
|
||||
'Column 1',
|
||||
'Column 2',
|
||||
'Column 3'
|
||||
]
|
||||
}
|
||||
|
||||
tbl = Rex::Text::Table.new(options)
|
||||
|
||||
tbl << [
|
||||
col_1_field,
|
||||
col_2_field,
|
||||
col_3_field
|
||||
]
|
||||
|
||||
expect(tbl).to match_table <<~TABLE
|
||||
Header
|
||||
======
|
||||
|
||||
Column 1 Column 2 Column 3
|
||||
-------- -------- --------
|
||||
AAAAA BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB CCCCCCCCCCCCCCC
|
||||
TABLE
|
||||
end
|
||||
|
||||
it 'should apply field formatters correctly and increase column length' do
|
||||
col_1_field = "A" * 5
|
||||
col_2_field = "B" * 50
|
||||
col_3_field = "C" * 15
|
||||
options = {
|
||||
'Header' => 'Header',
|
||||
'SearchTerm' => ['jim', 'bob'],
|
||||
'Columns' => [
|
||||
'Column 1',
|
||||
'Column 2',
|
||||
'Column 3'
|
||||
],
|
||||
'ColProps' => {
|
||||
'Column 2' => {
|
||||
'Formatters' => [formatter]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tbl = Rex::Text::Table.new(options)
|
||||
|
||||
tbl << [
|
||||
col_1_field,
|
||||
col_2_field,
|
||||
col_3_field
|
||||
]
|
||||
|
||||
expect(tbl).to match_table <<~TABLE
|
||||
Header
|
||||
======
|
||||
|
||||
Column 1 Column 2 Column 3
|
||||
-------- -------- --------
|
||||
AAAAA IHAVEBEENFORMATTEDBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB CCCCCCCCCCCCCCC
|
||||
TABLE
|
||||
end
|
||||
|
||||
it 'should apply field stylers correctly and NOT increase column length' do
|
||||
skip(
|
||||
"Functionality not implemented. Currently if there are colors present in a cell, the colors will break " \
|
||||
"when word wrapping occurs. This is a regression in functionality with a normal Rex Table."
|
||||
)
|
||||
|
||||
col_1_field = "A" * 5
|
||||
col_2_field = "B" * 50
|
||||
col_3_field = "C" * 15
|
||||
|
||||
options = {
|
||||
'Header' => 'Header',
|
||||
'SearchTerm' => ['jim', 'bob'],
|
||||
'Columns' => [
|
||||
'Column 1',
|
||||
'Column 2',
|
||||
'Column 3'
|
||||
],
|
||||
'ColProps' => {
|
||||
'Column 2' => {
|
||||
'Stylers' => [styler]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tbl = Rex::Text::Table.new(options)
|
||||
|
||||
tbl << [
|
||||
col_1_field,
|
||||
col_2_field,
|
||||
col_3_field
|
||||
]
|
||||
|
||||
expect(tbl).to match_table <<~TABLE
|
||||
Header
|
||||
======
|
||||
|
||||
Column 1 Column 2 Column 3
|
||||
-------- -------- --------
|
||||
AAAAA IHAVEBEENSTYLEDBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB CCCCCCCCCCCCCCC
|
||||
TABLE
|
||||
end
|
||||
|
||||
it 'handles multiple columns gracefully' do
|
||||
options = {
|
||||
'Header' => 'Hosts',
|
||||
'Indent' => 0,
|
||||
'Width' => 120,
|
||||
'Columns' => [
|
||||
'address',
|
||||
'mac',
|
||||
'name',
|
||||
'os_name',
|
||||
'os_flavor',
|
||||
'os_sp',
|
||||
'purpose',
|
||||
'info',
|
||||
'comments'
|
||||
]
|
||||
}
|
||||
|
||||
tbl = Rex::Text::Table.new(options)
|
||||
|
||||
tbl << [
|
||||
"127.0.0.1",
|
||||
"",
|
||||
"192.168.1.10",
|
||||
"macOS Mojave (macOS 10.14.6)",
|
||||
"",
|
||||
"",
|
||||
"device",
|
||||
"",
|
||||
""
|
||||
]
|
||||
|
||||
expect(tbl).to match_table <<~TABLE
|
||||
Hosts
|
||||
=====
|
||||
|
||||
address mac name os_name os_flavor os_sp purpose info comments
|
||||
------- --- ---- ------- --------- ----- ------- ---- --------
|
||||
127.0.0.1 192.168.1.10 macOS Mojave (macOS 10.14.6) device
|
||||
TABLE
|
||||
expect(tbl.to_s.lines).to all(have_maximum_width(120))
|
||||
end
|
||||
|
||||
it 'makes use of all available space' do
|
||||
options = {
|
||||
'Header' => 'Hosts',
|
||||
'Indent' => 0,
|
||||
'Width' => 80,
|
||||
'Columns' => [
|
||||
'address',
|
||||
'mac',
|
||||
'name',
|
||||
'os_name',
|
||||
'os_flavor',
|
||||
'os_sp',
|
||||
'purpose',
|
||||
'info',
|
||||
'comments'
|
||||
]
|
||||
}
|
||||
|
||||
tbl = Rex::Text::Table.new(options)
|
||||
|
||||
tbl << [
|
||||
"127.0.0.1",
|
||||
"",
|
||||
"192.168.1.10",
|
||||
"macOS Mojave (macOS 10.14.6)",
|
||||
"",
|
||||
"",
|
||||
"device",
|
||||
"",
|
||||
""
|
||||
]
|
||||
|
||||
expect(tbl).to match_table <<~TABLE
|
||||
Hosts
|
||||
=====
|
||||
|
||||
address mac name os_name os_flav os_sp purpose info comments
|
||||
or
|
||||
------- --- ---- ------- ------- ----- ------- ---- --------
|
||||
127.0.0. 192.168.1. macOS Moja device
|
||||
1 10 ve (macOS
|
||||
10.14.6)
|
||||
TABLE
|
||||
expect(tbl.to_s.lines).to all(have_maximum_width(80))
|
||||
end
|
||||
|
||||
context 'when word wrapping occurs' do
|
||||
it "Evenly distributes all data" do
|
||||
options = {
|
||||
'Header' => 'Header',
|
||||
'Width' => 80,
|
||||
'Columns' => [
|
||||
'id',
|
||||
'Column 2',
|
||||
'Column 3',
|
||||
]
|
||||
}
|
||||
|
||||
tbl = Rex::Text::Table.new(options)
|
||||
tbl << [
|
||||
'1',
|
||||
'Lorem ipsum dolor sit amet, consectetur adipiscing elite',
|
||||
'Pellentesque ac tellus lobortis, volutpat nibh sit amet'
|
||||
]
|
||||
|
||||
expect(tbl).to match_table <<~TABLE
|
||||
Header
|
||||
======
|
||||
|
||||
id Column 2 Column 3
|
||||
-- -------- --------
|
||||
1 Lorem ipsum dolor sit amet, consecte Pellentesque ac tellus lobortis, vol
|
||||
tur adipiscing elite utpat nibh sit amet
|
||||
TABLE
|
||||
expect(tbl.to_s.lines).to all(have_maximum_width(80))
|
||||
end
|
||||
|
||||
it "Evenly allows columns to have specified widths" do
|
||||
options = {
|
||||
'Header' => 'Header',
|
||||
'Width' => 80,
|
||||
'Columns' => [
|
||||
'name',
|
||||
'Column 2',
|
||||
'Column 3',
|
||||
],
|
||||
'ColProps' => {
|
||||
'Column 2' => {
|
||||
'MaxChar' => 30
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tbl = Rex::Text::Table.new(options)
|
||||
tbl << [
|
||||
'1',
|
||||
'Lorem ipsum dolor sit amet, consectetur adipiscing elite ' * 2,
|
||||
'Pellentesque ac tellus lobortis, volutpat nibh sit amet'
|
||||
]
|
||||
|
||||
expect(tbl).to match_table <<~TABLE
|
||||
Header
|
||||
======
|
||||
|
||||
name Column 2 Column 3
|
||||
---- -------- --------
|
||||
1 Lorem ipsum dolor sit amet, consect Pellentesque ac tellus lobortis, vo
|
||||
etur adipiscing elite Lorem ipsum d lutpat nibh sit amet
|
||||
olor sit amet, consectetur adipisci
|
||||
ng elite
|
||||
TABLE
|
||||
expect(tbl.to_s.lines).to all(have_maximum_width(80))
|
||||
end
|
||||
|
||||
it "handles multiple columns and rows" do
|
||||
options = {
|
||||
'Header' => 'Header',
|
||||
'Indent' => 2,
|
||||
'Width' => 80,
|
||||
'Columns' => [
|
||||
'Name',
|
||||
'Current Setting',
|
||||
'Required',
|
||||
'Description'
|
||||
]
|
||||
}
|
||||
|
||||
tbl = Rex::Text::Table.new(options)
|
||||
tbl << [
|
||||
"DESCRIPTION",
|
||||
"{PROCESS_NAME} needs your permissions to start. Please enter user credentials",
|
||||
"yes",
|
||||
"Message shown in the loginprompt"
|
||||
]
|
||||
tbl << [
|
||||
"PROCESS",
|
||||
"",
|
||||
"no",
|
||||
"Prompt if a specific process is started by the target. (e.g. calc.exe or specify * for all processes)"
|
||||
]
|
||||
tbl << [
|
||||
"SESSION",
|
||||
"",
|
||||
"yes",
|
||||
"The session to run this module on."
|
||||
]
|
||||
|
||||
expect(tbl).to match_table <<~TABLE
|
||||
Header
|
||||
======
|
||||
|
||||
Name Current Setting Required Description
|
||||
---- --------------- -------- -----------
|
||||
DESCRIPTION {PROCESS_NAME} need yes Message shown in the loginprompt
|
||||
s your permissions
|
||||
to start. Please en
|
||||
ter user credential
|
||||
s
|
||||
PROCESS no Prompt if a specific process is
|
||||
started by the target. (e.g. cal
|
||||
c.exe or specify * for all proce
|
||||
sses)
|
||||
SESSION yes The session to run this module o
|
||||
n.
|
||||
TABLE
|
||||
expect(tbl.to_s.lines).to all(have_maximum_width(80))
|
||||
end
|
||||
|
||||
it "handles strings in different encodings" do
|
||||
options = {
|
||||
'Header' => 'Header',
|
||||
'Indent' => 2,
|
||||
'Width' => 80,
|
||||
'Columns' => [
|
||||
'Name',
|
||||
'Value'
|
||||
]
|
||||
}
|
||||
|
||||
tbl = Rex::Text::Table.new(options)
|
||||
tbl << ["hello world".force_encoding("ASCII-8BIT")] * 2
|
||||
tbl << ["Administratör".force_encoding("UTF-8")] * 2
|
||||
tbl << ["Administrator’s Shares".force_encoding("UTF-16LE")] * 2
|
||||
tbl << [
|
||||
"администраторадминистраторадминистратор",
|
||||
"домен"
|
||||
]
|
||||
tbl << ["这是中文这是中文这是中文这是中文".force_encoding("UTF-8")] * 2
|
||||
tbl << ["お好み焼き".force_encoding("UTF-8")] * 2
|
||||
|
||||
# Note:
|
||||
expect(tbl).to match_table <<~TABLE.force_encoding("UTF-8")
|
||||
Header
|
||||
======
|
||||
|
||||
Name Value
|
||||
---- -----
|
||||
Administrator’s Shares Administrator’s Shares
|
||||
Administratör Administratör
|
||||
hello world hello world
|
||||
администраторадминистраторадминистратор домен
|
||||
お好み焼き お好み焼き
|
||||
这是中文这是中文这是中文这是中文 这是中文这是中文这是中文这是中文
|
||||
TABLE
|
||||
expect(tbl.to_s.lines).to all(have_maximum_width(80))
|
||||
end
|
||||
|
||||
it "Wraps columns as well as values" do
|
||||
options = {
|
||||
'Header' => 'Header',
|
||||
'Indent' => 2,
|
||||
'Width' => 80,
|
||||
'Columns' => [
|
||||
'A' * 80,
|
||||
'B' * 40
|
||||
]
|
||||
}
|
||||
|
||||
tbl = Rex::Text::Table.new(options)
|
||||
tbl << [
|
||||
'Foo',
|
||||
'Bar'
|
||||
]
|
||||
tbl << [
|
||||
'Foo',
|
||||
'Bar'
|
||||
]
|
||||
|
||||
expect(tbl).to match_table <<~TABLE
|
||||
Header
|
||||
======
|
||||
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA BBB
|
||||
AAAAAA
|
||||
------------------------------------- -------------------------------------
|
||||
Foo Bar
|
||||
Foo Bar
|
||||
TABLE
|
||||
expect(tbl.to_s.lines).to all(have_maximum_width(80))
|
||||
end
|
||||
|
||||
it "safely wordwraps cells that have colors present" do
|
||||
skip(
|
||||
"Functionality not implemented. Currently if there are colors present in a cell, the colors will break " \
|
||||
"when word wrapping occurs"
|
||||
)
|
||||
|
||||
options = {
|
||||
'Header' => 'Header',
|
||||
'Indent' => 2,
|
||||
'Width' => 80,
|
||||
'Columns' => [
|
||||
'Blue Column',
|
||||
'Red Column'
|
||||
]
|
||||
}
|
||||
|
||||
tbl = Rex::Text::Table.new(options)
|
||||
tbl << [
|
||||
"%blu#{'A' * 100}%clr",
|
||||
"%red#{'A' * 100}%clr",
|
||||
]
|
||||
expect(tbl).to match_table <<~TABLE
|
||||
Header
|
||||
======
|
||||
|
||||
Blue Column Red Column
|
||||
----------- ----------
|
||||
%bluAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA%clr %redAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA%clr
|
||||
%bluAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA%clr %redAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA%clr
|
||||
%bluAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA%clr %redAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA%clr
|
||||
TABLE
|
||||
expect(tbl.to_s.lines).to all(have_maximum_width(80))
|
||||
end
|
||||
|
||||
it "ensures specific columns can disable wordwrapping" do
|
||||
skip(
|
||||
"Functionality not implemented. Allowing certain columns to disable wordwrapping would allow the values to be " \
|
||||
"copy/pasted easily, i.e. password fields."
|
||||
)
|
||||
|
||||
options = {
|
||||
'Header' => 'Header',
|
||||
'Indent' => 2,
|
||||
'Width' => 80,
|
||||
'Columns' => [
|
||||
'#',
|
||||
'Name',
|
||||
'Disclosure Date',
|
||||
'Rank',
|
||||
'Check',
|
||||
'Description'
|
||||
],
|
||||
'ColProps' => {
|
||||
'Name' => {
|
||||
'WordWrap' => false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tbl = Rex::Text::Table.new(options)
|
||||
tbl << ['0', 'auxiliary/admin/2wire/xslt_password_reset', '2007-08-15', 'normal', 'No', '2Wire Cross-Site Request Forgery Password Reset Vulnerability']
|
||||
tbl << ['1', 'auxiliary/admin/android/google_play_store_uxss_xframe_rce', '', 'normal', 'No', 'Android Browser RCE Through Google Play Store XFO']
|
||||
tbl << ['2', 'auxiliary/admin/appletv/appletv_display_image', '', 'normal', 'No', 'Apple TV Image Remote Control']
|
||||
|
||||
expect(tbl).to match_table <<~TABLE
|
||||
...
|
||||
TABLE
|
||||
expect(tbl.to_s.lines).to all(have_maximum_width(80))
|
||||
end
|
||||
|
||||
it "continues to work when it's not possible to fit all of the columns into the available width" do
|
||||
options = {
|
||||
'Header' => 'Header',
|
||||
'Indent' => 2,
|
||||
'Width' => 10,
|
||||
'Columns' => [
|
||||
'Name',
|
||||
'Value',
|
||||
'Required',
|
||||
'Description'
|
||||
]
|
||||
}
|
||||
|
||||
tbl = Rex::Text::Table.new(options)
|
||||
tbl << [
|
||||
"ABCD",
|
||||
"ABCD",
|
||||
"Yes",
|
||||
"ABCD"
|
||||
]
|
||||
|
||||
# If it's not possible to fit all of the required data into the given space, we can either:
|
||||
#
|
||||
# 1. Ensure that all columns are allocated at least one character
|
||||
# 2. Show the left most column(s) that fit, truncate the rightmost columns, and attempt to add affordance.
|
||||
#
|
||||
# Example:
|
||||
#
|
||||
# Header
|
||||
# ======
|
||||
#
|
||||
# Name ...
|
||||
# ---- ---
|
||||
# Foo ...
|
||||
# Bar ...
|
||||
#
|
||||
# For simplicity the first option is chosen, as in either scenario the user will have to resize their terminal.
|
||||
expect(tbl).to match_table <<~TABLE
|
||||
Header
|
||||
======
|
||||
|
||||
N V R D
|
||||
a a e e
|
||||
m l q s
|
||||
e u u c
|
||||
e i r
|
||||
r i
|
||||
e p
|
||||
d t
|
||||
i
|
||||
o
|
||||
n
|
||||
- - - -
|
||||
A A Y A
|
||||
B B e B
|
||||
C C s C
|
||||
D D D
|
||||
TABLE
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,2 +1,11 @@
|
|||
$LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
|
||||
require 'rex/text'
|
||||
|
||||
RSpec.configure do |config|
|
||||
if ENV['CI']
|
||||
config.before(:example, :focus) { raise "Should not commit focused specs" }
|
||||
else
|
||||
config.filter_run focus: true
|
||||
config.run_all_when_everything_filtered = true
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in New Issue