rex-text 0.2.27 → 0.2.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 322c668b2627c7abeb1d4d7cfa032e4db2ee9268cbc968ff99184b41a0dbb5cc
4
- data.tar.gz: 1aa3470e473545171ae3753f6335e982132a653bff9e5ba31aa7bd12cf2c1e72
3
+ metadata.gz: ed0a3f361cc220f42bd061edb39b914add50ad3a52f196f1d02bce1e3e56254a
4
+ data.tar.gz: b9317aea70870fc37394cf4d60c53b12b314e0c0f5da7ed949e0af658f54a2de
5
5
  SHA512:
6
- metadata.gz: 2508a7436df9f53708d8da0d65b192e13baf2d5c65e143ade82d14bb4767807470d459ab2a26f7e430e7fe72eff513e5cdfdb1603eecd3670733675763d74b6f
7
- data.tar.gz: af3211dcadb3312cac7c68ce3d1c5f3d3086f2d1d655fada170d9a408cda91ae54e9a36a1b524d66d95a5be8371135f8822389a04e0740da2763431ab6c42664
6
+ metadata.gz: 984bc5972b4ac1abe5dbd98dbe651bcdc785724b61062062ee1c0aa5811cbd78d54c45bef34f5103e191b878b42fd341ebe37ad0bd482b7e99959b558f5aba4f
7
+ data.tar.gz: f21d06b23911998f4d178c081d420e18e1692e41855cf67d44c0d15509501efdc6745c592fbd3d5681171921c71a67c3e686004bf48e6c7bafdd4aa8747f2ad8
Binary file
data.tar.gz.sig CHANGED
Binary file
data/Gemfile CHANGED
@@ -2,3 +2,7 @@ source 'https://rubygems.org'
2
2
 
3
3
  # Specify your gem's dependencies in rex-text.gemspec
4
4
  gemspec
5
+
6
+ group :development do
7
+ gem 'pry-byebug'
8
+ end
@@ -29,7 +29,7 @@ require 'rex/text/xor'
29
29
 
30
30
  require 'rex/text/color'
31
31
  require 'rex/text/table'
32
-
32
+ require 'rex/text/wrapped_table'
33
33
 
34
34
  module Rex
35
35
 
@@ -12,6 +12,34 @@ module Text
12
12
  ###
13
13
  class Table
14
14
 
15
+ # Temporary forking logic for using the prototype `WrappedTable` implementation.
16
+ #
17
+ # This method replaces the default `Table.new` with the ability to call the `WrappedTable` class instead,
18
+ # to allow users to safely toggle between wrapped/unwrapped tables at a global level without changing
19
+ # their existing codebases. This approach will reduce the risk of enabling wrapped table behavior by default.
20
+ #
21
+ # To enforce all tables to be wrapped to the terminal's current width, call `Table.wrap_tables!`
22
+ # before invoking `Table.new` as normal.
23
+ def self.new(*args, &block)
24
+ if wrap_tables?
25
+ table_options = args[0]
26
+ return ::Rex::Text::WrappedTable.new(table_options)
27
+ end
28
+ return super(*args, &block)
29
+ end
30
+
31
+ def self.wrap_tables?
32
+ @@wrapped_tables_enabled ||= false
33
+ end
34
+
35
+ def self.wrap_tables!
36
+ @@wrapped_tables_enabled = true
37
+ end
38
+
39
+ def self.unwrap_tables!
40
+ @@wrapped_tables_enabled = false
41
+ end
42
+
15
43
  #
16
44
  # Initializes a text table instance using the supplied properties. The
17
45
  # Table class supports the following hash attributes:
@@ -441,7 +469,7 @@ protected
441
469
 
442
470
  def format_table_field(str, idx)
443
471
  str_cp = str.clone
444
-
472
+
445
473
  colprops[idx]['Formatters'].each do |f|
446
474
  str_cp = f.format(str_cp)
447
475
  end
@@ -451,7 +479,7 @@ protected
451
479
 
452
480
  def style_table_field(str, idx)
