hexdump 0.3.0 → 1.0.0

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 (52) hide show
  1. checksums.yaml +4 -4
  2. data/ChangeLog.md +68 -2
  3. data/Gemfile +1 -0
  4. data/README.md +486 -135
  5. data/benchmark.rb +29 -22
  6. data/lib/hexdump/chars.rb +46 -0
  7. data/lib/hexdump/core_ext/file.rb +68 -6
  8. data/lib/hexdump/core_ext/io.rb +2 -2
  9. data/lib/hexdump/core_ext/kernel.rb +7 -0
  10. data/lib/hexdump/core_ext/string.rb +2 -2
  11. data/lib/hexdump/core_ext/string_io.rb +2 -2
  12. data/lib/hexdump/core_ext.rb +1 -0
  13. data/lib/hexdump/format_string.rb +43 -0
  14. data/lib/hexdump/hexdump.rb +768 -75
  15. data/lib/hexdump/mixin.rb +198 -0
  16. data/lib/hexdump/module_methods.rb +132 -0
  17. data/lib/hexdump/numeric/binary.rb +55 -0
  18. data/lib/hexdump/numeric/char_or_int.rb +90 -0
  19. data/lib/hexdump/numeric/decimal.rb +56 -0
  20. data/lib/hexdump/numeric/exceptions.rb +11 -0
  21. data/lib/hexdump/numeric/hexadecimal.rb +59 -0
  22. data/lib/hexdump/numeric/octal.rb +55 -0
  23. data/lib/hexdump/numeric.rb +5 -0
  24. data/lib/hexdump/reader.rb +314 -0
  25. data/lib/hexdump/theme/ansi.rb +81 -0
  26. data/lib/hexdump/theme/rule.rb +159 -0
  27. data/lib/hexdump/theme.rb +61 -0
  28. data/lib/hexdump/type.rb +232 -0
  29. data/lib/hexdump/types.rb +108 -0
  30. data/lib/hexdump/version.rb +1 -1
  31. data/lib/hexdump.rb +12 -1
  32. data/spec/chars_spec.rb +76 -0
  33. data/spec/core_ext_spec.rb +10 -6
  34. data/spec/hexdump_class_spec.rb +1708 -0
  35. data/spec/hexdump_module_spec.rb +23 -0
  36. data/spec/mixin_spec.rb +37 -0
  37. data/spec/numeric/binary_spec.rb +239 -0
  38. data/spec/numeric/char_or_int_spec.rb +210 -0
  39. data/spec/numeric/decimal_spec.rb +317 -0
  40. data/spec/numeric/hexadecimal_spec.rb +320 -0
  41. data/spec/numeric/octal_spec.rb +239 -0
  42. data/spec/reader_spec.rb +863 -0
  43. data/spec/theme/ansi_spec.rb +242 -0
  44. data/spec/theme/rule_spec.rb +199 -0
  45. data/spec/theme_spec.rb +94 -0
  46. data/spec/type_spec.rb +317 -0
  47. data/spec/types_spec.rb +904 -0
  48. metadata +39 -10
  49. data/.gemtest +0 -0
  50. data/lib/hexdump/dumper.rb +0 -419
  51. data/spec/dumper_spec.rb +0 -329
  52. data/spec/hexdump_spec.rb +0 -30
data/benchmark.rb CHANGED
@@ -5,43 +5,50 @@ $LOAD_PATH.unshift(File.expand_path('../lib',__FILE__))
5
5
  require 'hexdump'
6
6
  require 'benchmark'
7
7
 
8
- DATA = ((0..255).map { |b| b.chr }.join) * (1024 * 100)
9
- OUTPUT = Class.new { def <<(data); end }.new
8
+ class NullOutput
9
+ def <<(data)
10
+ end
11
+ end
12
+
13
+ size_mb = 1
14
+ puts "Generating #{size_mb}Mb of random data ..."
15
+ data = Array.new(size_mb * 1024 * 1024) { rand(255).chr }.join
16
+ output = NullOutput.new
10
17
 
