memory_io 0.2.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.
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'memory_io/context'
4
+ require 'memory_io/error'
3
5
  require 'memory_io/types/type'
4
6
 
5
7
  module MemoryIO
@@ -8,64 +10,144 @@ module MemoryIO
8
10
  module Basic
9
11
  # Register numbers to {Types}.
10
12
  #
11
- # All types registered by this class are assumed as *little endian*.
13
+ # The byte order is taken from the {MemoryIO::Context} of the stream being
14
+ # read, so the same type reads correctly from memory of either byte order.
12
15
  #
13
- # This class registered (un)signed {8, 16, 32, 64)-bit integers and IEEE-754 floating numbers.
16
+ # This class registered (un)signed (8, 16, 32, 64)-bit integers and IEEE-754 floating numbers.
14
17
  class Number
18
+ # Indicators of the integer widths, by size in bytes.
19
+ # A single byte has no byte order to indicate.
20
+ INTEGER_INDICATORS = { 1 => 'C', 2 => 'S', 4 => 'L', 8 => 'Q' }.freeze
21
+
22
+ # Indicators of the real widths, by size in bytes and then byte order.
23
+ #
24
+ # @example
25
+ # # 'e' and 'E' are IEEE-754 single and double precision, little endian.
26
+ REAL_INDICATORS = { 4 => { little: 'e', big: 'g' }, 8 => { little: 'E', big: 'G' } }.freeze
27
+
28
+ # Appended to an integer indicator to fix its byte order.
29
+ ENDIAN_INDICATORS = { little: '<', big: '>' }.freeze
30
+
15
31
  # @param [Integer] bytes
16
32
  # Bytes.
17
33
  # @param [Boolean] signed
18
34
  # Signed or unsigned.
19
- # @param [String] pack_str
20
- # The indicator to be passed to +Array#pack+ and +String#unpack+.
21
- def initialize(bytes, signed, pack_str)
35
+ # @param [Boolean] real
36
+ # Whether this type holds a real number rather than an integer.
37
+ def initialize(bytes, signed, real: false)
22
38
  @bytes = bytes
23
39
  @signed = signed
24
- @pack_str = pack_str
40
+ @real = real
41
+ @range = value_range unless real
42
+ @indicators = ENDIAN_INDICATORS.keys.to_h { |e| [e, indicator(e)] }
25
43
  end
26
44
 
27
45
  # @return [Integer]
46
+ #
47
+ # @raise [EOFError]
48
+ # Fewer than +bytes+ bytes remain in +stream+.
28
49
  def read(stream)
29
- unpack(stream.read(@bytes))
50
+ endian = MemoryIO::Context.of(stream).endian
51
+ unpack(MemoryIO::Util.read_exactly(stream, @bytes), endian)
30
52
  end
31
53
 
32
54
  # @param [Integer] val
55
+ #
56
+ # @raise [MemoryIO::ValueOutOfRangeError]
57
+ # +val+ doesn't fit in this type, which would otherwise be written truncated.
33
58
  def write(stream, val)
34
- stream.write(pack(val))
59
+ endian = MemoryIO::Context.of(stream).endian
60
+ raise MemoryIO::ValueOutOfRangeError, out_of_range_message(val) if out_of_range?(val, endian)
61
+
62
+ stream.write(pack(val, endian))
35
63
  end
36
64
 
37
65
  private
38
66
 
