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 ADDED
@@ -0,0 +1,9 @@
1
+ = Packable --- History
2
+
3
+ == Version 1.1 - December 17, 2008
4
+ Fixed bug when packing objects implementing to_ary
5
+ Added inheritance of shortcuts & filters to documentation
6
+
7
+ == Version 1.0 - December 17, 2008
8
+
9
+ === Initial release.
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,4 @@
1
+ ---
2
+ patch: 1
3
+ major: 1
4
+ minor: 1
@@ -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
@@ -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 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.0
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-17 00:00:00 -08:00
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
- has_rdoc: false
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