11
- Benchmark.bm(33) do |b|
12
- b.report('Hexdump.dump (output)') do
13
- Hexdump.dump(DATA, :output => OUTPUT)
18
+ types = Hexdump::TYPES.values.uniq.map(&Hexdump::TYPES.method(:key))
19
+
20
+ Benchmark.bm(42) do |b|
21
+ b.report('Hexdump.hexdump(data)') do
22
+ Hexdump.hexdump(data, output: output)
14
23
  end
15
24
 
16
- b.report('Hexdump.dump width=256 (output)') do
17
- Hexdump.dump(DATA, width: 256, output: OUTPUT)
25
+ b.report("Hexdump.hexdump(data, repeating: false)") do
26
+ Hexdump.hexdump(data, repeating: false, output: output)
18
27
  end
19
28
 
20
- b.report('Hexdump.dump ascii=true (output)') do
21
- Hexdump.dump(DATA, ascii: true, output: OUTPUT)
29
+ b.report("Hexdump.hexdump(data, chars_column: false)") do
30
+ Hexdump.hexdump(data, chars_column: false, output: output)
22
31
  end
23
32
 
24
- [2, 4, 8].each do |word_size|
25
- b.report("Hexdump.dump word_size=#{word_size} (output)") do
26
- Hexdump.dump(DATA, word_size: word_size, output: OUTPUT)
27
- end
33
+ b.report('Hexdump.hexdump(data, columns: 256)') do
34
+ Hexdump.hexdump(data, columns: 256, output: output)
28
35
  end
29
36
 
30
- b.report('Hexdump.dump (block)') do
31
- Hexdump.dump(DATA) { |index,hex,print| }
37
+ b.report('Hexdump.hexdump(data, group_columns: 4)') do
38
+ Hexdump.hexdump(data, group_columns: 4, output: output)
32
39
  end
33
40
 
34
- b.report('Hexdump.dump width=256 (block)') do
35
- Hexdump.dump(DATA, width: 256) { |index,hex,print| }
41
+ b.report('Hexdump.hexdump(data, group_chars: 4)') do
42
+ Hexdump.hexdump(data, group_chars: 4, output: output)
36
43
  end
37
44
 
38
- b.report('Hexdump.dump ascii=true (block)') do
39
- Hexdump.dump(DATA, ascii: true) { |index,hex,print| }
45
+ b.report('Hexdump.hexdump(data, encoding: :utf8)') do
46
+ Hexdump.hexdump(data, encoding: :utf8, output: output)
40
47
  end
41
48
 
42
- [2, 4, 8].each do |word_size|
43
- b.report("Hexdump.dump word_size=#{word_size} (block)") do
44
- Hexdump.dump(DATA, word_size: word_size) { |index,hex,print| }
49
+ types.each do |type|
50
+ b.report("Hexdump.hexdump(data, type: #{type.inspect})") do
51
+ Hexdump.hexdump(data, type: type, output: output)
45
52
  end
46
53
  end
47
54
  end
@@ -0,0 +1,46 @@
1
+ module Hexdump
2
+ #
3
+ # @api private
4
+ #
5
+ # @since 1.0.0
6
+ #
7
+ class Chars
8
+
9
+ # The encoding to convert the characters to.
10
+ #
11
+ # @return [Encoding, nil]
12
+ attr_reader :encoding
13
+
14
+ #
15
+ # Initializes the chars formatter.
16
+ #
17
+ # @param [Encoding, nil] encoding
18
+ # The encoding to convert characters to.
19
+ #
20
+ def initialize(encoding=nil)
21
+ @encoding = encoding
22
+ end
23
+
24
+ #
25
+ # Formats a string of characters.
26
+ #
27
+ # @param [String] chars
28
+ # The input string of raw characters.
29
+ #
30
+ # @return [String]
31
+ # The formatted string of raw characters.
32
+ #
33
+ def scrub(chars)
34
+ if @encoding
35
+ chars.force_encoding(@encoding)
36
+ chars.scrub!('.')
37
+ chars.gsub!(/[^[:print:]]/u,'.')
38
+ else
39
+ chars.tr!("^\x20-\x7e",'.')
40
+ end
41
+
42
+ chars
43
+ end
44
+
45
+ end
46
+ end
@@ -1,4 +1,4 @@
1
- require 'hexdump/hexdump'
1
+ require 'hexdump/core_ext/io'
2
2
 
