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.
- checksums.yaml +7 -0
- data/.gitignore +52 -0
- data/.rspec +0 -0
- data/.ruby-version +1 -0
- data/Gemfile +5 -0
- data/Gemfile.lock +49 -0
- data/LICENSE +674 -0
- data/binstream.gemspec +28 -0
- data/lib/binstream.rb +11 -0
- data/lib/binstream/errors.rb +39 -0
- data/lib/binstream/streams/base.rb +333 -0
- data/lib/binstream/streams/file_reader.rb +49 -0
- data/lib/binstream/streams/string_reader.rb +14 -0
- data/lib/binstream/tracker.rb +63 -0
- data/lib/binstream/tracking.rb +31 -0
- data/lib/binstream/version.rb +3 -0
- metadata +115 -0
data/binstream.gemspec
ADDED
@@ -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
|
data/lib/binstream.rb
ADDED
@@ -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
|