bytepack 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (34) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +3 -0
  3. data/LICENSE +20 -0
  4. data/README.md +344 -0
  5. data/lib/bytepack.rb +75 -0
  6. data/lib/bytepack/any_type.rb +21 -0
  7. data/lib/bytepack/basic.rb +43 -0
  8. data/lib/bytepack/basic/fixed_size.rb +28 -0
  9. data/lib/bytepack/basic/fixed_size/decimal.rb +48 -0
  10. data/lib/bytepack/basic/fixed_size/float.rb +7 -0
  11. data/lib/bytepack/basic/fixed_size/integer_type.rb +18 -0
  12. data/lib/bytepack/basic/fixed_size/integer_type/byte.rb +7 -0
  13. data/lib/bytepack/basic/fixed_size/integer_type/integer.rb +7 -0
  14. data/lib/bytepack/basic/fixed_size/integer_type/long.rb +7 -0
  15. data/lib/bytepack/basic/fixed_size/integer_type/short.rb +7 -0
  16. data/lib/bytepack/basic/fixed_size/integer_type/u_byte.rb +7 -0
  17. data/lib/bytepack/basic/fixed_size/integer_type/u_integer.rb +7 -0
  18. data/lib/bytepack/basic/fixed_size/integer_type/u_long.rb +7 -0
  19. data/lib/bytepack/basic/fixed_size/integer_type/u_short.rb +7 -0
  20. data/lib/bytepack/basic/fixed_size/null.rb +11 -0
  21. data/lib/bytepack/basic/fixed_size/timestamp.rb +22 -0
  22. data/lib/bytepack/basic/string.rb +17 -0
  23. data/lib/bytepack/basic/symbol.rb +13 -0
  24. data/lib/bytepack/basic/varbinary.rb +34 -0
  25. data/lib/bytepack/complex.rb +9 -0
  26. data/lib/bytepack/complex/array.rb +22 -0
  27. data/lib/bytepack/complex/hash.rb +17 -0
  28. data/lib/bytepack/compound.rb +8 -0
  29. data/lib/bytepack/compound/single_type_array.rb +41 -0
  30. data/lib/bytepack/custom_data.rb +37 -0
  31. data/lib/bytepack/extensions.rb +18 -0
  32. data/lib/bytepack/type_info.rb +34 -0
  33. data/lib/bytepack/version.rb +3 -0
  34. metadata +89 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 5638d36e9962d27928e960c35999f7f7bea8d7b76b9cbe73258be0051c21b782
