hexdump 0.3.0 → 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ruby.yml +5 -6
  3. data/.gitignore +1 -0
  4. data/.yardopts +1 -1
  5. data/ChangeLog.md +79 -6
  6. data/Gemfile +3 -0
  7. data/LICENSE.txt +1 -1
  8. data/README.md +500 -137
  9. data/benchmark.rb +29 -22
  10. data/gemspec.yml +2 -1
  11. data/hexdump.gemspec +1 -4
  12. data/lib/hexdump/chars.rb +46 -0
  13. data/lib/hexdump/core_ext/file.rb +68 -6
  14. data/lib/hexdump/core_ext/io.rb +2 -2
  15. data/lib/hexdump/core_ext/kernel.rb +5 -0
  16. data/lib/hexdump/core_ext/string.rb +2 -2
  17. data/lib/hexdump/core_ext/string_io.rb +2 -2
  18. data/lib/hexdump/core_ext.rb +5 -4
  19. data/lib/hexdump/format_string.rb +43 -0
  20. data/lib/hexdump/hexdump.rb +766 -75
  21. data/lib/hexdump/mixin.rb +192 -0
  22. data/lib/hexdump/module_methods.rb +132 -0
  23. data/lib/hexdump/numeric/binary.rb +55 -0
  24. data/lib/hexdump/numeric/char_or_int.rb +95 -0
  25. data/lib/hexdump/numeric/decimal.rb +56 -0
  26. data/lib/hexdump/numeric/exceptions.rb +11 -0
  27. data/lib/hexdump/numeric/hexadecimal.rb +59 -0
  28. data/lib/hexdump/numeric/octal.rb +55 -0
  29. data/lib/hexdump/numeric.rb +5 -0
  30. data/lib/hexdump/reader.rb +313 -0
  31. data/lib/hexdump/theme/ansi.rb +82 -0
  32. data/lib/hexdump/theme/rule.rb +159 -0
  33. data/lib/hexdump/theme.rb +61 -0
  34. data/lib/hexdump/type.rb +233 -0
  35. data/lib/hexdump/types.rb +108 -0
  36. data/lib/hexdump/version.rb +1 -1
  37. data/lib/hexdump.rb +14 -3
  38. data/spec/chars_spec.rb +76 -0
  39. data/spec/core_ext_spec.rb +10 -6
  40. data/spec/format_string_spec.rb +22 -0
  41. data/spec/hexdump_class_spec.rb +1708 -0
  42. data/spec/hexdump_module_spec.rb +23 -0
  43. data/spec/mixin_spec.rb +37 -0
  44. data/spec/numeric/binary_spec.rb +239 -0
  45. data/spec/numeric/char_or_int_spec.rb +210 -0
  46. data/spec/numeric/decimal_spec.rb +317 -0
  47. data/spec/numeric/hexadecimal_spec.rb +320 -0
  48. data/spec/numeric/octal_spec.rb +239 -0
  49. data/spec/reader_spec.rb +866 -0
  50. data/spec/spec_helper.rb +2 -0
  51. data/spec/theme/ansi_spec.rb +242 -0
  52. data/spec/theme/rule_spec.rb +199 -0
  53. data/spec/theme_spec.rb +94 -0
  54. data/spec/type_spec.rb +317 -0
  55. data/spec/types_spec.rb +904 -0
  56. metadata +42 -12
  57. data/.gemtest +0 -0
  58. data/lib/hexdump/dumper.rb +0 -419
  59. data/lib/hexdump/extensions.rb +0 -2
  60. data/spec/dumper_spec.rb +0 -329
  61. data/spec/hexdump_spec.rb +0 -30
@@ -1,86 +1,777 @@
1
- require 'hexdump/dumper'
2
-
3
- #
4
- # Provides the {Hexdump.dump} method and can add hexdumping to other classes
5
- # by including the {Hexdump} module.
6
- #
7
- # class AbstractData
8
- #
9
- # include Hexdump
10
- #
11
- # def each_byte
12
- # # ...
13
- # end
14
- #
15
- # end
16
- #
17
- # data = AbstractData.new
18
- # data.hexdump
19
- #
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'types'
4
+ require_relative 'reader'
5
+ require_relative 'numeric'
6
+ require_relative 'chars'
7
+ require_relative 'theme'
8
+
20
9
  module Hexdump
