hexdump 0.3.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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