marcandre-packable 1.1.0 → 1.1.1
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.
- data/CHANGELOG.rdoc +9 -0
- data/README.rdoc +244 -0
- data/VERSION.yml +4 -0
- data/lib/packable/extensions/array.rb +47 -0
- data/lib/packable/extensions/float.rb +37 -0
- data/lib/packable/extensions/integer.rb +44 -0
- data/lib/packable/extensions/io.rb +93 -0
- data/lib/packable/extensions/object.rb +14 -0
- data/lib/packable/extensions/proc.rb +16 -0
- data/lib/packable/extensions/string.rb +34 -0
- data/lib/packable/jungle_survival_kit.rb +72 -0
- data/lib/packable/mixin.rb +57 -0
- data/lib/packable/packers.rb +106 -0
- data/lib/packable.rb +9 -0
- data/test/packing_doc_test.rb +105 -0
- data/test/packing_test.rb +87 -0
- data/test/test_helper.rb +8 -0
- metadata +30 -7
data/CHANGELOG.rdoc
ADDED
data/README.rdoc
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
= Packable Library - Intro
|
|
2
|
+
|
|
3
|
+
If you need to do read and write binary data, there is of course <tt>Array::pack</tt> and <tt>String::unpack</tt>.
|
|
4
|
+
The packable library makes (un)packing nicer, smarter and more powerful.
|
|
5
|
+
In case you are wondering why on earth someone would want to do serious (un)packing when YAML & XML are built-in:
|
|
6
|
+
I wrote this library to read and write FLV files...
|
|
7
|
+
|
|
8
|
+
== Feature summary:
|
|
9
|
+
|
|
10
|
+
=== Explicit forms
|
|
11
|
+
Strings, integers & floats have long forms instead of the cryptic letter notation. For example:
|
|
12
|
+
["answer", 42].pack("C3n")
|
|
13
|
+
can be written as:
|
|
14
|
+
["answer", 42].pack({:bytes => 3}, {:bytes => 2, :endian => :big})
|
|
15
|
+
This can look a bit too verbose, so let's introduce shortcuts right away:
|
|
16
|
+
=== Shortcuts
|
|
17
|
+
Most commonly used options have shortcuts and you can define your own. For example:
|
|
18
|
+
:unsigned_long <===> {:bytes => 4, :signed => false, :endian => :big}
|
|
19
|
+
=== IO
|
|
20
|
+
IO classes (File & StringIO) can use (un)packing routines.
|
|
21
|
+
For example:
|
|
22
|
+
signature, block_len, temperature = my_file >> [String, :bytes=>3] >> Integer >> :float
|
|
23
|
+
The method +each+ also accepts packing options:
|
|
24
|
+
StringIO.new("\000\001\000\002\000\003").each(:short).to_a ===> [1,2,3]
|
|
25
|
+
=== Custom classes
|
|
26
|
+
It's easy to make you own classes (un)packable. All the previous goodies are thus available:
|
|
27
|
+
File.open("great_flick.flv") do |f|
|
|
28
|
+
head = f.read(FLV::Header)
|
|
29
|
+
f.each(FLV::Tag) do |tag|
|
|
30
|
+
# do something meaningful with each tag...
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
=== Filters
|
|
35
|
+
It's also easy to define special shortcuts that will call blocks to (un)pack any classe.
|
|
36
|
+
As an example, this could be useful to add special packing features to String (without monkey patching String::pack).
|
|
37
|
+
|
|
38
|
+
== Installation
|
|
39
|
+
|
|
40
|
+
First, ensure that you're running at least RubyGems 1.2 (check <tt>gem --version</tt> if you're not sure -- to update: <tt>sudo gem update --system</tt>).
|
|
41
|
+
|
|
42
|
+
Add GitHub to your gem sources (if you haven't already):
|
|
43
|
+
|
|
44
|
+
sudo gem sources -a http://gems.github.com
|
|
45
|
+
|
|
46
|
+
Get the gem:
|
|
47
|
+
|
|
48
|
+
sudo gem install marcandre-packable
|
|
49
|
+
|
|
50
|
+
That's it! Simply <tt>require 'packable'</tt> in your code to use it.
|
|
51
|
+
|
|
52
|
+
Compatibility: Ruby 1.8, 1.9
|
|
53
|
+
|
|
54
|
+
= Documentation
|
|
55
|
+
|
|
56
|
+
== Packing and unpacking
|
|
57
|
+
|
|
58
|
+
The library was designed to be backward compatible, so the usual packing and unpacking methods still work as before.
|
|
59
|
+
All packable objects can also be packed directly (no need to use an array). For example:
|
|
60
|
+
|
|
61
|
+
42.pack("n") ===> "\000*"
|
|
62
|
+
|
|
63
|
+
In a similar fashion, unpacking can done using class methods:
|
|
64
|
+
|
|
65
|
+
Integer.unpack("\000*", "n") ===> 42
|
|
66
|
+
|
|
67
|
+
== Formats
|
|
68
|
+
|
|
69
|
+
Although the standard string formats can still be used, it is possible to pass a list of options (see example in feature summary).
|
|
70
|
+
These are the options for core types:
|
|
71
|
+
|
|
72
|
+
=== Integer
|
|
73
|
+
+bytes+:: Number of bytes (default is 4) to use.
|
|
74
|
+
+endian+:: Either <tt>:big</tt> (default) or <tt>:small</tt>.
|
|
75
|
+
+signed+:: Either +true+ (default) or not. This will make a difference only when unpacking.
|
|
76
|
+
|
|
77
|
+
=== Float
|
|
78
|
+
+precision+:: Either <tt>:single</tt> (default) or <tt>:double</tt>.
|
|
79
|
+
+endian+:: Either <tt>:big</tt> (default) or <tt>:small</tt>.
|
|
80
|
+
|
|
81
|
+
=== String
|
|
82
|
+
+bytes+:: Total length (default is the full length)
|
|
83
|
+
+fill+:: The string to use for filling when packing a string shorter than the specified bytes option. Default is a space.
|
|
84
|
+
|
|
85
|
+
=== Array
|
|
86
|
+
+repeat+:: This option can be used (when packing only) to repeat the current option. A value of <tt>:all</tt> will mean for all remaining elements of the array.
|
|
87
|
+
|
|
88
|
+
When unpacking, it is necessary to specify the class in addition to any option, like so:
|
|
89
|
+
|
|
90
|
+
"AB".unpack(Integer, :bytes => 2, :endian => :big, :signed => false) ===> 0x3132
|
|
91
|
+
|
|
92
|
+
== Shortcuts and default values
|
|
93
|
+
|
|
94
|
+
It's easy to add shortcuts for easier (un)packing:
|
|
95
|
+
|
|
96
|
+
String.packers.set :flv_signature, :bytes => 3, :fill => "FLV"
|
|
97
|
+
|
|
98
|
+
"x".pack(:flv_signature) ===> "xFL"
|
|
99
|
+
|
|
100
|
+
Two shortcut names have special meanings: +default+ and +merge_all+. +default+ specifies the options to use when
|
|
101
|
+
nothing is specified, while +merge_all+ will be merged with all options. For example:
|
|
102
|
+
|
|
103
|
+
String.packers do |p|
|
|
104
|
+
p.set :merge_all, :fill => "*" # Unless explicitly specified, :fill will now be "*"
|
|
105
|
+
p.set :default, :bytes => 8 # If no option is given, this will act as default
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
"ab".pack ===> "ab******"
|
|
109
|
+
"ab".pack(:bytes=>4) ===> "ab**"
|
|
110
|
+
"ab".pack(:fill => "!") ===> "ab" # Not "ab!!"
|
|
111
|
+
|
|
112
|
+
A shortcut can refer to another shortcut, as so:
|
|
113
|
+
|
|
114
|
+
String.packers do |p|
|
|
115
|
+
p.set :creator, :bytes => 4
|
|
116
|
+
p.set :app_type, :creator
|
|
117
|
+
end
|
|
118
|
+
"hello".pack(:app_type) ===> "hell"
|
|
119
|
+
|
|
120
|
+
The following shortcuts and defaults are built-in the library:
|
|
121
|
+
|
|
122
|
+
=== Integer
|
|
123
|
+
:merge_all => :bytes=>4, :signed=>true, :endian=>:big
|
|
124
|
+
:default => :long
|
|
125
|
+
:long => {}
|
|
126
|
+
:short => :bytes=>2
|
|
127
|
+
:byte => :bytes=>1
|
|
128
|
+
:unsigned_long => :bytes=>4, :signed=>false
|
|
129
|
+
:unsigned_short => :bytes=>2, :signed=>false
|
|
130
|
+
|
|
131
|
+
=== Float
|
|
132
|
+
:merge_all => :precision => :single, :endian => :big
|
|
133
|
+
:default => :float
|
|
134
|
+
:double => :precision => :double
|
|
135
|
+
:float => {}
|
|
136
|
+
|
|
137
|
+
=== String
|
|
138
|
+
:merge_all => :fill => " "
|
|
139
|
+
|
|
140
|
+
== Files and StringIO
|
|
141
|
+
|
|
142
|
+
All IO objects (in particular files) can deal with packing easily. These examples will all return an array with 3 elements (a string, an integer and another string):
|
|
143
|
+
|
|
144
|
+
io >> :flv_signature >> Integer >> [String, {:bytes => 8}]
|
|
145
|
+
io.read(:flv_signature, Integer, [String, {:bytes => 8}])
|
|
146
|
+
io.read(:flv_signature, Integer, String, {:bytes => 8})
|
|
147
|
+
[io.read(:flv_signature), io.read(Integer), io.read(String, :bytes => 8)]
|
|
148
|
+
|
|
149
|
+
In a similar fashion, these have the same effect although the return value is different
|
|
150
|
+
|
|
151
|
+
io << "x".pack(:flv_signature) << 66.pack << "Hello".pack(:bytes => 8) # returns io
|
|
152
|
+
io << ["x", 66, "Hello"].pack(:flv_signature, {} , {:bytes => 8}) # returns io
|
|
153
|
+
io.write("x", :flv_signature, 66, "Hello", {:bytes => 8}) # returns the # of bytes written
|
|
154
|
+
io.packed << ["x",:flv_signature] << 66 << ["Hello", {:bytes => 8}] # returns a "packed io"
|
|
155
|
+
|
|
156
|
+
The last example shows how <tt>io.packed</tt> returns a special IO object (a packing IO) that will pack arguments before writing it.
|
|
157
|
+
This is to insure compatibility with the usual behavior of IO objects:
|
|
158
|
+
io << 66 ==> appends "66"
|
|
159
|
+
io.packed << 66 ==> appends "\000\000\000B"
|
|
160
|
+
|
|
161
|
+
We "cheated" in the previous example; instead of writing <tt>io.packed.write(...)</tt> we used the shorter form.
|
|
162
|
+
This works because we're passing more than one argument; for only one argument we must call <tt>io.packed.write(66)</tt>
|
|
163
|
+
less the usual +write+ method is called.
|
|
164
|
+
|
|
165
|
+
Since the standard library desn't define the <tt>>></tt> operator for IO objects, we are free to use either <tt>io.packed</tt> or <tt>io</tt> directly.
|
|
166
|
+
Note that reading one value only will return that value directly, not an array containing that value:
|
|
167
|
+
|
|
168
|
+
io.read(Integer) ===> 42, not [42]
|
|
169
|
+
io.read(Integer,Integer) ===> [42,43]
|
|
170
|
+
io << Integer ===> [42]
|
|
171
|
+
|
|
172
|
+
== Custom classes
|
|
173
|
+
|
|
174
|
+
Including the mixin +Packable+ will make a class (un)packable. Packable relies on +write_packed+
|
|
175
|
+
and unpacking on +read_packed+. For example:
|
|
176
|
+
|
|
177
|
+
class MyHeader < Struct.new(:signature, :nb_blocks)
|
|
178
|
+
include Packable
|
|
179
|
+
|
|
180
|
+
def write_packed(packedio, options)
|
|
181
|
+
packedio << [signature, {:bytes=>3}] << [nb_blocks, :short]
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def self.read_packed(packedio, options)
|
|
185
|
+
h = MyHeader.new
|
|
186
|
+
h.signature, h.nb_blocks = packedio >> [String, {:bytes => 3}] >> :short
|
|
187
|
+
h
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
We used the argument name +packedio+ to remind us that these are packed IO objects, i.e.
|
|
192
|
+
they will write their arguments after packing them instead of converting them to string like normal IO objects.
|
|
193
|
+
With this definition, +MyHeader+ can be both packed and unpacked:
|
|
194
|
+
|
|
195
|
+
h = MyHeader.new("FLV", 65)
|
|
196
|
+
h.pack ===> "FLV\000A"
|
|
197
|
+
StringIO.new("FLV\000A") >> Signature ===> [a copy of h]
|
|
198
|
+
|
|
199
|
+
A default <tt>self.read_packed</tt> is provided by the +Packable+ mixin, which allows you to define +read_packed+ as
|
|
200
|
+
an instance method instead of a class method. In that case, +read_packed+ instance method is called with
|
|
201
|
+
the same arguments and should modify +self+ accordingly (instead of returning a new object).
|
|
202
|
+
It is not necessary to return +self+. The previous example can thus be shortened:
|
|
203
|
+
|
|
204
|
+
class MyHeader
|
|
205
|
+
#...
|
|
206
|
+
def read_packed(packedio, options)
|
|
207
|
+
self.signature, self.nb_blocks = packedio >> [String, {:bytes => 3}] >> :short
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
== Filter
|
|
212
|
+
|
|
213
|
+
Instead of writing a full-fledge class, sometimes it can be convenient to define a sort of wrapper we'll call filter. Here's an example:
|
|
214
|
+
|
|
215
|
+
String.packers.set :length_encoded do |packer|
|
|
216
|
+
packer.write { |packedio| packedio << length << self }
|
|
217
|
+
packer.read { |packedio| packedio.read(packedio.read(Integer)) }
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
"hello!".pack(:length_encoded) ===> "\000\000\000\006hello!"
|
|
221
|
+
["this", "is", "great!"].pack(*[:length_encoded]*3).unpack(*[:length_encoded]*3) ===> ["this", "is", "great!"]
|
|
222
|
+
|
|
223
|
+
Note that the +write+ block will be executed as an instance method (which is why we could use +length+ & +self+),
|
|
224
|
+
while +read+ is a normal block that must return the newly read object.
|
|
225
|
+
|
|
226
|
+
== Inheritance
|
|
227
|
+
|
|
228
|
+
A final note to say that packers are inherited in some way. For instance one could define a filter for all objects:
|
|
229
|
+
|
|
230
|
+
Object.packers.set :with_class do |packer|
|
|
231
|
+
packer.write { |io| io << [self.class.name, :length_encoded] << self }
|
|
232
|
+
packer.read do |io|
|
|
233
|
+
klass = eval(io.read(:length_encoded))
|
|
234
|
+
io.read(klass)
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
[42, MyHeader.new("Wow", 1)].pack(:with_class, :with_class).unpack(:with_class, :with_class) ===> [42, MyHeader.new("Wow", 1)]
|
|
239
|
+
|
|
240
|
+
= License
|
|
241
|
+
|
|
242
|
+
packable is licensed under the terms of the (modified) BSD License, see the included LICENSE file.
|
|
243
|
+
|
|
244
|
+
Author:: Marc-André Lafortune
|
data/VERSION.yml
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
require 'stringio'
|
|
2
|
+
|
|
3
|
+
module Packable
|
|
4
|
+
module Extensions #:nodoc:
|
|
5
|
+
module Array #:nodoc:
|
|
6
|
+
def self.included(base)
|
|
7
|
+
base.class_eval do
|
|
8
|
+
alias_method_chain :pack, :long_form
|
|
9
|
+
include Packable
|
|
10
|
+
extend ClassMethods
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def pack_with_long_form(*arg)
|
|
15
|
+
return pack_without_long_form(*arg) if arg.first.is_a? String
|
|
16
|
+
pio = StringIO.new.packed
|
|
17
|
+
write_packed(pio, *arg)
|
|
18
|
+
pio.string
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def write_packed(io, *how)
|
|
22
|
+
return io << self.original_pack(*how) if how.first.is_a? String
|
|
23
|
+
how = [:repeat => :all] if how.empty?
|
|
24
|
+
current = -1
|
|
25
|
+
how.each do |options|
|
|
26
|
+
repeat = options.is_a?(Hash) ? options.delete(:repeat) || 1 : 1
|
|
27
|
+
repeat = length - 1 - current if repeat == :all
|
|
28
|
+
repeat.times do
|
|
29
|
+
io.write(self[current+=1],options)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
module ClassMethods #:nodoc:
|
|
35
|
+
def read_packed(io, *how)
|
|
36
|
+
raise "Can't support builtin format for arrays" if (how.length == 1) && (how.first.is_a? String)
|
|
37
|
+
how.inject [] do |r, options|
|
|
38
|
+
repeat = options.is_a? Hash ? options.delete(:repeat) || 1 : 1
|
|
39
|
+
(0...repeat).inject r do
|
|
40
|
+
r << io.read(options)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
module Packable
|
|
2
|
+
module Extensions #:nodoc:
|
|
3
|
+
module Float #:nodoc:
|
|
4
|
+
def self.included(base)
|
|
5
|
+
base.class_eval do
|
|
6
|
+
include Packable
|
|
7
|
+
extend ClassMethods
|
|
8
|
+
packers do |p|
|
|
9
|
+
p.set :merge_all, :precision => :single, :endian => :big
|
|
10
|
+
p.set :double , :precision => :double
|
|
11
|
+
p.set :float , {}
|
|
12
|
+
p.set :default , :float
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def write_packed(io, options)
|
|
18
|
+
io << pack(self.class.pack_option_to_format(options))
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
module ClassMethods #:nodoc:
|
|
22
|
+
def pack_option_to_format(options)
|
|
23
|
+
format = {:big => "G", :small => "E"}[options[:endian]]
|
|
24
|
+
format.downcase! if options[:precision] == :single
|
|
25
|
+
format
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def read_packed(io, options)
|
|
29
|
+
io.read({:single => 4, :double => 8}[options[:precision]]) \
|
|
30
|
+
.unpack(pack_option_to_format(options)) \
|
|
31
|
+
.first
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
module Packable
|
|
2
|
+
module Extensions #:nodoc:
|
|
3
|
+
module Integer #:nodoc:
|
|
4
|
+
def self.included(base)
|
|
5
|
+
base.class_eval do
|
|
6
|
+
include Packable
|
|
7
|
+
extend ClassMethods
|
|
8
|
+
packers do |p|
|
|
9
|
+
p.set :merge_all , :bytes=>4, :signed=>true, :endian=>:big
|
|
10
|
+
p.set :default , :long
|
|
11
|
+
p.set :long , {}
|
|
12
|
+
p.set :short , :bytes=>2
|
|
13
|
+
p.set :char , :bytes=>1, :signed=>false
|
|
14
|
+
p.set :byte , :bytes=>1
|
|
15
|
+
p.set :unsigned_long , :bytes=>4, :signed=>false
|
|
16
|
+
p.set :unsigned_short , :bytes=>2, :signed=>false
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def write_packed(io, options)
|
|
22
|
+
val = self
|
|
23
|
+
chars = (0...options[:bytes]).collect do
|
|
24
|
+
byte = val & 0xFF
|
|
25
|
+
val >>= 8
|
|
26
|
+
byte.chr
|
|
27
|
+
end
|
|
28
|
+
chars.reverse! if options[:endian] == :big
|
|
29
|
+
io << chars.join
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
module ClassMethods #:nodoc:
|
|
33
|
+
def unpack_string(s,options)
|
|
34
|
+
s = s.reverse if options[:endian != :big]
|
|
35
|
+
r = 0
|
|
36
|
+
s.each_byte {|b| r = (r << 8) + b}
|
|
37
|
+
r -= 1 << (8 * options[:bytes]) if options[:signed] && (1 == r >> (8 * options[:bytes] - 1))
|
|
38
|
+
r
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
require 'enumerator'
|
|
2
|
+
|
|
3
|
+
module Packable
|
|
4
|
+
module Extensions #:nodoc:
|
|
5
|
+
module IO
|
|
6
|
+
def self.included(base) #:nodoc:
|
|
7
|
+
base.alias_method_chain :read, :packing
|
|
8
|
+
base.alias_method_chain :write, :packing
|
|
9
|
+
base.alias_method_chain :each, :packing
|
|
10
|
+
attr_accessor :throw_on_error
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Returns the change in io.pos caused by the block.
|
|
14
|
+
# Has nothing to do with packing, but quite helpful and so simple...
|
|
15
|
+
def pos_change(&block)
|
|
16
|
+
delta =- pos
|
|
17
|
+
yield
|
|
18
|
+
delta += pos
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Usage:
|
|
22
|
+
# io >> Class
|
|
23
|
+
# io >> [Class, options]
|
|
24
|
+
# io >> :shortcut
|
|
25
|
+
def >> (options)
|
|
26
|
+
r = []
|
|
27
|
+
class << r
|
|
28
|
+
attr_accessor :stream
|
|
29
|
+
def >> (options)
|
|
30
|
+
self << stream.read(options)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
r.stream = self
|
|
34
|
+
r >> options
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Returns (or yields) a modified IO object that will always pack/unpack when writing/reading.
|
|
38
|
+
def packed
|
|
39
|
+
packedio = clone
|
|
40
|
+
class << packedio
|
|
41
|
+
def << (arg)
|
|
42
|
+
arg = [arg, :default] unless arg.instance_of?(::Array)
|
|
43
|
+
pack_and_write(*arg)
|
|
44
|
+
self
|
|
45
|
+
end
|
|
46
|
+
def packed
|
|
47
|
+
block_given? ? yield(self) : self
|
|
48
|
+
end
|
|
49
|
+
alias_method :write, :pack_and_write #bypass test for argument length
|
|
50
|
+
end
|
|
51
|
+
block_given? ? yield(packedio) : packedio
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def each_with_packing(*options, &block)
|
|
55
|
+
return each_without_packing(*options, &block) if (Integer === options.first) || (String === options.first)
|
|
56
|
+
return Enumerable::Enumerator.new(self, :each_with_packing, *options) unless block_given?
|
|
57
|
+
yield read(*options) until eof?
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def write_with_packing(*arg)
|
|
61
|
+
(arg.length == 1) ? write_without_packing(*arg) : pack_and_write(*arg)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def read_with_packing(*arg)
|
|
65
|
+
return read_without_packing(*arg) if (arg.length == 0) || arg.first.is_a?(Numeric)
|
|
66
|
+
return *Packable::Packers.to_class_option_list(*arg).map do |klass, options, original|
|
|
67
|
+
if eof?
|
|
68
|
+
raise EOFError, "End of IO when attempting to read #{klass} with options #{original.inspect}" if @throw_on_eof
|
|
69
|
+
nil
|
|
70
|
+
elsif options[:read_packed]
|
|
71
|
+
options[:read_packed].call(self)
|
|
72
|
+
else
|
|
73
|
+
klass.read_packed(self, options)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def pack_and_write(*arg)
|
|
79
|
+
original_pos = pos
|
|
80
|
+
Packable::Packers.to_object_option_list(*arg).each do |obj, options|
|
|
81
|
+
if options[:write_packed]
|
|
82
|
+
options[:write_packed].bind(obj).call(self)
|
|
83
|
+
else
|
|
84
|
+
obj.write_packed(self, options)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
pos - original_pos
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
module Packable
|
|
2
|
+
module Extensions #:nodoc:
|
|
3
|
+
module Object #:nodoc:
|
|
4
|
+
def self.included(base) #:nodoc:
|
|
5
|
+
base.class_eval do
|
|
6
|
+
class << self
|
|
7
|
+
# include only packers method into Object
|
|
8
|
+
include PackersClassMethod
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
module Packable
|
|
2
|
+
module Extensions #:nodoc:
|
|
3
|
+
module Proc
|
|
4
|
+
# A bit of wizardry to return an +UnboundMethod+ which can be bound to any object
|
|
5
|
+
def unbind
|
|
6
|
+
Object.send(:define_method, :__temp_bound_method, &self)
|
|
7
|
+
Object.instance_method(:__temp_bound_method)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
# Shortcut for <tt>unbind.bind(to)</tt>
|
|
11
|
+
def bind(to)
|
|
12
|
+
unbind.bind(to)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
require 'stringio'
|
|
2
|
+
|
|
3
|
+
module Packable
|
|
4
|
+
module Extensions #:nodoc:
|
|
5
|
+
module String #:nodoc:
|
|
6
|
+
|
|
7
|
+
def self.included(base)
|
|
8
|
+
base.class_eval do
|
|
9
|
+
include Packable
|
|
10
|
+
extend ClassMethods
|
|
11
|
+
alias_method_chain :unpack, :long_form
|
|
12
|
+
packers.set :merge_all, :fill => " "
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def write_packed(io, options)
|
|
17
|
+
return io.write_without_packing(self) unless options[:bytes]
|
|
18
|
+
io.write_without_packing(self[0...options[:bytes]].ljust(options[:bytes], options[:fill] || "\000"))
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def unpack_with_long_form(*arg)
|
|
22
|
+
return unpack_without_long_form(*arg) if arg.first.is_a? String
|
|
23
|
+
StringIO.new(self).packed.read(*arg)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
module ClassMethods #:nodoc:
|
|
27
|
+
def unpack_string(s, options)
|
|
28
|
+
s
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Insures that the following basic utilities (standard with ruby 1.9 and/or rails) are defined
|
|
2
|
+
# so we can get out of the uncivilized jungle (straight ruby 1.8) alive:
|
|
3
|
+
# - +require_relative+
|
|
4
|
+
# - +try+
|
|
5
|
+
# - +tap+
|
|
6
|
+
# - +alias_method_chain+
|
|
7
|
+
# - &:some_symbol
|
|
8
|
+
|
|
9
|
+
# Standard in ruby 1.9. Adapted from Pragmatic's "Programming Ruby" (since their version was buggy...)
|
|
10
|
+
module Kernel
|
|
11
|
+
def require_relative(relative_feature)
|
|
12
|
+
file = caller.first.split(/:\d/,2).first
|
|
13
|
+
if /\A\((.*)\)/ =~ file # eval, etc.
|
|
14
|
+
raise LoadError, "require_relative is called in #{$1}"
|
|
15
|
+
end
|
|
16
|
+
require File.expand_path(relative_feature, File.dirname(file))
|
|
17
|
+
end unless method_defined? :require_relative
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
class Object
|
|
21
|
+
# Standard in rails...
|
|
22
|
+
def try(method_id, *args, &block)
|
|
23
|
+
send(method_id, *args, &block) if respond_to?(method_id, true)
|
|
24
|
+
end unless method_defined? :try
|
|
25
|
+
|
|
26
|
+
# Standard in ruby 1.9
|
|
27
|
+
def tap
|
|
28
|
+
yield self
|
|
29
|
+
self
|
|
30
|
+
end unless method_defined? :tap
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
class Module
|
|
34
|
+
# Standard in rails...
|
|
35
|
+
def alias_method_chain(target, feature)
|
|
36
|
+
# Strip out punctuation on predicates or bang methods since
|
|
37
|
+
# e.g. target?_without_feature is not a valid method name.
|
|
38
|
+
aliased_target, punctuation = target.to_s.sub(/([?!=])$/, ''), $1
|
|
39
|
+
yield(aliased_target, punctuation) if block_given?
|
|
40
|
+
|
|
41
|
+
with_method, without_method = "#{aliased_target}_with_#{feature}#{punctuation}", "#{aliased_target}_without_#{feature}#{punctuation}"
|
|
42
|
+
|
|
43
|
+
alias_method without_method, target
|
|
44
|
+
alias_method target, with_method
|
|
45
|
+
|
|
46
|
+
case
|
|
47
|
+
when public_method_defined?(without_method)
|
|
48
|
+
public target
|
|
49
|
+
when protected_method_defined?(without_method)
|
|
50
|
+
protected target
|
|
51
|
+
when private_method_defined?(without_method)
|
|
52
|
+
private target
|
|
53
|
+
end
|
|
54
|
+
end unless method_defined? :alias_method_chain
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Standard in ruby 1.9 & rails
|
|
58
|
+
unless :to_proc.respond_to?(:to_proc)
|
|
59
|
+
class Symbol
|
|
60
|
+
# Turns the symbol into a simple proc, which is especially useful for enumerations. Examples:
|
|
61
|
+
#
|
|
62
|
+
# # The same as people.collect { |p| p.name }
|
|
63
|
+
# people.collect(&:name)
|
|
64
|
+
#
|
|
65
|
+
# # The same as people.select { |p| p.manager? }.collect { |p| p.salary }
|
|
66
|
+
# people.select(&:manager?).collect(&:salary)
|
|
67
|
+
def to_proc
|
|
68
|
+
Proc.new { |*args| args.shift.__send__(self, *args) }
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# The Packable mixin itself...
|
|
2
|
+
|
|
3
|
+
require 'stringio'
|
|
4
|
+
|
|
5
|
+
module Packable
|
|
6
|
+
def self.included(base) #:nodoc:
|
|
7
|
+
base.class_eval do
|
|
8
|
+
class << self
|
|
9
|
+
include PackersClassMethod
|
|
10
|
+
include ClassMethods
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# +options+ can be a Hash, a shortcut (Symbol) or a String (old-style)
|
|
16
|
+
def pack(options = :default)
|
|
17
|
+
return [self].pack(options) if options.is_a? String
|
|
18
|
+
(StringIO.new.packed << [self, options]).string
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
module PackersClassMethod
|
|
22
|
+
# Returns or yields a the Packers.for(class)
|
|
23
|
+
# Normal use is packers.set ...
|
|
24
|
+
# (see docs or Packers::set for usage)
|
|
25
|
+
def packers
|
|
26
|
+
yield packers if block_given?
|
|
27
|
+
Packers.for(self)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
module ClassMethods
|
|
32
|
+
def unpack(s, options = :default)
|
|
33
|
+
return s.unpack(options).first if options.is_a? String
|
|
34
|
+
StringIO.new(s).packed.read(self, options)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Default +read_packed+ calls either the instance method <tt>read_packed</tt> or the
|
|
38
|
+
# class method +unpack_string+. Choose:
|
|
39
|
+
# * define a class method +read_packed+ that returns the newly read object
|
|
40
|
+
# * define an instance method +read_packed+ which reads the io into +self+
|
|
41
|
+
# * define a class method +unpack_string+ that reads and returns an object from the string. In this case, options[:bytes] should be specified!
|
|
42
|
+
def read_packed(io, options)
|
|
43
|
+
if method_defined? :read_packed
|
|
44
|
+
mandatory = instance_method(:initialize).arity
|
|
45
|
+
mandatory = -1-mandatory if mandatory < 0
|
|
46
|
+
obj = new(*[nil]*mandatory)
|
|
47
|
+
obj.read_packed(io, options)
|
|
48
|
+
obj
|
|
49
|
+
else
|
|
50
|
+
len = options[:bytes]
|
|
51
|
+
s = len ? io.read(len) : io.read
|
|
52
|
+
unpack_string(s, options)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
module Packable
|
|
2
|
+
|
|
3
|
+
# Packers for any packable class.
|
|
4
|
+
class Packers < Hash
|
|
5
|
+
SPECIAL = [:default, :merge_all].freeze
|
|
6
|
+
|
|
7
|
+
# Usage:
|
|
8
|
+
# PackableClass.packers.set :shortcut, :option => value, ...
|
|
9
|
+
# PackableClass.packers { |p| p.set...; p.set... }
|
|
10
|
+
# PackableClass.packers.set :shortcut, :another_shortcut
|
|
11
|
+
# PackableClass.packers.set :shortcut do |packer|
|
|
12
|
+
# packer.write{|io| io << self.something... }
|
|
13
|
+
# packer.read{|io| Whatever.new(io.read(...)) }
|
|
14
|
+
# end
|
|
15
|
+
def set(key, options_or_shortcut={})
|
|
16
|
+
if block_given?
|
|
17
|
+
packer = FilterCapture.new options_or_shortcut
|
|
18
|
+
yield packer
|
|
19
|
+
end
|
|
20
|
+
self[key] = options_or_shortcut
|
|
21
|
+
self
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def initialize(klass) #:nodoc:
|
|
25
|
+
@klass = klass
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def lookup(key) #:nodoc:
|
|
29
|
+
k = @klass
|
|
30
|
+
begin
|
|
31
|
+
if found = Packers.for(k)[key]
|
|
32
|
+
return found
|
|
33
|
+
end
|
|
34
|
+
k = k.superclass
|
|
35
|
+
end while k
|
|
36
|
+
SPECIAL.include?(key) ? {} : raise("Unknown option #{key} for #{@klass}")
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def finalize(options) #:nodoc:
|
|
40
|
+
options = lookup(options) while options.is_a? Symbol
|
|
41
|
+
lookup(:merge_all).merge(options)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
@@packers_for_class = Hash.new{|h, klass| h[klass] = Packers.new(klass)}
|
|
45
|
+
|
|
46
|
+
# Returns the configuration for the given +klass+.
|
|
47
|
+
def self.for(klass)
|
|
48
|
+
@@packers_for_class[klass]
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def self.to_class_option_list(*arg) #:nodoc:
|
|
52
|
+
r = []
|
|
53
|
+
until arg.empty? do
|
|
54
|
+
k, options = original = arg.shift
|
|
55
|
+
k, options = global_lookup(k) if k.is_a? Symbol
|
|
56
|
+
options ||= arg.first.is_a?(Hash) ? arg.shift.tap{|o| original = [original, o]} : :default
|
|
57
|
+
r << [k, k.packers.finalize(options), original]
|
|
58
|
+
end
|
|
59
|
+
r
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def self.to_object_option_list(*arg) #:nodoc:
|
|
63
|
+
r=[]
|
|
64
|
+
until arg.empty? do
|
|
65
|
+
obj = arg.shift
|
|
66
|
+
options = case arg.first
|
|
67
|
+
when Hash, Symbol
|
|
68
|
+
arg.shift
|
|
69
|
+
else
|
|
70
|
+
:default
|
|
71
|
+
end
|
|
72
|
+
r << [obj, obj.class.packers.finalize(options)]
|
|
73
|
+
end
|
|
74
|
+
r
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
def self.global_lookup(key) #:nodoc:
|
|
79
|
+
@@packers_for_class.each do |klass, packers|
|
|
80
|
+
if options = packers[key]
|
|
81
|
+
return [klass, options]
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
raise "Couldn't find packing option #{key}"
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Use to capture the blocks given to read/write
|
|
91
|
+
class FilterCapture #:nodoc:
|
|
92
|
+
attr_accessor :options
|
|
93
|
+
def initialize(options)
|
|
94
|
+
self.options = options
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def read(&block)
|
|
98
|
+
options[:read_packed] = block
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def write(&block)
|
|
102
|
+
options[:write_packed] = block.unbind
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
end
|
data/lib/packable.rb
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
require File.dirname(__FILE__)+'/packable/jungle_survival_kit'
|
|
2
|
+
|
|
3
|
+
require_relative 'packable/packers'
|
|
4
|
+
require_relative 'packable/mixin'
|
|
5
|
+
[Object, Array, String, Integer, Float, IO, Proc].each do |klass|
|
|
6
|
+
require_relative 'packable/extensions/' + klass.name.downcase
|
|
7
|
+
klass.class_eval { include Packable::Extensions.const_get(klass.name) }
|
|
8
|
+
end
|
|
9
|
+
StringIO.class_eval { include Packable::Extensions::IO } # Since StringIO doesn't inherit from IO
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
require File.dirname(__FILE__) + '/test_helper'
|
|
2
|
+
# Warning: ugly...
|
|
3
|
+
class MyHeader < Struct.new(:signature, :nb_blocks)
|
|
4
|
+
include Packable
|
|
5
|
+
|
|
6
|
+
def write_packed(packedio, options)
|
|
7
|
+
packedio << [signature, {:bytes=>3}] << [nb_blocks, :short]
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def read_packed(packedio, options)
|
|
11
|
+
self.signature, self.nb_blocks = packedio >> [String, {:bytes => 3}] >> :short
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def ohoh
|
|
15
|
+
:ahah
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class PackableDocTest < Test::Unit::TestCase
|
|
21
|
+
def test_doc
|
|
22
|
+
|
|
23
|
+
assert_equal [1,2,3], StringIO.new("\000\001\000\002\000\003").each(:short).to_a
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
String.packers.set :flv_signature, :bytes => 3, :fill => "FLV"
|
|
27
|
+
|
|
28
|
+
assert_equal "xFL", "x".pack(:flv_signature)
|
|
29
|
+
|
|
30
|
+
String.packers do |p|
|
|
31
|
+
p.set :merge_all, :fill => "*" # Unless explicitly specified, :fill will now be "*"
|
|
32
|
+
p.set :default, :bytes => 8 # If no option is given, this will act as default
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
assert_equal "ab******", "ab".pack
|
|
36
|
+
assert_equal "ab**", "ab".pack(:bytes=>4)
|
|
37
|
+
assert_equal "ab", "ab".pack(:fill => "!")
|
|
38
|
+
assert_equal "ab!!", "ab".pack(:fill => "!", :bytes => 4)
|
|
39
|
+
|
|
40
|
+
String.packers do |p|
|
|
41
|
+
p.set :creator, :bytes => 4
|
|
42
|
+
p.set :app_type, :creator
|
|
43
|
+
p.set :default, {} # Reset to a sensible default...
|
|
44
|
+
p.set :merge_all, :fill => " "
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
assert_equal "hello".pack(:app_type), "hell"
|
|
48
|
+
|
|
49
|
+
assert_equal [["sig", 1, "hello, w"]]*4,
|
|
50
|
+
[
|
|
51
|
+
lambda { |io| io >> :flv_signature >> Integer >> [String, {:bytes => 8}] },
|
|
52
|
+
lambda { |io| io.read(:flv_signature, Integer, [String, {:bytes => 8}]) },
|
|
53
|
+
lambda { |io| io.read(:flv_signature, Integer, String, {:bytes => 8}) },
|
|
54
|
+
lambda { |io| [io.read(:flv_signature), io.read(Integer), io.read(String, {:bytes => 8})] }
|
|
55
|
+
].map {|proc| proc.call(StringIO.new("sig\000\000\000\001hello, world"))}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
ex = "xFL\000\000\000BHello "
|
|
59
|
+
[
|
|
60
|
+
lambda { |io| io << "x".pack(:flv_signature) << 66.pack << "Hello".pack(:bytes => 8)}, # returns io
|
|
61
|
+
lambda { |io| io << ["x", 66, "Hello"].pack(:flv_signature, :default , {:bytes => 8})}, # returns io
|
|
62
|
+
lambda { |io| io.write("x", :flv_signature, 66, "Hello", {:bytes => 8}) }, # returns the # of bytes written
|
|
63
|
+
lambda { |io| io.packed << ["x",:flv_signature] << 66 << ["Hello", {:bytes => 8}] } # returns io.packed
|
|
64
|
+
].zip([StringIO, StringIO, ex.length, StringIO.new.packed.class]) do |proc, compare|
|
|
65
|
+
ios = StringIO.new
|
|
66
|
+
assert_operator compare, :===, proc.call(ios)
|
|
67
|
+
ios.rewind
|
|
68
|
+
assert_equal ex, ios.read, "With #{proc}"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
#insure StringIO class is not affected
|
|
72
|
+
ios = StringIO.new
|
|
73
|
+
ios.packed
|
|
74
|
+
ios << 66
|
|
75
|
+
ios.rewind
|
|
76
|
+
assert_equal "66", ios.read
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
String.packers.set :length_encoded do |packer|
|
|
80
|
+
packer.write { |io| io << length << self }
|
|
81
|
+
packer.read { |io| io.read(io.read(Integer)) }
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
assert_equal "\000\000\000\006hello!", "hello!".pack(:length_encoded)
|
|
85
|
+
assert_equal ["this", "is", "great!"], ["this", "is", "great!"].pack(*[:length_encoded]*3).unpack(*[:length_encoded]*3)
|
|
86
|
+
|
|
87
|
+
h = MyHeader.new("FLV", 65)
|
|
88
|
+
assert_equal "FLV\000A", h.pack
|
|
89
|
+
h2, = StringIO.new("FLV\000A") >> MyHeader
|
|
90
|
+
assert_equal h, h2
|
|
91
|
+
assert_equal h.ohoh, h2.ohoh
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
Object.packers.set :with_class do |packer|
|
|
96
|
+
packer.write { |io| io << [self.class.name, :length_encoded] << self }
|
|
97
|
+
packer.read do |io|
|
|
98
|
+
klass = eval(io.read(:length_encoded))
|
|
99
|
+
io.read(klass)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
ar = [42, MyHeader.new("FLV", 65)]
|
|
103
|
+
assert_equal ar, ar.pack(:with_class, :with_class).unpack(:with_class, :with_class)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
require File.dirname(__FILE__) + '/test_helper'
|
|
2
|
+
# Warning: ugly...
|
|
3
|
+
|
|
4
|
+
class XYZ
|
|
5
|
+
include Packable
|
|
6
|
+
def write_packed(io, options)
|
|
7
|
+
io << "xyz"
|
|
8
|
+
end
|
|
9
|
+
def self.unpack_string(s, options)
|
|
10
|
+
raise "baddly packed XYZ: #{s}" unless "xyz" == s
|
|
11
|
+
XYZ.new
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
class TestingPack < Test::Unit::TestCase
|
|
16
|
+
|
|
17
|
+
context "Original form" do
|
|
18
|
+
should "pack like before" do
|
|
19
|
+
assert_equal "a \000\000\000\001", ["a",1,66].pack("A3N")
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
should "be equivalent to new form" do
|
|
23
|
+
assert_equal ["a",1,2.34, 66].pack({:bytes=>3}, {:bytes=>4, :endian=>:big}, {:precision=>:double, :endian=>:big}), ["a",1,2.34, 66].pack("A3NG")
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def test_shortcuts
|
|
28
|
+
assert_equal 0x123456.pack(:short), 0x123456.pack(:bytes => 2)
|
|
29
|
+
assert_equal 0x3456, 0x123456.pack(:short).unpack(:short)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def test_custom_form
|
|
33
|
+
assert_equal "xyz", XYZ.new.pack
|
|
34
|
+
assert_equal XYZ, "xyz".unpack(XYZ).class
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def test_pack_default
|
|
38
|
+
assert_equal "\000\000\000\006", 6.pack
|
|
39
|
+
assert_equal "abcd", "abcd".pack
|
|
40
|
+
assert_equal "\000\000\000\006abcd", [6,"abcd"].pack
|
|
41
|
+
String.packers.set :flv_signature, :bytes => 3, :fill => "FLV"
|
|
42
|
+
assert_equal "xFL", "x".pack(:flv_signature)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def test_integer
|
|
46
|
+
assert_equal "\002\001\000", 258.pack(:bytes => 3, :endian => :small)
|
|
47
|
+
assert_equal (1<<24)-1, -1.pack(:bytes => 3).unpack(Integer, :bytes => 3, :signed => false)
|
|
48
|
+
assert_equal -1, -1.pack(:bytes => 3).unpack(Integer, :bytes => 3, :signed => true)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def test_io
|
|
52
|
+
io = StringIO.new("\000\000\000\006abcd!")
|
|
53
|
+
n, s = io >> [Fixnum, {:signed=>false}] >> [String, {:bytes => 4}]
|
|
54
|
+
assert_equal n, 6
|
|
55
|
+
assert_equal s, "abcd"
|
|
56
|
+
assert_equal "!", io.read
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
context "Filters" do
|
|
60
|
+
context "for Object" do
|
|
61
|
+
Object.packers.set :generic_class_writer do |packer|
|
|
62
|
+
packer.write do |io|
|
|
63
|
+
io << self.class.name << self
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
should "be follow accessible everywhere" do
|
|
67
|
+
assert_equal "StringHello", "Hello".pack(:generic_class_writer)
|
|
68
|
+
assert_equal "Fixnum\000\000\000\006", 6.pack(:generic_class_writer)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
context "for a specific class" do
|
|
72
|
+
String.packers.set :specific_writer do |packer|
|
|
73
|
+
packer.write do |io|
|
|
74
|
+
io << "Hello"
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
should "be accessible only from that class and descendants" do
|
|
79
|
+
assert_equal "Hello", "World".pack(:specific_writer)
|
|
80
|
+
assert_raise RuntimeError do
|
|
81
|
+
6.pack(:specific_writer)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
end
|
data/test/test_helper.rb
ADDED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: marcandre-packable
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.1.
|
|
4
|
+
version: 1.1.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- "Marc-Andr\xC3\xA9 Lafortune"
|
|
@@ -9,7 +9,7 @@ autorequire:
|
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
11
|
|
|
12
|
-
date: 2008-12-
|
|
12
|
+
date: 2008-12-19 00:00:00 -08:00
|
|
13
13
|
default_executable:
|
|
14
14
|
dependencies: []
|
|
15
15
|
|
|
@@ -21,13 +21,36 @@ extensions: []
|
|
|
21
21
|
|
|
22
22
|
extra_rdoc_files: []
|
|
23
23
|
|
|
24
|
-
files:
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
files:
|
|
25
|
+
- CHANGELOG.rdoc
|
|
26
|
+
- README.rdoc
|
|
27
|
+
- VERSION.yml
|
|
28
|
+
- lib/packable
|
|
29
|
+
- lib/packable/extensions
|
|
30
|
+
- lib/packable/extensions/array.rb
|
|
31
|
+
- lib/packable/extensions/float.rb
|
|
32
|
+
- lib/packable/extensions/integer.rb
|
|
33
|
+
- lib/packable/extensions/io.rb
|
|
34
|
+
- lib/packable/extensions/object.rb
|
|
35
|
+
- lib/packable/extensions/proc.rb
|
|
36
|
+
- lib/packable/extensions/string.rb
|
|
37
|
+
- lib/packable/jungle_survival_kit.rb
|
|
38
|
+
- lib/packable/mixin.rb
|
|
39
|
+
- lib/packable/packers.rb
|
|
40
|
+
- lib/packable.rb
|
|
41
|
+
- test/packing_doc_test.rb
|
|
42
|
+
- test/packing_test.rb
|
|
43
|
+
- test/test_helper.rb
|
|
44
|
+
has_rdoc: true
|
|
27
45
|
homepage: http://github.com/marcandre/packable
|
|
28
46
|
post_install_message:
|
|
29
|
-
rdoc_options:
|
|
30
|
-
|
|
47
|
+
rdoc_options:
|
|
48
|
+
- --title
|
|
49
|
+
- Packable library
|
|
50
|
+
- --main
|
|
51
|
+
- README.rdoc
|
|
52
|
+
- --line-numbers
|
|
53
|
+
- --inline-source
|
|
31
54
|
require_paths:
|
|
32
55
|
- lib
|
|
33
56
|
required_ruby_version: !ruby/object:Gem::Requirement
|