21
10
  #
22
- # Hexdumps the given data.
23
- #
24
- # @param [#each_byte] data
25
- # The data to be hexdumped.
26
- #
27
- # @param [Integer] width (16)
28
- # The number of bytes to dump for each line.
29
- #
30
- # @param [Integer] endian (:little)
31
- # The endianness that the bytes are organized in. Supported endianness
32
- # include `:little` and `:big`.
33
- #
34
- # @param [Integer] word_size (1)
35
- # The number of bytes within a word.
36
- #
37
- # @param [Symbol, Integer] base (:hexadecimal)
38
- # The base to print bytes in. Supported bases include, `:hexadecimal`,
39
- # `:hex`, `16, `:decimal`, `:dec`, `10, `:octal`, `:oct`, `8`,
40
- # `:binary`, `:bin` and `2`.
41
- #
42
- # @param [Boolean] ascii (false)
43
- # Print ascii characters when possible.
11
+ # Handles the parsing of data and formatting of the hexdump.
44
12
  #
45
- # @param [#<<] output ($stdout)
46
- # The output to print the hexdump to.
13
+ # @since 1.0.0
47
14
  #
48
- # @yield [index,numeric,printable]
49
- # The given block will be passed the hexdump break-down of each
50
- # segment.
15
+ # @api semipublic
51
16
  #
