hexdump 0.3.0 → 1.0.1

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