binstream 1.0.0

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