marcandre-packable 1.1.0 → 1.1.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|