marcandre-packable 1.1.0 → 1.1.1

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