binstream 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.
@@ -0,0 +1,28 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'binstream/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "binstream"
8
+ spec.version = Binstream::VERSION
9
+ spec.authors = ["Mitch Dempsey"]
10
+ spec.email = ["gems@mitchdempsey.com"]
11
+
12
+ spec.summary = %q{Binary stream processor}
13
+ spec.description = %q{Binary stream processor}
14
+ spec.homepage = ""
15
+ spec.license = "GPL"
16
+
17
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
18
+ spec.bindir = "exe"
19
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
20
+ spec.require_paths = ["lib"]
21
+
22
+ spec.required_ruby_version = ">= 2.5.0"
23
+
24
+ spec.add_development_dependency "bundler", "~> 2.0"
25
+ spec.add_development_dependency "rake", "~> 10.0"
26
+ spec.add_development_dependency "rspec"
27
+ spec.add_development_dependency "simplecov"
28
+ end
@@ -0,0 +1,11 @@
1
+ require "singleton"
2
+ require 'forwardable'
3
+
4
+ require 'binstream/version'
5
+ require 'binstream/errors'
6
+ require 'binstream/tracker'
7
+ require 'binstream/tracking'
8
+
9
+ require 'binstream/streams/base'
10
+ require 'binstream/streams/file_reader'
11
+ require 'binstream/streams/string_reader'
@@ -0,0 +1,39 @@
1
+ module Binstream
2
+ class Error < ::StandardError
3
+ end
4
+
5
+ # PARSER
6
+
7
+ class ParseError < ::StandardError
8
+ end
9
+
10
+ class InvalidLengthError < ParseError
11
+ def initialize(len)
12
+ super("You must provide a length greater than 0")
13
+ end
14
+ end
15
+
16
+ class StreamOverrunError < ParseError
17
+ def initialize(length, remaining, position)
18
+ super(sprintf("Overrun! Reading %d bytes (remaining=%d pos=%d)", length, remaining, position))
19
+ end
20
+ end
21
+
22
+ class InvalidPositionError < ParseError
23
+ def initialize(proposal)
24
+ super(sprintf("Wanted to seek to %d!!", proposal))
25
+ end
26
+ end
27
+
28
+ class InvalidBooleanValueError < ParseError
29
+ def initialize(bad_value, position)
30
+ super(sprintf("Expected boolean value of 1 or 0, but got %d (0x%02X) pos=%d", bad_value, bad_value, position))
31
+ end
32
+ end
33
+
34
+ class InvalidFloatValueError < ParseError
35
+ def initialize(position)
36
+ super(sprintf("Expected float, but got NaN pos=%d", position))
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,333 @@
1
+ # frozen_string_literal: true
2
+ module Binstream
3
+ module Streams
4
+ class Base
5
+ extend Forwardable
6
+ include Tracking
7
+
8
+ def_delegators :@stream, :close
9
+ attr_reader :stopper
10
+
11
+ def initialize(stream, startpos: nil, whence: IO::SEEK_SET, read_length: nil, **kwargs)
12
+ @stream = stream
13
+ reset
14
+ setup_stopper(startpos, whence, read_length)
15
+ end
16
+
17
+ def setup_stopper(startpos, whence, max_length)
18
+ if whence == IO::SEEK_CUR
19
+ startpos += @stream.tell
20
+ end
21
+
22
+ @startpos = startpos || @stream.tell
23
+
24
+ if max_length
25
+ @stopper = startpos + max_length
26
+ else
27
+ @stopper = @stream.size
28
+ end
29
+ end
30
+
31
+ def starting_offset
32
+ @startpos
33
+ end
34
+
35
+ def reset
36
+ @cur_offset = 0
37
+ @stopper = nil
38
+ @startpos = 0
39
+ end
40
+
41
+ # Create a new stream based off this one using an offset and length
42
+ def slice(new_length, new_offset=nil)
43
+ new_stream = peek_slice(new_length, new_offset)
44
+
45
+ # advance our pointer
46
+ @cur_offset += new_length
47
+
48
+ return new_stream
49
+ end
50
+
51
+ # slice!(length, offset)
52
+ # Get new stream using absolute position
53
+ def slice!(new_length, new_offset)
54
+ self.class.new(@stream,
55
+ startpos: (@startpos + new_offset),
56
+ read_length: new_length
57
+ )
58
+ end
59
+
60
+ # Get a new stream at the current position, but don't advance our internal pointer
61
+ def peek_slice(new_length, offset_adjustment=nil)
62
+ offset_adjustment ||= 0
63
+
64
+ self.class.new(@stream,
65
+ startpos: (@startpos + @cur_offset + offset_adjustment),
66
+ read_length: new_length
67
+ )
68
+ end
69
+
70
+ # How many remaining bytes are there
71
+ def remaining
72
+ size - tell
73
+ end
74
+
75
+ # Do we have any remaining bytes?
76
+ def remaining?(len_to_check=1)
77
+ remaining >= len_to_check
78
+ end
79
+
80
+ # Reset the position pointer back to the start
81
+ def rewind
82
+ @cur_offset = 0
83
+ end
84
+
85
+ def read(length=nil)
86
+
87
+ original_pos = stell
88
+ read_size = (length || size)
89
+
90
+ if remaining - read_size < 0
91
+ raise StreamOverrunError.new(read_size, remaining, original_pos)
92
+ end
93
+
94
+ @stream.seek(@startpos + @cur_offset, IO::SEEK_SET)
95
+ resp = @stream.read(length)
96
+ @cur_offset += read_size
97
+
98
+ return resp
99
+ rescue => e
100
+ raise
101
+ ensure
102
+ # put the stream back
103
+ # TODO: possibly remove this, it just makes it slower
104
+ @stream.seek(original_pos, IO::SEEK_SET)
105
+ end
106
+
107
+ # Returns data without advancing the offset pointer
108
+ def peek(length=nil)
109
+ original_pos = stell
110
+ read_size = (length || size)
111
+
112
+ if remaining - read_size < 0
113
+ raise StreamOverrunError.new(read_size, remaining, original_pos)
114
+ end
115
+
116
+ @stream.seek(@startpos + @cur_offset, IO::SEEK_SET)
117
+ resp = @stream.read(length)
118
+
119
+ return resp
120
+ rescue => e
121
+ raise
122
+ ensure
123
+ @stream.seek(original_pos, IO::SEEK_SET)
124
+ end
125
+
126
+ # Seek to a specific position, or relative
127
+ def seek(seek_len, whence=IO::SEEK_CUR)
128
+ raise ArgumentError.new("Position must be an integer") if seek_len.nil?
129
+
130
+ case whence
131
+ when IO::SEEK_SET, :SET
132
+ proposal = seek_len
133
+ when IO::SEEK_CUR, :CUR
134
+ proposal = @cur_offset + seek_len
135
+ when IO::SEEK_END, :END
136
+ proposal = @stopper + seek_len # This will actually be a +(-999)
137
+ else
138
+ raise ArgumentError.new("whence must be :SET, :CUR, :END")
139
+ end
140
+
141
+ if valid_position?(proposal)
142
+ @cur_offset = proposal
143
+ else
144
+ raise InvalidPositionError.new(proposal)
145
+ end
146
+ return true
147
+ end
148
+
149
+ # Is this actually a valid position?
150
+ def valid_position?(proposal)
151
+ proposal.abs <= size
152
+ end
153
+
154
+ def eof?
155
+ remaining <= 0
156
+ end
157
+
158
+ # Position in our current high level stream
159
+ def tell
160
+ @cur_offset
161
+ end
162
+ alias_method :pos, :tell
163
+
164
+ # Position on the underlying stream
165
+ def stell
166
+ @stream.tell
167
+ end
168
+
169
+ def total_size
170
+ stopper - @startpos
171
+ end
172
+ alias_method :size, :total_size
173
+
174
+ # Reads a null terminated string off the stream
175
+ def read_string(length, encoding: "UTF-8", packfmt: "Z*")
176
+ if length > 0
177
+ res = read_single(packfmt, length).force_encoding(encoding).encode(encoding)
178
+ track res
179
+ else
180
+ raise InvalidLengthError.new(length)
181
+ end
182
+ end
183
+
184
+ # 8 bit boolean
185
+ def read_bool
186
+ res = read_single("C", 1)
187
+
188
+ if res != 0 && res != 1
189
+ raise InvalidBooleanValueError.new(res, (tell - 1))
190
+ end
191
+
192
+ track(res != 0)
193
+ end
194
+ alias_method :read_bool8, :read_bool
195
+
196
+ # 8 Bits
197
+ def read_int8
198
+ track read_single("c", 1)
199
+ end
200
+ def read_uint8
201
+ track read_single("C", 1)
202
+ end
203
+ def read_byte
204
+ track read_single("c", 1)
205
+ end
206
+
207
+ # 16 Bits
208
+ def read_uint16
209
+ track read_single("S<", 2)
210
+ end
211
+ def read_uint16be
212
+ track read_single("S>", 2)
213
+ end
214
+ alias_method :read_uint16le, :read_uint16
215
+
216
+ def read_int16
217
+ track read_single("s<", 2)
218
+ end
219
+ def read_int16be
220
+ track read_single("s>", 2)
221
+ end
222
+ alias_method :read_int16le, :read_int16
223
+
224
+ # 32 bits
225
+ def read_int32
226
+ track read_single("l<", 4)
227
+ end
228
+ def read_int32be
229
+ track read_single("l>", 4)
230
+ end
231
+ alias_method :read_int32le, :read_int32
232
+
233
+ def read_uint32
234
+ track read_single("L<", 4)
235
+ end
236
+ def read_uint32be
237
+ track read_single("L>", 4)
238
+ end
239
+ alias_method :read_uint32le, :read_uint32
240
+
241
+
242
+ # 64 bits
243
+ def read_int64
244
+ track read_single("q<", 8)
245
+ end
246
+ def read_int64be
247
+ track read_single("q>", 8)
248
+ end
249
+ alias_method :read_int64le, :read_int64
250
+
251
+ def read_uint64
252
+ track read_single("Q<", 8)
253
+ end
254
+ def read_uint64be
255
+ track read_single("Q>", 8)
256
+ end
257
+ alias_method :read_uint64le, :read_uint64
258
+
259
+ # 4 byte floats
260
+ def read_float
261
+ res = read_single("e", 4)
262
+ if res.nan?
263
+ raise InvalidFloatValueError.new(tell - 4)
264
+ end
265
+ track res
266
+ end
267
+ def read_floatbe
268
+ res = read_single("g", 4)
269
+ if res.nan?
270
+ raise InvalidFloatValueError.new(tell - 4)
271
+ end
272
+ track res
273
+ end
274
+ alias_method :read_floatle, :read_float
275
+
276
+
277
+ # 8 byte double
278
+ def read_double
279
+ res = read_single("E", 8)
280
+ if res.nan?
281
+ raise InvalidFloatValueError.new(tell - 8)
282
+ end
283
+ track res
284
+ end
285
+ def read_doublebe
286
+ res = read_single("G", 8)
287
+ if res.nan?
288
+ raise InvalidFloatValueError.new(tell - 8)
289
+ end
290
+ track res
291
+ end
292
+ alias_method :read_doublele, :read_double
293
+
294
+
295
+ def read_binary(len)
296
+ track { sprintf("READ_BINARY(%d+%d = %d)", tell, len, (tell+len)) }
297
+ read(len)
298
+ end
299
+
300
+ def read_hash(len)
301
+ track read_single("H*", len)
302
+ end
303
+
304
+ def read_single(fmt, bytes = 4)
305
+ read(bytes).unpack1(fmt)
306
+ end
307
+
308
+ def read_unpack(bytes, fmt)
309
+ read(bytes).unpack(fmt)
310
+ end
311
+
312
+ # Dump entire stream to a file (for debugging)
313
+ def dump(filename)
314
+ # return nil unless $TESTING
315
+ @stream.seek(@startpos, IO::SEEK_SET)
316
+ File.open(filename, "wb") do |f|
317
+ f.write(@stream.read(@stopper - @startpos))
318
+ end
319
+ end
320
+
321
+ ##### MISC
322
+
323
+ def method_missing(meth_name, *args, &block)
324
+ meth = "read_#{meth_name}".to_sym
325
+ if respond_to?(meth)
326
+ public_send(meth, *args, &block)
327
+ else
328
+ super
329
+ end
330
+ end
331
+ end
332
+ end
333
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+ module Binstream
3
+ module Streams
4
+ class FileReader < Base
5
+
6
+ def initialize(path_or_io, **kwargs)
7
+ if path_or_io.is_a?(::IO)
8
+ super(path_or_io, **kwargs)
9
+ else
10
+ super(File.open(path_or_io, "rb"), **kwargs)
11
+ end
12
+ end
13
+
14
+ def self.open(path, **kwargs)
15
+ return new(File.open(path, "rb"), **kwargs)
16
+ end
17
+
18
+ def filepath
19
+ @stream.path
20
+ rescue => e
21
+ nil
22
+ end
23
+
24
+ def stell
25
+ @startpos + @cur_offset
26
+ end
27
+
28
+ # OVERRIDING
29
+
30
+ def peek(length = nil)
31
+ read_size = (length || size)
32
+
33
+ if remaining - read_size < 0
34
+ raise StreamOverrunError.new(read_size, remaining, @startpos + @cur_offset)
35
+ end
36
+
37
+ resp = @stream.pread(read_size, @startpos + @cur_offset)
38
+ return resp
39
+ end
40
+
41
+ def read(length = nil)
42
+ resp = peek(length)
43
+ @cur_offset += resp.bytesize
44
+
45
+ return resp
46
+ end
47
+ end
48
+ end
49
+ end