4
+ data.tar.gz: 2c03181ab1a272ac8caf74f1866673759948744dc3c29147ff4b5c426f145f1f
5
+ SHA512:
6
+ metadata.gz: 34bd43f7e60a131d50b3e883acfdd1d7dab5c931b8ee1d1723dba21848e86d61e11006126fa1cf54fed3c856aeeae45c48e0920cda12c54868c8d9ef61bacca0
7
+ data.tar.gz: 03055a552a8240b5a837db21e55db3aa0249293ad08b4dc48a3545288ed194cf0c42206b3a03a2088f9003482a3e70454885ba33449b90482fd875ec6fe9eb28
data/CHANGELOG.md ADDED
@@ -0,0 +1,3 @@
1
+ ## [0.0.1] - 2019-05-16
2
+ ### Initial release
3
+ - See docs and just try it!
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ MIT License
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,344 @@
1
+ # Bytepack
2
+
3
+ ### Tool for byte-serialization of various Ruby data structures
4
+
5
+ #### Packing & unpacking various Ruby data to/from a byte string, incl. arrays, hashes and custom data structures
6
+
7
+ ## Compatibility
8
+
9
+ Compatible with Ruby **MRI 2.4**> & **JRuby 9.2**>
10
+
11
+ ## Installation
12
+
13
+ $ gem install bytepack
14
+
15
+ ## Basic usage
16
+
17
+ Require Gem
18
+
19
+ require 'bytepack'
20
+
21
+ Packing specific datatype returns a byteset:
22
+
23
+ Bytepack::String.pack("test") # One argument as source value
24
+ => "\x03\x04test"
25
+
26
+ Unpacking specific datatype returns an Array consists of Ruby object and resulted bytes offset as integer:
27
+
28
+ bytes = Bytepack::String.pack("test")
29
+ => "\x03\x04test"
30
+ Bytepack::String.unpack(bytes) # Two arguments: byteset & offset as integer (optional)
31
+ => ["test", 6]
32
+
33
+ Testing unpacking authenticity *(pack & unpack)*:
34
+
35
+ Bytepack::String.testpacking("test")
36
+ => ["test", 6]
37
+
38
+ ## Ruby Standard Library basic datatypes
39
+
40
+ ### Byte Integer
41
+
42
+ 8-bit Integer in range [-127..127]
43
+
44
+ Bytepack::Byte.pack(34)
45
+ => "\""
46
+
47
+ Bytepack::Byte.unpack("\"".b)
48
+ => [34, 1]
49
+
50
+ 8-bit Unsigned Integer in range [1..127]
51
+
52
+ Bytepack::UByte.pack(34)
53
+ => "\""
54
+
55
+ Bytepack::UByte.unpack("\"".b)
56
+ => [34, 1]
57
+
58
+ ### Short Integer
59
+
60
+ 16-bit Integer in range [-32767..32767]
61
+
62
+ Bytepack::Short.pack(23423)
63
+ => "[\x7F"
64
+
65
+ Bytepack::Short.unpack("[\x7F".b)
66
+ => [23423, 2]
67
+
68
+ 16-bit Unsigned Integer in range [1..32767]
69
+
70
+ Bytepack::UShort.pack(23423)
71
+ => "[\x7F"
72
+
73
+ Bytepack::UShort.unpack("[\x7F".b)
74
+ => [23423, 2]
75
+
76
+ ### Integer
77
+
78
+ 32-bit Integer in range [-2147483647..2147483647]
79
+
80
+ Bytepack::Integer.pack(12323423)
81
+ => "\x00\xBC\n_"
82
+
83
+ Bytepack::Integer.unpack("\x00\xBC\n_".b)
84
+ => [12323423, 4]
85
+
86
+ 32-bit Unsigned Integer in range [1..2147483647]
87
+
88
+ Bytepack::UInteger.pack(12323423)
89
+ => "\x00\xBC\n_"
90
+
91
+ Bytepack::UInteger.unpack("\x00\xBC\n_".b)
92
+ => [12323423, 4]
93
+
94
+ ### Long Integer
95
+
96
+ 64-bit Integer in range [-9223372036854775807..9223372036854775807]
97
+
98
+ Bytepack::Long.pack(98712323423)
99
+ => "\x00\x00\x00\x16\xFB\xB6\x85_"
100
+
101
+ Bytepack::Long.unpack("\x00\x00\x00\x16\xFB\xB6\x85_".b)
102
+ => [98712323423, 8]
103
+
104
+ 64-bit Unsigned Integer in range [1..9223372036854775807]
105
+
106
+ Bytepack::ULong.pack(98712323423)
107
+ => "\x00\x00\x00\x16\xFB\xB6\x85_"
108
+
109
+ Bytepack::ULong.unpack("\x00\x00\x00\x16\xFB\xB6\x85_".b)
110
+ => [98712323423, 8]
111
+
112
+ ### Various length Integer
113
+
114
+ 128-bit Long Long signed Integer
115
+
116
+ Bytepack::Basic.intToBytes(16, 2345980343453498712323423) # Two arguments: bytesize & value
117
+ => "\x00\x00\x00\x00\x00\x01\xF0\xC7\xD9hbd>\f\xF5_"
118
+
119
+ Bytepack::Basic.bytesToInt(16, "\x00\x00\x00\x00\x00\x01\xF0\xC7\xD9hbd>\f\xF5_".b) # Three arguments: bytesize, byteset, offset as integer (optional)
120
+ => [2345980343453498712323423, 8]
121
+
122
+ ### The shortest length Integer
123
+
124
+ Use universal **Bytepack::AnyType** class for that
125
+
126
+ Bytepack::AnyType.pack(8934)
127
+ => "\x04\"\xE6" # Packed as 1 meta-byte & 16-bit short Integer (total 3 bytes)
128
+
129
+ Bytepack::AnyType.unpack("\x04\"\xE6".b)
130
+ => [8934, 3]
131
+
132
+ ### Float
133
+
134
+ Bytepack::Float.pack(3.1415926)
135
+ => "@\t!\xFBM\x12\xD8J"
136
+
137
+ Bytepack::Float.unpack("@\t!\xFBM\x12\xD8J".b)
138
+ => [3.1415926, 8]
139
+
140
+ ### BigDecimal
141
+
142
+ value = BigDecimal('3.1415926')
143
+ => 0.31415926e1
144
+
145
+ Bytepack::Decimal.pack(value)
146
+ => "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\xDBu\x82\xCD\xC0"
147
+
148
+ Bytepack::Decimal.unpack("\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\xDBu\x82\xCD\xC0".b)
149
+ => [0.31415926e1, 16]
150
+
151
+ ### NilClass
152
+
153
+ Bytepack::Null.pack(nil)
154
+ => "\x80"
155
+
156
+ Bytepack::Null.unpack("\x80".b)
157
+ => [nil, 1]
158
+
159
+ ### String ASCII-8BIT encoded (Binary data)
160
+
161
+ Pack ASCII-8BIT encoded string:
162
+
163
+ value = "\x04v\x1A\xE8wev\xD6".b
164
+
165
+ Bytepack::Varbinary.pack(value)
166
+ => "\x03\b\x04v\x1A\xE8wev\xD6" # includes 1 meta-byte, 1-8 bytes of value's length integer and a byteset of value
167
+
168
+ Bytepack::Varbinary.unpack("\x03\b\x04v\x1A\xE8wev\xD6".b)
169
+ => ["\x04v\x1A\xE8wev\xD6", 10]
170
+
171
+ By default, value's length serialized by *Byteset::AnyType* as a shortest integer possible (Byte, Short, Int or Long) and allways 2 bytes or more. You can override length datatype globally and make it static:
172
+
173
+ Bytepack::Varbinary.config(:LENGTH_TYPE, Bytepack::Integer)
174
+ value = "\x04v\x1A\xE8wev\xD6".b
175
+ Bytepack::Varbinary.pack(value)
176
+ => "\x00\x00\x00\b\x04v\x1A\xE8wev\xD6" # includes 1 meta-byte, 4 bytes of value's length and a byteset of value
177
+
178
+ Byteset size now is *2 bytes more*, but allways **4 bytes (32-bit)**. That's useful in cases where byteset length make sense in the current project.
179
+
180
+ ### String UTF-8 encoded (Regular string)
181
+
182
+ Pack UTF-8 encoded string:
183
+
184
+ Bytepack::String.pack("Words like violence")
185
+ => "\x03\x13Words like violence" # includes 1 meta-byte, 1-8 bytes of value's length integer and a byteset of value
186
+
187
+ Bytepack::String.unpack("\x03\x13Words like violence".b)
188
+ => ["Words like violence", 21]
189
+
190
+ By default, value's length serialized the same way as *Bytepack::Varbinary*, but you can specify length datatype and make it static:
191
+
192
+ Bytepack::String.config(:LENGTH_TYPE, Bytepack::Integer)
193
+ Bytepack::String.pack("Words like violence")
194
+ => "\x00\x00\x00\x13Words like violence" # includes 1 meta-byte, 4 bytes of value's length and a byteset of value
195
+
196
+ ### Symbol
197
+
198
+ Works almost the same way as String serialization do:
199
+
200
+ Bytepack::Symbol.pack(:key_this_value)
201
+ => "\x03\x0Ekey_this_value" # includes 1 meta-byte, 1-8 bytes of value's length integer and a byteset of value
202
+
203
+ Bytepack::Symbol.unpack("\x03\x0Ekey_this_value".b)
204
+ => [:key_this_value, 18]
205
+
206
+ By default, value's length serialized the same way as *Bytepack::Varbinary*, but you can specify length datatype and make it static:
207
+
208
+ Bytepack::Symbol.config(:LENGTH_TYPE, Bytepack::Integer)
209
+ Bytepack::Symbol.pack(:key_this_value)
210
+ => "\x00\x00\x00\x0Ekey_this_value" # includes 1 meta-byte, 4 bytes of value's length and a byteset of value
211
+
212
+ ### Time
213
+
214
+ All objects are represented as *Bytepack::Long* values (64-bit). The signed integer represents the number of microseconds before or after Unix epoch (Jan. 1 1970 00:00:00 GMT).
215
+
216
+ Bytepack::Timestamp.pack(Time.now)
217
+ => "\x00\x05\x89\x02H\xF8\xDA\x9B"
218
+
219
+ Bytepack::Timestamp.unpack("\x00\x05\x89\x02H\xF8\xDA\x9B".b)
220
+ => [2019-05-16 17:43:10 +0300, 8]
221
+
222
+ ## Arrays and hashes
223
+
224
+ Arrays can be serialized in two modes:
225
+
226
+ ### Single Type Array
227
+
228
+ Arrays consists of elements belongs to one datatype:
229
+
230
+ array = [1,2,3,4,5,6,4,3,2,123,3223,-23,0,12,89,100] # All elements are integers
231
+
232
+ You can pass specific datatype as the first array's element:
233
+
234
+ byteset = Bytepack::SingleTypeArray.pack([Bytepack::Short, *array])
235
+ => "\x04\x03\x10\x00\x01\x00\x02\x00\x03\x00\x04\x00\x05\x00\x06\x00\x04\x00\x03\x00\x02\x00{\f\x97\xFF\xE9\x00\x00\x00\f\x00Y\x00d"
236
+
237
+ Bytepack::SingleTypeArray.unpack(byteset)
238
+ => [[1, 2, 3, 4, 5, 6, 4, 3, 2, 123, 3223, -23, 0, 12, 89, 100], 35]
239
+
240
+ Or you wouldn't do it, it recognizes automatically by the longest integer:
241
+
242
+ byteset = Bytepack::SingleTypeArray.pack(array)
243
+ => "\x04\x03\x10\x00\x01\x00\x02\x00\x03\x00\x04\x00\x05\x00\x06\x00\x04\x00\x03\x00\x02\x00{\f\x97\xFF\xE9\x00\x00\x00\f\x00Y\x00d"
244
+
245
+ Bytepack::SingleTypeArray.unpack(byteset)
246
+ => [[1, 2, 3, 4, 5, 6, 4, 3, 2, 123, 3223, -23, 0, 12, 89, 100], 35]
247
+
248
+ By default, array's size serialized by *Byteset::AnyType* as a shortest integer possible (Byte, Short, Int or Long), you can override length datatype globally and make it static:
249
+
250
+ Bytepack::SingleTypeArray.config(:LENGTH_TYPE, Bytepack::Byte)
251
+ byteset = Bytepack::SingleTypeArray.pack(array)
252
+ Bytepack::SingleTypeArray.unpack(byteset)
253
+ => [[1, 2, 3, 4, 5, 6, 4, 3, 2, 123, 3223, -23, 0, 12, 89, 100], 34]
254
+
255
+ Byteset size now is **34** instead of **35**. Why, because in previous example length packed as *2-bytes Byteset::AnyType, including 1 meta-byte and 1 byte integer itself*. Setting explicitly the length type as *Byteset::Byte*, it just 1 byte. That's useful in cases where byteset length make sense in the current project.
256
+
257
+ ### Various Type Array (Regular array)
258
+
259
+ Arrays consists of elements belongs to different datatypes:
260
+
261
+ array = [1,2,"3",4,:"five",6,[7,8,9],10,123,3223,-23,0,12,89,100] # Chaos array
262
+
263
+ byteset = Bytepack::Array.pack(array)
264
+ => "\x03\x0F\x03\x01\x03\x02\t\x03\x013\x03\x04\n\x03\x04five\x03\x06\x9E\x03\x03\a\b\t\x03\n\x03{\x04\f\x97\x03\xE9\x03\x00\x03\f\x03Y\x03d"
265
+
266
+ Bytepack::Array.unpack(byteset)
267
+ => [[1, 2, "3", 4, :five, 6, [7, 8, 9], 10, 123, 3223, -23, 0, 12, 89, 100], 44]
268
+
269
+ By default, array's size serialized by *Byteset::AnyType* as a shortest integer possible (Byte, Short, Int or Long), you can override length datatype globally and make it static:
270
+
271
+ Bytepack::Array.config(:LENGTH_TYPE, Bytepack::Byte)
272
+ byteset = Bytepack::Array.pack(array)
273
+ Bytepack::Array.unpack(byteset)
274
+ => [[1, 2, "3", 4, :five, 6, [7, 8, 9], 10, 123, 3223, -23, 0, 12, 89, 100], 43]
275
+
276
+ Byteset size now is **43** instead of **44**.
277
+
278
+ ### Hash
279
+
280
+ Technically Hash serialized as two arrays: keys and values. Serialization uses length types of current *Array and SingleTypeArray* settings. Let's say we have mashed hash.
281
+
282
+ hash = {:key1 => 1, :key2 => "2", "key3" => "key3", :key4 => 4, :key5 => :key5, :array => [1,2,3,"text",:sym, {:nil => nil, :foo => "bar"}]}
283
+ byteset = Bytepack::Hash.pack(hash)
284
+ => "\x9D\x06\n\x03\x04key1\n\x03\x04key2\t\x03\x04key3\n\x03\x04key4\n\x03\x04key5\n\x03\x05array\x9D\x06\x03\x01\t\x03\x012\t\x03\x04key3\x03\x04\n\x03\x04key5\x9D\x06\x03\x01\x03\x02\x03\x03\t\x03\x04text\n\x03\x03sym\xA7\x9E\n\x02\x03\x03nil\x03\x03foo\x9D\x02\x01\x80\t\x03\x03bar"
285
+
286
+ Hash serialized into 114 bytes. Not, recover it:
287
+
288
+ Bytepack::Hash.unpack(byteset)
289
+ => [{:key1=>1, :key2=>"2", "key3"=>"key3", :key4=>4, :key5=>:key5, :array=>[1, 2, 3, "text", :sym, {:nil=>nil, :foo=>"bar"}]}, 114]
290
+
291
+ ## Custom datatypes
292
+
293
+ Of course, not all available data structures are implemented out of the box. You can serialize any type of data and do it in a shorter way than *Marshal* does. For these purposes, use **Bytepack::CustomData**.
294
+
295
+ 1) Create class inherited from **Bytepack::CustomData** class.
296
+ 2) Class must include constant **TYPE_CODE** as a Byte integer [-127..127]. Value must be unique and not in the list of *Bytepack::TypeInfo.codes.keys* (reserved by Gem itselt)
297
+ 3) Class must include constant **RUBY_TYPE** as a class in available namespace.
298
+ 4) Like all OOB structures class must include the class method **pack()** which accepts one required argument as input value. Method returns the byteset as a result of serialization.
299
+ 5) Like all OOB structures class must include the class method **unpack()** which accepts one required and one optional arguments:
300
+ - byteset as a *String* object;
301
+ - offset as an *Integer* object (optional, default=0).
302
+ The **unpack()** method returns two element array, where the first element is deserialized Ruby object and the second one is resulted offset. The return offset must be correct! For what every Gem's structures returns the same dataset when unpacking.
303
+
304
+ Example of serialization of **ActiveSupport::Duration** objects:
305
+
306
+ class DurationBytePack < Bytepack::CustomData
307
+ TYPE_CODE = 26
308
+ RUBY_TYPE = ActiveSupport::Duration
309
+
310
+ DIRECTIVE = 'cl>'
311
+ DURATION_PARTS = [:years, :months, :weeks, :days, :hours, :minutes, :seconds] # see ActiveSupport::Duration
312
+
313
+ class << self
314
+ def pack(val)
315
+ parts = val.parts
316
+ format = DIRECTIVE * parts.size
317
+ Bytepack::Byte.pack(parts.size) + parts.map {|part| [DURATION_PARTS.find_index(part[0]), part[1]]}.flatten.pack(format)
318
+ end
319
+
320
+ def unpack(bytes, offset = 0)
321
+ length, offset = *Bytepack::Byte.unpack(bytes, offset)
322
+ unpacked = bytes.unpack("@#{offset}#{"cl>"*length}").each_slice(2).sum do |idx, value|
323
+ offset += 5
324
+ value.send(DURATION_PARTS[idx])
325
+ end
326
+ [unpacked, offset]
327
+ end
328
+
329
+ end
330
+ end
331
+
332
+ Try it now in the Rails console:
333
+
334
+ DurationBytePack.pack(3.days)
335
+ => "\x01\x03\x00\x00\x00\x03"
336
+
337
+ DurationBytePack.unpack("\x01\x03\x00\x00\x00\x03".b)
338
+ => [3 days, 6]
339
+
340
+ Bytepack::Array.pack([1,2,3,4,5.days])
341
+ => "\x00\x05\x03\x01\x03\x02\x03\x03\x03\x04\x1A\x01\x03\x00\x00\x00\x05"
342
+
343
+ Bytepack::Array.unpack("\x00\x05\x03\x01\x03\x02\x03\x03\x03\x04\x1A\x01\x03\x00\x00\x00\x05".b)
344
+ => [[1, 2, 3, 4, 5 days], 17]
data/lib/bytepack.rb ADDED
@@ -0,0 +1,75 @@
1
+ module Bytepack
2
+
3
+ class Struct
4
+ class << self
5
+ def config(const, value)
6
+ begin
7
+ remove_const(const) if const_defined?(const)
8
+ rescue NameError
9
+ end
10
+ const_set(const, value)
11
+ end
12
+
13
+ def packingDataType(val) # Ruby data type conversion
14
+ case val
15
+ when ::Array then single_type_array?(val) ? SingleTypeArray : Array
16
+ when ::Hash then Hash
17
+ when ::NilClass then Null # Byte::NULL_INDICATOR
18
+ when ::Integer then
19
+ case val.bit_length
20
+ when (0..7) then Byte
21
+ when (8..15) then Short
22
+ when (16..31) then Integer
23
+ else
24
+ Long if val.bit_length >= 32
25
+ end
26
+ when ::Float then Float
27
+ when ::String then
28
+ val.encoding.name == "UTF-8" ? String : Varbinary # See "sometext".encoding
29
+ when ::Symbol then Symbol
30
+ when ::Time then Timestamp
31
+ when ::BigDecimal then Decimal
32
+ else # CustomData
33
+ CustomData.struct_by_ruby_type(val)
34
+ end
35
+ end
36
+
37
+ def single_type_array?(array)
38
+ first_type = array[0].class
39
+ begin
40
+ array.each {|e| raise(Exception) unless e.is_a?(first_type)}
41
+ rescue Exception
42
+ false
43
+ else
44
+ true
45
+ end
46
+ end
47
+
48
+ def classifyDataType(val)
49
+ case val
50
+ when Class then val if val < Struct
51
+ when ::String, ::Symbol then
52
+ begin
53
+ Bytepack.const_get("#{val}")
54
+ rescue
55
+ nil
56
+ end
57
+ end
58
+ end
59
+
60
+ def testpacking(val)
61
+ unpack(pack(val))
62
+ end
63
+
64
+ end
65
+ end
66
+
67
+ end
68
+
69
+ require 'bytepack/any_type'
70
+ require 'bytepack/basic'
71
+ require 'bytepack/complex'
72
+ require 'bytepack/compound'
73
+ require 'bytepack/custom_data'
74
+ require 'bytepack/extensions'
75
+ require 'bytepack/type_info'
@@ -0,0 +1,21 @@
1
+ module Bytepack
2
+ class AnyType < Struct
3
+
4
+ class << self
5
+ def pack(val)
6
+ dataType = packingDataType(val)
7
+ TypeInfo.pack(dataType) + dataType.pack(val)
8
+ end
9
+
10
+ def unpack(bytes, offset = 0)
11
+ dataType, offset = *TypeInfo.unpack(bytes, offset)
12
+ if dataType.nil?
13
+ [nil, offset]
14
+ else
15
+ dataType.unpack(bytes, offset)
16
+ end
17
+ end
18
+ end
19
+
20
+ end
21
+ end
@@ -0,0 +1,43 @@
1
+ module Bytepack
2
+ class Basic < Struct
3
+
4
+ class << self
5
+ def preprocess(bytes, offset, format, length = 0)
6
+ format += length.to_s if !(self <= FixedSize) && length > 1
7
+ format = offset > 0 ? "@#{offset}#{format}" : format
8
+ offset += length
9
+ [offset, format]
10
+ end
11
+
12
+ def intToBytes(length, value)
13
+ bitlength = length*8
14
+ div = nil
15
+ x = [[32, 'N'], [16, 'n'], [8, 'C']].find {|a| (div = bitlength.divmod(a[0])) && div[0] != 0 && div[1] == 0}
16
+ b = (2**x[0])-1
17
+ ([value & b] + (2..div[0]).map {|i| (value >> x[0]*(i-1)) & b}).reverse.pack(x[1]*div[0])
18
+ end
19
+
20
+ def bytesToInt(length, bytes, offset = 0) # bytes = array of 8-bit unsigned
21
+ format = "C#{length}"
22
+ format.prepend("@#{offset}") if offset > 0
23
+ bytes = bytes.unpack(format)
24
+ most_significant_bit = 1 << 7
25
+ negative = (bytes[0] & most_significant_bit) != 0
26
+ unscaled_value = -(bytes[0] & most_significant_bit) << length*8-8
27
+ # Clear the highest bit
28
+ # Unleash the powers of the butterfly
29
+ bytes[0] &= ~most_significant_bit
30
+ # Get the 2's complement
31
+ (0..length-1).each {|i| unscaled_value += bytes[i] << ((length-1 - i) * 8)}
32
+ unscaled_value * -1 if negative
33
+ unscaled_value
34
+ end
35
+ end
36
+
37
+ end
38
+ end
39
+
40
+ require 'bytepack/basic/fixed_size'
41
+ require 'bytepack/basic/varbinary'
42
+ require 'bytepack/basic/string'
43
+ require 'bytepack/basic/symbol'
@@ -0,0 +1,28 @@
1
+ module Bytepack
2
+ class FixedSize < Basic
3
+
4
+ class << self
5
+ def pack(val)
6
+ val ||= self::NULL_INDICATOR
7
+ [val].pack(self::DIRECTIVE)
8
+ end
9
+
10
+ def unpack(bytes, offset = 0)
11
+ offset, format = *preprocess(bytes, offset, self::DIRECTIVE, self::LENGTH)
12
+ unpacked = bytes.unpack1(format)
13
+ if unpacked == self::NULL_INDICATOR
14
+ [nil, offset]
15
+ else
16
+ [unpacked, offset]
17
+ end
18
+ end
19
+ end
20
+
21
+ end
22
+ end
23
+
24
+ require 'bytepack/basic/fixed_size/integer_type'
25
+ require 'bytepack/basic/fixed_size/float'
26
+ require 'bytepack/basic/fixed_size/timestamp' # Date
27
+ require 'bytepack/basic/fixed_size/decimal'
28
+ require 'bytepack/basic/fixed_size/null'
@@ -0,0 +1,48 @@
1
+ require 'bigdecimal'
2
+ require 'bigdecimal/util'
3
+
4
+ module Bytepack
5
+ class Decimal < FixedSize
6
+ DIRECTIVE = 'C16'
7
+ NULL_INDICATOR = -170141183460469231731687303715884105728 # NULL indicator for object type serializations
8
+ LENGTH = 16
9
+ PRECISION = 38
10
+ SCALE = 12
11
+
12
+ class << self
13
+ def pack(val)
14
+ (val == self::NULL_INDICATOR || val.nil?) ? intToBytes(self::LENGTH, self::NULL_INDICATOR) : serialize(val)
15
+ end
16
+
17
+ def unpack(bytes, offset = 0)
18
+ [deserialize(bytes, offset), offset + self::LENGTH]
19
+ end
20
+
21
+ def serialize(val)
22
+ num = case val
23
+ when BigDecimal then val
24
+ else val.to_d
25
+ end
26
+ sign, digits, base, exponent = *num.split
27
+ scale = digits.size - exponent
28
+ precision = digits.size
29
+ raise(::ArgumentError, "Scale of this decimal is #{scale} and the max is #{self::SCALE}") if scale > self::SCALE
30
+ rest = precision - scale
31
+ raise(::ArgumentError, "Precision to the left of the decimal point is #{rest} and the max is #{self::PRECISION-self::SCALE}") if rest > 26
32
+ scale_factor = self::SCALE - scale
33
+ unscaled_int = sign * digits.to_i * base ** scale_factor # Unscaled integer
34
+ intToBytes(self::LENGTH, unscaled_int)
35
+ end
36
+
37
+ def deserialize(val, offset = 0)
38
+ unscaled = bytesToInt(self::LENGTH, val, offset)
39
+ if unscaled != self::NULL_INDICATOR
40
+ unscaled = unscaled.to_s
41
+ scaled = unscaled.insert(unscaled.size - self::SCALE, ".")
42
+ BigDecimal(scaled)
43
+ end
44
+ end
45
+ end
46
+
47
+ end
48
+ end
@@ -0,0 +1,7 @@
1
+ module Bytepack
2
+ class Float < FixedSize
3
+ DIRECTIVE = 'G'
4
+ LENGTH = 8
5
+ NULL_INDICATOR = -1.7E308 # NULL indicator for object type serializations
6
+ end
7
+ end
@@ -0,0 +1,18 @@
1
+ module Bytepack
2
+ class IntegerType < FixedSize
3
+ # Reserved class
4
+
5
+ class << self
6
+ end
7
+
8
+ end
9
+ end
10
+
11
+ require 'bytepack/basic/fixed_size/integer_type/byte'
12
+ require 'bytepack/basic/fixed_size/integer_type/u_byte'
13
+ require 'bytepack/basic/fixed_size/integer_type/short'
14
+ require 'bytepack/basic/fixed_size/integer_type/u_short'
15
+ require 'bytepack/basic/fixed_size/integer_type/integer'
16
+ require 'bytepack/basic/fixed_size/integer_type/u_integer'
17
+ require 'bytepack/basic/fixed_size/integer_type/long'
18
+ require 'bytepack/basic/fixed_size/integer_type/u_long'
@@ -0,0 +1,7 @@
1
+ module Bytepack
2
+ class Byte < IntegerType
3
+ DIRECTIVE = 'c'
4
+ LENGTH = 1
5
+ NULL_INDICATOR = -128 # NULL indicator for object type serializations
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ module Bytepack
2
+ class Integer < IntegerType
3
+ DIRECTIVE = 'l>'
4
+ LENGTH = 4
5
+ NULL_INDICATOR = -2147483648 # NULL indicator for object type serializations
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ module Bytepack
2
+ class Long < IntegerType
3
+ DIRECTIVE = 'q>'
4
+ LENGTH = 8
5
+ NULL_INDICATOR = -9223372036854775808 # NULL indicator for object type serializations
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ module Bytepack
2
+ class Short < IntegerType
3
+ DIRECTIVE = 's>'
4
+ LENGTH = 2
5
+ NULL_INDICATOR = -32768 # NULL indicator for object type serializations
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ module Bytepack
2
+ class UByte < IntegerType # Unsigned
3
+ DIRECTIVE = 'C'
4
+ LENGTH = 1
5
+ NULL_INDICATOR = 0 # NULL indicator for object type serializations
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ module Bytepack
2
+ class UInteger < IntegerType # Unsigned
3
+ DIRECTIVE = 'L>'
4
+ LENGTH = 4
5
+ NULL_INDICATOR = 0 # NULL indicator for object type serializations
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ module Bytepack
2
+ class ULong < IntegerType # Unsigned
3
+ DIRECTIVE = 'Q>'
4
+ LENGTH = 8
5
+ NULL_INDICATOR = 0 # NULL indicator for object type serializations
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ module Bytepack
2
+ class UShort < IntegerType # Unsigned
3
+ DIRECTIVE = 'S>'
4
+ LENGTH = 2
5
+ NULL_INDICATOR = 0 # NULL indicator for object type serializations
6
+ end
7
+ end
@@ -0,0 +1,11 @@
1
+ module Bytepack
2
+ class Null < Byte
3
+
4
+ class << self
5
+ def pack(*)
6
+ super(Byte::NULL_INDICATOR)
7
+ end
8
+ end
9
+
10
+ end
11
+ end
@@ -0,0 +1,22 @@
1
+ module Bytepack
2
+ class Timestamp < Long
3
+
4
+ class << self
5
+ def pack(val)
6
+ # All dates are represented as Long values. This signed number represents the number of microseconds before or after Jan. 1 1970 00:00:00 GMT, the Unix epoch. Note that the units are microseconds, not milliseconds.
7
+ val = case val
8
+ when ::Integer then val
9
+ when ::Time then val.to_i*1000000 + val.usec # Microseconds
10
+ end
11
+ super(val)
12
+ end
13
+
14
+ def unpack(bytes, offset = 0)
15
+ unpacked = super(bytes, offset)
16
+ unpacked[0] = Time.at(unpacked[0]/1000000.to_f) if unpacked[0] # Microseconds
17
+ unpacked
18
+ end
19
+ end
20
+
21
+ end
22
+ end
@@ -0,0 +1,17 @@
1
+ module Bytepack
2
+ class String < Varbinary
3
+
4
+ class << self
5
+ def unpack(bytes, offset = 0)
6
+ vb = super(bytes, offset)
7
+ vb[0] = vb[0].force_encoding("utf-8") unless vb[0].nil?
8
+ vb
9
+ end
10
+
11
+ def convert_input(val)
12
+ val.to_s.encode("utf-8")
13
+ end
14
+ end
15
+
16
+ end
17
+ end
@@ -0,0 +1,13 @@
1
+ module Bytepack
2
+ class Symbol < Varbinary
3
+
4
+ class << self
5
+ def unpack(bytes, offset = 0)
6
+ vb = super(bytes, offset)
7
+ vb[0] = vb[0].to_sym unless vb[0].nil?
8
+ vb
9
+ end
10
+ end
11
+
12
+ end
13
+ end
@@ -0,0 +1,34 @@
1
+ module Bytepack
2
+ class Varbinary < Basic
3
+ DIRECTIVE = 'a'
4
+ NULL_INDICATOR = -1 # NULL indicator for object type serializations
5
+ LENGTH_TYPE = AnyType
6
+
7
+ class << self
8
+ def pack(val)
9
+ if val.nil?
10
+ AnyType.pack(self::NULL_INDICATOR)
11
+ else
12
+ val = convert_input(val)
13
+ self::LENGTH_TYPE.pack(val.bytesize) + val
14
+ end
15
+ end
16
+
17
+ def unpack(bytes, offset = 0)
18
+ length, offset = *self::LENGTH_TYPE.unpack(bytes, offset)
19
+ case length
20
+ when self::NULL_INDICATOR then [nil, offset]
21
+ when 0 then ["", offset]
22
+ else
23
+ offset, format = *preprocess(bytes, offset, self::DIRECTIVE, length)
24
+ [bytes.unpack1(format), offset]
25
+ end
26
+ end
27
+
28
+ def convert_input(val)
29
+ val.to_s.encode("ascii-8bit")
30
+ end
31
+ end
32
+
33
+ end
34
+ end
@@ -0,0 +1,9 @@
1
+ module Bytepack
2
+ class Complex < Struct
3
+ class << self
4
+ end
5
+ end
6
+ end
7
+
8
+ require 'bytepack/complex/array'
9
+ require 'bytepack/complex/hash'
@@ -0,0 +1,22 @@
1
+ module Bytepack
2
+ class Array < Complex
3
+ LENGTH_TYPE = AnyType
4
+
5
+ class << self
6
+ def pack(array = [])
7
+ elements_count = array.size
8
+ self::LENGTH_TYPE.pack(elements_count) + array.map {|val| AnyType.pack(val)}.join
9
+ end
10
+
11
+ def unpack(bytes, offset = 0)
12
+ elements_count, offset = *self::LENGTH_TYPE.unpack(bytes, offset)
13
+ elements = elements_count.times.map do
14
+ element, offset = *AnyType.unpack(bytes, offset)
15
+ element
16
+ end
17
+ [elements, offset]
18
+ end
19
+ end
20
+
21
+ end
22
+ end
@@ -0,0 +1,17 @@
1
+ module Bytepack
2
+ class Hash < Complex
3
+
4
+ class << self
5
+ def pack(hash = {})
6
+ AnyType.pack(hash.keys) + AnyType.pack(hash.values)
7
+ end
8
+
9
+ def unpack(bytes, offset = 0)
10
+ keys, offset = *AnyType.unpack(bytes, offset)
11
+ values, offset = *AnyType.unpack(bytes, offset)
12
+ [::Hash[keys.map.with_index {|key, index| [key, values[index]]}], offset]
13
+ end
14
+ end
15
+
16
+ end
17
+ end
@@ -0,0 +1,8 @@
1
+ module Bytepack
2
+ class Compound < Struct
3
+ class << self
4
+ end
5
+ end
6
+ end
7
+
8
+ require 'bytepack/compound/single_type_array'
@@ -0,0 +1,41 @@
1
+ module Bytepack
2
+ class SingleTypeArray < Compound
3
+ LENGTH_TYPE = AnyType
4
+
5
+ class << self
6
+ def pack(val = []) # First element is a data type indicator (Integer, Struct, String/Symbol)
7
+ array = val[1..-1]
8
+ dataType = classifyDataType(val[0])
9
+ unless dataType # No indicator recognized
10
+ array = val
11
+ dataType = autodetect_dataType(array)
12
+ end
13
+ TypeInfo.pack(dataType) + self::LENGTH_TYPE.pack(array.size) + array.map {|e| dataType.pack(e)}.join
14
+ end
15
+
16
+ def unpack(bytes, offset = 0)
17
+ dataType, offset = *TypeInfo.unpack(bytes, offset)
18
+ if dataType.nil?
19
+ array = nil
20
+ else
21
+ array_size, offset = *self::LENGTH_TYPE.unpack(bytes, offset)
22
+ array = array_size.times.map do
23
+ element, offset = *dataType.unpack(bytes, offset)
24
+ element
25
+ end
26
+ end
27
+ [array, offset]
28
+ end
29
+
30
+ def autodetect_dataType(array)
31
+ if array[0].is_a?(::Integer)
32
+ max_int = array.select {|e| e.is_a?(::Integer)}.max
33
+ packingDataType(max_int)
34
+ else
35
+ packingDataType(array[0])
36
+ end
37
+ end
38
+ end
39
+
40
+ end
41
+ end
@@ -0,0 +1,37 @@
1
+ module Bytepack
2
+ class CustomData < Struct
3
+
4
+ @subclasses = []
5
+
6
+ class << self
7
+ def inherited(child)
8
+ @subclasses << child
9
+ end
10
+
11
+ def subclasses(&block)
12
+ selected = nil
13
+ @subclasses.each do |child| # .find() is too slow
14
+ if yield(child)
15
+ selected = child
16
+ break
17
+ end
18
+ end
19
+ selected if block_given?
20
+ end
21
+
22
+ def struct_by_ruby_type(val)
23
+ subclasses {|child| val.is_a?(child::RUBY_TYPE)}
24
+ end
25
+
26
+ def code_by_struct(struct)
27
+ struct::TYPE_CODE if struct < Struct
28
+ end
29
+
30
+ def struct_by_code(code)
31
+ subclasses {|child| child::TYPE_CODE == code}
32
+ end
33
+ end
34
+
35
+ end
36
+
37
+ end
@@ -0,0 +1,18 @@
1
+ module Bytepack
2
+ module Extensions
3
+
4
+ module CodeValuesHash
5
+ attr_reader :codes, :code_values
6
+
7
+ def inherited(child)
8
+ child.instance_variable_set(:@codes, Hash[@codes.map {|a| [a[0], a[1].dup]}]) if instance_variable_defined?("@codes")
9
+ end
10
+
11
+ def hash_codes(*arrays)
12
+ @codes ||= ::Hash[arrays]
13
+ @code_values ||= ::Hash[arrays.map(&:reverse)]
14
+ end
15
+ end
16
+
17
+ end
18
+ end
@@ -0,0 +1,34 @@
1
+ module Bytepack
2
+ class TypeInfo < Byte
3
+ extend Extensions::CodeValuesHash
4
+
5
+ hash_codes [-99, Array], # ARRAY
6
+ [-98, SingleTypeArray], # SingleTypeArray
7
+ [-89, Hash], # Hash
8
+ [1, Null], # NULL
9
+ [3, Byte], # TINYINT
10
+ [4, Short], # SMALLINT
11
+ [5, Integer], # INTEGER
12
+ [6, Long], # BIGINT
13
+ [8, Float], # FLOAT
14
+ [9, String], # STRING
15
+ [10, Symbol], # Symbol
16
+ [11, Timestamp], # TIMESTAMP
17
+ [22, Decimal], # DECIMAL
18
+ [25, Varbinary] # VARBINARY
19
+
20
+ class << self
21
+ def pack(val)
22
+ val = val.is_a?(::Integer) ? val : code_values[val]||CustomData.code_by_struct(val)
23
+ super(val)
24
+ end
25
+
26
+ def unpack(bytes, offset = 0)
27
+ unpacked = super(bytes, offset)
28
+ unpacked[0] = codes[unpacked[0]]||CustomData.struct_by_code(unpacked[0]) if unpacked[0]
29
+ unpacked
30
+ end
31
+ end
32
+
33
+ end
34
+ end
@@ -0,0 +1,3 @@
1
+ module Bytepack
2
+ VERSION = "0.0.1"
3
+ end
metadata ADDED
@@ -0,0 +1,89 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: bytepack
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Valery Kvon
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2019-05-16 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: minitest
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '5.11'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '5.11'
27
+ description: Packing & unpacking various Ruby data to/from a byte string, incl. arrays,
28
+ hashes and custom data structures
29
+ email: addagger@gmail.com
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - CHANGELOG.md
35
+ - LICENSE
36
+ - README.md
37
+ - lib/bytepack.rb
38
+ - lib/bytepack/any_type.rb
39
+ - lib/bytepack/basic.rb
40
+ - lib/bytepack/basic/fixed_size.rb
41
+ - lib/bytepack/basic/fixed_size/decimal.rb
42
+ - lib/bytepack/basic/fixed_size/float.rb
43
+ - lib/bytepack/basic/fixed_size/integer_type.rb
44
+ - lib/bytepack/basic/fixed_size/integer_type/byte.rb
45
+ - lib/bytepack/basic/fixed_size/integer_type/integer.rb
46
+ - lib/bytepack/basic/fixed_size/integer_type/long.rb
47
+ - lib/bytepack/basic/fixed_size/integer_type/short.rb
48
+ - lib/bytepack/basic/fixed_size/integer_type/u_byte.rb
49
+ - lib/bytepack/basic/fixed_size/integer_type/u_integer.rb
50
+ - lib/bytepack/basic/fixed_size/integer_type/u_long.rb
51
+ - lib/bytepack/basic/fixed_size/integer_type/u_short.rb
52
+ - lib/bytepack/basic/fixed_size/null.rb
53
+ - lib/bytepack/basic/fixed_size/timestamp.rb
54
+ - lib/bytepack/basic/string.rb
55
+ - lib/bytepack/basic/symbol.rb
56
+ - lib/bytepack/basic/varbinary.rb
57
+ - lib/bytepack/complex.rb
58
+ - lib/bytepack/complex/array.rb
59
+ - lib/bytepack/complex/hash.rb
60
+ - lib/bytepack/compound.rb
61
+ - lib/bytepack/compound/single_type_array.rb
62
+ - lib/bytepack/custom_data.rb
63
+ - lib/bytepack/extensions.rb
64
+ - lib/bytepack/type_info.rb
65
+ - lib/bytepack/version.rb
66
+ homepage: https://github.com/addagger/bytepack
67
+ licenses:
68
+ - MIT
69
+ metadata: {}
70
+ post_install_message:
71
+ rdoc_options: []
72
+ require_paths:
73
+ - lib
74
+ required_ruby_version: !ruby/object:Gem::Requirement
75
+ requirements:
76
+ - - ">="
77
+ - !ruby/object:Gem::Version
78
+ version: 2.4.0
79
+ required_rubygems_version: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - ">="
82
+ - !ruby/object:Gem::Version
83
+ version: 1.3.6
84
+ requirements: []
85
+ rubygems_version: 3.0.3
86
+ signing_key:
87
+ specification_version: 4
88
+ summary: Tool for byte-serialization of various Ruby data structures
89
+ test_files: []