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
@@ -0,0 +1,313 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'hexdump/type'
4
+
5
+ module Hexdump
6
+ #
7
+ # @api private
8
+ #
9
+ # @since 1.0.0
10
+ #
11
+ class Reader
12
+
13
+ # The type to decode the data as.
14
+ #
15
+ # @return [Type]
16
+ attr_reader :type
17
+
18
+ # Controls whether to offset N number of bytes before starting to read data.
19
+ #
20
+ # @return [Integer, nil]
21
+ attr_reader :offset
22
+
23
+ # Controls control many bytes to read.
24
+ #
25
+ # @return [Integer, nil]
26
+ attr_reader :length
27
+
28
+ #
29
+ # Initializes the reader.
30
+ #
31
+ # @param [Type] type
32
+ # The type to decode the data as.
33
+ #
34
+ # @param [Integer, nil] offset
35
+ # Controls whether to offset N number of bytes before starting to read
36
+ # data.
37
+ #
38
+ # @param [Integer, nil] length
39
+ # Controls control many bytes to read.
40
+ #
41
+ # @param [Boolean] zero_pad
42
+ # Controls whether the remaining data will be padded with zeros.
43
+ #
44
+ def initialize(type, offset: nil, length: nil, zero_pad: false)
45
+ @type = type
46
+ @offset = offset
47
+ @length = length
48
+ @zero_pad = zero_pad
49
+ end
50
+
51
+ def zero_pad?
52
+ @zero_pad
53
+ end
54
+
55
+ #
56
+ # Reads each byte from the given data.
57
+ #
58
+ # @yield [byte]
59
+ #
60
+ # @yieldparam [Integer] byte
61
+ #
62
+ # @return [Enumerator]
63
+ #
64
+ # @raise [ArgumentError]
65
+ #
66
+ def each_byte(data)
67
+ return enum_for(__method__,data) unless block_given?
68
+
69
+ unless data.respond_to?(:each_byte)
70
+ raise(ArgumentError,"the given data must respond to #each_byte")
71
+ end
72
+
73
+ count = 0
74
+
75
+ data.each_byte do |b|
76
+ count += 1
77
+
78
+ # offset the first @offset number of bytes
79
+ if @offset.nil? || count > @offset
80
+ yield b
81
+ end
82
+
83
+ # stop reading after @length number of bytes
84
+ break if @length && count >= @length
85
+ end
86
+ end
87
+
88
+ #
89
+ # Reads each string of the same number of bytes as the {#type}'s
90
+ # {Type#size size}.
91
+ #
92
+ # @param [#each_byte] data
93
+ #
94
+ # @yield [slice]
95
+ #
96
+ # @yieldparam [String] slice
97
+ #
98
+ # @raise [ArgumentError]
99
+ #
100
+ def each_slice(data)
101
+ return enum_for(__method__,data) unless block_given?
102
+
103
+ unless data.respond_to?(:each_byte)
104
+ raise(ArgumentError,"the given data must respond to #each_byte")
105
+ end
106
+
107
+ if @type.size == 1
108
+ each_byte(data) do |b|
109
+ yield b.chr
110
+ end
111
+ else
112
+ buffer = String.new("\0" * @type.size, capacity: @type.size,
113
+ encoding: Encoding::BINARY)
114
+ index = 0
115
+
116
+ each_byte(data) do |b|
117
+ buffer[index] = b.chr(Encoding::BINARY)
118
+ index += 1
119
+
120
+ if index >= @type.size
121
+ yield buffer.dup
122
+ index = 0
123
+ end
124
+ end
125
+
126
+ if index > 0
127
+ if @zero_pad
128
+ # zero pad the rest of the buffer
129
+ (index..(@type.size - 1)).each do |i|
130
+ buffer[i] = "\0"
131
+ end
132
+
133
+ yield buffer
134
+ else
135
+ # yield the remaining partial buffer
136
+ yield buffer[0,index]
137
+ end
138
+ end
139
+ end
140
+ end
141
+
142
+ #
143
+ # @param [#each_byte] data
144
+ #
145
+ # @yield [raw, uint]
146
+ #
147
+ # @yieldparam [String] raw
148
+ #
149
+ # @yieldparam [Integer, nil] uint
150
+ #
151
+ # @return [Enumerator]
152
+ #
153
+ # @raise [ArgumentError]
154
+ #
155
+ def each_uint(data,&block)
156
+ return enum_for(__method__,data) unless block_given?
157
+
158
+ unless data.respond_to?(:each_byte)
159
+ raise(ArgumentError,"the given data must respond to #each_byte")
160
+ end
161
+
162
+ if @type.size == 1
163
+ each_byte(data) do |b|
164
+ yield b.chr, b
165
+ end
166
+ else
167
+ pack_format = case @type.size
168
+ when 1
169
+ 'c'
170
+ when 2
171
+ case @type.endian
172
+ when :little then 'S<'
173
+ when :big then 'S>'
174
+ else
175
+ raise(TypeError,"unsupported endian #{@type.endian} for #{@type.inspect}")
176
+ end
177
+ when 4
178
+ case @type.endian
179
+ when :little then 'L<'
180
+ when :big then 'L>'
181
+ else
182
+ raise(TypeError,"unsupported endian #{@type.endian} for #{@type.inspect}")
183
+ end
184
+ when 8
185
+ case @type.endian
186
+ when :little then 'Q<'
187
+ when :big then 'Q>'
188
+ else
189
+ raise(TypeError,"unsupported endian #{@type.endian} for #{@type.inspect}")
190
+ end
191
+ else
192
+ raise(TypeError,"unsupported type size #{@type.size} for #{@type.inspect}")
193
+ end
194
+
195
+ each_slice(data) do |slice|
196
+ yield slice, slice.unpack1(pack_format)
197
+ end
198
+ end
199
+ end
200
+
201
+ #
202
+ # @param [#each_byte] data
203
+ #
204
+ # @yield [raw, int]
205
+ #
206
+ # @yieldparam [String] raw
207
+ #
208
+ # @yieldparam [Integer, nil] int
209
+ #
210
+ # @return [Enumerator]
211
+ #
212
+ def each_int(data)
213
+ return enum_for(__method__,data) unless block_given?
214
+
215
+ pack_format = case @type.size
216
+ when 1
217
+ 'c'
218
+ when 2
219
+ case @type.endian
220
+ when :little then 's<'
221
+ when :big then 's>'
222
+ else
223
+ raise(TypeError,"unsupported endian #{@type.endian} for #{@type.inspect}")
224
+ end
225
+ when 4
226
+ case @type.endian
227
+ when :little then 'l<'
228
+ when :big then 'l>'
229
+ else
230
+ raise(TypeError,"unsupported endian #{@type.endian} for #{@type.inspect}")
231
+ end
232
+ when 8
233
+ case @type.endian
234
+ when :little then 'q<'
235
+ when :big then 'q>'
236
+ else
237
+ raise(TypeError,"unsupported endian #{@type.endian} for #{@type.inspect}")
238
+ end
239
+ else
240
+ raise(TypeError,"unsupported type size #{@type.size} for #{@type.inspect}")
241
+ end
242
+
243
+ each_slice(data) do |slice|
244
+ yield slice, slice.unpack1(pack_format)
245
+ end
246
+ end
247
+
248
+ #
249
+ # @param [#each_byte] data
250
+ #
251
+ # @yield [raw, float]
252
+ #
253
+ # @yieldparam [String] raw
254
+ #
255
+ # @yieldparam [Float, nil] float
256
+ #
257
+ # @return [Enumerator]
258
+ #
259
+ def each_float(data)
260
+ return enum_for(__method__,data) unless block_given?
261
+
262
+ pack_format = case @type.endian
263
+ when :little
264
+ case @type.size
265
+ when 4 then 'e'
266
+ when 8 then 'E'
267
+ else
268
+ raise(TypeError,"unsupported type size #{@type.size} for #{@type.inspect}")
269
+ end
270
+ when :big
271
+ case @type.size
272
+ when 4 then 'g'
273
+ when 8 then 'G'
274
+ else
275
+ raise(TypeError,"unsupported type size #{@type.size} for #{@type.inspect}")
276
+ end
277
+ else
278
+ raise(TypeError,"unsupported endian #{@type.endian} for #{@type.inspect}")
279
+ end
280
+
281
+ each_slice(data) do |slice|
282
+ yield slice, slice.unpack1(pack_format)
283
+ end
284
+ end
285
+
286
+ #
287
+ # @param [#each_byte] data
288
+ #
289
+ # @yield [raw, value]
290
+ #
291
+ # @yieldparam [String] raw
292
+ #
293
+ # @yieldparam [Integer, Float, nil] value
294
+ #
295
+ # @return [Enumerator]
296
+ #
297
+ def each(data,&block)
298
+ return enum_for(__method__,data) unless block
299
+
300
+ case @type
301
+ when Type::UInt
302
+ each_uint(data,&block)
303
+ when Type::Float
304
+ each_float(data,&block)
305
+ when Type::Int
306
+ each_int(data,&block)
307
+ else
308
+ raise(TypeError,"unsupported type: #{@type.inspect}")
309
+ end
310
+ end
311
+
312
+ end
313
+ end
@@ -0,0 +1,82 @@
1
+ module Hexdump
2
+ class Theme
3
+ #
4
+ # Represents an ANSI control sequence.
5
+ #
6
+ # @api private
7
+ #
8
+ # @since 1.0.0
9
+ #
10
+ class ANSI
11
+ # ANSI reset control sequence
12
+ RESET = "\e[0m"
13
+
14
+ PARAMETERS = {
15
+ reset: RESET,
16
+
17
+ bold: "\e[1m",
18
+ faint: "\e[2m",
19
+ italic: "\e[3m",
20
+ underline: "\e[4m",
21
+ invert: "\e[7m",
22
+ strike: "\e[9m",
23
+ black: "\e[30m",
24
+ red: "\e[31m",
25
+ green: "\e[32m",
26
+ yellow: "\e[33m",
27
+ blue: "\e[34m",
28
+ magenta: "\e[35m",
29
+ cyan: "\e[36m",
30
+ white: "\e[37m",
31
+
32
+ on_black: "\e[40m",
33
+ on_red: "\e[41m",
34
+ on_green: "\e[42m",
35
+ on_yellow: "\e[43m",
36
+ on_blue: "\e[44m",
37
+ on_magenta: "\e[45m",
38
+ on_cyan: "\e[46m",
39
+ on_white: "\e[47m"
40
+ }
41
+
42
+ # The style name(s).
43
+ #
44
+ # @return [Symbol, Array<Symbol>] style
45
+ attr_reader :parameters
46
+
47
+ # The ANSI string.
48
+ #
49
+ # @return [String]
50
+ attr_reader :string
51
+
52
+ #
53
+ # Initializes an ANSI control sequence.
54
+ #
55
+ # @param [Symbol, Array<Symbol>] parameters
56
+ # The ANSI styling parameters. See {PARAMETERS}.
57
+ #
58
+ def initialize(parameters)
59
+ @parameters = parameters
60
+
61
+ @string = String.new
62
+
63
+ Array(parameters).each do |name|
64
+ @string << PARAMETERS.fetch(name) do
65
+ raise(ArgumentError,"unknown ANSI parameter: #{name}")
66
+ end
67
+ end
68
+ end
69
+
70
+ #
71
+ # Returns the ANSI string.
72
+ #
73
+ # @return [String]
74
+ #
75
+ def to_s
76
+ @string
77
+ end
78
+
79
+ alias to_str to_s
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,159 @@
1
+ require_relative 'ansi'
2
+
3
+ require 'strscan'
4
+
5
+ module Hexdump
6
+ class Theme
7
+ #
8
+ # Represents a color highlighting rule.
9
+ #
10
+ # @api private
11
+ #
12
+ # @since 1.0.0
13
+ #
14
+ class Rule
15
+
16
+ # The default style to apply to strings.
17
+ #
18
+ # @return [ANSI, nil]
19
+ attr_reader :style
20
+
21
+ # Highlighting rules for exact strings.
22
+ #
23
+ # @return [Hash{String => ANSI}]
24
+ attr_reader :highlight_strings
25
+
26
+ # Highlighting rules for matching substrings.
27
+ #
28
+ # @return [Hash{String => ANSI}]
29
+ attr_reader :highlight_regexps
30
+
31
+ #
32
+ # Initializes the color.
33
+ #
34
+ # @param [Symbol, Array<Symbol>] style
35
+ # The default style name(s). See {ANSI::PARAMETERS}.
36
+ #
37
+ # @param [Hash{String,Regexp => Symbol,Array<Symbol>}, nil] highlights
38
+ # Optional highlighting rules.
39
+ #
40
+ def initialize(style: nil, highlights: nil)
41
+ @reset = ANSI::RESET
42
+
43
+ @style = if style
44
+ ANSI.new(style)
45
+ end
46
+
47
+ @highlight_strings = {}
48
+ @highlight_regexps = {}
49
+
50
+ if highlights
51
+ highlights.each do |pattern,style|
52
+ highlight(pattern,style)
53
+ end
54
+ end
55
+ end
56
+
57
+ #
58
+ # The highlighting rules.
59
+ #
60
+ # @return [Hash{String,Regexp => ANSI}]
61
+ # The combination of {#highlight_strings} and {#highlight_regexps}.
62
+ #
63
+ def highlights
64
+ @highlight_strings.merge(@highlight_regexps)
65
+ end
66
+
67
+ #
68
+ # Adds a highlighting rule.
69
+ #
70
+ # @param [String, Regexp] pattern
71
+ # The exact String to highlight or regular expression to highlight.
72
+ #
73
+ # @param [Symbol, Array<Symbol>] style
74
+ # The style name(s). See {ANSI::PARAMETERS}.
75
+ #
76
+ # @raise [ArgumentError]
77
+ # The given pattern was not a String or Regexp.
78
+ #
79
+ # @example
80
+ # hexdump.style.numeric.highlight('00', :faint)
81
+ #
82
+ # @example
83
+ # hexdump.style.index.highlight(/00$/, [:white, :bold])
84
+ #
85
+ # @api public
86
+ #
87
+ def highlight(pattern,style)
88
+ ansi = ANSI.new(style)
89
+
90
+ case pattern
91
+ when String
92
+ @highlight_strings[pattern] = ansi
93
+ when Regexp
94
+ @highlight_regexps[pattern] = ansi
95
+ else
96
+ raise(ArgumentError,"pattern must be a String or Regexp: #{pattern.inspect}")
97
+ end
98
+ end
99
+
100
+ #
101
+ # Applies coloring/highlighting to a string.
102
+ #
103
+ # @param [String] string
104
+ # The string to color/highlight.
105
+ #
106
+ # @return [String]
107
+ # The colored the string.
108
+ #
109
+ def apply(string)
110
+ if (!@highlight_strings.empty? || !@highlight_regexps.empty?)
111
+ apply_highlight(string)
112
+ elsif @style
113
+ "#{@style}#{string}#{@reset}"
114
+ else
115
+ string
116
+ end
117
+ end
118
+
119
+ private
120
+
121
+ def apply_highlight(string)
122
+ if (ansi = @highlight_strings[string])
123
+ # highlight the whole string
124
+ "#{ansi}#{string}#{@reset}"
125
+ else
126
+ scanner = StringScanner.new(string)
127
+ new_string = String.new
128
+
129
+ until scanner.eos?
130
+ matched = false
131
+
132
+ @highlight_regexps.each do |regexp,ansi|
133
+ if (match = scanner.scan(regexp))
134
+ # highlight the match
135
+ new_string << "#{ansi}#{match}#{@reset}"
136
+ new_string << "#{@style}" unless scanner.eos?
137
+ matched = true
138
+ break
139
+ end
140
+ end
141
+
142
+ unless matched
143
+ # next char
144
+ new_string << scanner.getch
145
+ end
146
+ end
147
+
148
+ if @style
149
+ new_string.prepend(@style) unless new_string.start_with?("\e")
150
+ new_string << @reset unless new_string.end_with?(@reset)
151
+ end
152
+
153
+ new_string
154
+ end
155
+ end
156
+
157
+ end
158
+ end
159
+ end
@@ -0,0 +1,61 @@
1
+ require_relative 'theme/rule'
2
+
3
+ module Hexdump
4
+ #
5
+ # Represents a hexdump theme (styling + highlighting).
6
+ #
7
+ # @api semipublic
8
+ #
9
+ # @since 1.0.0
10
+ #
11
+ class Theme
12
+
13
+ # The index styling/highlights.
14
+ #
15
+ # @return [Rule, nil]
16
+ #
17
+ # @api public
18
+ attr_reader :index
19
+
20
+ # The numeric styling/highlights.
21
+ #
22
+ # @return [Rule, nil]
23
+ #
24
+ # @api public
25
+ attr_reader :numeric
26
+
27
+ # The chars styling/highlights.
28
+ #
29
+ # @return [Rule, nil]
30
+ #
31
+ # @api public
32
+ attr_reader :chars
33
+
34
+ #
35
+ # Initializes the theme.
36
+ #
37
+ # @param [Hash{:index,:numeric,:chars => Symbol,Array<Symbol>,nil}] style
38
+ # The default style of the index, numeric, and/or chars columns.
39
+ #
40
+ # @param [Hash{:index,:numeric,:chars => Hash{String,Regexp => Symbol,Array<Symbol>},nil}] highlights
41
+ # The highlighting rules for the index, numeric, and/or chars columns.
42
+ #
43
+ def initialize(style: {}, highlights: {})
44
+ @index = Rule.new(
45
+ style: style[:index],
46
+ highlights: highlights[:index]
47
+ )
48
+
49
+ @numeric = Rule.new(
50
+ style: style[:numeric],
51
+ highlights: highlights[:numeric]
52
+ )
53
+
54
+ @chars = Rule.new(
55
+ style: style[:chars],
56
+ highlights: highlights[:chars]
57
+ )
58
+ end
59
+
60
+ end
61
+ end