Land #48, Adds color support for wrapped tables
This commit is contained in:
commit
17a41105cb
|
@ -98,6 +98,21 @@ module Rex
|
|||
end
|
||||
|
||||
|
||||
# @return [Regexp] Matches a valid color code, i.e. "%blu,%yel,...etc"
|
||||
COLOR_CODES_REGEX = /#{Regexp.union(Rex::Text::Color::SUPPORTED_FORMAT_CODES.compact).source}/
|
||||
private_constant :COLOR_CODES_REGEX
|
||||
|
||||
#
|
||||
# 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 ignoring color codes.
|
||||
#
|
||||
# @param [String] str
|
||||
# @return [Integer]
|
||||
def self.display_width(str)
|
||||
str.gsub(COLOR_CODES_REGEX, '').length
|
||||
end
|
||||
|
||||
#
|
||||
# Convert 16-byte string to a GUID string
|
||||
#
|
||||
|
|
|
@ -10,6 +10,7 @@ module Text
|
|||
#
|
||||
###
|
||||
module Color
|
||||
SUPPORTED_FORMAT_CODES = %w[%cya %red %grn %blu %yel %whi %mag %blk %dred %dgrn %dblu %dyel %dcya %dwhi %dmag %und %bld %clr %bgblu %bgyel %bggrn %bgmag %bgblk %bgred %bgcyn %bgwhi]
|
||||
|
||||
AnsiAttributes =
|
||||
{
|
||||
|
|
|
@ -390,6 +390,7 @@ protected
|
|||
# Converts a row to a string.
|
||||
#
|
||||
def row_to_s(row) # :nodoc:
|
||||
row = row.each_with_index.map { |cell, index| style_table_field(cell, index) }
|
||||
optimal_widths = calculate_optimal_widths
|
||||
values_as_chunks = chunk_values(row, optimal_widths)
|
||||
chunks_to_s(values_as_chunks, optimal_widths)
|
||||
|
@ -401,17 +402,134 @@ protected
|
|||
# widths. For now it simply returns the string's length.
|
||||
#
|
||||
def display_width(str)
|
||||
str.length
|
||||
Rex::Text.display_width(str)
|
||||
end
|
||||
|
||||
#
|
||||
# Returns a string of color/formatting codes made up of the previously stored color_state
|
||||
# e.g. if a `%blu` color code spans multiple lines this will return a string of `%blu` to be appended to
|
||||
# the beginning of each row
|
||||
#
|
||||
# @param [Hash<String, String>] color_state tracks current color/formatting codes within table row
|
||||
# @returns [String] Color code string such as `%blu%grn'
|
||||
def color_code_string_for(color_state)
|
||||
result = ''.dup
|
||||
color_state.each do |_format, value|
|
||||
if value.is_a?(Array)
|
||||
result << value.uniq.join
|
||||
else
|
||||
result << value
|
||||
end
|
||||
end
|
||||
result
|
||||
end
|
||||
|
||||
# @returns [Hash<String, String>] The supported color codes from {Rex::Text::Color} grouped into sections
|
||||
def color_code_groups
|
||||
return @color_code_groups if @color_code_groups
|
||||
|
||||
@color_code_groups = {
|
||||
foreground: %w[
|
||||
%cya %red %grn %blu %yel %whi %mag %blk
|
||||
%dred %dgrn %dblu %dyel %dcya %dwhi %dmag
|
||||
],
|
||||
background: %w[
|
||||
%bgblu %bgyel %bggrn %bgmag %bgblk %bgred %bgcyn %bgwhi
|
||||
],
|
||||
decoration: %w[
|
||||
%und %bld
|
||||
],
|
||||
clear: %w[
|
||||
%clr
|
||||
]
|
||||
}
|
||||
|
||||
# Developer exception raised to ensure all color codes are accounted for. Verified via tests.
|
||||
missing_color_codes = (Rex::Text::Color::SUPPORTED_FORMAT_CODES - @color_code_groups.values.flatten)
|
||||
raise "Unsupported color codes #{missing_color_codes.join(', ')}" if missing_color_codes.any?
|
||||
|
||||
@color_code_groups
|
||||
end
|
||||
|
||||
# Find the preceding color type and value of a given string
|
||||
# @param [String] string A string such as '%bgyel etc'
|
||||
# @returns [Array,nil] A tuple with the color type and value, or nil
|
||||
def find_color_type_and_value(string)
|
||||
color_code_groups.each do |color_type, color_values|
|
||||
color_value = color_values.find { |color_value| string.start_with?(color_value) }
|
||||
if color_value
|
||||
return [color_type, color_value]
|
||||
end
|
||||
end
|
||||
|
||||
nil
|
||||
end
|
||||
|
||||
#
|
||||
# Takes an array of row values and an integer of optimal column width, loops over array and parses
|
||||
# each string to gather color/formatting tags and handles those appropriately while not increasing the column width
|
||||
#
|
||||
# e.g. if a formatting "%blu" spans across multiple lines it needs to be added to the beginning off every following
|
||||
# line, and each line will have a "%clr" added to the end of each row
|
||||
#
|
||||
# @param [Array<String>] values
|
||||
# @param [Integer] optimal_widths
|
||||
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|
|
||||
color_state = {}
|
||||
column_width = optimal_widths[idx]
|
||||
value
|
||||
.chars
|
||||
.each_slice(column_width)
|
||||
.map(&:join)
|
||||
chunks = []
|
||||
current_chunk = nil
|
||||
chars = value.chars
|
||||
char_index = 0
|
||||
|
||||
# Check if any color code(s) from previous the string need appended
|
||||
while char_index < chars.length do
|
||||
# If a new chunk has started, start the chunk with any previous color codes
|
||||
if current_chunk.nil? && color_state.any?
|
||||
current_chunk = color_code_string_for(color_state)
|
||||
end
|
||||
current_chunk ||= ''.dup
|
||||
|
||||
# Check if the remaining chars start with a color code such as %blu
|
||||
color_type_and_value = chars[char_index] == '%' ? find_color_type_and_value(chars[char_index..].join) : nil
|
||||
if color_type_and_value.nil?
|
||||
current_chunk << chars[char_index]
|
||||
char_index += 1
|
||||
else
|
||||
color_type, color_code = color_type_and_value
|
||||
|
||||
if color_type == :clear
|
||||
color_state.clear
|
||||
elsif color_type == :decoration
|
||||
# Multiple decorations can be enabled
|
||||
color_state[:decoration] ||= []
|
||||
color_state[:decoration] << color_code
|
||||
else
|
||||
# There can only be one foreground or background color
|
||||
color_state[color_type] = color_code
|
||||
end
|
||||
|
||||
current_chunk << color_code
|
||||
char_index += color_code.length
|
||||
end
|
||||
|
||||
# If we've reached the final character of the string, or need to word wrap
|
||||
# it's time to push the current chunk, and start a new row. Also discard
|
||||
# any values that are purely colors and have no display_width
|
||||
is_final_character = char_index >= chars.length
|
||||
display_width = display_width(current_chunk)
|
||||
if (is_final_character && display_width != 0) || display_width == column_width
|
||||
if color_state.any? && !current_chunk.end_with?('%clr')
|
||||
current_chunk << '%clr'
|
||||
end
|
||||
chunks.push(current_chunk)
|
||||
current_chunk = nil
|
||||
end
|
||||
end
|
||||
|
||||
chunks
|
||||
end
|
||||
|
||||
values_as_chunks
|
||||
|
@ -424,12 +542,13 @@ protected
|
|||
line = ""
|
||||
row_chunks.each_with_index do |chunk, idx|
|
||||
column_width = optimal_widths[idx]
|
||||
chunk_length_with_padding = column_width + (chunk.to_s.length - display_width(chunk.to_s))
|
||||
|
||||
if idx == 0
|
||||
line << ' ' * indent
|
||||
end
|
||||
|
||||
line << chunk.to_s.ljust(column_width)
|
||||
line << chunk.to_s.ljust(chunk_length_with_padding)
|
||||
line << ' ' * cellpad
|
||||
end
|
||||
|
||||
|
@ -519,13 +638,12 @@ protected
|
|||
str_cp
|
||||
end
|
||||
|
||||
def style_table_field(str, _idx)
|
||||
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
|
||||
colprops[idx]['Stylers'].each do |s|
|
||||
str_cp = s.style(str_cp)
|
||||
end
|
||||
|
||||
str_cp
|
||||
end
|
||||
|
|
|
@ -61,10 +61,75 @@ describe Rex::Text::Table do
|
|||
|
||||
it "ignores the user preference when set to false" do
|
||||
expect(described_class.wrap_table?(disable_wrapped_table_options)).to be false
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it 'should return a blank table as no search terms were matched' 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_s).to eql <<~TABLE
|
||||
Header
|
||||
======
|
||||
|
||||
Column 1 Column 2 Column 3
|
||||
-------- -------- --------
|
||||
TABLE
|
||||
end
|
||||
|
||||
it 'should return the row as the row contains a match for the search term' do
|
||||
col_1_field = "jim"
|
||||
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_s).to eql <<~TABLE
|
||||
Header
|
||||
======
|
||||
|
||||
Column 1 Column 2 Column 3
|
||||
-------- -------- --------
|
||||
jim BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB CCCCCCCCCCCCCCC
|
||||
TABLE
|
||||
end
|
||||
|
||||
it 'should space columns correctly' do
|
||||
col_1_field = "A" * 5
|
||||
col_2_field = "B" * 50
|
||||
|
@ -72,7 +137,6 @@ describe Rex::Text::Table do
|
|||
|
||||
options = {
|
||||
'Header' => 'Header',
|
||||
'SearchTerm' => ['jim', 'bob'],
|
||||
'Columns' => [
|
||||
'Column 1',
|
||||
'Column 2',
|
||||
|
@ -105,7 +169,6 @@ describe Rex::Text::Table do
|
|||
|
||||
options = {
|
||||
'Header' => 'Header',
|
||||
'SearchTerm' => ['jim', 'bob'],
|
||||
'Columns' => [
|
||||
'Column 1',
|
||||
'Column 2',
|
||||
|
@ -144,7 +207,6 @@ describe Rex::Text::Table do
|
|||
|
||||
options = {
|
||||
'Header' => 'Header',
|
||||
'SearchTerm' => ['jim', 'bob'],
|
||||
'Columns' => [
|
||||
'Column 1',
|
||||
'Column 2',
|
||||
|
|
|
@ -17,7 +17,7 @@ describe Rex::Text::Table do
|
|||
let(:styler) do
|
||||
clazz = Class.new do
|
||||
def style(str)
|
||||
"IHAVEBEENSTYLED#{str}"
|
||||
"%blu#{str}%clr"
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -89,7 +89,7 @@ describe Rex::Text::Table do
|
|||
"<EFBFBD>_<EFBFBD><EFBFBD>\u0010\u007F\u0011N<EFBFBD><EFBFBD><EFBFBD>:T3A<33>","四Ⅰ"
|
||||
"👍👍👍👍👍👍","hello 日本"
|
||||
TABLE
|
||||
expect(tbl.to_s.lines).to all(have_maximum_width(80))
|
||||
expect(tbl.to_s.lines).to all(have_maximum_display_width(80))
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -124,6 +124,71 @@ describe Rex::Text::Table do
|
|||
end
|
||||
end
|
||||
|
||||
it 'should return a blank table as no search terms were matched' 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_s).to eql <<~TABLE
|
||||
Header
|
||||
======
|
||||
|
||||
Column 1 Column 2 Column 3
|
||||
-------- -------- --------
|
||||
TABLE
|
||||
end
|
||||
|
||||
it 'should return the row as the row contains a match for the search term' do
|
||||
col_1_field = "jim"
|
||||
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_s).to eql <<~TABLE
|
||||
Header
|
||||
======
|
||||
|
||||
Column 1 Column 2 Column 3
|
||||
-------- -------- --------
|
||||
jim BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB CCCCCCCCCCCCCCC
|
||||
TABLE
|
||||
end
|
||||
|
||||
it 'should space columns correctly' do
|
||||
col_1_field = "A" * 5
|
||||
col_2_field = "B" * 50
|
||||
|
@ -131,7 +196,6 @@ describe Rex::Text::Table do
|
|||
|
||||
options = {
|
||||
'Header' => 'Header',
|
||||
'SearchTerm' => ['jim', 'bob'],
|
||||
'Columns' => [
|
||||
'Column 1',
|
||||
'Column 2',
|
||||
|
@ -161,9 +225,9 @@ describe Rex::Text::Table 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',
|
||||
|
@ -195,18 +259,12 @@ describe Rex::Text::Table do
|
|||
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',
|
||||
|
@ -233,7 +291,59 @@ describe Rex::Text::Table do
|
|||
|
||||
Column 1 Column 2 Column 3
|
||||
-------- -------- --------
|
||||
AAAAA IHAVEBEENSTYLEDBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB CCCCCCCCCCCCCCC
|
||||
AAAAA %bluBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB%clr CCCCCCCCCCCCCCC
|
||||
TABLE
|
||||
end
|
||||
|
||||
it 'should apply field stylers correctly and NOT increase column length when having a low width value' do
|
||||
options = {
|
||||
'Header' => 'Header',
|
||||
'Width' => 3,
|
||||
'Columns' => [
|
||||
'Column 1',
|
||||
'Column 2',
|
||||
'Column 3'
|
||||
],
|
||||
'ColProps' => {
|
||||
'Column 2' => {
|
||||
'Stylers' => [styler]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tbl = Rex::Text::Table.new(options)
|
||||
|
||||
tbl << [
|
||||
"A" * 5,
|
||||
"ABC ABCD ABC" * 1,
|
||||
"C" * 5
|
||||
]
|
||||
|
||||
expect(tbl).to match_table <<~TABLE
|
||||
Header
|
||||
======
|
||||
|
||||
C C C
|
||||
o o o
|
||||
l l l
|
||||
u u u
|
||||
m m m
|
||||
n n n
|
||||
|
||||
1 2 3
|
||||
- - -
|
||||
A %bluA%clr C
|
||||
A %bluB%clr C
|
||||
A %bluC%clr C
|
||||
A %blu %clr C
|
||||
A %bluA%clr C
|
||||
%bluB%clr
|
||||
%bluC%clr
|
||||
%bluD%clr
|
||||
%blu %clr
|
||||
%bluA%clr
|
||||
%bluB%clr
|
||||
%bluC%clr
|
||||
TABLE
|
||||
end
|
||||
|
||||
|
@ -277,7 +387,7 @@ describe Rex::Text::Table do
|
|||
------- --- ---- ------- --------- ----- ------- ---- --------
|
||||
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))
|
||||
expect(tbl.to_s.lines).to all(have_maximum_display_width(120))
|
||||
end
|
||||
|
||||
it 'makes use of all available space' do
|
||||
|
@ -323,7 +433,7 @@ describe Rex::Text::Table do
|
|||
1 10 ve (macOS
|
||||
10.14.6)
|
||||
TABLE
|
||||
expect(tbl.to_s.lines).to all(have_maximum_width(80))
|
||||
expect(tbl.to_s.lines).to all(have_maximum_display_width(80))
|
||||
end
|
||||
|
||||
context 'when word wrapping occurs' do
|
||||
|
@ -354,7 +464,7 @@ describe Rex::Text::Table do
|
|||
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))
|
||||
expect(tbl.to_s.lines).to all(have_maximum_display_width(80))
|
||||
end
|
||||
|
||||
it "Evenly allows columns to have specified widths" do
|
||||
|
@ -391,7 +501,7 @@ describe Rex::Text::Table do
|
|||
olor sit amet, consectetur adipisci
|
||||
ng elite
|
||||
TABLE
|
||||
expect(tbl.to_s.lines).to all(have_maximum_width(80))
|
||||
expect(tbl.to_s.lines).to all(have_maximum_display_width(80))
|
||||
end
|
||||
|
||||
it "handles multiple columns and rows" do
|
||||
|
@ -445,7 +555,7 @@ describe Rex::Text::Table do
|
|||
SESSION yes The session to run this module o
|
||||
n.
|
||||
TABLE
|
||||
expect(tbl.to_s.lines).to all(have_maximum_width(80))
|
||||
expect(tbl.to_s.lines).to all(have_maximum_display_width(80))
|
||||
end
|
||||
|
||||
it "handles strings in different encodings" do
|
||||
|
@ -513,7 +623,7 @@ describe Rex::Text::Table do
|
|||
<EFBFBD>_<EFBFBD><EFBFBD>\u0010\u007F\u0011N<EFBFBD><EFBFBD><EFBFBD>:T3A<EFBFBD> 四Ⅰ
|
||||
👍👍👍👍👍👍 hello 日本
|
||||
TABLE
|
||||
expect(tbl.to_s.lines).to all(have_maximum_width(80))
|
||||
expect(tbl.to_s.lines).to all(have_maximum_display_width(80))
|
||||
end
|
||||
|
||||
it "Wraps columns as well as values" do
|
||||
|
@ -548,18 +658,46 @@ describe Rex::Text::Table do
|
|||
Foo Bar
|
||||
Foo Bar
|
||||
TABLE
|
||||
expect(tbl.to_s.lines).to all(have_maximum_width(80))
|
||||
expect(tbl.to_s.lines).to all(have_maximum_display_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"
|
||||
)
|
||||
it "safely wordwraps cells when an % symbol that is not associated with a color/format codes is present" do
|
||||
|
||||
options = {
|
||||
'Header' => 'Header',
|
||||
'Indent' => 2,
|
||||
'Indent' => 0,
|
||||
'Width' => 80,
|
||||
'Columns' => [
|
||||
'Blue Column',
|
||||
'Red Column'
|
||||
]
|
||||
}
|
||||
|
||||
tbl = Rex::Text::Table.new(options)
|
||||
tbl << [
|
||||
"%#{'A' * 49}%#{'A' * 49}",
|
||||
"%#{'A' * 49}%#{'A' * 49}",
|
||||
]
|
||||
|
||||
expect(tbl).to match_table <<~TABLE
|
||||
Header
|
||||
======
|
||||
|
||||
Blue Column Red Column
|
||||
----------- ----------
|
||||
%AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA %AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAAAAAAA%AAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAA%AAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAA
|
||||
TABLE
|
||||
|
||||
expect(tbl.to_s.lines).to all(have_maximum_display_width(80))
|
||||
end
|
||||
|
||||
it "safely wordwraps cells that have a single color/format across multiple lines" do
|
||||
|
||||
options = {
|
||||
'Header' => 'Header',
|
||||
'Indent' => 0,
|
||||
'Width' => 80,
|
||||
'Columns' => [
|
||||
'Blue Column',
|
||||
|
@ -572,17 +710,243 @@ describe Rex::Text::Table do
|
|||
"%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
|
||||
Blue Column Red Column
|
||||
----------- ----------
|
||||
%bluAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA%clr %redAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA%clr
|
||||
%bluAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA%clr %redAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA%clr
|
||||
%bluAAAAAAAAAAAAAAAAAAAAAAAA%clr %redAAAAAAAAAAAAAAAAAAAAAAAA%clr
|
||||
TABLE
|
||||
expect(tbl.to_s.lines).to all(have_maximum_width(80))
|
||||
|
||||
expect(tbl.to_s.lines).to all(have_maximum_display_width(80))
|
||||
end
|
||||
|
||||
it "safely wordwraps cells that have a single color/format across a single line" do
|
||||
|
||||
options = {
|
||||
'Header' => 'Header',
|
||||
'Indent' => 0,
|
||||
'Width' => 80,
|
||||
'Columns' => [
|
||||
'Blue Column',
|
||||
'Red Column'
|
||||
]
|
||||
}
|
||||
|
||||
tbl = Rex::Text::Table.new(options)
|
||||
tbl << [
|
||||
"#{'A' * 40}%bluA%clr#{'A' * 59}",
|
||||
"#{'A' * 40}%redA%clr#{'A' * 59}",
|
||||
]
|
||||
|
||||
expect(tbl).to match_table <<~TABLE
|
||||
Header
|
||||
======
|
||||
|
||||
Blue Column Red Column
|
||||
----------- ----------
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AA%bluA%clrAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AA%redA%clrAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAA
|
||||
TABLE
|
||||
|
||||
expect(tbl.to_s.lines).to all(have_maximum_display_width(80))
|
||||
end
|
||||
|
||||
it "safely wordwraps cells that have a multiple color/format across a multiple line" do
|
||||
|
||||
options = {
|
||||
'Header' => 'Header',
|
||||
'Indent' => 0,
|
||||
'Width' => 80,
|
||||
'Columns' => [
|
||||
'Blue Column',
|
||||
'Red Column'
|
||||
]
|
||||
}
|
||||
|
||||
tbl = Rex::Text::Table.new(options)
|
||||
tbl << [
|
||||
"%blu%undA%magA%yel#{'A' * 109}%clr",
|
||||
"%red%undA%magA%yel#{'A' * 109}%clr",
|
||||
]
|
||||
|
||||
expect(tbl).to match_table <<~TABLE
|
||||
Header
|
||||
======
|
||||
|
||||
Blue Column Red Column
|
||||
----------- ----------
|
||||
%blu%undA%magA%yelAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA%clr %red%undA%magA%yelAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA%clr
|
||||
%yel%undAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA%clr %yel%undAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA%clr
|
||||
%yel%undAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA%clr %yel%undAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA%clr
|
||||
TABLE
|
||||
|
||||
expect(tbl.to_s.lines).to all(have_maximum_display_width(80))
|
||||
end
|
||||
|
||||
it "safely wordwraps cells when there is no color/formatting codes present" do
|
||||
|
||||
options = {
|
||||
'Header' => 'Header',
|
||||
'Indent' => 0,
|
||||
'Width' => 80,
|
||||
'Columns' => [
|
||||
'Blue Column',
|
||||
'Red Column'
|
||||
]
|
||||
}
|
||||
|
||||
tbl = Rex::Text::Table.new(options)
|
||||
tbl << [
|
||||
"#{'A' * 40}",
|
||||
"#{'A' * 40}",
|
||||
]
|
||||
|
||||
expect(tbl).to match_table <<~TABLE
|
||||
Header
|
||||
======
|
||||
|
||||
Blue Column Red Column
|
||||
----------- ----------
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AA AA
|
||||
TABLE
|
||||
|
||||
expect(tbl.to_s.lines).to all(have_maximum_display_width(80))
|
||||
end
|
||||
|
||||
it "verify that formatting aligns correctly" do
|
||||
|
||||
options = {
|
||||
'Header' => 'Header',
|
||||
'Indent' => 0,
|
||||
'Width' => 80,
|
||||
'Columns' => [
|
||||
'Blue Column',
|
||||
'Red Column'
|
||||
]
|
||||
}
|
||||
|
||||
tbl = Rex::Text::Table.new(options)
|
||||
tbl << [
|
||||
"%blu#{'A' * 40}%clr",
|
||||
"%red#{'A' * 40}%clr",
|
||||
]
|
||||
|
||||
expect(tbl).to match_table <<~TABLE
|
||||
Header
|
||||
======
|
||||
|
||||
Blue Column Red Column
|
||||
----------- ----------
|
||||
%bluAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA%clr %redAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA%clr
|
||||
%bluAA%clr %redAA%clr
|
||||
TABLE
|
||||
|
||||
expect(tbl.to_s.lines).to all(have_maximum_display_width(80))
|
||||
end
|
||||
|
||||
|
||||
it "verify that formatting aligns correctly" do
|
||||
|
||||
options = {
|
||||
'Header' => 'Header',
|
||||
'Indent' => 0,
|
||||
'Width' => 80,
|
||||
'Columns' => [
|
||||
'Blue Column',
|
||||
'Red Column'
|
||||
]
|
||||
}
|
||||
|
||||
tbl = Rex::Text::Table.new(options)
|
||||
tbl << [
|
||||
"%blu#{'A' * 40}%clr",
|
||||
"%red#{'A' * 40}%clr",
|
||||
]
|
||||
|
||||
expect(tbl).to match_table <<~TABLE
|
||||
Header
|
||||
======
|
||||
|
||||
Blue Column Red Column
|
||||
----------- ----------
|
||||
%bluAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA%clr %redAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA%clr
|
||||
%bluAA%clr %redAA%clr
|
||||
TABLE
|
||||
|
||||
expect(tbl.to_s.lines).to all(have_maximum_display_width(80))
|
||||
end
|
||||
|
||||
it 'supports dark color codes' do
|
||||
|
||||
options = {
|
||||
'Header' => 'Header',
|
||||
'Indent' => 0,
|
||||
'Width' => 80,
|
||||
'Columns' => [
|
||||
'Blue Column',
|
||||
'Red Column'
|
||||
]
|
||||
}
|
||||
|
||||
tbl = Rex::Text::Table.new(options)
|
||||
tbl << [
|
||||
"%dyel#{'A' * 40}%clr",
|
||||
"%dcya#{'A' * 40}%clr",
|
||||
]
|
||||
|
||||
expect(tbl).to match_table <<~TABLE
|
||||
Header
|
||||
======
|
||||
|
||||
Blue Column Red Column
|
||||
----------- ----------
|
||||
%dyelAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA%clr %dcyaAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA%clr
|
||||
%dyelAA%clr %dcyaAA%clr
|
||||
TABLE
|
||||
|
||||
expect(tbl.to_s.lines).to all(have_maximum_display_width(80))
|
||||
end
|
||||
|
||||
it "verify that formatting aligns correctly" do
|
||||
|
||||
options = {
|
||||
'Indent' => 0,
|
||||
'Width' => 1,
|
||||
'Columns' => [
|
||||
'Blue Column',
|
||||
'Red Column'
|
||||
]
|
||||
}
|
||||
|
||||
tbl = Rex::Text::Table.new(options)
|
||||
tbl << %w[%bgyel%blu%bld%undAAA%clr %bgyel%blu%bld%undAAA%clr]
|
||||
|
||||
expect(tbl).to match_table <<~TABLE
|
||||
B R
|
||||
l e
|
||||
u d
|
||||
e
|
||||
C
|
||||
C o
|
||||
o l
|
||||
l u
|
||||
u m
|
||||
m n
|
||||
n
|
||||
- -
|
||||
%bgyel%blu%bld%undA%clr %bgyel%blu%bld%undA%clr
|
||||
%bgyel%blu%bld%undA%clr %bgyel%blu%bld%undA%clr
|
||||
%bgyel%blu%bld%undA%clr %bgyel%blu%bld%undA%clr
|
||||
TABLE
|
||||
|
||||
expect(tbl.to_s.lines).to all(have_maximum_display_width(7))
|
||||
end
|
||||
|
||||
it "ensures specific columns can disable wordwrapping" do
|
||||
|
@ -618,7 +982,7 @@ describe Rex::Text::Table do
|
|||
expect(tbl).to match_table <<~TABLE
|
||||
...
|
||||
TABLE
|
||||
expect(tbl.to_s.lines).to all(have_maximum_width(80))
|
||||
expect(tbl.to_s.lines).to all(have_maximum_display_width(80))
|
||||
end
|
||||
|
||||
it "continues to work when it's not possible to fit all of the columns into the available width" do
|
||||
|
@ -659,25 +1023,25 @@ describe Rex::Text::Table do
|
|||
#
|
||||
# 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
|
||||
======
|
||||
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
|
||||
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
|
||||
|
|
|
@ -95,6 +95,49 @@ RSpec.describe Rex::Text do
|
|||
end
|
||||
end
|
||||
|
||||
describe ".display_width" do
|
||||
it "verifies that display width calculates correct display width when no format codes are present" do
|
||||
str = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
|
||||
expect(described_class.display_width(str)).to eq(100)
|
||||
end
|
||||
|
||||
it "verifies that display width calculates correct display width when format codes are present" do
|
||||
str = "%bluAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA%clr"
|
||||
expect(described_class.display_width(str)).to eq(100)
|
||||
end
|
||||
|
||||
it "verifies that display width calculates correct display width when format codes are chained together" do
|
||||
str = "%bgyel%blu%bld%grn%undAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA%clr"
|
||||
expect(described_class.display_width(str)).to eq(100)
|
||||
end
|
||||
|
||||
it "verifies that display width calculates correct display width when format codes are chained together with multiple seperate formatting instances" do
|
||||
str = "%bgyel%blu%bld%undhello%clr %bgyel%blu%bld%undworld%clr"
|
||||
expect(described_class.display_width(str)).to eq(11)
|
||||
end
|
||||
|
||||
it "verifies that display width calculates correct display width when format codes are chained together with multiple seperate formatting instances" do
|
||||
str = "%dgrnhello world%clr"
|
||||
expect(described_class.display_width(str)).to eq(11)
|
||||
end
|
||||
|
||||
it "verifies that display width calculates correct display width when emojis with modifiers are passed" do
|
||||
# https://stackoverflow.com/questions/42778709/the-longest-character-in-utf-8
|
||||
str = "🤦🏻♂️".force_encoding('UTF-8')
|
||||
|
||||
# For future travelers, this could technically be a width of 1, but is dependant on how the console renders it
|
||||
expect(described_class.display_width(str)).to eq(5)
|
||||
end
|
||||
|
||||
it "verifies that display width calculates correct display width when emojis with modifiers are passed" do
|
||||
# https://stackoverflow.com/questions/42778709/the-longest-character-in-utf-8
|
||||
str = "🤦🏻♂️".force_encoding('ASCII-8BIT')
|
||||
|
||||
# For future travelers, this could technically be a width of 1, but is dependant on how the console renders it
|
||||
expect(described_class.display_width(str)).to eq(17)
|
||||
end
|
||||
end
|
||||
|
||||
context ".gzip" do
|
||||
it "should return a properly formatted gzip file" do
|
||||
str = described_class.gzip("hi mom")
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
RSpec::Matchers.define :have_maximum_display_width do |expected|
|
||||
match do |actual|
|
||||
Rex::Text.display_width(actual) <= expected
|
||||
end
|
||||
|
||||
failure_message do |actual|
|
||||
"expected '#{actual}' to have a length than or equal to #{expected}, instead got #{actual.length}"
|
||||
end
|
||||
end
|
Loading…
Reference in New Issue