52
- # @yieldparam [Integer] index
53
- # The index of the hexdumped segment.
54
- #
55
- # @yieldparam [Array<String>] numeric
56
- # The numeric representation of the segment.
57
- #
58
- # @yieldparam [Array<String>] printable
59
- # The printable representation of the segment.
60
- #
61
- # @return [nil]
62
- #
63
- # @raise [ArgumentError]
64
- # The given data does not define the `#each_byte` method,
65
- # the `:output` value does not support the `#<<` method or
66
- # the `:base` value was unknown.
67
- #
68
- def self.dump(data, output: $stdout, **options,&block)
69
- dumper = Dumper.new(**options)
17
+ class Hexdump
18
+
19
+ # Default number of columns
20
+ #
21
+ # @since 1.0.0
22
+ DEFAULT_COLUMNS = 16
23
+
24
+ # Numeric bases and their formatting classes.
25
+ BASES = {
26
+ 16 => Numeric::Hexadecimal,
27
+ 10 => Numeric::Decimal,
28
+ 8 => Numeric::Octal,
29
+ 2 => Numeric::Binary
30
+ }
31
+
32
+ # The reader object.
33
+ #
34
+ # @return [Reader]
35
+ attr_reader :reader
36
+
37
+ # The format of the index number.
38
+ #
39
+ # @return [Numeric::Hexadecimal,
40
+ # Numeric::Decimal,
41
+ # Numeric::Octal,
42
+ # Numeric::Binary]
43
+ attr_reader :index
44
+
45
+ # The numeric base format.
46
+ #
47
+ # @return [Numeric::Hexadecimal,
48
+ # Numeric::Decimal,
49
+ # Numeric::Octal,
50
+ # Numeric::Binary]
51
+ attr_reader :numeric
52
+
53
+ # The characters formatter.
54
+ #
55
+ # @return [Chars, nil]
56
+ attr_reader :chars
57
+
58
+ #
59
+ # Initializes a hexdump format.
60
+ #
61
+ # @param [:int8, :uint8, :char, :uchar, :byte, :int16, :int16_le, :int16_be, :int16_ne, :uint16, :uint16_le, :uint16_be, :uint16_ne, :short, :short_le, :short_be, :short_ne, :ushort, :ushort_le, :ushort_be, :ushort_ne, :int32, :int32_le, :int32_be, :int32_ne, :uint32, :uint32_le, :uint32_be, :uint32_ne, :int, :long, :long_le, :long_be, :long_ne, :uint, :ulong, :ulong_le, :ulong_be, :ulong_ne, :int64, :int64_le, :int64_be, :int64_ne, :uint64, :uint64_le, :uint64_be, :uint64_ne, :long_long, :long_long_le, :long_long_be, :long_long_ne, :ulong_long, :ulong_long_le, :ulong_long_be, :ulong_long_ne, :float, :float_le, :float_be, :float_ne, :double, :double_le, :double_be, :double_ne] type (:byte)
62
+ # The type to decode the data as.
63
+ #
64
+ # @param [Integer, nil] offset
65
+ # Controls whether to skip N number of bytes before starting to read data.
66
+ #
67
+ # @param [Integer, nil] length
68
+ # Controls control many bytes to read.
69
+ #
70
+ # @param [Boolean] zero_pad
71
+ # Enables or disables zero padding of data, so that the remaining bytes
72
+ # can be decoded as a uint, int, or float.
73
+ #
74
+ # @param [Boolean] repeating
75
+ # Controls whether to omit repeating duplicate rows data with a `*`.
76
+ #
77
+ # @param [Integer] columns
78
+ # The number of columns per hexdump line. Defaults to `16 / sizeof(type)`.
79
+ #
80
+ # @param [Integer, nil] group_columns
81
+ # Separate groups of columns with an additional space.
82
+ #
83
+ # @param [Integer, :type, nil] group_chars
84
+ # Group chars into columns.
85
+ # If `:type`, then the chars will be grouped by the `type`'s size.
86
+ #
87
+ # @param [16, 10, 8, 2] base
88
+ # The base to print bytes in. Defaults to 16, or to 10 if printing floats.
89
+ #
90
+ # @param [16, 10, 8, 2] index_base
91
+ # Control the base that the index is displayed in. Defaults to base 16.
92
+ #
93
+ # @param [Integer] index_offset
94
+ # The offset to start the index at.
95
+ #
96
+ # @param [Boolean] chars_column
97
+ # Controls whether to display the characters column.
98
+ #
99
+ # @param [:ascii, :utf8, Encoding, nil] encoding
100
+ # The encoding to display the characters in.
101
+ #
102
+ # @param [Boolean, Hash{:index,:numeric,:chars => Symbol,Array<Symbol>}] style
103
+ # Enables theming of index, numeric, or chars columns.
104
+ #
105
+ # @param [Boolean, Hash{:index,:numeric,:chars => Hash{String,Regexp => Symbol,Array<Symbol>}}] highlights
106
+ # Enables selective highlighting of index, numeric, or chars columns.
107
+ #
108
+ # @yield [self]
109
+ # If a block is given, it will be passed the newly initialized hexdump
110
+ # instance.
111
+ #
112
+ # @raise [ArgumentError]
113
+ # The values for `:base` or `:endian` were unknown.
114
+ #
115
+ # @example Initializing styling:
116
+ # Hexdump::Hexdump.new(style: {index: :white, numeric: :green, chars: :cyan})
117
+ #
118
+ # @example Initializing highlighting:
119
+ # Hexdump::Hexdump.new(
120
+ # highlights: {
121
+ # index: {/00$/ => [:white, :bold]},
122
+ # numeric: {
123
+ # /^[8-f][0-9a-f]$/ => :faint,
124
+ # /f/ => :cyan,
125
+ # '00' => [:black, :on_red]
126
+ # },
127
+ # chars: {/[^\.]+/ => :green}
128
+ # }
129
+ # )
130
+ #
131
+ # @example Initializing with a block:
132
+ # Hexdump::Hexdump.new do |hexdump|
133
+ # hexdump.type = :uint16
134
+ # # ...
135
+ #
136
+ # hexdump.theme do |theme|
137
+ # theme.index.highlight(/00$/, [:white, :bold])
138
+ # theme.numeric.highlight(/^[8-f][0-9a-f]$/, :faint)
139
+ # # ...
140
+ # end
141
+ # end
142
+ #
143
+ def initialize(type: :byte, offset: nil, length: nil, zero_pad: false, repeating: false, columns: nil, group_columns: nil, group_chars: nil, base: nil, index_base: 16, index_offset: nil, chars_column: true, encoding: nil, style: nil, highlights: nil)
144
+ # reader options
145
+ self.type = type
146
+ self.offset = offset
147
+ self.length = length
148
+ self.zero_pad = zero_pad
149
+ self.repeating = repeating
150
+
151
+ # numeric formatting options
152
+ self.base = base if base
153
+ self.columns = columns
154
+ self.group_columns = group_columns
155
+
156
+ # index options
157
+ self.index_base = index_base
158
+ self.index_offset = index_offset || offset
159
+
160
+ # chars formatting options
161
+ self.encoding = encoding
162
+ self.chars_column = chars_column
163
+ self.group_chars = group_chars
164
+
165
+ @theme = if (style.kind_of?(Hash) || highlights.kind_of?(Hash))
166
+ Theme.new(
167
+ style: style || {},
168
+ highlights: highlights || {}
169
+ )
170
+ end
171
+
172
+ yield self if block_given?
173
+
174
+ @reader = Reader.new(@type, offset: @offset,
175
+ length: @length,
176
+ zero_pad: @zero_pad)
177
+
178
+ # default the numeric base
179
+ @base ||= case @type
180
+ when Type::Float, Type::Char, Type::UChar then 10
181
+ else 16
182
+ end
183
+
184
+ # default the number of columns based on the type's size
185
+ @columns ||= (DEFAULT_COLUMNS / @type.size)
186
+
187
+ @index = BASES.fetch(@index_base).new(TYPES[:uint32])
188
+ @numeric = BASES.fetch(@base).new(@type)
70
189
 
