byteinterpreter 1.1.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/LICENSE.md +7 -0
- data/README.md +181 -0
- data/lib/byteinterpreter/instructions.rb +110 -0
- data/lib/byteinterpreter.rb +227 -0
- metadata +51 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: b0eb3381f96d234f733f66860614a9690253ea1a182560555cc438cfaff3bc31
|
4
|
+
data.tar.gz: 90025a8f011378e986a058348ddc8c638196fdc5850d83c602dc84069f53eefe
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 13d197967fb4e723bdc5500d69901fcacc234f583a4fa3dbf4cc0b6ed76b5ec44582864af580b1b96e21e017603eee0ea862c333627772d2a4c398b7d431e4cf
|
7
|
+
data.tar.gz: f2aed83bbb6a1f9316c7c59af66f49b1e588ba5b51bccbfe39d41e7f216d9691b8148b2aab662dcf1ff38b20b89be90d49a1dac46b0a81bc7b6eea47017306ae
|
data/LICENSE.md
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
Copyright 2018 Michael K Gremillion II
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
4
|
+
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
6
|
+
|
7
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,181 @@
|
|
1
|
+
ByteInterpreter is a tool to interpret binary data in a fixed-length data
|
2
|
+
structure into another format, or to encode data from another format into that
|
3
|
+
same fixed-length data structure.
|
4
|
+
|
5
|
+
## Introduction
|
6
|
+
ByteInterpreter was made to assist with modifying an old Dreamcast-era RPG, by
|
7
|
+
translating some of its content (spells, abilities, etc) from binary data into
|
8
|
+
a human-readable format. Since the potential applications for this tool are more
|
9
|
+
broad than just making mods for a singular videogame, I decided to spin it off
|
10
|
+
into its own little tool for others to use.
|
11
|
+
|
12
|
+
## Scope
|
13
|
+
ByteInterpreter isn't overly-ambitious -- it's just there to read and write
|
14
|
+
bytes for *fixed-length* data structures. That "fixed-length" bit is important
|
15
|
+
-- there are no plans for ByteInterpreter to support data structures with
|
16
|
+
variable length values. In the future I may expand ByteInterpreter's ability to
|
17
|
+
cope with variable-length data structures, but only once it works the best it
|
18
|
+
can with fixed-length structures.
|
19
|
+
|
20
|
+
Right now, it can interpret binary data in four sizes - 8-bit, 16-bit, 32-bit,
|
21
|
+
and 64-bit. It can also read strings in any arbitrary size. In the near future,
|
22
|
+
I'd like to support reading bit fields and to handle pointers appropriately.
|
23
|
+
|
24
|
+
## Installation
|
25
|
+
ByteInterpreter can be installed with the following command -
|
26
|
+
```
|
27
|
+
gem install byteinterpreter
|
28
|
+
```
|
29
|
+
Afterwards, it's just a matter of requiring it in your code -
|
30
|
+
```ruby
|
31
|
+
require 'byteinterpreter'
|
32
|
+
```
|
33
|
+
|
34
|
+
## Examples
|
35
|
+
In this example, we're trying to mod an old RPG named *Last Illusion VII*. We
|
36
|
+
have the binary file that contains the game's spells, and we know the format
|
37
|
+
for those spells is something like:
|
38
|
+
1. Unsigned 8-bit integer - Spell element
|
39
|
+
2. String, 20 characters - Spell name
|
40
|
+
3. Unsigned 16-bit integer - Spell damage
|
41
|
+
4. String, 50 characters - Spell description
|
42
|
+
5. Signed 8-bit integer - Spell speed
|
43
|
+
|
44
|
+
So how do we extract the information from this binary file?
|
45
|
+
|
46
|
+
### Reading bit by bit
|
47
|
+
The simplest way is to make a new instance of ByteInterpreter, load the binary
|
48
|
+
file, and read the information bit by bit with `#interpret_bytes` and
|
49
|
+
`#interpret_string`.
|
50
|
+
```ruby
|
51
|
+
require 'byteinterpreter'
|
52
|
+
|
53
|
+
f = File.open("SPELLS.BIN", "rb")
|
54
|
+
bi = ByteInterpreter.new(stream: f)
|
55
|
+
|
56
|
+
# Interpret the first spell's element
|
57
|
+
element = bi.interpret_bytes(size: 1, signed: false)
|
58
|
+
# => 1
|
59
|
+
|
60
|
+
# Interpret the first spell's name
|
61
|
+
name = bi.interpret_string(size: 20)
|
62
|
+
# => "Fireball "
|
63
|
+
```
|
64
|
+
These two methods can read most simple structures in binary files. Take care
|
65
|
+
to read values as `signed` when appropriate! For instance, in our example format,
|
66
|
+
spell speed is signed, so we'd read it thus:
|
67
|
+
```ruby
|
68
|
+
bi.interpret_bytes(size: 1, signed: true)
|
69
|
+
# => -50
|
70
|
+
```
|
71
|
+
Technically speaking, nothing truly horrid will happen if you forget, but your
|
72
|
+
data will be inaccurate.
|
73
|
+
|
74
|
+
The other method of reading bytes from a binary file is to use Instructions.
|
75
|
+
|
76
|
+
### Using Instructions
|
77
|
+
In order to use Instructions, you must first load them. As of writing this
|
78
|
+
README, ByteInterpreter only knows how to load Instructions from JSON. You can
|
79
|
+
load JSON instructions with the `#load_instructions` method:
|
80
|
+
```ruby
|
81
|
+
bi.load_instructions(type: :json, filename: "spell_instructions.json")
|
82
|
+
```
|
83
|
+
The expected format of a JSON file for Instructions is an array containing
|
84
|
+
objects, one object for each field we'll be reading. The objects should each
|
85
|
+
have the following attributes: `"key"`, `"type"`, `"size"`, and `"signed"`.
|
86
|
+
* `key` - The name of the field we're reading (e.g. "Element" or "Name")
|
87
|
+
* `type` - The type of the field, either "bin" for binary values or "str" for
|
88
|
+
strings.
|
89
|
+
* `size` - How many bytes the field is. Binary values can only be 1, 2, 4, or
|
90
|
+
8 bytes, but string values can be as short or long as you wish.
|
91
|
+
* `signed` - For binary values, if the resulting integer is signed or not.
|
92
|
+
Totally ignored for strings.
|
93
|
+
|
94
|
+
So a JSON Instructions file for our example might look like this:
|
95
|
+
```json
|
96
|
+
[
|
97
|
+
{
|
98
|
+
"key": "element", "type": "bin", "size": 1, "signed": false
|
99
|
+
},
|
100
|
+
{
|
101
|
+
"key": "name", "type": "str", "size": 20, "signed": false
|
102
|
+
},
|
103
|
+
{
|
104
|
+
"key": "power", "type": "bin", "size": 2, "signed": false
|
105
|
+
},
|
106
|
+
{
|
107
|
+
"key": "description", "type": "str", "size": 50, "signed": false
|
108
|
+
},
|
109
|
+
{
|
110
|
+
"key": "speed", "type": "bin", "size": 1, "signed": true
|
111
|
+
}
|
112
|
+
]
|
113
|
+
```
|
114
|
+
If your Instructions are incorrectly formatted, never fear: ByteInterpreter
|
115
|
+
will raise a `ValidationError` and let you know what you did wrong. Also note:
|
116
|
+
it's very important that the objects in the array are in the same order as the
|
117
|
+
fields in the structure you're reading, as it will read the binary file in the
|
118
|
+
exact same order as specified in the JSON array. The validation methods won't
|
119
|
+
catch this, as ByteInterpreter has no idea what format your file is supposed to
|
120
|
+
be in.
|
121
|
+
|
122
|
+
Once you load these instructions, using them is very easy. We use the
|
123
|
+
`#interpret_from_instructions` method, which takes a block and passes the
|
124
|
+
key and interpreted value of each individual instruction to the given block.
|
125
|
+
```ruby
|
126
|
+
spell = {}
|
127
|
+
bi.load_instructions(type: :json, filename: "spell_instructions.json")
|
128
|
+
bi.interpret_from_instructions do |key, value|
|
129
|
+
spell[key] = value
|
130
|
+
end
|
131
|
+
|
132
|
+
spell.inspect
|
133
|
+
# => {:element => 1, :name => "Fireball ", etc.
|
134
|
+
```
|
135
|
+
_**Note:** You may have noticed in the examples, but ByteInterpreter will
|
136
|
+
preserve any whitespace given in strings. It makes no assumptions about what
|
137
|
+
you want to do with the interpreted string, so if you want to remove any
|
138
|
+
whitespace you will have to call `#chomp` yourself._
|
139
|
+
|
140
|
+
Reading the entire spell file would just involve wrapping your call to
|
141
|
+
`#interpret_from_instructions` inside a loop of some sort:
|
142
|
+
```ruby
|
143
|
+
spellbook = Array.new
|
144
|
+
30.times do
|
145
|
+
spell = {}
|
146
|
+
bi.interpret_from_instructions do |key, value|
|
147
|
+
spell[key] = value
|
148
|
+
end
|
149
|
+
spellbook.push(spell)
|
150
|
+
end
|
151
|
+
```
|
152
|
+
|
153
|
+
### Writing bytes
|
154
|
+
Writing bytes is almost as easy as reading them.
|
155
|
+
```ruby
|
156
|
+
f = File.new("NEW_SPELLS.BIN", "wb")
|
157
|
+
bi = ByteInterpreter.new(stream: f)
|
158
|
+
|
159
|
+
bi.encode_bytes(value: 1, size: 1, signed: false)
|
160
|
+
bi.encode_string(value: "Fireball", size: 20)
|
161
|
+
```
|
162
|
+
These methods work about how you expect them to. `#encode_bytes` makes no
|
163
|
+
attempt to ensure the value you're passing to it actually fits into the given
|
164
|
+
size in bytes, so you should probably do some checking before encoding.
|
165
|
+
Thankfully, `#encode_strings` is friendlier and will either pad or truncate
|
166
|
+
your string to fit the appropriate size.
|
167
|
+
|
168
|
+
And of course, encoding works with instructions as well.
|
169
|
+
```ruby
|
170
|
+
new_spell = {element: 1, name: "Fireball", etc.}
|
171
|
+
bi.encode_from_instructions(values: new_spell)
|
172
|
+
```
|
173
|
+
`#encode_from_instructions` takes only one argument, a `Hash` with keys
|
174
|
+
matching each field key in the Instructions file, and values paired with those
|
175
|
+
keys that match the type for that field in the Instructions file. It is
|
176
|
+
**very** important that your given `Hash` contains one key for each field;
|
177
|
+
otherwise `#encode_from_instructions` will attempt to read a nonexistant key.
|
178
|
+
|
179
|
+
If you'd like to write your own Instructions, see the
|
180
|
+
[instructions.rb file](./lib/byte_interpreter/instructions.rb), it has a fairly
|
181
|
+
in-depth explanation on doing so.
|
@@ -0,0 +1,110 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "json"
|
4
|
+
|
5
|
+
class ByteInterpreter
|
6
|
+
##
|
7
|
+
# The Instructions class represents a collection of ordered operations to
|
8
|
+
# perform on an IO stream. This class is used by ByteInterpreter to either
|
9
|
+
# interpret or encode bytes in a rigid, structured way.
|
10
|
+
#
|
11
|
+
# At the most basic level, Instructions are just an Array filled with Hashes,
|
12
|
+
# each Hash having exactly four keys -- :key, :type, :size, and :signed. Each
|
13
|
+
# key has requirements for its value:
|
14
|
+
# - :key -- Must be a value easily convertible to a Symbol object.
|
15
|
+
# - :type -- Must match one of the elements in the constant VALID_TYPES.
|
16
|
+
# - :size -- For binary types ("bin"), must match one of the elements in the
|
17
|
+
# constant VALID_BIN_SIZES. String types ("str") must be a
|
18
|
+
# positive Integer.
|
19
|
+
# - :signed -- For binary types ("bin"), must be the +true+ or +false+
|
20
|
+
# literals. String types ("str") ignore this value completely.
|
21
|
+
#
|
22
|
+
# Writing your own method for loading instructions is fairly simple. The
|
23
|
+
# method must call #add_field in the desired order of instruction execution,
|
24
|
+
# passing to it a Hash that conforms to the requirements above. The method
|
25
|
+
# must also be named +load_from_type+, where +type+ is what will be
|
26
|
+
# passed into ByteInterpreter#load_instructions. See #load_from_json for an
|
27
|
+
# example of this.
|
28
|
+
class Instructions
|
29
|
+
##
|
30
|
+
# Raised by instruction validation methods.
|
31
|
+
class ValidationError < StandardError
|
32
|
+
end
|
33
|
+
|
34
|
+
##
|
35
|
+
# Valid values for the :type key in the instructions Hash.
|
36
|
+
VALID_TYPES = %w[bin str].freeze
|
37
|
+
|
38
|
+
##
|
39
|
+
# Valid values for binary types for the :size key in the instructions Hash.
|
40
|
+
VALID_BIN_SIZES = [1, 2, 4, 8].freeze
|
41
|
+
|
42
|
+
##
|
43
|
+
# Keys that are in every properly-formatted instructions Hash.
|
44
|
+
FIELD_NAMES = %i[key type size signed].freeze
|
45
|
+
|
46
|
+
##
|
47
|
+
# Creates a blank Instructions object.
|
48
|
+
def initialize
|
49
|
+
@data = []
|
50
|
+
end
|
51
|
+
|
52
|
+
##
|
53
|
+
# Passes the given block to the internal Array's #each method.
|
54
|
+
def each(&block)
|
55
|
+
@data.each(&block)
|
56
|
+
end
|
57
|
+
|
58
|
+
##
|
59
|
+
# Clears all loaded instructions.
|
60
|
+
def clear
|
61
|
+
@data.clear
|
62
|
+
end
|
63
|
+
|
64
|
+
##
|
65
|
+
# Adds the given Hash to the end of the list of instructions, and validates
|
66
|
+
# it.
|
67
|
+
# @param field [Hash] A properly-formatted instructions Hash. See the
|
68
|
+
# documentation for this class on the appropriate format for this Hash.
|
69
|
+
# @return [void]
|
70
|
+
def add_field(field:)
|
71
|
+
@data.push(field.select { |k, _v| FIELD_NAMES.include? k })
|
72
|
+
validate_field(field: @data.last)
|
73
|
+
end
|
74
|
+
|
75
|
+
##
|
76
|
+
# Loads instructions from a JSON file. The JSON file should contain a
|
77
|
+
# top-level array, with each element being an object with the appropriate
|
78
|
+
# keys and values. Keys are automatically converted from strings to
|
79
|
+
# symbols, but boolean values are not converted from strings to literals.
|
80
|
+
# @param filename [String] The filename of the JSON file to load,
|
81
|
+
# **including** the filetype extension, if any.
|
82
|
+
# @return [void]
|
83
|
+
def load_from_json(filename:)
|
84
|
+
json_fields = JSON.parse(File.open(filename, "rt", &:read), symbolize_names: true)
|
85
|
+
json_fields.each do |field|
|
86
|
+
add_field(field: field)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
##
|
91
|
+
# Validates a given Hash to ensure it conforms to the instruction format.
|
92
|
+
# @param field [Hash] The Hash object to evaluate.
|
93
|
+
# @return [Boolean]
|
94
|
+
# @raise [ValidationError] if the Hash does not conform to the instruction
|
95
|
+
# format
|
96
|
+
def validate_field(field:)
|
97
|
+
unless VALID_TYPES.include? field[:type]
|
98
|
+
raise ValidationError, "Illegal type defined at key \"#{field[:key]}\": #{field[:type]}.
|
99
|
+
Valid types are #{VALID_TYPES}."
|
100
|
+
end
|
101
|
+
|
102
|
+
if (field[:type] == "bin") && !VALID_BIN_SIZES.include?(field[:size])
|
103
|
+
raise ValidationError, "Illegal size defined for binary field at key \"#{field[:key]}\": #{field[:size]}.
|
104
|
+
Valid sizes for binary values are #{VALID_BIN_SIZES}."
|
105
|
+
end
|
106
|
+
|
107
|
+
true
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,227 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "byteinterpreter/instructions.rb"
|
4
|
+
|
5
|
+
##
|
6
|
+
# The ByteInterpreter is a tool used to extract bytes and strings from a binary
|
7
|
+
# file, and also to encode bytes and strings into a binary file. It can also
|
8
|
+
# take a series of instructions to extract or encode data in an ordinal manner,
|
9
|
+
# suitable for writing binary files with rigid structure size requirements.
|
10
|
+
class ByteInterpreter
|
11
|
+
##
|
12
|
+
# Reads the endian mode being used by the interpreter.
|
13
|
+
attr_reader :endian_mode
|
14
|
+
|
15
|
+
##
|
16
|
+
# Creates and sets up a new ByteInterpreter.
|
17
|
+
# @param endian [:little, :big, nil] Default for this value is nil. The
|
18
|
+
# endian mode that will be used by the interpreter for reading/writing
|
19
|
+
# bytes. If nil is specified, the interpreter will assume machine-native
|
20
|
+
# endianness.
|
21
|
+
# @param stream [#read, #write] The IO stream, or IO-like object, that the
|
22
|
+
# interpreter will perform operations on. The interpreter will not open or
|
23
|
+
# close the stream for you, and assumes you have already changed the
|
24
|
+
# position to the appropriate offset for its operations. The interpreter
|
25
|
+
# also assumes you have opened the stream as binary (as opposed to text),
|
26
|
+
# and for the appropriate operations (read/write).
|
27
|
+
def initialize(endian: nil, stream:)
|
28
|
+
@endian_mode = endian
|
29
|
+
@instructions = nil
|
30
|
+
@iostream = stream
|
31
|
+
end
|
32
|
+
|
33
|
+
##
|
34
|
+
# Changes the stream being used by the interpreter for operations.
|
35
|
+
# @param new_stream [#read, #write] The IO stream, or IO-like object, that
|
36
|
+
# the interpreter will perform operations on. See #new for what is expected
|
37
|
+
# of this stream.
|
38
|
+
# @return [void]
|
39
|
+
def iostream=(new_stream)
|
40
|
+
raise ArgumentError "Object given is not stream-like." unless stream_like?(obj: new_stream)
|
41
|
+
@iostream = new_stream
|
42
|
+
end
|
43
|
+
|
44
|
+
##
|
45
|
+
# Reads a set number of bytes, interprets them into an integer, and returns
|
46
|
+
# the result.
|
47
|
+
# @param size [1, 2, 4, 8] The number of bytes to read from the stream.
|
48
|
+
# ByteInterpreter can only interpret 8-, 16-, 32-, and 64-bit values at
|
49
|
+
# this time, so this parameter is limited to just a few numbers.
|
50
|
+
# @param signed [Boolean] Default for this value is false. Set this to
|
51
|
+
# true if the bytes being read can be negative or positive.
|
52
|
+
# @return [Integer] the interpreted byte
|
53
|
+
def interpret_bytes(size: 2, signed: false)
|
54
|
+
bytes = @iostream.read(size)
|
55
|
+
directive = build_directive(size: size, signed: signed)
|
56
|
+
|
57
|
+
bytes.unpack(directive).first
|
58
|
+
end
|
59
|
+
|
60
|
+
##
|
61
|
+
# Reads a set number of bytes, interprets them into a string, and returns the
|
62
|
+
# result.
|
63
|
+
# @param size [Integer] The number of bytes to read from the stream. Unlike
|
64
|
+
# #interpret_bytes, this size can be any positive integer.
|
65
|
+
# @return [String] the interpreted string
|
66
|
+
def interpret_string(size:)
|
67
|
+
@iostream.read(size)
|
68
|
+
end
|
69
|
+
|
70
|
+
##
|
71
|
+
# Writes a set number of bytes, encoded from the given value.
|
72
|
+
# @param value [Integer] The value to encode and write.
|
73
|
+
# @param size [1,2,4,8] The size of the value in bytes.
|
74
|
+
# @param signed [Boolean] Set this to true if the bytes being written can be
|
75
|
+
# negative and positive, false otherwise.
|
76
|
+
# @return [void]
|
77
|
+
# @note The interpreter makes no attempt to ensure that your +value+ fits
|
78
|
+
# into +size+ bytes. To avoid unintended behavior, you should validate your
|
79
|
+
# input into this method.
|
80
|
+
def encode_bytes(value:, size:, signed:)
|
81
|
+
value = Array(value) unless value.respond_to? :pack
|
82
|
+
@iostream.write(value.pack(build_directive(size: size, signed: signed)))
|
83
|
+
end
|
84
|
+
|
85
|
+
##
|
86
|
+
# Writes a string into a given number of bytes.
|
87
|
+
# @param value [String] The value to write to the stream.
|
88
|
+
# @param size [Integer] The size of the value in bytes. Unlike
|
89
|
+
# #encode_bytes, this size can be any positive integer.
|
90
|
+
# @return [void]
|
91
|
+
# @note If +value+ is smaller than +size+, the interpreter will pad +value+
|
92
|
+
# with 0x20 to fill the remaining space. Even so, care should be taken to
|
93
|
+
# validate your input to this method, especially if you want to handle
|
94
|
+
# strings that are larger than +size+, or want to handle size differences
|
95
|
+
# differently than this method.
|
96
|
+
def encode_string(value:, size:)
|
97
|
+
@iostream.write(value.slice(0, size).ljust(size, "\x20"))
|
98
|
+
end
|
99
|
+
|
100
|
+
##
|
101
|
+
# Loads instructions from a file for structured, ordinal operations.
|
102
|
+
# @param type [Symbol] The type of the file that holds the instructions.
|
103
|
+
# This argument **must** have a corresponding method in the
|
104
|
+
# ByteInterpreter::Instructions class, named +load_from_type+, replacing
|
105
|
+
# +type+ with the actual name of the filetype.
|
106
|
+
# @param filename [String] The filename of the instructions to load.
|
107
|
+
# @return [void]
|
108
|
+
# @note ByteInterpreter comes with only one +type+ built-in: JSON.
|
109
|
+
def load_instructions(type:, filename:)
|
110
|
+
@instructions = Instructions.new if @instructions.nil?
|
111
|
+
@instructions.clear
|
112
|
+
@instructions.send("load_from_" + type.to_s, filename: filename)
|
113
|
+
end
|
114
|
+
|
115
|
+
##
|
116
|
+
# Uses the loaded instructions (you did call #load_instructions first,
|
117
|
+
# right?) to interpret bytes and strings from the stream, passing them as
|
118
|
+
# arguments to the given block.
|
119
|
+
# @yieldparam key [Symbol] The key of the interpreted data. Typically used to
|
120
|
+
# set variables in the calling object.
|
121
|
+
# @yieldparam value [Integer, String] The value of the interpreted data.
|
122
|
+
# @return [Integer] the combined size, in bytes, of the operation
|
123
|
+
def interpret_from_instructions
|
124
|
+
struct_size = 0
|
125
|
+
@instructions.each do |field|
|
126
|
+
if field[:type] == "bin"
|
127
|
+
value = interpret_bytes(size: field[:size], signed: field[:signed])
|
128
|
+
elsif field[:type] == "str"
|
129
|
+
value = interpret_string(size: field[:size])
|
130
|
+
end
|
131
|
+
|
132
|
+
struct_size += field[:size]
|
133
|
+
|
134
|
+
yield field[:key], value
|
135
|
+
end
|
136
|
+
|
137
|
+
struct_size
|
138
|
+
end
|
139
|
+
|
140
|
+
##
|
141
|
+
# Uses the loaded instructions (you did call #load_instructions first,
|
142
|
+
# right?) to encode the given values into bytes and strings, and write them
|
143
|
+
# to the stream.
|
144
|
+
#
|
145
|
+
# This method encodes and writes bytes in the order of the loaded
|
146
|
+
# instructions; this means it will seek each key from the given Hash, instead
|
147
|
+
# of seeking around the file and writing in whatever order the Hash may be
|
148
|
+
# in.
|
149
|
+
# @param values [Hash] The values to read and encode. This Hash **must**
|
150
|
+
# have keys that match *all* keys from the loaded instructions.
|
151
|
+
# @return [Integer] the combined size, in bytes, of the operation
|
152
|
+
def encode_from_instructions(values:)
|
153
|
+
struct_size = 0
|
154
|
+
@instructions.each do |field|
|
155
|
+
key = field[:key].to_sym
|
156
|
+
|
157
|
+
if field[:type] == "bin"
|
158
|
+
encode_bytes(value: values[key], size: field[:size], signed: field[:signed])
|
159
|
+
elsif field[:type] == "str"
|
160
|
+
encode_string(value: values[key], size: field[:size])
|
161
|
+
end
|
162
|
+
|
163
|
+
struct_size += field[:size]
|
164
|
+
end
|
165
|
+
|
166
|
+
struct_size
|
167
|
+
end
|
168
|
+
|
169
|
+
private
|
170
|
+
|
171
|
+
##
|
172
|
+
# This constant maps byte lengths to their respective Strings for the
|
173
|
+
# directives in Array#pack and String#unpack.
|
174
|
+
DIRECTIVE_SIZES = { 1 => "C", 2 => "S", 4 => "L", 8 => "Q" }.freeze
|
175
|
+
|
176
|
+
##
|
177
|
+
# Uses DIRECTIVE_SIZES to translate a byte length to a usable String.
|
178
|
+
# @param size [1,2,4,8] The byte length to translate.
|
179
|
+
# @raise [ArgumentError] if +size+ is not 1, 2, 4, or 8.
|
180
|
+
# @return [String] the translated directive String.
|
181
|
+
def determine_directive_letter(size:)
|
182
|
+
raise ArgumentError "Invalid size argument (#{size})." unless DIRECTIVE_SIZES.key?(size)
|
183
|
+
DIRECTIVE_SIZES[size].dup
|
184
|
+
end
|
185
|
+
|
186
|
+
##
|
187
|
+
# Returns the glyph for the set endianness, for use in building the directive
|
188
|
+
# String.
|
189
|
+
# @return [String] if endian_mode is non-nil
|
190
|
+
# @return [nil] if endian_mode is nil
|
191
|
+
def determine_endian_glyph
|
192
|
+
case endian_mode
|
193
|
+
when :little
|
194
|
+
"<"
|
195
|
+
when :big
|
196
|
+
">"
|
197
|
+
else
|
198
|
+
""
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
##
|
203
|
+
# Builds a directive String, fit for use in Array#pack and String#unpack.
|
204
|
+
# @param size [1,2,4,8] The size to translate into a directive String.
|
205
|
+
# @param signed [Boolean] Set this to true if the bytes being written can be
|
206
|
+
# negative and positive, false otherwise.
|
207
|
+
# @return [String] the built directive String
|
208
|
+
def build_directive(size:, signed:)
|
209
|
+
directive = determine_directive_letter(size: size)
|
210
|
+
directive.downcase! if signed
|
211
|
+
|
212
|
+
directive += determine_endian_glyph if "SsLlQqJjIi".include?(directive)
|
213
|
+
|
214
|
+
directive
|
215
|
+
end
|
216
|
+
|
217
|
+
##
|
218
|
+
# Checks if the given object is stream-like -- that is, responds to #read
|
219
|
+
# and #write.
|
220
|
+
# @param obj [Object] The object to test.
|
221
|
+
# @return [Boolean]
|
222
|
+
# @note For fun, consider making an inverse of this method named
|
223
|
+
# "illiterate?"
|
224
|
+
def stream_like?(obj:)
|
225
|
+
obj.respond_to?(:read) && obj.respond_to?(:write)
|
226
|
+
end
|
227
|
+
end
|
metadata
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: byteinterpreter
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Michael K Gremillion
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2018-09-13 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: |
|
14
|
+
The ByteInterpreter is a tool to interpret bytes from and encode bytes to
|
15
|
+
binary files. It can either do this piecemeal, or via a set of rigid,
|
16
|
+
ordered instructions that define a data structure.
|
17
|
+
email:
|
18
|
+
executables: []
|
19
|
+
extensions: []
|
20
|
+
extra_rdoc_files: []
|
21
|
+
files:
|
22
|
+
- LICENSE.md
|
23
|
+
- README.md
|
24
|
+
- lib/byteinterpreter.rb
|
25
|
+
- lib/byteinterpreter/instructions.rb
|
26
|
+
homepage: https://github.com/mkgremillion/ByteInterpreter
|
27
|
+
licenses:
|
28
|
+
- MIT
|
29
|
+
metadata:
|
30
|
+
source_code_uri: https://github.com/mkgremillion/ByteInterpreter
|
31
|
+
post_install_message:
|
32
|
+
rdoc_options: []
|
33
|
+
require_paths:
|
34
|
+
- lib
|
35
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - ">="
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '0'
|
40
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
41
|
+
requirements:
|
42
|
+
- - ">="
|
43
|
+
- !ruby/object:Gem::Version
|
44
|
+
version: '0'
|
45
|
+
requirements: []
|
46
|
+
rubyforge_project:
|
47
|
+
rubygems_version: 2.7.6
|
48
|
+
signing_key:
|
49
|
+
specification_version: 4
|
50
|
+
summary: A tool to interpret bytes from and encode bytes to binary files.
|
51
|
+
test_files: []
|