453
481
  str_cp = str.clone
454
-
482
+
455
483
  colprops[idx]['Stylers'].each do |s|
456
484
  str_cp = s.style(str_cp)
457
485
  end
@@ -1,5 +1,5 @@
1
1
  module Rex
2
2
  module Text
3
- VERSION = "0.2.27"
3
+ VERSION = "0.2.28"
4
4
  end
5
5
  end
@@ -0,0 +1,538 @@
1
+ # -*- coding: binary -*-
2
+ require 'ipaddr'
3
+ require 'io/console'
4
+
5
+ module Rex
6
+ module Text
7
+
8
+ ###
9
+ #
10
+ # Prints text in a tablized format. Pretty lame at the moment, but
11
+ # whatever.
12
+ #
13
+ ###
14
+ # private_constant
15
+ class WrappedTable
16
+
17
+ #
18
+ # Initializes a text table instance using the supplied properties. The
19
+ # Table class supports the following hash attributes:
20
+ #
21
+ # Header
22
+ #
23
+ # The string to display as a heading above the table. If none is
24
+ # specified, no header will be displayed.
25
+ #
26
+ # HeaderIndent
27
+ #
28
+ # The amount of space to indent the header. The default is zero.
29
+ #
30
+ # Columns
31
+ #
32
+ # The array of columns that will exist within the table.
33
+ #
34
+ # Rows
35
+ #
36
+ # The array of rows that will exist.
37
+ #
38
+ # Width
39
+ #
40
+ # The maximum width of the table in characters.
41
+ #
42
+ # Indent
43
+ #
44
+ # The number of characters to indent the table.
45
+ #
46
+ # CellPad
47
+ #
48
+ # The number of characters to put between each horizontal cell.
49
+ #
50
+ # Prefix
51
+ #
52
+ # The text to prefix before the table.
53
+ #
54
+ # Postfix
55
+ #
56
+ # The text to affix to the end of the table.
57
+ #
58
+ # Sortindex
59
+ #
60
+ # The column to sort the table on, -1 disables sorting.
61
+ #
62
+ # ColProps
63
+ #
64
+ # A hash specifying column MaxWidth, Stylers, and Formatters.
65
+ #
66
+ def initialize(opts = {})
67
+ self.header = opts['Header']
68
+ self.headeri = opts['HeaderIndent'] || 0
69
+ self.columns = opts['Columns'] || []
70
+ # updated below if we got a "Rows" option
71
+ self.rows = []
72
+
73
+ # TODO: Discuss a cleaner way to handle this information
74
+ self.width = opts['Width'] || ::IO.console.winsize[1]
75
+ self.indent = opts['Indent'] || 0
76
+ self.cellpad = opts['CellPad'] || 2
77
+ self.prefix = opts['Prefix'] || ''
78
+ self.postfix = opts['Postfix'] || ''
79
+ self.colprops = []
80
+ self.scterm = /#{opts['SearchTerm']}/mi if opts['SearchTerm']
81
+
82
+ self.sort_index = opts['SortIndex'] || 0
83
+ self.sort_order = opts['SortOrder'] || :forward
84
+
85
+ # Default column properties
86
+ self.columns.length.times { |idx|
87
+ self.colprops[idx] = {}
88
+ self.colprops[idx]['MaxWidth'] = self.columns[idx].length
89
+ self.colprops[idx]['WordWrap'] = true
90
+ self.colprops[idx]['Stylers'] = []
91
+ self.colprops[idx]['Formatters'] = []
92
+ }
93
+
94
+ # ensure all our internal state gets updated with the given rows by
95
+ # using add_row instead of just adding them to self.rows. See #3825.
96
+ opts['Rows'].each { |row| add_row(row) } if opts['Rows']
97
+
98
+ # Merge in options
99
+ if (opts['ColProps'])
100
+ opts['ColProps'].each_key { |col|
101
+ idx = self.columns.index(col)
102
+
103
+ if (idx)
104
+ self.colprops[idx].merge!(opts['ColProps'][col])
105
+ end
106
+ }
107
+ end
108
+
109
+ end
110
+
111
+ #
112
+ # Converts table contents to a string.
113
+ #
114
+ def to_s
115
+ str = prefix.dup
116
+ str << header_to_s || ''
117
+ str << columns_to_s || ''
118
+ str << hr_to_s || ''
119
+
120
+ sort_rows
121
+ rows.each { |row|
122
+ if (is_hr(row))
123
+ str << hr_to_s
124
+ else
125
+ str << row_to_s(row) if row_visible(row)
126
+ end
127
+ }
128
+
129
+ str << postfix
130
+
131
+ return str
132
+ end
133
+
134
+ #
135
+ # Converts table contents to a csv
136
+ #
137
+ def to_csv
138
+ str = ''
139
+ str << ( columns.join(",") + "\n" )
140
+ rows.each { |row|
141
+ next if is_hr(row) || !row_visible(row)
142
+ str << ( row.map{|x|
143
+ x = x.to_s
144
+ x.gsub(/[\r\n]/, ' ').gsub(/\s+/, ' ').gsub('"', '""')
145
+ }.map{|x| "\"#{x}\"" }.join(",") + "\n" )
146
+ }
147
+ str
148
+ end
149
+
150
+ #
151
+ #
152
+ # Returns the header string.
153
+ #
154
+ def header_to_s # :nodoc:
155
+ if (header)
156
+ pad = " " * headeri
157
+
158
+ return pad + header + "\n" + pad + "=" * header.length + "\n\n"
159
+ end
160
+
161
+ return ''
162
+ end
163
+
164
+ #
165
+ # Prints the contents of the table.
166
+ #
167
+ def print
168
+ puts to_s
169
+ end
170
+
171
+ #
172
+ # Adds a row using the supplied fields.
173
+ #
174
+ def <<(fields)
175
+ add_row(fields)
176
+ end
177
+
178
+ #
179
+ # Adds a row with the supplied fields.
180
+ #
181
+ def add_row(fields = [])
182
+ if fields.length != self.columns.length
183
+ raise RuntimeError, 'Invalid number of columns!'
184
+ end
185
+ formatted_fields = fields.map.with_index { |field, idx|
186
+ # Remove whitespace and ensure String format
187
+ field = format_table_field(field.to_s.strip, idx)
188
+
189
+ if (colprops[idx]['MaxWidth'] < display_width(field.to_s))
190
+ old = colprops[idx]['MaxWidth']
191
+ colprops[idx]['MaxWidth'] = display_width(field.to_s)
192
+ end
193
+
194
+ field
195
+ }
196
+
197
+ rows << formatted_fields
198
+ end
199
+
200
+ def ip_cmp(a, b)
201
+ begin
202
+ a = IPAddr.new(a.to_s)
203
+ b = IPAddr.new(b.to_s)
204
+ return 1 if a.ipv6? && b.ipv4?
205
+ return -1 if a.ipv4? && b.ipv6?
206
+ a <=> b
207
+ rescue IPAddr::Error
208
+ nil
209
+ end
210
+ end
211
+
212
+ #
213
+ # Sorts the rows based on the supplied index of sub-arrays
214
+ # If the supplied index is an IPv4 address, handle it differently, but
215
+ # avoid actually resolving domain names.
216
+ #
217
+ def sort_rows(index = sort_index, order = sort_order)
218
+ return if index == -1
219
+ return unless rows
220
+ rows.sort! do |a,b|
221
+ if a[index].nil?
222
+ cmp = -1
223
+ elsif b[index].nil?
224
+ cmp = 1
225
+ elsif a[index] =~ /^[0-9]+$/ and b[index] =~ /^[0-9]+$/
226
+ cmp = a[index].to_i <=> b[index].to_i
227
+ elsif (cmp = ip_cmp(a[index], b[index])) != nil
228
+ else
229
+ cmp = a[index] <=> b[index] # assumes otherwise comparable.
230
+ end
231
+ cmp ||= 0
232
+ order == :forward ? cmp : -cmp
233
+ end
234
+ end
235
+
236
+ #
237
+ # Adds a horizontal line.
238
+ #
239
+ def add_hr
240
+ rows << '__hr__'
241
+ end
242
+
243
+ #
244
+ # Returns new sub-table with headers and rows maching column names submitted
245
+ #
246
+ #
247
+ # Flips table 90 degrees left
248
+ #
249
+ def drop_left
250
+ tbl = self.class.new(
251
+ 'Columns' => Array.new(self.rows.count+1,' '),
252
+ 'Header' => self.header,
253
+ 'Indent' => self.indent)
254
+ (self.columns.count+1).times do |ti|
255
+ row = self.rows.map {|r| r[ti]}.unshift(self.columns[ti]).flatten
256
+ # insert our col|row break. kind of hackish
257
+ row[1] = "| #{row[1]}" unless row.all? {|e| e.nil? || e.empty?}
258
+ tbl << row
259
+ end
260
+ return tbl
261
+ end
262
+
263
+ def valid_ip?(value)
264
+ begin
265
+ IPAddr.new value
266
+ true
267
+ rescue IPAddr::Error
268
+ false
269
+ end
270
+ end
271
+
272
+ #
273
+ # Build table from CSV dump
274
+ #
275
+ def self.new_from_csv(csv)
276
+ # Read in or keep data, get CSV or die
277
+ if csv.is_a?(String)
278
+ csv = File.file?(csv) ? CSV.read(csv) : CSV.parse(csv)
279
+ end
280
+ # Adjust for skew
281
+ if csv.first == ["Keys", "Values"]
282
+ csv.shift # drop marker
283
+ cols = []
284
+ rows = []
285
+ csv.each do |row|
286
+ cols << row.shift
287
+ rows << row
288
+ end
289
+ tbl = self.new('Columns' => cols)
290
+ rows.in_groups_of(cols.count) {|r| tbl << r.flatten}
291
+ else
292
+ tbl = self.new('Columns' => csv.shift)
293
+ while !csv.empty? do
294
+ tbl << csv.shift
295
+ end
296
+ end
297
+ return tbl
298
+ end
299
+
300
+ def [](*col_names)
301
+ tbl = self.class.new('Indent' => self.indent,
302
+ 'Header' => self.header,
303
+ 'Columns' => col_names)
304
+ indexes = []
305
+
306
+ col_names.each do |col_name|
307
+ index = self.columns.index(col_name)
308
+ raise RuntimeError, "Invalid column name #{col_name}" if index.nil?
309
+ indexes << index
310
+ end
311
+
312
+ self.rows.each do |old_row|
313
+ new_row = []
314
+ indexes.map {|i| new_row << old_row[i]}
315
+ tbl << new_row
316
+ end
317
+
318
+ return tbl
319
+ end
320
+
321
+
322
+ alias p print
323
+
324
+ attr_accessor :header, :headeri # :nodoc:
325
+ attr_accessor :columns, :rows, :colprops # :nodoc:
326
+ attr_accessor :width, :indent, :cellpad # :nodoc:
327
+ attr_accessor :prefix, :postfix # :nodoc:
328
+ attr_accessor :sort_index, :sort_order, :scterm # :nodoc:
329
+
330
+ protected
331
+
332
+ #
333
+ # Returns if a row should be visible or not
334
+ #
335
+ def row_visible(row)
336
+ return true if self.scterm.nil?
337
+ row_to_s(row).match(self.scterm)
338
+ end
339
+
340
+ #
341
+ # Defaults cell widths and alignments.
342
+ #
343
+ def defaults # :nodoc:
344
+ self.columns.length.times { |idx|
345
+ }
346
+ end
347
+
348
+ #
349
+ # Checks to see if the row is an hr.
350
+ #
351
+ def is_hr(row) # :nodoc:
352
+ return ((row.kind_of?(String)) && (row == '__hr__'))
353
+ end
354
+
355
+ #
356
+ # Converts the columns to a string.
357
+ #
358
+ def columns_to_s # :nodoc:
359
+ optimal_widths = calculate_optimal_widths
360
+ values_as_chunks = chunk_values(columns, optimal_widths)
361
+ result = chunks_to_s(values_as_chunks, optimal_widths)
362
+
363
+ barline = ""
364
+ columns.each.with_index do |_column, idx|
365
+ bar_width = display_width(values_as_chunks[idx].first)
366
+ column_width = optimal_widths[idx]
367
+
368
+ if idx == 0
369
+ barline << ' ' * indent
370
+ end
371
+
372
+ barline << '-' * bar_width
373
+ is_last_column = (idx + 1) == columns.length
374
+ unless is_last_column
375
+ barline << ' ' * (column_width - bar_width)
376
+ barline << ' ' * cellpad
377
+ end
378
+ end
379
+
380
+ result + barline
381
+ end
382
+
383
+ #
384
+ # Converts an hr to a string.
385
+ #
386
+ def hr_to_s # :nodoc:
387
+ return "\n"
388
+ end
389
+
390
+ #
391
+ # Converts a row to a string.
392
+ #
393
+ def row_to_s(row) # :nodoc:
394
+ optimal_widths = calculate_optimal_widths
395
+ values_as_chunks = chunk_values(row, optimal_widths)
396
+ chunks_to_s(values_as_chunks, optimal_widths)
397
+ end
398
+
399
+ #
400
+ # Placeholder function that aims to calculate the display width of the given string.
401
+ # In the future this will be aware of East Asian characters having different display
402
+ # widths. For now it simply returns the string's length.
403
+ #
404
+ def display_width(str)
405
+ str.length
406
+ end
407
+
408
+ def chunk_values(values, optimal_widths)
409
+ # First split long strings into an array of chunks, where each chunk size is the calculated column width
410
+ values_as_chunks = values.each_with_index.map do |value, idx|
411
+ column_width = optimal_widths[idx]
412
+ value
413
+ .split('')
414
+ .each_slice(column_width)
415
+ .map(&:join)
416
+ end
417
+
418
+ values_as_chunks
419
+ end
420
+
421
+ def chunks_to_s(values_as_chunks, optimal_widths)
422
+ result = ''
423
+
424
+ interleave(values_as_chunks).each do |row_chunks|
425
+ line = ""
426
+ row_chunks.each_with_index do |chunk, idx|
427
+ column_width = optimal_widths[idx]
428
+
429
+ if idx == 0
430
+ line << ' ' * indent
431
+ end
432
+
433
+ line << chunk.to_s.ljust(column_width)
434
+ line << ' ' * cellpad
435
+ end
436
+
437
+ result << line.rstrip << "\n"
438
+ end
439
+
440
+ result
441
+ end
442
+
443
+ def interleave(arrays)
444
+ max_length = arrays.map(&:size).max
445
+ padding = [nil] * max_length
446
+ with_left_extra_column = padding.zip(*arrays)
447
+ without_extra_column = with_left_extra_column.map { |columns| columns.drop(1) }
448
+
449
+ without_extra_column
450
+ end
451
+
452
+ def calculate_optimal_widths
453
+ # Calculate the minimum width each column can be. This is dictated by the user.
454
+ user_influenced_column_widths = colprops.map do |colprop|
455
+ if colprop['WordWrap'] == false
456
+ colprop['MaxWidth']
457
+ raise 'Not implemented'
458
+ else
459
+ nil
460
+ end
461
+ end
462
+
463
+ required_padding = indent + (colprops.length) * cellpad
464
+ available_space = self.width - user_influenced_column_widths.sum(&:to_i) - required_padding
465
+ remaining_column_calculations = user_influenced_column_widths.select(&:nil?).count
466
+
467
+ # Calculate the initial widths, which will need an additional refinement to reallocate surplus space
468
+ naive_optimal_width_calculations = colprops.map.with_index do |colprop, index|
469
+ shared_column_width = available_space / [remaining_column_calculations, 1].max
470
+ remaining_column_calculations -= 1
471
+
472
+ if user_influenced_column_widths[index]
473
+ { width: user_influenced_column_widths[index], wrapped: false }
474
+ elsif colprop['MaxWidth'] < shared_column_width
475
+ available_space -= colprop['MaxWidth']
476
+ { width: colprop['MaxWidth'], wrapped: false }
477
+ else
478
+ available_space -= shared_column_width
479
+ { width: shared_column_width, wrapped: true }
480
+ end
481
+ end
482
+
483
+ # Naively redistribute any surplus space to columns that were wrapped, and try to fit the cell on one line still
484
+ current_width = naive_optimal_width_calculations.sum { |width| width[:width] }
485
+ surplus_width = self.width - current_width - required_padding
486
+ # revisit all columns that were wrapped and add add additional characters
487
+ revisiting_column_counts = naive_optimal_width_calculations.count { |width| width[:wrapped] }
488
+ optimal_widths = naive_optimal_width_calculations.map.with_index do |naive_width, index|
489
+ additional_column_width = surplus_width / [revisiting_column_counts, 1].max
490
+ revisiting_column_counts -= 1
491
+
492
+ if naive_width[:wrapped]
493
+ max_width = colprops[index]['MaxWidth']
494
+ if max_width < (naive_width[:width] + additional_column_width)
495
+ surplus_width -= max_width - naive_width[:width]
496
+ max_width
497
+ else
498
+ surplus_width -= additional_column_width
499
+ naive_width[:width] + additional_column_width
500
+ end
501
+ else
502
+ naive_width[:width]
503
+ end
504
+ end
505
+
506
+ # In certain scenarios columns can be allocated 0 widths if it's completely impossible to fit the columns into the
507
+ # given space. There's different ways to handle that, for instance truncating data in the table to the initial
508
+ # columns that can fit. For now, we just ensure every width is at least 1 or more character wide, and in the future
509
+ # it may have to truncate columns entirely.
510
+ optimal_widths.map { |width| [1, width].max }
511
+ end
512
+
513
+ def format_table_field(str, idx)
514
+ str_cp = str.dup
515
+
516
+ colprops[idx]['Formatters'].each do |f|
517
+ str_cp = f.format(str_cp)
518
+ end
519
+
520
+ str_cp.dup.force_encoding('UTF-8')
521
+ end
522
+
523
+ def style_table_field(str, _idx)
524
+ str_cp = str.dup
525
+
526
+ # Not invoking as color currently conflicts with the wrapping of tables
527
+ # colprops[idx]['Stylers'].each do |s|
528
+ # str_cp = s.style(str_cp)
529
+ # end
530
+
531
+ str_cp
532
+ end
533
+
534
+ end
535
+
536
+ end
537
+ end
538
+
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rex-text
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.27
4
+ version: 0.2.28
5
5
  platform: ruby
6
6
  authors:
7
7
  - David 'thelightcosine' Maloney
@@ -93,7 +93,7 @@ cert_chain:
93
93
  JI/W23RbIRksG2pioMhd4dCXq3FLLlkOV1YfCwWixNB+iIhQPPZVaPNfgPhCn4Dt
94
94
  DeGjje/qA4fkLtRmOtb9PUBq3ToRDE4=
95
95
  -----END CERTIFICATE-----
96
- date: 2020-07-13 00:00:00.000000000 Z
96
+ date: 2020-08-07 00:00:00.000000000 Z
97
97
  dependencies:
98
98
  - !ruby/object:Gem::Dependency
99
99
  name: bundler
@@ -177,6 +177,7 @@ files:
177
177
  - lib/rex/text/table.rb
178
178
  - lib/rex/text/unicode.rb
179
179
  - lib/rex/text/version.rb
180
+ - lib/rex/text/wrapped_table.rb
180
181
  - lib/rex/text/xor.rb
181
182
  - rex-text.gemspec
182
183
  homepage: https://github.com/rapid7/rex-text
metadata.gz.sig CHANGED
Binary file