packable 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
+