3
3
  class File
4
4
 
@@ -8,14 +8,76 @@ class File
8
8
  # @param [String] path
9
9
  # The path of the file.
10
10
  #
11
- # @param [Hash] options
12
- # Additional options.
11
+ # @param [Hash{Symbol => Object}] kwargs
12
+ # Additional keyword arguments for {Format#initialize}.
13
13
  #
14
- # @see Hexdump.dump
14
+ # @option kwargs [#print] :output ($stdout)
15
+ # The output to print the hexdump to.
15
16
  #
16
- def self.hexdump(path,options={},&block)
17
+ # @option kwargs [: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)
18
+ # The type to decode the data as.
19
+ #
20
+ # @option kwargs [Integer, nil] :offset
21
+ # Controls whether to skip N number of bytes before starting to read data.
22
+ #
23
+ # @option kwargs [Integer, nil] :length
24
+ # Controls control many bytes to read.
25
+ #
26
+ # @option kwargs [Boolean] :zero_pad (false)
27
+ # Enables or disables zero padding of data, so that the remaining bytes
28
+ # can be decoded as a uint, int, or float.
29
+ #
30
+ # @option kwargs [Integer] :columns (16)
31
+ # The number of bytes to dump for each line.
32
+ #
33
+ # @option kwargs [Integer, nil] :group_columns
34
+ # Separate groups of columns with an additional space.
35
+ #
36
+ # @option kwargs [Integer, :type, nil] :group_chars
37
+ # Group chars into columns.
38
+ # If `:type`, then the chars will be grouped by the `type`'s size.
39
+ #
40
+ # @option kwargs [Boolean] :repeating
41
+ # Controls whether to omit repeating duplicate rows data with a `*`.
42
+ #
43
+ # @option kwargs [16, 10, 8, 2] :base (16)
44
+ # The base to print bytes in.
45
+ #
46
+ # @option kwargs [16, 10, 8, 2] :index_base (16)
47
+ # Control the base that the index is displayed in.
48
+ #
49
+ # @option kwargs [Integer, nil] :index_offset
50
+ # The offset to start the index at.
51
+ #
52
+ # @option kwargs [Boolean] :chars_column (true)
53
+ # Controls whether to display the characters column.
54
+ #
55
+ # @option kwargs [:ascii, :utf8, Encoding, nil] :encoding
56
+ # The encoding to display the characters in.
57
+ #
58
+ # @option kwargs [Boolean, Hash{:index,:numeric,:chars => Symbol,Array<Symbol>}] :style
59
+ # Enables theming of index, numeric, or chars columns.
60
+ #
61
+ # @option kwargs [Boolean, Hash{:index,:numeric,:chars => Hash{String,Regexp => Symbol,Array<Symbol>}}] :highlights
62
+ # Enables selective highlighting of index, numeric, or chars columns.
63
+ #
64
+ # @yield [hexdump]
65
+ # If a block is given, it will be passed the newly initialized hexdump
66
+ # instance.
67
+ #
68
+ # @yieldparam [Hexdump::Hexdump] hexdump
69
+ # The newly initialized hexdump instance.
70
+ #
71
+ # @see IO#hexdump
72
+ #
73
+ # @example
74
+ # File.hexdump("/bin/ls")
75
+ # # 00000000 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 |.ELF............|
76
+ # # ...
77
+ #
78
+ def self.hexdump(path,**kwargs,&block)
17
79
  self.open(path,'rb') do |file|
18
- Hexdump.dump(file,options,&block)
80
+ file.hexdump(**kwargs,&block)
19
81
  end
20
82
  end
21
83
 
@@ -1,7 +1,7 @@
1
- require 'hexdump/hexdump'
1
+ require 'hexdump/mixin'
2
2
 
