packable 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG.rdoc ADDED
@@ -0,0 +1,13 @@
1
+ = Packable --- History
2
+
3
+ == Version 1.2 - April 2nd, 2009
4
+ Compatible with ruby 1.9.1.
5
+ The 'jungle_survival_kit' is now in its own 'backports' gem.
6
+
7
+ == Version 1.1 - December 17, 2008
8
+ Fixed bug when packing objects implementing to_ary
9
+ Added inheritance of shortcuts & filters to documentation
10
+
11
+ == Version 1.0 - December 17, 2008
12
+
13
+ === Initial release.
data/LICENSE ADDED
@@ -0,0 +1,26 @@
1
+ # packable library
2
+ # Copyright (c) 2008, Marc-André Lafortune.
3
+ # All rights reserved.
4
+ # Licensed under the terms of the (modified) BSD License below:
5
+ #
6
+ # Redistribution and use in source and binary forms, with or without
7
+ # modification, are permitted provided that the following conditions are met:
8
+ # * Redistributions of source code must retain the above copyright
9
+ # notice, this list of conditions and the following disclaimer.
10
+ # * Redistributions in binary form must reproduce the above copyright
11
+ # notice, this list of conditions and the following disclaimer in the
12
+ # documentation and/or other materials provided with the distribution.
13
+ # * Neither the name of the author nor the
14
+ # names of its contributors may be used to endorse or promote products
15
+ # derived from this software without specific prior written permission.
16
+ #
17
+ # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ''AS IS'' AND ANY
18
+ # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
19
+ # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
20
+ # DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
21
+ # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
22
+ # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
23
+ # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
24
+ # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
+ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
26
+ # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
data/README.rdoc ADDED
@@ -0,0 +1,246 @@
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
53
+
54
+ Designed to work with ruby 1.8 & 1.9.
55
+
56
+ = Documentation
57
+
58
+ == Packing and unpacking
59
+
60
+ The library was designed to be backward compatible, so the usual packing and unpacking methods still work as before.
61
+ All packable objects can also be packed directly (no need to use an array). For example:
62
+
63
+ 42.pack("n") ===> "\000*"
64
+
65
+ In a similar fashion, unpacking can done using class methods:
66
+
67
+ Integer.unpack("\000*", "n") ===> 42
68
+
69
+ == Formats
70
+
71
+ Although the standard string formats can still be used, it is possible to pass a list of options (see example in feature summary).
72
+ These are the options for core types:
73
+
74
+ === Integer
75
+ [+bytes+] Number of bytes (default is 4) to use.
76
+ [+endian+] Either <tt>:big</tt> (or :network, default) or <tt>:little</tt>.
77
+ [+signed+] Either +true+ (default) or +false+. This will make a difference only when unpacking.
78
+
79
+ === Float
80
+ [+precision+] Either <tt>:single</tt> (default) or <tt>:double</tt>.
81
+ [+endian+] Either <tt>:big</tt> (or :network, default) or <tt>:little</tt>.
82
+
83
+ === String
84
+ [+bytes+] Total length (default is the full length)
85
+ [+fill+] The string to use for filling when packing a string shorter than the specified bytes option. Default is a space.
86
+
87
+ === Array
88
+ [+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.
89
+
90
+ When unpacking, it is necessary to specify the class in addition to any option, like so:
91
+
92
+ "AB".unpack(Integer, :bytes => 2, :endian => :big, :signed => false) ===> 0x3132
93
+
94
+ == Shortcuts and default values
95
+
96
+ It's easy to add shortcuts for easier (un)packing:
97
+
98
+ String.packers.set :flv_signature, :bytes => 3, :fill => "FLV"
99
+
100
+ "x".pack(:flv_signature) ===> "xFL"
101
+
102
+ Two shortcut names have special meanings: +default+ and +merge_all+. +default+ specifies the options to use when
103
+ nothing is specified, while +merge_all+ will be merged with all options. For example:
104
+
105
+ String.packers do |p|
106
+ p.set :merge_all, :fill => "*" # Unless explicitly specified, :fill will now be "*"
107
+ p.set :default, :bytes => 8 # If no option is given, this will act as default
108
+ end
109
+
110
+ "ab".pack ===> "ab******"
111
+ "ab".pack(:bytes=>4) ===> "ab**"
112
+ "ab".pack(:fill => "!") ===> "ab" # Not "ab!!"
113
+
114
+ A shortcut can refer to another shortcut, as so:
115
+
116
+ String.packers do |p|
117
+ p.set :creator, :bytes => 4
118
+ p.set :app_type, :creator
119
+ end
120
+ "hello".pack(:app_type) ===> "hell"
121
+
122
+ The following shortcuts and defaults are built-in the library:
123
+
124
+ === Integer
125
+ :merge_all => :bytes=>4, :signed=>true, :endian=>:big
126
+ :default => :long
127
+ :long => {}
128
+ :short => :bytes=>2
129
+ :byte => :bytes=>1
130
+ :unsigned_long => :bytes=>4, :signed=>false
131
+ :unsigned_short => :bytes=>2, :signed=>false
132
+
133
+ === Float
134
+ :merge_all => :precision => :single, :endian => :big
135
+ :default => :float
136
+ :double => :precision => :double
137
+ :float => {}
138
+
139
+ === String
140
+ :merge_all => :fill => " "
141
+
142
+ == Files and StringIO
143
+
144
+ 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):
145
+
146
+ io >> :flv_signature >> Integer >> [String, {:bytes => 8}]
147
+ io.read(:flv_signature, Integer, [String, {:bytes => 8}])
148
+ io.read(:flv_signature, Integer, String, {:bytes => 8})
149
+ [io.read(:flv_signature), io.read(Integer), io.read(String, :bytes => 8)]
150
+
151
+ In a similar fashion, these have the same effect although the return value is different
152
+
153
+ io << "x".pack(:flv_signature) << 66.pack << "Hello".pack(:bytes => 8) # returns io
154
+ io << ["x", 66, "Hello"].pack(:flv_signature, {} , {:bytes => 8}) # returns io
155
+ io.write("x", :flv_signature, 66, "Hello", {:bytes => 8}) # returns the # of bytes written
156
+ io.packed << ["x",:flv_signature] << 66 << ["Hello", {:bytes => 8}] # returns a "packed io"
157
+
158
+ The last example shows how <tt>io.packed</tt> returns a special IO object (a packing IO) that will pack arguments before writing it.
159
+ This is to insure compatibility with the usual behavior of IO objects:
160
+ io << 66 ==> appends "66"
161
+ io.packed << 66 ==> appends "\000\000\000B"
162
+
163
+ We "cheated" in the previous example; instead of writing <tt>io.packed.write(...)</tt> we used the shorter form.
164
+ This works because we're passing more than one argument; for only one argument we must call <tt>io.packed.write(66)</tt>
165
+ less the usual +write+ method is called.
166
+
167
+ 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.
168
+ Note that reading one value only will return that value directly, not an array containing that value:
169
+
170
+ io.read(Integer) ===> 42, not [42]
171
+ io.read(Integer,Integer) ===> [42,43]
172
+ io << Integer ===> [42]
173
+
174
+ == Custom classes
175
+
176
+ Including the mixin +Packable+ will make a class (un)packable. Packable relies on +write_packed+
177
+ and unpacking on +read_packed+. For example:
178
+
179
+ class MyHeader < Struct.new(:signature, :nb_blocks)
180
+ include Packable
181
+
182
+ def write_packed(packedio, options)
183
+ packedio << [signature, {:bytes=>3}] << [nb_blocks, :short]
184
+ end
185
+
186
+ def self.read_packed(packedio, options)
187
+ h = MyHeader.new
188
+ h.signature, h.nb_blocks = packedio >> [String, {:bytes => 3}] >> :short
189
+ h
190
+ end
191
+ end
192
+
193
+ We used the argument name +packedio+ to remind us that these are packed IO objects, i.e.
194
+ they will write their arguments after packing them instead of converting them to string like normal IO objects.
195
+ With this definition, +MyHeader+ can be both packed and unpacked:
196
+
197
+ h = MyHeader.new("FLV", 65)
198
+ h.pack ===> "FLV\000A"
199
+ StringIO.new("FLV\000A") >> Signature ===> [a copy of h]
200
+
201
+ A default <tt>self.read_packed</tt> is provided by the +Packable+ mixin, which allows you to define +read_packed+ as
202
+ an instance method instead of a class method. In that case, +read_packed+ instance method is called with
203
+ the same arguments and should modify +self+ accordingly (instead of returning a new object).
204
+ It is not necessary to return +self+. The previous example can thus be shortened:
205
+
206
+ class MyHeader
207
+ #...
208
+ def read_packed(packedio, options)
209
+ self.signature, self.nb_blocks = packedio >> [String, {:bytes => 3}] >> :short
210
+ end
211
+ end
212
+
213
+ == Filter
214
+
215
+ 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:
216
+
217
+ String.packers.set :length_encoded do |packer|
218
+ packer.write { |packedio| packedio << length << self }
219
+ packer.read { |packedio| packedio.read(packedio.read(Integer)) }
220
+ end
221
+
222
+ "hello!".pack(:length_encoded) ===> "\000\000\000\006hello!"
223
+ ["this", "is", "great!"].pack(*[:length_encoded]*3).unpack(*[:length_encoded]*3) ===> ["this", "is", "great!"]
224
+
225
+ Note that the +write+ block will be executed as an instance method (which is why we could use +length+ & +self+),
226
+ while +read+ is a normal block that must return the newly read object.
227
+
228
+ == Inheritance
229
+
230
+ A final note to say that packers are inherited in some way. For instance one could define a filter for all objects:
231
+
232
+ Object.packers.set :with_class do |packer|
233
+ packer.write { |io| io << [self.class.name, :length_encoded] << self }
234
+ packer.read do |io|
235
+ klass = eval(io.read(:length_encoded))
236
+ io.read(klass)
237
+ end
238
+ end
239
+
240
+ [42, MyHeader.new("Wow", 1)].pack(:with_class, :with_class).unpack(:with_class, :with_class) ===> [42, MyHeader.new("Wow", 1)]
241
+
242
+ = License
243
+
244
+ packable is licensed under the terms of the (modified) BSD License, see the included LICENSE file.
245
+
246
+ Author:: Marc-André Lafortune
data/VERSION.yml ADDED
@@ -0,0 +1,4 @@
1
+ ---
2
+ :patch: 0
3
+ :major: 1
4
+ :minor: 2
data/lib/packable.rb ADDED
@@ -0,0 +1,9 @@
1
+ require 'rubygems'
2
+ require 'backports'
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,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", :network => "G", :little => "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! unless options[:endian] == :little
29
+ io << chars.join
30
+ end
31
+
32
+ module ClassMethods #:nodoc:
33
+ def unpack_string(s,options)
34
+ s = s.reverse if options[:endian] == :little
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,96 @@
1
+ require 'enumerator'
2
+ Enumerator = Enumerable::Enumerator unless defined?(Enumerator)
3
+
4
+ module Packable
5
+ module Extensions #:nodoc:
6
+ module IO
7
+ def self.included(base) #:nodoc:
8
+ base.alias_method_chain :read, :packing
9
+ base.alias_method_chain :write, :packing
10
+ base.alias_method_chain :each, :packing
11
+ attr_accessor :throw_on_error
12
+ end
13
+
14
+ # Returns the change in io.pos caused by the block.
15
+ # Has nothing to do with packing, but quite helpful and so simple...
16
+ def pos_change(&block)
17
+ delta =- pos
18
+ yield
19
+ delta += pos
20
+ end
21
+
22
+ # Usage:
23
+ # io >> Class
24
+ # io >> [Class, options]
25
+ # io >> :shortcut
26
+ def >> (options)
27
+ r = []
28
+ class << r
29
+ attr_accessor :stream
30
+ def >> (options)
31
+ self << stream.read(options)
32
+ end
33
+ end
34
+ r.stream = self
35
+ r >> options
36
+ end
37
+
38
+ # Returns (or yields) a modified IO object that will always pack/unpack when writing/reading.
39
+ def packed
40
+ packedio = clone
41
+ packedio.set_encoding("ascii-8bit") if packedio.respond_to? :set_encoding
42
+ class << packedio
43
+ def << (arg)
44
+ arg = [arg, :default] unless arg.instance_of?(::Array)
45
+ pack_and_write(*arg)
46
+ self
47
+ end
48
+ def packed
49
+ block_given? ? yield(self) : self
50
+ end
51
+ alias_method :write, :pack_and_write #bypass test for argument length
52
+ end
53
+ block_given? ? yield(packedio) : packedio
54
+ end
55
+
56
+ def each_with_packing(*options, &block)
57
+ return each_without_packing(*options, &block) if (Integer === options.first) || (String === options.first)
58
+ return Enumerator.new(self, :each_with_packing, *options) unless block_given?
59
+ yield read(*options) until eof?
60
+ end
61
+
62
+ def write_with_packing(*arg)
63
+ (arg.length == 1) ? write_without_packing(*arg) : pack_and_write(*arg)
64
+ end
65
+
66
+ def read_with_packing(*arg)
67
+ return read_without_packing(*arg) if (arg.length == 0) || (arg.first.is_a?(Numeric) && (arg.length == 1))
68
+ values = Packable::Packers.to_class_option_list(*arg).map do |klass, options, original|
69
+ if eof?
70
+ raise EOFError, "End of IO when attempting to read #{klass} with options #{original.inspect}" if @throw_on_eof
71
+ nil
72
+ elsif options[:read_packed]
73
+ options[:read_packed].call(self)
74
+ else
75
+ klass.read_packed(self, options)
76
+ end
77
+ end
78
+ return values.size > 1 ? values : values.first
79
+ end
80
+
81
+ def pack_and_write(*arg)
82
+ original_pos = pos
83
+ Packable::Packers.to_object_option_list(*arg).each do |obj, options|
84
+ if options[:write_packed]
85
+ options[:write_packed].bind(obj).call(self)
86
+ else
87
+ obj.write_packed(self, options)
88
+ end
89
+ end
90
+ pos - original_pos
91
+ end
92
+
93
+
94
+ end
95
+ end
96
+ 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,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,107 @@
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
+ raise TypeError, "Expected a class or symbol: #{k.inspect}" unless k.instance_of? Class
57
+ options ||= arg.first.is_a?(Hash) ? arg.shift.tap{|o| original = [original, o]} : :default
58
+ r << [k, k.packers.finalize(options), original]
59
+ end
60
+ r
61
+ end
62
+
63
+ def self.to_object_option_list(*arg) #:nodoc:
64
+ r=[]
65
+ until arg.empty? do
66
+ obj = arg.shift
67
+ options = case arg.first
68
+ when Hash, Symbol
69
+ arg.shift
70
+ else
71
+ :default
72
+ end
73
+ r << [obj, obj.class.packers.finalize(options)]
74
+ end
75
+ r
76
+ end
77
+
78
+ private
79
+ def self.global_lookup(key) #:nodoc:
80
+ @@packers_for_class.each do |klass, packers|
81
+ if options = packers[key]
82
+ return [klass, options]
83
+ end
84
+ end
85
+ raise "Couldn't find packing option #{key}"
86
+ end
87
+
88
+
89
+ end
90
+
91
+ # Use to capture the blocks given to read/write
92
+ class FilterCapture #:nodoc:
93
+ attr_accessor :options
94
+ def initialize(options)
95
+ self.options = options
96
+ end
97
+
98
+ def read(&block)
99
+ options[:read_packed] = block
100
+ end
101
+
102
+ def write(&block)
103
+ options[:write_packed] = block.unbind
104
+ end
105
+ end
106
+
107
+ end
@@ -0,0 +1,106 @@
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
+ p.set :eigth_bytes, :bytes => 8
46
+ end
47
+
48
+ assert_equal "hello".pack(:app_type), "hell"
49
+
50
+ assert_equal [["sig", 1, "hello, w"]]*4,
51
+ [
52
+ lambda { |io| io >> :flv_signature >> Integer >> [String, {:bytes => 8}] },
53
+ lambda { |io| io.read(:flv_signature, Integer, [String, {:bytes => 8}]) },
54
+ lambda { |io| io.read(:flv_signature, Integer, String, {:bytes => 8}) },
55
+ lambda { |io| [io.read(:flv_signature), io.read(Integer), io.read(String, {:bytes => 8})] }
56
+ ].map {|proc| proc.call(StringIO.new("sig\000\000\000\001hello, world"))}
57
+
58
+
59
+ ex = "xFL\000\000\000BHello "
60
+ [
61
+ lambda { |io| io << "x".pack(:flv_signature) << 66.pack << "Hello".pack(:bytes => 8)}, # returns io
62
+ lambda { |io| io << ["x", 66, "Hello"].pack(:flv_signature, :default , {:bytes => 8})}, # returns io
63
+ lambda { |io| io.write("x", :flv_signature, 66, "Hello", {:bytes => 8}) }, # returns the # of bytes written
64
+ lambda { |io| io.packed << ["x",:flv_signature] << 66 << ["Hello", {:bytes => 8}] } # returns io.packed
65
+ ].zip([StringIO, StringIO, ex.length, StringIO.new.packed.class]) do |proc, compare|
66
+ ios = StringIO.new
67
+ assert_operator compare, :===, proc.call(ios)
68
+ ios.rewind
69
+ assert_equal ex, ios.read, "With #{proc}"
70
+ end
71
+
72
+ #insure StringIO class is not affected
73
+ ios = StringIO.new
74
+ ios.packed
75
+ ios << 66
76
+ ios.rewind
77
+ assert_equal "66", ios.read
78
+
79
+
80
+ String.packers.set :length_encoded do |packer|
81
+ packer.write { |io| io << length << self }
82
+ packer.read { |io| io.read(io.read(Integer)) }
83
+ end
84
+
85
+ assert_equal "\000\000\000\006hello!", "hello!".pack(:length_encoded)
86
+ assert_equal ["this", "is", "great!"], ["this", "is", "great!"].pack(*[:length_encoded]*3).unpack(*[:length_encoded]*3)
87
+
88
+ h = MyHeader.new("FLV", 65)
89
+ assert_equal "FLV\000A", h.pack
90
+ h2, = StringIO.new("FLV\000A") >> MyHeader
91
+ assert_equal h, h2
92
+ assert_equal h.ohoh, h2.ohoh
93
+
94
+
95
+
96
+ Object.packers.set :with_class do |packer|
97
+ packer.write { |io| io << [self.class.name, :length_encoded] << self }
98
+ packer.read do |io|
99
+ klass = eval(io.read(:length_encoded))
100
+ io.read(klass)
101
+ end
102
+ end
103
+ ar = [42, MyHeader.new("FLV", 65)]
104
+ assert_equal ar, ar.pack(:with_class, :with_class).unpack(:with_class, :with_class)
105
+ end
106
+ end
@@ -0,0 +1,93 @@
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 => :little)
47
+ assert_equal 258, Integer.unpack("\002\001\000", :bytes => 3, :endian => :little)
48
+ assert_equal (1<<24)-1, -1.pack(:bytes => 3).unpack(Integer, :bytes => 3, :signed => false)
49
+ assert_equal -1, -1.pack(:bytes => 3).unpack(Integer, :bytes => 3, :signed => true)
50
+ end
51
+
52
+ def test_io
53
+ io = StringIO.new("\000\000\000\006abcdE!")
54
+ n, s, c = io >> [Fixnum, {:signed=>false}] >> [String, {:bytes => 4}] >> :char
55
+ assert_equal 6, n
56
+ assert_equal "abcd", s
57
+ assert_equal 69, c
58
+ assert_equal "!", io.read
59
+ end
60
+
61
+ should "do basic type checking" do
62
+ assert_raise(TypeError) {"".unpack(42, :short)}
63
+ end
64
+
65
+ context "Filters" do
66
+ context "for Object" do
67
+ Object.packers.set :generic_class_writer do |packer|
68
+ packer.write do |io|
69
+ io << self.class.name << self
70
+ end
71
+ end
72
+ should "be follow accessible everywhere" do
73
+ assert_equal "StringHello", "Hello".pack(:generic_class_writer)
74
+ assert_equal "Fixnum\000\000\000\006", 6.pack(:generic_class_writer)
75
+ end
76
+ end
77
+ context "for a specific class" do
78
+ String.packers.set :specific_writer do |packer|
79
+ packer.write do |io|
80
+ io << "Hello"
81
+ end
82
+ end
83
+
84
+ should "be accessible only from that class and descendants" do
85
+ assert_equal "Hello", "World".pack(:specific_writer)
86
+ assert_raise RuntimeError do
87
+ 6.pack(:specific_writer)
88
+ end
89
+ end
90
+ end
91
+ end
92
+
93
+ end
@@ -0,0 +1,8 @@
1
+ require 'rubygems'
2
+ require 'test/unit'
3
+ require 'shoulda'
4
+ require 'mocha'
5
+ require File.dirname(__FILE__)+'/../lib/packable'
6
+
7
+ class Test::Unit::TestCase
8
+ end
metadata ADDED
@@ -0,0 +1,88 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: packable
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.2.0
5
+ platform: ruby
6
+ authors:
7
+ - "Marc-Andr\xC3\xA9 Lafortune"
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-04-03 00:00:00 -04:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: marcandre-backports
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: "0"
24
+ version:
25
+ description: If you need to do read and write binary data, there is of course <Array::pack and String::unpack The packable library makes (un)packing nicer, smarter and more powerful.
26
+ email: github@marc-andre.ca
27
+ executables: []
28
+
29
+ extensions: []
30
+
31
+ extra_rdoc_files:
32
+ - README.rdoc
33
+ - LICENSE
34
+ files:
35
+ - CHANGELOG.rdoc
36
+ - README.rdoc
37
+ - VERSION.yml
38
+ - lib/packable
39
+ - lib/packable/extensions
40
+ - lib/packable/extensions/array.rb
41
+ - lib/packable/extensions/float.rb
42
+ - lib/packable/extensions/integer.rb
43
+ - lib/packable/extensions/io.rb
44
+ - lib/packable/extensions/object.rb
45
+ - lib/packable/extensions/proc.rb
46
+ - lib/packable/extensions/string.rb
47
+ - lib/packable/mixin.rb
48
+ - lib/packable/packers.rb
49
+ - lib/packable.rb
50
+ - test/packing_doc_test.rb
51
+ - test/packing_test.rb
52
+ - test/test_helper.rb
53
+ - LICENSE
54
+ has_rdoc: true
55
+ homepage: http://github.com/marcandre/packable
56
+ post_install_message:
57
+ rdoc_options:
58
+ - --title
59
+ - Packable library
60
+ - --main
61
+ - README.rdoc
62
+ - --line-numbers
63
+ - --inline-source
64
+ - --inline-source
65
+ - --charset=UTF-8
66
+ require_paths:
67
+ - lib
68
+ required_ruby_version: !ruby/object:Gem::Requirement
69
+ requirements:
70
+ - - ">="
71
+ - !ruby/object:Gem::Version
72
+ version: "0"
73
+ version:
74
+ required_rubygems_version: !ruby/object:Gem::Requirement
75
+ requirements:
76
+ - - ">="
77
+ - !ruby/object:Gem::Version
78
+ version: "0"
79
+ version:
80
+ requirements: []
81
+
82
+ rubyforge_project: marcandre
83
+ rubygems_version: 1.3.1
84
+ signing_key:
85
+ specification_version: 2
86
+ summary: Extensive packing and unpacking capabilities
87
+ test_files: []
88
+