71
- if block then dumper.each(data,&block)
72
- else dumper.dump(data,output)
190
+ case @type
191
+ when Type::Char, Type::UChar
192
+ # display characters inline for the :char and :uchar type, and disable
193
+ # the characters column
194
+ @numeric = Numeric::CharOrInt.new(@numeric,@encoding)
195
+
196
+ @chars = nil
197
+ @chars_column = false
198
+ else
199
+ @chars = Chars.new(@encoding) if @chars_column
200
+ end
73
201
  end
74
202
 
75
- return nil
76
- end
203
+ #
204
+ # @group Reader Configuration
205
+ #
206
+
207
+ # The word type to decode the byte stream as.
208
+ #
209
+ # @return [Type]
210
+ #
211
+ # @api public
212
+ attr_reader :type
213
+
214
+ # The optional offset to start the index at.
215
+ #
216
+ # @return [Integer, nil]
217
+ #
218
+ # @api public
219
+ attr_accessor :offset
220
+
221
+ # The optional length of data to read.
222
+ #
223
+ # @return [Integer, nil]
224
+ #
225
+ # @api public
226
+ attr_accessor :length
227
+
228
+ # Controls whether to zero-pad the data so it aligns with the type's size.
229
+ #
230
+ # @return [Boolean]
231
+ #
232
+ # @api public
233
+ attr_accessor :zero_pad
234
+
235
+ alias zero_pad? zero_pad
236
+
237
+ # Controls whether repeating duplicate rows will be omitted with a `*`.
238
+ #
239
+ # @return [Boolean]
240
+ #
241
+ # @api public
242
+ attr_accessor :repeating
243
+
244
+ alias repeating? repeating
245
+
246
+ #
247
+ # Sets the hexdump type.
248
+ #
249
+ # @param [:int8, :uint8, :char, :uchar, :byte, :int16, :int16_le, :int16_be, :int16_ne, :uint16, :uint16_le, :uint16_be, :uint16_ne, :short, :short_le, :short_be, :short_ne, :ushort, :ushort_le, :ushort_be, :ushort_ne, :int32, :int32_le, :int32_be, :int32_ne, :uint32, :uint32_le, :uint32_be, :uint32_ne, :int, :long, :long_le, :long_be, :long_ne, :uint, :ulong, :ulong_le, :ulong_be, :ulong_ne, :int64, :int64_le, :int64_be, :int64_ne, :uint64, :uint64_le, :uint64_be, :uint64_ne, :long_long, :long_long_le, :long_long_be, :long_long_ne, :ulong_long, :ulong_long_le, :ulong_long_be, :ulong_long_ne, :float, :float_le, :float_be, :float_ne, :double, :double_le, :double_be, :double_ne] value
250
+ #
251
+ # @return [Type]
252
+ #
253
+ # @raise [ArgumentError]
254
+ #
255
+ # @api public
256
+ #
257
+ def type=(value)
258
+ @type = TYPES.fetch(value) do
259
+ raise(ArgumentError,"unsupported type: #{value.inspect}")
260
+ end
261
+ end
262
+
263
+ #
264
+ # @group Numeric Configuration
265
+ #
266
+
267
+ # The base to dump words as.
268
+ #
269
+ # @return [16, 10, 8, 2]
270
+ #
271
+ # @api public
272
+ attr_accessor :base
273
+
274
+ #
275
+ # Sets the numeric column base.
276
+ #
277
+ # @param [16, 10, 8, 2] value
278
+ #
279
+ # @return [16, 10, 8, 2]
280
+ #
281
+ # @raise [ArgumentError]
282
+ #
283
+ # @api public
284
+ #
285
+ def base=(value)
286
+ case value
287
+ when 16, 10, 8, 2
288
+ @base = value
289
+ else
290
+ raise(ArgumentError,"unsupported base: #{value.inspect}")
291
+ end
292
+ end
293
+
294
+ # The number of columns per hexdump line.
295
+ #
296
+ # @return [Integer]
297
+ #
298
+ # @api public
299
+ attr_accessor :columns
300
+
301
+ # The number of columns to group together.
302
+ #
303
+ # @return [Integer, nil]
304
+ #
305
+ # @api public
306
+ attr_accessor :group_columns
307
+
308
+ #
309
+ # @group Index Configuration
310
+ #
311
+
312
+ # The base to format the index column as.
313
+ #
314
+ # @return [16, 10, 8, 2]
315
+ #
316
+ # @api public
317
+ attr_reader :index_base
318
+
319
+ #
320
+ # Sets the index column base.
321
+ #
322
+ # @param [16, 10, 8, 2] value
323
+ #
324
+ # @return [16, 10, 8, 2]
325
+ #
326
+ # @raise [ArgumentError]
327
+ #
328
+ # @api public
329
+ #
330
+ def index_base=(value)
331
+ case value
332
+ when 16, 10, 8, 2
333
+ @index_base = value
334
+ else
335
+ raise(ArgumentError,"unsupported index base: #{value.inspect}")
336
+ end
337
+ end
338
+
339
+ # Starts the index at the given offset.
340
+ #
341
+ # @return [Integer, nil]
342
+ #
343
+ # @api public
344
+ attr_accessor :index_offset
345
+
346
+ #
347
+ # @group Characters Configuration
348
+ #
349
+
350
+ # The encoding to use when decoding characters.
351
+ #
352
+ # @return [Encoding, nil]
353
+ #
354
+ # @api public
355
+ attr_reader :encoding
356
+
357
+ #
358
+ # Sets the encoding.
359
+ #
360
+ # @param [:ascii, :utf8, Encoding, nil] value
361
+ #
362
+ # @return [Encoding, nil]
363
+ #
364
+ # @api public
365
+ #
366
+ def encoding=(value)
367
+ @encoding = case value
368
+ when :ascii then nil
369
+ when :utf8 then Encoding::UTF_8
370
+ when Encoding then value
371
+ when nil then nil
372
+ else
373
+ raise(ArgumentError,"encoding must be nil, :ascii, :utf8, or an Encoding object")
374
+ end
375
+ end
376
+
377
+ # Controls whether to display the characters column.
378
+ #
379
+ # @return [Boolean]
380
+ #
381
+ # @api public
382
+ attr_accessor :chars_column
383
+
384
+ alias chars_column? chars_column
385
+
386
+ # Groups the characters together into groups.
387
+ #
388
+ # @return [Integer, nil]
389
+ #
390
+ # @api public
391
+ attr_reader :group_chars
392
+
393
+ #
394
+ # Sets the character grouping.
395
+ #
396
+ # @param [Integer, :type] value
397
+ #
398
+ # @return [Integer, nil]
399
+ #
400
+ # @api public
401
+ #
402
+ def group_chars=(value)
403
+ @group_chars = case value
404
+ when Integer then value
405
+ when :type then @type.size
406
+ when nil then nil
407
+ else
408
+ raise(ArgumentError,"invalid group_chars value: #{value.inspect}")
409
+ end
410
+ end
411
+
412
+ #
413
+ # @group Theme Configuration
414
+ #
415
+
416
+ #
417
+ # Determines if hexdump styling/highlighting has been enabled.
418
+ #
419
+ # @return [Boolean]
420
+ #
421
+ # @api public
422
+ #
423
+ def theme?
424
+ !@theme.nil?
425
+ end
426
+
427
+ #
428
+ # The hexdump theme.
429
+ #
430
+ # @yield [theme]
431
+ # If a block is given, the theme will be auto-initialized and yielded.
432
+ #
433
+ # @yieldparam [Theme] theme
434
+ # The hexdump theme.
435
+ #
436
+ # @return [Theme, nil]
437
+ # The initialized hexdump theme.
438
+ #
439
+ # @api public
440
+ #
441
+ def theme(&block)
442
+ if block
443
+ @theme ||= Theme.new
444
+ @theme.tap(&block)
445
+ else
446
+ @theme
447
+ end
448
+ end
449
+
450
+ #
451
+ # @group Formatting Methods
452
+ #
453
+
454
+ #
455
+ # Enumerates over each slice of read values.
456
+ #
457
+ # @param [#each_byte] data
458
+ # The data to be hexdumped.
459
+ #
460
+ # @yield [slice]
461
+ # The given block will be passed the hexdump break-down of each
462
+ # row.
463
+ #
464
+ # @yieldparam [Array<(String, Integer)>, Array<(String, Float)>] slice
465
+ # The decoded values.
466
+ #
467
+ # @return [Enumerator]
468
+ # If no block is given, an Enumerator will be returned.
469
+ #
470
+ def each_slice(data,&block)
471
+ @reader.each(data).each_slice(@columns,&block)
472
+ end
473
+
474
+ #
475
+ # Enumerates each row of values read from the given data.
476
+ #
477
+ # @param [#each_byte] data
478
+ # The data to be hexdumped.
479
+ #
480
+ # @yield [index, values, chars]
481
+ # The given block will be passed the hexdump break-down of each
482
+ # row.
483
+ #
484
+ # @yieldparam [Integer] index
485
+ # The index of the hexdumped row.
486
+ #
487
+ # @yieldparam [Array<Integer>, Array<Float>] values
488
+ # The decoded values.
489
+ #
490
+ # @yieldparam [Array<String>, nil] chars
491
+ # The underlying raw characters that were read.
492
+ #
493
+ # @return [Integer, Enumerator]
494
+ # If a block is given, then the final number of bytes read is returned.
495
+ # If no block is given, an Enumerator will be returned.
496
+ #
497
+ def each_row(data,&block)
498
+ return enum_for(__method__,data) unless block_given?
499
+
500
+ index = @index_offset || 0
501
+ chars = nil
502
+
503
+ each_slice(data) do |slice|
504
+ numeric = []
505
+ chars = [] if @chars
506
+
507
+ next_index = index
508
+
509
+ slice.each do |(raw,value)|
510
+ numeric << value
511
+ chars << raw if @chars
512
+
513
+ next_index += raw.length
514
+ end
515
+
516
+ yield index, numeric, chars
517
+ index = next_index
518
+ end
519
+
520
+ return index
521
+ end
522
+
523
+ #
524
+ # Enumerates each non-repeating row of hexdumped data.
525
+ #
526
+ # @param [#each_byte] data
527
+ # The data to be hexdumped.
528
+ #
529
+ # @yield [index, numeric, chars]
530
+ # The given block will be passed the hexdump break-down of each
531
+ # row.
532
+ #
533
+ # @yieldparam [Integer, '*'] index
534
+ # The index of the hexdumped row.
535
+ # If the index is `'*'`, then it indicates the beginning of repeating
536
+ # rows of data.
537
+ #
538
+ # @yieldparam [Array<Integer>, Array<Float>, nil] values
539
+ # The decoded values.
540
+ #
541
+ # @yieldparam [String, nil] chars
542
+ # A raw characters that were read.
543
+ #
544
+ # @return [Integer, Enumerator]
545
+ # If a block is given, the final number of bytes read will be returned.
546
+ # If no block is given, an Enumerator will be returned.
547
+ #
548
+ def each_non_repeating_row(data)
549
+ return enum_for(__method__,data) unless block_given?
550
+
551
+ previous_row = nil
552
+ is_repeating = false
553
+
554
+ each_row(data) do |index,*row|
555
+ if row == previous_row
556
+ unless is_repeating
557
+ yield '*'
558
+ is_repeating = true
559
+ end
560
+ else
561
+ if is_repeating
562
+ previous_row = nil
563
+ is_repeating = false
564
+ end
565
+
566
+ yield index, *row
567
+ previous_row = row
568
+ end
569
+ end
570
+ end
571
+
572
+ #
573
+ # Enumerates each formatted row of hexdumped data.
574
+ #
575
+ # @param [#each_byte] data
576
+ # The data to be hexdumped.
577
+ #
578
+ # @param [Boolean] ansi
579
+ # Whether to enable styling/highlighting.
580
+ #
581
+ # @yield [index, numeric, chars]
582
+ # The given block will be passed the hexdump break-down of each
583
+ # row.
584
+ #
585
+ # @yieldparam [String] index
586
+ # The index of the hexdumped row.
587
+ # If the index is `'*'`, then it indicates the beginning of repeating
588
+ # rows of data.
589
+ #
590
+ # @yieldparam [Array<String>, nil] numeric
591
+ # The numeric representation of the row.
592
+ #
593
+ # @yieldparam [String, nil] chars
594
+ # The printable representation of the row.
595
+ #
596
+ # @return [String, Enumerator]
597
+ # If a block is given, the final number of bytes read will be returned.
598
+ # If no block is given, an Enumerator will be returned.
599
+ #
600
+ def each_formatted_row(data, ansi: theme?, **kwargs)
601
+ return enum_for(__method__,data, ansi: ansi) unless block_given?
602
+
603
+ format_index = lambda { |index|
604
+ formatted = @index % index
605
+ formatted = @theme.index.apply(formatted) if ansi
606
+ formatted
607
+ }
608
+
609
+ blank = ' ' * @numeric.width
610
+
611
+ format_numeric = lambda { |value|
612
+ if value
613
+ formatted = @numeric % value
614
+ formatted = @theme.numeric.apply(formatted) if ansi
615
+ formatted
616
+ else
617
+ blank
618
+ end
619
+ }
620
+
621
+ # cache the formatted numbers for 8bit and 16bit values
622
+ numeric_cache = if @type.size <= 2
623
+ Hash.new do |hash,value|
624
+ hash[value] = format_numeric.call(value)
625
+ end
626
+ else
627
+ format_numeric
628
+ end
629
+
630
+ if @chars
631
+ format_chars = lambda { |chars|
632
+ formatted = @chars.scrub(chars.join)
633
+ formatted = @theme.chars.apply(formatted) if ansi
634
+ formatted
635
+ }
636
+ end
637
+
638
+ enum = if @repeating then each_row(data)
639
+ else each_non_repeating_row(data)
640
+ end
641
+
642
+ index = enum.each do |index,numeric,chars=nil|
643
+ if index == '*'
644
+ yield index
645
+ else
646
+ formatted_index = format_index[index]
647
+ formatted_numbers = numeric.map { |value| numeric_cache[value] }
648
+
649
+ formatted_chars = if @chars
650
+ if @group_chars
651
+ chars.join.chars.each_slice(@group_chars).map(&format_chars)
652
+ else
653
+ format_chars.call(chars)
654
+ end
655
+ end
656
+
657
+ yield formatted_index, formatted_numbers, formatted_chars
658
+ end
659
+ end
660
+
661
+ return format_index[index]
662
+ end
663
+
664
+ #
665
+ # Enumerates over each line in the hexdump.
666
+ #
667
+ # @param [#each_byte] data
668
+ # The data to be hexdumped.
669
+ #
670
+ # @param [Hash{Symbol => Object}] kwargs
671
+ # Additional keyword arguments for {#each_formatted_row}.
672
+ #
673
+ # @yield [line]
674
+ # The given block will be passed each line from the hexdump.
675
+ #
676
+ # @yieldparam [String] line
677
+ # Each line from the hexdump output, terminated with a newline character.
678
+ #
679
+ # @return [Enumerator]
680
+ # If no block is given, an Enumerator object will be returned
681
+ #
682
+ # @return [nil]
683
+ #
684
+ def each_line(data,**kwargs)
685
+ return enum_for(__method__,data,**kwargs) unless block_given?
686
+
687
+ join_numeric = if @group_columns
688
+ lambda { |numeric|
689
+ numeric.each_slice(@group_columns).map { |numbers|
690
+ numbers.join(' ')
691
+ }.join(' ')
692
+ }
693
+ else
694
+ lambda { |numeric| numeric.join(' ') }
695
+ end
696
+
697
+ index = each_formatted_row(data,**kwargs) do |index,numeric,chars=nil|
698
+ if index == '*'
699
+ yield "#{index}#{$/}"
700
+ else
701
+ numeric_column = join_numeric.call(numeric)
702
+
703
+ if numeric.length < @columns
704
+ missing_columns = (@columns - numeric.length)
705
+ column_width = @numeric.width + 1
706
+
707
+ spaces = (missing_columns * column_width)
708
+ spaces += ((missing_columns / @group_columns) - 1) if @group_columns
709
+
710
+ numeric_column << ' ' * spaces
711
+ end
712
+
713
+ line = if @chars
714
+ if @group_chars
715
+ chars = chars.join('|')
716
+ end
717
+
718
+ "#{index} #{numeric_column} |#{chars}|#{$/}"
719
+ else
720
+ "#{index} #{numeric_column}#{$/}"
721
+ end
722
+
723
+ yield line
724
+ end
725
+ end
726
+
727
+ yield "#{index}#{$/}"
728
+ return nil
729
+ end
730
+
731
+ #
732
+ # Prints the hexdump.
733
+ #
734
+ # @param [#each_byte] data
735
+ # The data to be hexdumped.
736
+ #
737
+ # @param [#<<] output
738
+ # The output to dump the hexdump to.
739
+ #
740
+ # @return [nil]
741
+ #
742
+ # @raise [ArgumentError]
743
+ # The output value does not support the `#<<` method.
744
+ #
745
+ def hexdump(data, output: $stdout)
746
+ unless output.respond_to?(:<<)
747
+ raise(ArgumentError,"output must support the #<< method")
748
+ end
749
+
750
+ ansi = theme? && output.tty?
751
+
752
+ each_line(data, ansi: ansi) do |line|
753
+ output << line
754
+ end
755
+ end
756
+
757
+ #
758
+ # Outputs the hexdump to a String.
759
+ #
760
+ # @param [#each_byte] data
761
+ # The data to be hexdumped.
762
+ #
763
+ # @return [String]
764
+ # The output of the hexdump.
765
+ #
766
+ # @note
767
+ # **Caution:** this method appends each line of the hexdump to a String,
768
+ # and that String can grow quite large and consume a lot of memory.
769
+ #
770
+ def dump(data)
771
+ String.new.tap do |string|
772
+ hexdump(data, output: string)
773
+ end
774
+ end
77
775
 
78
- #
79
- # Hexdumps the object.
80
- #
81
- # @see Hexdump.dump
82
- #
83
- def hexdump(**options,&block)
84
- Hexdump.dump(self,**options,&block)
85
776
  end
86
777
  end