Add initial support for word wrapping rex tables

This commit is contained in:
Alan Foster 2020-05-13 14:59:09 +01:00
parent 474640afec
commit 59e2b747a6
No known key found for this signature in database
GPG Key ID: 3BD4FA3818818F04
6 changed files with 1226 additions and 3 deletions

View File

@ -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

View File

@ -29,7 +29,7 @@ require 'rex/text/xor'
require 'rex/text/color'
require 'rex/text/table'
require 'rex/text/wrapped_table'
module Rex

View File

@ -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

View File

@ -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

View File

@ -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 << [
"Administrators Shares".force_encoding("UTF-16LE"),
"Administrators 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"
"Administrators Shares","Administrators 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 << ["Administrators 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
---- -----
Administrators Shares Administrators 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

View File

@ -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