39
- def unpack(str)
40
- val = str.unpack(@pack_str).first
41
- val -= (2**(@bytes * 8)) if @signed && val >= (2**(@bytes * 8 - 1))
67
+ # Anything that isn't a number of the matching kind is left for
68
+ # +Array#pack+ to reject.
69
+ #
70
+ # @return [Boolean]
71
+ def out_of_range?(val, endian)
72
+ if @range
73
+ val.is_a?(Integer) && !@range.cover?(val)
74
+ else
75
+ val.is_a?(Numeric) && overflows?(val, endian)
76
+ end
77
+ end
78
+
79
+ # A finite value that packs to an infinity has exceeded what the type
80
+ # can represent. Losing precision is inherent to the type and allowed,
81
+ # as is writing an infinity that was asked for.
82
+ #
83
+ # @return [Boolean]
84
+ def overflows?(val, endian)
85
+ val.infinite?.nil? && pack(val, endian).unpack1(@indicators[endian]).infinite?
86
+ end
87
+
88
+ # @return [Range]
89
+ def value_range
90
+ bits = @bytes * 8
91
+ @signed ? (-(2**(bits - 1))..((2**(bits - 1)) - 1)) : (0..((2**bits) - 1))
92
+ end
93
+
94
+ # @return [String]
95
+ def out_of_range_message(val)
96
+ return format('%s exceeds the range of %d-bit floating number', val, @bytes * 8) unless @range
97
+
98
+ format('%s is out of range for %d-bit %s integer (%s..%s)',
99
+ hex(val), @bytes * 8, @signed ? 'signed' : 'unsigned',
100
+ hex(@range.first), hex(@range.last))
101
+ end
102
+
103
+ # +format+'s '%#x' renders a negative number in two's complement notation.
104
+ #
105
+ # @return [String]
106
+ #
107
+ # @example
108
+ # hex(-128)
109
+ # #=> '-0x80'
110
+ def hex(val)
111
+ format('%s0x%x', val.negative? ? '-' : '', val.abs)
112
+ end
113
+
114
+ # @return [String]
115
+ # The +Array#pack+ indicator of this type in +endian+ byte order.
116
+ def indicator(endian)
117
+ return REAL_INDICATORS[@bytes][endian] if @real
118
+ return INTEGER_INDICATORS[@bytes] if @bytes == 1
119
+
120
+ INTEGER_INDICATORS[@bytes] + ENDIAN_INDICATORS[endian]
121
+ end
122
+
123
+ def unpack(str, endian)
124
+ val = str.unpack1(@indicators[endian])
125
+ # a real is already signed by its representation
126
+ return val if @real
127
+
128
+ val -= (2**(@bytes * 8)) if @signed && val >= (2**((@bytes * 8) - 1))
42
129
  val
43
130
  end
44
131
 
45
- def pack(val)
46
- [val].pack(@pack_str)
132
+ def pack(val, endian)
133
+ [val].pack(@indicators[endian])
47
134
  end
48
135
 
49
136
  # Register (un)signed n-bits integers.