3
3
  class IO
4
4
 
5
- include Hexdump
5
+ include Hexdump::Mixin
6
6
 
7
7
  end
@@ -0,0 +1,7 @@
1
+ require 'hexdump/module_methods'
2
+
3
+ module ::Kernel
4
+ private
5
+
6
+ include Hexdump::ModuleMethods
7
+ end
@@ -1,7 +1,7 @@
1
- require 'hexdump/hexdump'
1
+ require 'hexdump/mixin'
2
2
 
3
3
  class String
4
4
 
5
- include Hexdump
5
+ include Hexdump::Mixin
6
6
 
7
7
  end
@@ -1,9 +1,9 @@
1
- require 'hexdump/hexdump'
1
+ require 'hexdump/mixin'
2
2
 
3
3
  require 'stringio'
4
4
 
5
5
  class StringIO
6
6
 
7
- include Hexdump
7
+ include Hexdump::Mixin
8
8
 
9
9
  end
@@ -2,3 +2,4 @@ require 'hexdump/core_ext/string'
2
2
  require 'hexdump/core_ext/string_io'
3
3
  require 'hexdump/core_ext/io'
4
4
  require 'hexdump/core_ext/file'
5
+ require 'hexdump/core_ext/kernel'
@@ -0,0 +1,43 @@
1
+ module Hexdump
2
+ #
3
+ # @api private
4
+ #
5
+ # @since 1.0.0
6
+ #
7
+ class FormatString
8
+
9
+ #
10
+ # Initializes the format string.
11
+ #
12
+ # @param [String] fmt
13
+ # The format string.
14
+ #
15
+ def initialize(fmt)
16
+ @fmt = fmt
17
+ end
18
+
19
+ #
20
+ # Formats the given value.
21
+ #
22
+ # @param [Integer, Float] value
23
+ # The given value.
24
+ #
25
+ # @return [String]
26
+ # The formatted value.
27
+ #
28
+ def %(value)
29
+ sprintf(@fmt,value)
30
+ end
31
+
32
+ #
33
+ # Converts the format string back into a String.
34
+ #
35
+ # @return [String]
36
+ # The raw format string.
37
+ #
38
+ def to_s
39
+ @fmt
40
+ end
41
+
42
+ end
43
+ end
@@ -1,86 +1,779 @@
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 'hexdump/types'
4
+ require 'hexdump/reader'
5
+ require 'hexdump/numeric'
6
+ require 'hexdump/chars'
7
+ require 'hexdump/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
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
+ # If the index is `'*'`, then it indicates the beginning of repeating
487
+ # rows of data.
488
+ #
489
+ # @yieldparam [Array<Integer>, Array<Float>] values
490
+ # The decoded values.
491
+ #
492
+ # @yieldparam [String] chars
493
+ # A raw characters that were read.
494
+ #
495
+ # @return [Integer, Enumerator]
496
+ # If a block is given, then the final number of bytes read is returned.
497
+ # If no block is given, an Enumerator will be returned.
498
+ #
499
+ def each_row(data,&block)
500
+ return enum_for(__method__,data) unless block_given?
501
+
502
+ index = @index_offset || 0
503
+ chars = nil
504
+
505
+ each_slice(data) do |slice|
506
+ numeric = []
507
+ chars = [] if @chars
508
+
509
+ next_index = index
510
+
511
+ slice.each do |(raw,value)|
512
+ numeric << value
513
+ chars << raw if @chars
514
+
515
+ next_index += raw.length
516
+ end
517
+
518
+ yield index, numeric, chars
519
+ index = next_index
520
+ end
521
+
522
+ return index
523
+ end
524
+
525
+ #
526
+ # Enumerates each non-repeating row of hexdumped data.
527
+ #
528
+ # @param [#each_byte] data
529
+ # The data to be hexdumped.
530
+ #
531
+ # @yield [index, numeric, chars]
532
+ # The given block will be passed the hexdump break-down of each
533
+ # row.
534
+ #
535
+ # @yieldparam [Integer, '*'] index
536
+ # The index of the hexdumped row.
537
+ # If the index is `'*'`, then it indicates the beginning of repeating
538
+ # rows of data.
539
+ #
540
+ # @yieldparam [Array<Integer>, Array<Float>, nil] values
541
+ # The decoded values.
542
+ #
543
+ # @yieldparam [String, nil] chars
544
+ # A raw characters that were read.
545
+ #
546
+ # @return [Integer, Enumerator]
547
+ # If a block is given, the final number of bytes read will be returned.
548
+ # If no block is given, an Enumerator will be returned.
549
+ #
550
+ def each_non_repeating_row(data)
551
+ return enum_for(__method__,data) unless block_given?
552
+
553
+ previous_row = nil
554
+ is_repeating = false
555
+
556
+ each_row(data) do |index,*row|
557
+ if row == previous_row
558
+ unless is_repeating
559
+ yield '*'
560
+ is_repeating = true
561
+ end
562
+ else
563
+ if is_repeating
564
+ previous_row = nil
565
+ is_repeating = false
566
+ end
567
+
568
+ yield index, *row
569
+ previous_row = row
570
+ end
571
+ end
572
+ end
573
+
574
+ #
575
+ # Enumerates each formatted row of hexdumped data.
576
+ #
577
+ # @param [#each_byte] data
578
+ # The data to be hexdumped.
579
+ #
580
+ # @param [Boolean] ansi
581
+ # Whether to enable styling/highlighting.
582
+ #
583
+ # @yield [index, numeric, chars]
584
+ # The given block will be passed the hexdump break-down of each
585
+ # row.
586
+ #
587
+ # @yieldparam [Integer, '*'] index
588
+ # The index of the hexdumped row.
589
+ # If the index is `'*'`, then it indicates the beginning of repeating
590
+ # rows of data.
591
+ #
592
+ # @yieldparam [Array<String>, nil] numeric
593
+ # The numeric representation of the row.
594
+ #
595
+ # @yieldparam [Array<String>, nil] chars
596
+ # The printable representation of the row.
597
+ #
598
+ # @return [String, Enumerator]
599
+ # If a block is given, the final number of bytes read will be returned.
600
+ # If no block is given, an Enumerator will be returned.
601
+ #
602
+ def each_formatted_row(data, ansi: theme?, **kwargs)
603
+ return enum_for(__method__,data, ansi: ansi) unless block_given?
604
+
605
+ format_index = lambda { |index|
606
+ formatted = @index % index
607
+ formatted = @theme.index.apply(formatted) if ansi
608
+ formatted
609
+ }
610
+
611
+ blank = ' ' * @numeric.width
612
+
613
+ format_numeric = lambda { |value|
614
+ if value
615
+ formatted = @numeric % value
616
+ formatted = @theme.numeric.apply(formatted) if ansi
617
+ formatted
618
+ else
619
+ blank
620
+ end
621
+ }
622
+
623
+ # cache the formatted numbers for 8bit and 16bit values
624
+ numeric_cache = if @type.size <= 2
625
+ Hash.new do |hash,value|
626
+ hash[value] = format_numeric.call(value)
627
+ end
628
+ else
629
+ format_numeric
630
+ end
631
+
632
+ if @chars
633
+ format_chars = lambda { |chars|
634
+ formatted = @chars.scrub(chars.join)
635
+ formatted = @theme.chars.apply(formatted) if ansi
636
+ formatted
637
+ }
638
+ end
639
+
640
+ enum = if @repeating then each_row(data)
641
+ else each_non_repeating_row(data)
642
+ end
643
+
644
+ index = enum.each do |index,numeric,chars=nil|
645
+ if index == '*'
646
+ yield index
647
+ else
648
+ formatted_index = format_index[index]
649
+ formatted_numbers = numeric.map { |value| numeric_cache[value] }
650
+
651
+ formatted_chars = if @chars
652
+ if @group_chars
653
+ chars.join.chars.each_slice(@group_chars).map(&format_chars)
654
+ else
655
+ format_chars.call(chars)
656
+ end
657
+ end
658
+
659
+ yield formatted_index, formatted_numbers, formatted_chars
660
+ end
661
+ end
662
+
663
+ return format_index[index]
664
+ end
665
+
666
+ #
667
+ # Enumerates over each line in the hexdump.
668
+ #
669
+ # @param [#each_byte] data
670
+ # The data to be hexdumped.
671
+ #
672
+ # @param [Hash{Symbol => Object}] kwargs
673
+ # Additional keyword arguments for {#each_formatted_row}.
674
+ #
675
+ # @yield [line]
676
+ # The given block will be passed each line from the hexdump.
677
+ #
678
+ # @yieldparam [String] line
679
+ # Each line from the hexdump output, terminated with a newline character.
680
+ #
681
+ # @return [Enumerator]
682
+ # If no block is given, an Enumerator object will be returned
683
+ #
684
+ # @return [nil]
685
+ #
686
+ def each_line(data,**kwargs)
687
+ return enum_for(__method__,data,**kwargs) unless block_given?
688
+
689
+ join_numeric = if @group_columns
690
+ lambda { |numeric|
691
+ numeric.each_slice(@group_columns).map { |numbers|
692
+ numbers.join(' ')
693
+ }.join(' ')
694
+ }
695
+ else
696
+ lambda { |numeric| numeric.join(' ') }
697
+ end
698
+
699
+ index = each_formatted_row(data,**kwargs) do |index,numeric,chars=nil|
700
+ if index == '*'
701
+ yield "#{index}#{$/}"
702
+ else
703
+ numeric_column = join_numeric.call(numeric)
704
+
705
+ if numeric.length < @columns
706
+ missing_columns = (@columns - numeric.length)
707
+ column_width = @numeric.width + 1
708
+
709
+ spaces = (missing_columns * column_width)
710
+ spaces += ((missing_columns / @group_columns) - 1) if @group_columns
711
+
712
+ numeric_column << ' ' * spaces
713
+ end
714
+
715
+ line = if @chars
716
+ if @group_chars
717
+ chars = chars.join('|')
718
+ end
719
+
720
+ "#{index} #{numeric_column} |#{chars}|#{$/}"
721
+ else
722
+ "#{index} #{numeric_column}#{$/}"
723
+ end
724
+
725
+ yield line
726
+ end
727
+ end
728
+
729
+ yield "#{index}#{$/}"
730
+ return nil
731
+ end
732
+
733
+ #
734
+ # Prints the hexdump.
735
+ #
736
+ # @param [#each_byte] data
737
+ # The data to be hexdumped.
738
+ #
739
+ # @param [#print] output
740
+ # The output to dump the hexdump to.
741
+ #
742
+ # @return [nil]
743
+ #
744
+ # @raise [ArgumentError]
745
+ # The output value does not support the `#<<` method.
746
+ #
747
+ def hexdump(data, output: $stdout)
748
+ unless output.respond_to?(:<<)
749
+ raise(ArgumentError,"output must support the #<< method")
750
+ end
751
+
752
+ ansi = theme? && $stdout.tty?
753
+
754
+ each_line(data, ansi: ansi) do |line|
755
+ output << line
756
+ end
757
+ end
758
+
759
+ #
760
+ # Outputs the hexdump to a String.
761
+ #
762
+ # @param [#each_byte] data
763
+ # The data to be hexdumped.
764
+ #
765
+ # @return [String]
766
+ # The output of the hexdump.
767
+ #
768
+ # @note
769
+ # **Caution:** this method appends each line of the hexdump to a String,
770
+ # and that String can grow quite large and consume a lot of memory.
771
+ #
772
+ def dump(data)
773
+ String.new.tap do |string|
774
+ hexdump(data, output: string)
775
+ end
776
+ end
77
777
 
78
- #
79
- # Hexdumps the object.
80
- #
81
- # @see Hexdump.dump
82
- #
83
- def hexdump(**options,&block)
84
- Hexdump.dump(self,**options,&block)
85
778
  end
86
779
  end