50
- {
51
- 8 => 'C',
52
- 16 => 'S',
53
- 32 => 'I',
54
- 64 => 'Q'
55
- }.each do |t, c|
56
- Type.register(Number.new(t / 8, true, c),
137
+ [8, 16, 32, 64].each do |t|
138
+ Type.register(Number.new(t / 8, true),
57
139
  alias: [:"basic/s#{t}", :"s#{t}"],
58
140
  doc: "A signed #{t}-bit integer.")
59
- Type.register(Number.new(t / 8, false, c),
141
+ Type.register(Number.new(t / 8, false),
60
142
  alias: [:"basic/u#{t}", :"u#{t}"],
61
143
  doc: "An unsigned #{t}-bit integer.")
62
144
  end
63
145
 
64
146
  # Register floating numbers.
65
- Type.register(Number.new(4, false, 'F'),
147
+ Type.register(Number.new(4, true, real: true),
66
148
  alias: %i[basic/float float],
67
149
  doc: 'IEEE-754 32-bit floating number.')
68
- Type.register(Number.new(8, false, 'D'),
150
+ Type.register(Number.new(8, true, real: true),
69
151
  alias: %i[basic/double double],
70
152
  doc: 'IEEE-754 64-bit floating number.')
71
153
  end
@@ -12,17 +12,31 @@ module MemoryIO
12
12
  # A null-terminated string.
13
13
  class CStr < Types::Type
14
14
 
15
+ # Number of bytes to fetch at a time while searching for the terminator.
16
+ CHUNK_SIZE = 1024
17
+
15
18
  # @api private
16
19
  #
20
+ # The terminator is searched for in blocks rather than byte by byte,
21
+ # and +stream+ is left just after it so the next read starts at the
22
+ # following string.
23
+ #
17
24
  # @return [String]
18
25
  # String without null byte.
19
26
  def self.read(stream)
20
27
  ret = +''
21
28
  loop do
22
- c = stream.read(1)
23
- break if c.nil? || c == '' || c == "\x00"
29
+ chunk = stream.read(CHUNK_SIZE)
30
+ break if chunk.nil? || chunk.empty?
31
+
32
+ terminator = chunk.index("\x00")
33
+ if terminator
34
+ ret << chunk[0, terminator]
35
+ stream.pos -= chunk.size - terminator - 1
36
+ break
37
+ end
24
38
 
25
- ret << c
39
+ ret << chunk
26
40
  end
27
41
  ret
28
42
  end
@@ -1,6 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'memory_io/context'
4
+ require 'memory_io/logger'
3
5
  require 'memory_io/types/type'
6
+ require 'memory_io/util'
4
7
 
5
8
  module MemoryIO
6
9
  module Types
@@ -22,9 +25,7 @@ module MemoryIO
22
25
  # std::string uses inlined-buffer if string length isn't larger than {LOCAL_CAPACITY}.
23
26
  LOCAL_CAPACITY = 15
24
27
 
25
- attr_reader :data # @return [::String]
26
- attr_reader :capacity # @return [Integer]
27
- attr_reader :dataplus # @return [Integer]
28
+ attr_reader :data, :capacity, :dataplus # @return [::String] # @return [Integer] # @return [Integer]
28
29
 
29
30
  # Instantiate a {CPP::String} object.
30
31
  #
@@ -33,6 +34,7 @@ module MemoryIO
33
34
  # @param [Integer] dataplus
34
35
  # A pointer.
35
36
  def initialize(data, capacity, dataplus)
37
+ super()
36
38
  @data = data
37
39
  @capacity = capacity
38
40
  @dataplus = dataplus
@@ -51,21 +53,20 @@ module MemoryIO
51
53
  # @param [String] str
52
54
  def data=(str)
53
55
  @data = str
54
- warn("Length of str (#{str.size}) is larger than capacity (#{capacity})") if str.size > capacity
56
+ return unless str.size > capacity
57
+
58
+ MemoryIO.logger.warn("Length of str (#{str.size}) is larger than capacity (#{capacity})")
55
59
  end
56
60
 
57
61
  # Custom inspect view.
58
62
  #
59
63
  # @return [String]
60
- #
61
- # @todo
62
- # Let it be colorful in pry.
63
64
  def inspect
64
- format("#<%s @data=%s, @capacity=%d, @dataplus=0x%0#{SIZE_T * 2}x>",
65
+ format('#<%s @data=%s, @capacity=%d, @dataplus=0x%s>',
65
66
  self.class.name,
66
67
  data.inspect,
67
68
  capacity,
68
- dataplus)
69
+ dataplus.to_s(16).rjust(SIZE_T * 2, '0'))
69
70
  end
70
71
 
71
72
  class << self
@@ -73,6 +74,11 @@ module MemoryIO
73
74
  #
74
75
  # @return [CPP::String]
75
76
  #
77
+ # @raise [EOFError]
78
+ # The object is incomplete, or its characters cannot be read from
79
+ # where it points. {MemoryIO::IO#read} answers with the objects it
80
+ # read in full rather than one that is partly filled in.
81
+ #
76
82
  # @example
77
83
  # # echo '#include <string>\n#include <cstdio>\nint main() {' > a.cpp && \
78
84
  # # echo 'std::string a="abcd"; printf("%p\\n", &a);' >> a.cpp && \
@@ -87,10 +93,11 @@ module MemoryIO
87
93
  def read(stream)
88
94
  dataplus = read_size_t(stream)
89
95
  length = read_size_t(stream)
90
- union = stream.read(LOCAL_CAPACITY + 1)
96
+ union = MemoryIO::Util.read_exactly(stream, LOCAL_CAPACITY + 1)
91
97
  if length > LOCAL_CAPACITY
92
- capacity = MemoryIO::Util.unpack(union[0, Type::SIZE_T])
93
- data = keep_pos(stream, pos: dataplus) { |s| s.read(length) }
98
+ context = MemoryIO::Context.of(stream)
99
+ capacity = MemoryIO::Util.unpack(union[0, context.pointer_size], context.endian)
100
+ data = keep_pos(stream, pos: dataplus) { |s| MemoryIO::Util.read_exactly(s, length) }
94
101
  else
95
102
  capacity = LOCAL_CAPACITY
96
103
  data = union[0, length]
@@ -109,10 +116,10 @@ module MemoryIO
109
116
  write_size_t(stream, obj.length)
110
117
  pos = stream.pos
111
118
  if obj.length > LOCAL_CAPACITY
112
- keep_pos(stream, pos: obj.dataplus) { |s| s.write(obj.data + "\x00") }
119
+ keep_pos(stream, pos: obj.dataplus) { |s| s.write("#{obj.data}\u0000") }
113
120
  write_size_t(stream, obj.capacity)
114
121
  else
115
- stream.write(obj.data + "\x00")
122
+ stream.write("#{obj.data}\u0000")
116
123
  end
117
124
  stream.pos = pos + LOCAL_CAPACITY + 1
118
125
  end
@@ -19,12 +19,14 @@ module MemoryIO
19
19
  #
20
20
  # @param [Object] object
21
21
  # @param [Array<Symbol>] keys
22
+ # @param [Hash] option
23
+ # Extra options.
22
24
  #
23
- # @option [Thread::Backtrace::Location] caller
24
- # This option should present if and only if +object+ is a subclass of {Types::Type}.
25
- # @option [String] doc
26
- # Docstring.
27
- # Automatically parse from caller location if this parameter isn't present.
25
+ # @option option [Thread::Backtrace::Location] :caller
26
+ # This option should be present if and only if +object+ is a subclass of {Types::Type}.
27
+ # @option option [String] :doc
28
+ # Doc-string.
29
+ # Automatically parsed from caller location if this option isn't present.
28
30
  def initialize(object, keys, option = {})
29
31
  @obj = object
30
32
  @keys = keys
@@ -60,14 +62,14 @@ module MemoryIO
60
62
  str.strip!
61
63
  break unless str.start_with?('#')
62
64
 
63
- strings.unshift(str[2..-1] || '')
65
+ strings.unshift(str[2..] || '')
64
66
  end
65
67
  trim_docstring(strings)
66
68
  end
67
69
 
68
70
  def trim_docstring(strings)
69
71
  strings = strings.drop_while { |s| s.start_with?('@') }.take_while { |s| !s.start_with?('@') }
70
- strings.drop_while(&:empty?).reverse.drop_while(&:empty?).reverse.join("\n") + "\n"
72
+ "#{strings.drop_while(&:empty?).reverse.drop_while(&:empty?).reverse.join("\n")}\n"
71
73
  end
72
74
  end
73
75
  end
@@ -1,19 +1,22 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'ostruct'
4
-
3
+ require 'memory_io/context'
5
4
  require 'memory_io/types/record'
6
5
  require 'memory_io/util'
7
6
 
8
7
  module MemoryIO
9
8
  module Types
10
- # The base class, all descendants of this class would be consider as a valid 'type'.
9
+ # The base class, all descendants of this class would be considered as a valid 'type'.
11
10
  class Type
12
- # The size of +size_t+. i.e. +sizeof(size_t)+.
13
- SIZE_T = 8
11
+ # The size of +size_t+ assumed when the stream says nothing more specific.
12
+ #
13
+ # @see MemoryIO::Context#pointer_size
14
+ SIZE_T = MemoryIO::Context::DEFAULT_POINTER_SIZE
14
15
 
15
16
  class << self
16
- # Read {Type::SIZE_T} bytes and cast to a little endian unsigned integer.
17
+ # Read one +size_t+ and cast it to an unsigned integer.
18
+ #
19
+ # Its width and byte order are taken from the {MemoryIO::Context} of +stream+.
17
20
  #
18
21
  # @param [#read] stream
19
22
  # Stream to read.
@@ -21,15 +24,21 @@ module MemoryIO
21
24
  # @return [Integer]
22
25
  # Result.
23
26
  #
27
+ # @raise [EOFError]
28
+ # Fewer bytes than a +size_t+ remain in +stream+.
29
+ #
24
30
  # @example
25
31
  # s = StringIO.new("\xEF\xBE\xAD\xDExV4\x00")
26
32
  # Type.read_size_t(s).to_s(16)
27
33
  # #=> '345678deadbeef'
28
34
  def read_size_t(stream)
29
- MemoryIO::Util.unpack(stream.read(SIZE_T))
35
+ context = MemoryIO::Context.of(stream)
36
+ MemoryIO::Util.unpack(MemoryIO::Util.read_exactly(stream, context.pointer_size), context.endian)
30
37
  end
31
38
 
32
- # Pack +val+ into {Type::SIZE_T} bytes and write to +stream+.
39
+ # Pack +val+ into one +size_t+ and write it to +stream+.
40
+ #
41
+ # Its width and byte order are taken from the {MemoryIO::Context} of +stream+.
33
42
  #
34
43
  # @param [#write] stream
35
44
  # Stream to write.
@@ -44,11 +53,16 @@ module MemoryIO
44
53
  # s.string
45
54
  # #=> "\x23\x01\x00\x00\x00\x00\x00\x00"
46
55
  def write_size_t(stream, val)
47
- stream.write(MemoryIO::Util.pack(val, SIZE_T))
56
+ context = MemoryIO::Context.of(stream)
57
+ stream.write(MemoryIO::Util.pack(val, context.pointer_size, context.endian))
48
58
  end
49
59
 
50
60
  # Yield a block and resume the position of stream.
51
61
  #
62
+ # The position is resumed even if the block raises,
63
+ # so a failure while following an invalid pointer doesn't
64
+ # leave the stream at an unexpected position.
65
+ #
52
66
  # @param [#pos, #pos=] stream
53
67
  # Stream.
54
68
  # @param [Integer] pos
@@ -71,9 +85,11 @@ module MemoryIO
71
85
  def keep_pos(stream, pos: nil)
72
86
  org = stream.pos
73
87
  stream.pos = pos if pos
74
- ret = yield stream
75
- stream.pos = org
76
- ret
88
+ begin
89
+ yield stream
90
+ ensure
91
+ stream.pos = org
92
+ end
77
93
  end
78
94
 
79
95
  # @api private
@@ -83,8 +99,9 @@ module MemoryIO
83
99
  # @param [Symbol] symbol
84
100
  # Symbol that has been registered in {.register}.
85
101
  #
86
- # @return [{Symbol => Object}]
87
- # The object that registered in {.register}.
102
+ # @return [MemoryIO::Types::Record?]
103
+ # The record that was registered in {.register},
104
+ # or +nil+ if +symbol+ has never been registered.
88
105
  #
89
106
  # @see .register
90
107
  def find(symbol)
@@ -95,10 +112,12 @@ module MemoryIO
95
112
  #
96
113
  # @param [#read, #write] object
97
114
  # Normally, +object+ is a descendant class of {Type}.
115
+ # @param [Hash] option
116
+ # Extra options.
98
117
  #
99
- # @option [Symbol, Array<Symbol>] alias
118
+ # @option option [Symbol, Array<Symbol>] :alias
100
119
  # Custom symbol name(s) that can be used in {.find}.
101
- # @option [String] doc
120
+ # @option option [String] :doc
102
121
  # Doc string that will be shown in README.md.
103
122
  #
104
123
  # @return [Array<Symbol>]
@@ -119,11 +138,11 @@ module MemoryIO
119
138
  #
120
139
  # @note
121
140
  # If all symbols in +alias+ have been registered, an ArgumentError will be raised.
122
- # However, if at least one of aliases hasn't been used, registration will success.
141
+ # However, if at least one of aliases hasn't been used, registration will succeed.
123
142
  #
124
143
  # @see .find
125
144
  def register(object, option = {})
126
- @map ||= OpenStruct.new
145
+ @map ||= {}
127
146
  aliases = Array(option[:alias])
128
147
  reg_fail = ArgumentError.new(<<-EOS.strip)
129
148
  Register '#{object.inspect}' fails because another object with same name has been registered.
@@ -152,6 +171,7 @@ Specify an alias such as `register(MyClass, alias: :custom_alias_name)`.
152
171
  #
153
172
  # To record descendants.
154
173
  def inherited(klass)
174
+ super
155
175
  register(klass, caller: caller_locations(1, 1).first)
156
176
  end
157
177
 
@@ -3,7 +3,7 @@
3
3
  require 'memory_io/types/type'
4
4
  require 'memory_io/util'
5
5
 
6
- Dir.glob(File.join(__dir__, '**', '*.rb')).sort.each { |f| require f unless f == __FILE__ }
6
+ Dir.glob(File.join(__dir__, '**', '*.rb')).each { |f| require f unless f == __FILE__ }
7
7
 
8
8
  module MemoryIO
9
9
  # Module that includes multiple types.
@@ -18,22 +18,22 @@ module MemoryIO
18
18
  #
19
19
  # This method will search all descendants of {Types::Type}.
20
20
  #
21
- # @return [Symbol] name
21
+ # @param [Symbol] name
22
22
  # Class name to be searched.
23
23
  #
24
24
  # @return [#read, #write]
25
25
  # Any object that implemented method +read+ and +write+.
26
- # Usually returns a class inherit {Types::Type}.
26
+ # Usually returns a class inherited from {Types::Type}.
27
27
  #
28
28
  # @example
29
29
  # Types.find(:c_str)
30
- # #=> MemoryIO::Types::CStr
30
+ # #=> MemoryIO::Types::Clang::CStr
31
31
  #
32
32
  # Types.find(:u64)
33
33
  # #=> #<MemoryIO::Types::Number:0x000055ecc017a310 @bytes=8, @pack_str="Q", @signed=false>
34
34
  def find(name)
35
35
  obj = Types::Type.find(name)
36
- return obj.obj if obj
36
+ obj&.obj
37
37
  end
38
38
 
39
39
  # @api private
@@ -50,7 +50,7 @@ module MemoryIO
50
50
  #
51
51
  # @example
52
52
  # Types.get_proc(:c_str, :write)
53
- # #=> #<Method: MemoryIO::Types::CStr.write>
53
+ # #=> #<Method: MemoryIO::Types::Clang::CStr.write>
54
54
  # Types.get_proc(:s32, :read)
55
55
  # #=> #<Method: MemoryIO::Types::Number#read>
56
56
  def get_proc(name, rw)