packable-1.3.14/0000755000004100000410000000000013674353043013402 5ustar www-datawww-datapackable-1.3.14/test/0000755000004100000410000000000013674353043014361 5ustar www-datawww-datapackable-1.3.14/test/packing_doc_test.rb0000644000004100000410000000714713674353043020217 0ustar www-datawww-datarequire File.expand_path(File.dirname(__FILE__) + '/test_helper') # Warning: ugly... class MyHeader < Struct.new(:signature, :nb_blocks) include Packable def write_packed(packedio, options) packedio << [signature, {:bytes=>3}] << [nb_blocks, :short] end def read_packed(packedio, options) self.signature, self.nb_blocks = packedio >> [String, {:bytes => 3}] >> :short end def ohoh :ahah end end class PackableDocTest < Minitest::Test def test_doc assert_equal [1,2,3], StringIO.new("\000\001\000\002\000\003").each(:short).to_a String.packers.set :flv_signature, :bytes => 3, :fill => "FLV" assert_equal "xFL", "x".pack(:flv_signature) String.packers do |p| p.set :merge_all, :fill => "*" # Unless explicitly specified, :fill will now be "*" p.set :default, :bytes => 8 # If no option is given, this will act as default end assert_equal "ab******", "ab".pack assert_equal "ab**", "ab".pack(:bytes=>4) assert_equal "ab", "ab".pack(:fill => "!") assert_equal "ab!!", "ab".pack(:fill => "!", :bytes => 4) String.packers do |p| p.set :creator, :bytes => 4 p.set :app_type, :creator p.set :default, {} # Reset to a sensible default... p.set :merge_all, :fill => " " p.set :eigth_bytes, :bytes => 8 end assert_equal "hello".pack(:app_type), "hell" assert_equal [["sig", 1, "hello, w"]]*4, [ lambda { |io| io >> :flv_signature >> Integer >> [String, {:bytes => 8}] }, lambda { |io| io.read(:flv_signature, Integer, [String, {:bytes => 8}]) }, lambda { |io| io.read(:flv_signature, Integer, String, {:bytes => 8}) }, lambda { |io| [io.read(:flv_signature), io.read(Integer), io.read(String, {:bytes => 8})] } ].map {|proc| proc.call(StringIO.new("sig\000\000\000\001hello, world"))} ex = "xFL\000\000\000BHello " [ lambda { |io| io << "x".pack(:flv_signature) << 66.pack << "Hello".pack(:bytes => 8)}, # returns io lambda { |io| io << ["x", 66, "Hello"].pack(:flv_signature, :default , {:bytes => 8})}, # returns io lambda { |io| io.write("x", :flv_signature, 66, "Hello", {:bytes => 8}) }, # returns the # of bytes written lambda { |io| io.packed << ["x",:flv_signature] << 66 << ["Hello", {:bytes => 8}] } # returns io.packed ].zip([StringIO, StringIO, ex.length, StringIO.new.packed.class]) do |proc, compare| ios = StringIO.new assert_operator compare, :===, proc.call(ios) ios.rewind assert_equal ex, ios.read, "With #{proc}" end #insure StringIO class is not affected ios = StringIO.new ios.packed ios << 66 ios.rewind assert_equal "66", ios.read String.packers.set :length_encoded do |packer| packer.write { |io| io << length << self } packer.read { |io| io.read(io.read(Integer)) } end assert_equal "\000\000\000\006hello!", "hello!".pack(:length_encoded) assert_equal ["this", "is", "great!"], ["this", "is", "great!"].pack(*[:length_encoded]*3).unpack(*[:length_encoded]*3) h = MyHeader.new("FLV", 65) assert_equal "FLV\000A", h.pack h2, = StringIO.new("FLV\000A") >> MyHeader assert_equal h, h2 assert_equal h.ohoh, h2.ohoh Object.packers.set :with_class do |packer| packer.write { |io| io << [self.class.name, :length_encoded] << self } packer.read do |io| klass = eval(io.read(:length_encoded)) io.read(klass) end end ar = [42, MyHeader.new("FLV", 65)] assert_equal ar, ar.pack(:with_class, :with_class).unpack(:with_class, :with_class) end end packable-1.3.14/test/packing_test.rb0000644000004100000410000001115413674353043017363 0ustar www-datawww-datarequire File.expand_path(File.dirname(__FILE__) + '/test_helper') # Warning: ugly... class XYZ include Packable def write_packed(io, options) io << "xyz" end def self.unpack_string(s, options) raise "baddly packed XYZ: #{s}" unless "xyz" == s XYZ.new end end class TestingPack < Minitest::Test context "Original form" do should "pack like before" do assert_equal "a \000\000\000\001", ["a",1,66].pack("A3N") end should "be equivalent to new form" do 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") end end def test_shortcuts assert_equal 0x123456.pack(:short), 0x123456.pack(:bytes => 2) assert_equal 0x3456, 0x123456.pack(:short).unpack(:short) end def test_custom_form assert_equal "xyz", XYZ.new.pack assert_equal XYZ, "xyz".unpack(XYZ).class end def test_pack_default assert_equal "\000\000\000\006", 6.pack assert_equal "abcd", "abcd".pack assert_equal "\000\000\000\006abcd", [6,"abcd"].pack String.packers.set :flv_signature, :bytes => 3, :fill => "FLV" assert_equal "xFL", "x".pack(:flv_signature) end def test_integer assert_equal "\002\001\000", 258.pack(:bytes => 3, :endian => :little) assert_equal 258, Integer.unpack("\002\001\000", :bytes => 3, :endian => :little) assert_equal (1<<24)-1, -1.pack(:bytes => 3).unpack(Integer, :bytes => 3, :signed => false) assert_equal -1, -1.pack(:bytes => 3).unpack(Integer, :bytes => 3, :signed => true) assert_equal 42, 42.pack('L').unpack(Integer, :bytes => 4, :endian => :native) assert_raises(ArgumentError){ 42.pack(:endian => "Geronimo")} end def test_bignum assert_equal 1.pack(:long), ((1 << 69) + 1).pack(:long) assert_equal "*" + ("\000" * 15), (42 << (8*15)).pack(:bytes => 16) assert_equal 42 << (8*15), (42 << (8*15)).pack(:bytes => 16).unpack(Integer, :bytes => 16) assert_equal 42 << (8*15), (42 << (8*15)).pack(:bytes => 16).unpack(Bignum, :bytes => 16) end def test_float assert_raises(ArgumentError){ Math::PI.pack(:endian => "Geronimo")} assert_equal Math::PI, Math::PI.pack(:precision => :double, :endian => :native).unpack(Float, :precision => :double, :endian => :native) # Issue #1 assert_equal Math::PI.pack(:precision => :double), Math::PI.pack('G') assert_equal Math::PI.pack(:precision => :single), Math::PI.pack('g') assert_equal Math::PI.pack(:precision => :double), Math::PI.pack('G') end def test_io io = StringIO.new("\000\000\000\006abcdE!") n, s, c = io >> [Fixnum, {:signed=>false}] >> [String, {:bytes => 4}] >> :char assert_equal 6, n assert_equal "abcd", s assert_equal 69, c assert_equal "!", io.read end def test_io_read_nil # library was failing to call read_without_packing when invoked with nil. io = StringIO.new("should read(nil)") assert_equal "should read(nil)", io.read(nil) end def test_io_read_to_outbuf # library was failing to call read_without_packing when invoked with fixnum and output buffer. io = StringIO.new("should read(fixnum, buf)") io.read(11, outbuf='') assert_equal "should read", outbuf end should "do basic type checking" do assert_raises(TypeError) {"".unpack(42, :short)} end context "Reading beyond the eof" do should "raises an EOFError when reading" do ["", "x"].each do |s| io = StringIO.new(s) assert_raises(EOFError) {io.read(:double)} assert_raises(EOFError) {io.read(:short)} assert_raises(EOFError) {io.read(String, :bytes => 4)} end end should "return nil for unpacking" do assert_nil "".unpack(:double) assert_nil "".unpack(:short) assert_nil "x".unpack(:double) assert_nil "x".unpack(:short) end end context "Filters" do context "for Object" do Object.packers.set :generic_class_writer do |packer| packer.write do |io| io << self.class.name << self end end should "be follow accessible everywhere" do assert_equal "StringHello", "Hello".pack(:generic_class_writer) assert_equal "Fixnum\000\000\000\006", 6.pack(:generic_class_writer) end end context "for a specific class" do String.packers.set :specific_writer do |packer| packer.write do |io| io << "Hello" end end should "be accessible only from that class and descendants" do assert_equal "Hello", "World".pack(:specific_writer) assert_raises RuntimeError do 6.pack(:specific_writer) end end end end end packable-1.3.14/test/test_helper.rb0000644000004100000410000000023413674353043017223 0ustar www-datawww-datarequire 'rubygems' require 'minitest/autorun' require 'shoulda' require 'mocha' require File.dirname(__FILE__)+'/../lib/packable' class Minitest::Test end packable-1.3.14/.gitignore0000644000004100000410000000006313674353043015371 0ustar www-datawww-data*.sw? .DS_Store coverage rdoc .bundle Gemfile.lock packable-1.3.14/CHANGELOG.rdoc0000644000004100000410000000106013674353043015537 0ustar www-datawww-data= Packable --- History == Version 1.3 - April 8, 2009 Added :endian => :native for Integers and Floats (thanks to Jay Daley) Raises an exception if :endian is not one of :little, :big, :network or :native Added some tests for Bignum == Version 1.2 - April 2nd, 2009 Compatible with ruby 1.9.1. The 'jungle_survival_kit' is now in its own 'backports' gem. == Version 1.1 - December 17, 2008 Fixed bug when packing objects implementing to_ary Added inheritance of shortcuts & filters to documentation == Version 1.0 - December 17, 2008 === Initial release. packable-1.3.14/packable.gemspec0000644000004100000410000000217513674353043016516 0ustar www-datawww-data# -*- encoding: utf-8 -*- lib = File.expand_path('../lib', __FILE__) $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) require 'packable/version' Gem::Specification.new do |gem| gem.name = "packable" gem.version = Packable::VERSION gem.homepage = "https://github.com/marcandre/packable" gem.authors = ["Marc-André Lafortune"] gem.email = ["github@marc-andre.ca"] gem.description = %q{If you need to do read and write binary data, there is of course value, ... # PackableClass.packers { |p| p.set...; p.set... } # PackableClass.packers.set :shortcut, :another_shortcut # PackableClass.packers.set :shortcut do |packer| # packer.write{|io| io << self.something... } # packer.read{|io| Whatever.new(io.read(...)) } # end def set(key, options_or_shortcut={}) if block_given? packer = FilterCapture.new options_or_shortcut yield packer end self[key] = options_or_shortcut self end def initialize(klass) #:nodoc: @klass = klass end def lookup(key) #:nodoc: k = @klass begin if found = Packers.for(k)[key] return found end k = k.superclass end while k SPECIAL.include?(key) ? {} : raise("Unknown option #{key} for #{@klass}") end def finalize(options) #:nodoc: options = lookup(options) while options.is_a? Symbol lookup(:merge_all).merge(options) end @@packers_for_class = Hash.new{|h, klass| h[klass] = Packers.new(klass)} # Returns the configuration for the given +klass+. def self.for(klass) @@packers_for_class[klass] end def self.to_class_option_list(*arg) #:nodoc: r = [] until arg.empty? do k, options = original = arg.shift k, options = global_lookup(k) if k.is_a? Symbol raise TypeError, "Expected a class or symbol: #{k.inspect}" unless k.instance_of? Class options ||= arg.first.is_a?(Hash) ? arg.shift.tap{|o| original = [original, o]} : :default r << [k, k.packers.finalize(options), original] end r end def self.to_object_option_list(*arg) #:nodoc: r=[] until arg.empty? do obj = arg.shift options = case arg.first when Hash, Symbol arg.shift else :default end r << [obj, obj.class.packers.finalize(options)] end r end private def self.global_lookup(key) #:nodoc: @@packers_for_class.each do |klass, packers| if options = packers[key] return [klass, options] end end raise "Couldn't find packing option #{key}" end end # Use to capture the blocks given to read/write class FilterCapture #:nodoc: attr_accessor :options def initialize(options) self.options = options end def read(&block) options[:read_packed] = block end def write(&block) options[:write_packed] = block.unbind end end end packable-1.3.14/lib/packable/mixin.rb0000644000004100000410000000334713674353043017372 0ustar www-datawww-data# The Packable mixin itself... require 'stringio' module Packable def self.included(base) #:nodoc: base.class_eval do class << self include PackersClassMethod include ClassMethods end end end # +options+ can be a Hash, a shortcut (Symbol) or a String (old-style) def pack(options = :default) return [self].pack(options) if options.is_a? String (StringIO.new.packed << [self, options]).string end module PackersClassMethod # Returns or yields the Packers.for(class) # Normal use is packers.set ... # (see docs or Packers::set for usage) # def packers yield packers if block_given? Packers.for(self) end end module ClassMethods def unpack(s, options = :default) return s.unpack(options).first if options.is_a? String StringIO.new(s).packed.read(self, options) end # Default +read_packed+ calls either the instance method read_packed or the # class method +unpack_string+. Choose: # * define a class method +read_packed+ that returns the newly read object # * define an instance method +read_packed+ which reads the io into +self+ # * define a class method +unpack_string+ that reads and returns an object from the string. In this case, options[:bytes] should be specified! # def read_packed(io, options) if method_defined? :read_packed mandatory = instance_method(:initialize).arity mandatory = -1-mandatory if mandatory < 0 obj = new(*[nil]*mandatory) obj.read_packed(io, options) obj else len = options[:bytes] s = len ? io.read_exactly(len) : io.read unpack_string(s, options) unless s.nil? end end end end packable-1.3.14/lib/packable/extensions/0000755000004100000410000000000013674353043020111 5ustar www-datawww-datapackable-1.3.14/lib/packable/extensions/array.rb0000644000004100000410000000251313674353043021555 0ustar www-datawww-datarequire 'stringio' module Packable module Extensions #:nodoc: module Array #:nodoc: def self.included(base) base.class_eval do alias_method_chain :pack, :long_form include Packable extend ClassMethods end end def pack_with_long_form(*arg) return pack_without_long_form(*arg) if arg.first.is_a? String pio = StringIO.new.packed write_packed(pio, *arg) pio.string end def write_packed(io, *how) return io << self.original_pack(*how) if how.first.is_a? String how = [:repeat => :all] if how.empty? current = -1 how.each do |options| repeat = options.is_a?(Hash) ? options.delete(:repeat) || 1 : 1 repeat = length - 1 - current if repeat == :all repeat.times do io.write(self[current+=1],options) end end end module ClassMethods #:nodoc: def read_packed(io, *how) raise "Can't support builtin format for arrays" if (how.length == 1) && (how.first.is_a? String) how.inject [] do |r, options| repeat = options.is_a? Hash ? options.delete(:repeat) || 1 : 1 (0...repeat).inject r do r << io.read(options) end end end end end end end packable-1.3.14/lib/packable/extensions/integer.rb0000644000004100000410000000305513674353043022076 0ustar www-datawww-datamodule Packable module Extensions #:nodoc: module Integer #:nodoc: NEEDS_REVERSAL = Hash.new{|h, endian| raise ArgumentError, "Endian #{endian} is not valid. It must be one of #{h.keys.join(', ')}"}. merge!(:little => true, :big => false, :network => false, :native => "*\x00\x00\x00".unpack('L').first == 42).freeze def self.included(base) base.class_eval do include Packable extend ClassMethods packers do |p| p.set :merge_all , :bytes=>4, :signed=>true, :endian=>:big p.set :default , :long p.set :long , {} p.set :short , :bytes=>2 p.set :char , :bytes=>1, :signed=>false p.set :byte , :bytes=>1 p.set :unsigned_long , :bytes=>4, :signed=>false p.set :unsigned_short , :bytes=>2, :signed=>false end end end def write_packed(io, options) val = self chars = (0...options[:bytes]).collect do byte = val & 0xFF val >>= 8 byte.chr end chars.reverse! unless NEEDS_REVERSAL[options[:endian]] io << chars.join end module ClassMethods #:nodoc: def unpack_string(s,options) s = s.reverse if NEEDS_REVERSAL[options[:endian]] r = 0 s.each_byte {|b| r = (r << 8) + b} r -= 1 << (8 * options[:bytes]) if options[:signed] && (1 == r >> (8 * options[:bytes] - 1)) r end end end end end packable-1.3.14/lib/packable/extensions/string.rb0000644000004100000410000000155513674353043021752 0ustar www-datawww-datarequire 'stringio' module Packable module Extensions #:nodoc: module String #:nodoc: def self.included(base) base.class_eval do include Packable extend ClassMethods alias_method_chain :unpack, :long_form packers.set :merge_all, :fill => " " end end def write_packed(io, options) return io.write_without_packing(self) unless options[:bytes] io.write_without_packing(self[0...options[:bytes]].ljust(options[:bytes], options[:fill] || "\000")) end def unpack_with_long_form(*arg) return unpack_without_long_form(*arg) if arg.first.is_a? String StringIO.new(self).packed.read(*arg) rescue EOFError nil end module ClassMethods #:nodoc: def unpack_string(s, options) s end end end end end packable-1.3.14/lib/packable/extensions/float.rb0000644000004100000410000000274713674353043021555 0ustar www-datawww-datamodule Packable module Extensions #:nodoc: module Float #:nodoc: def self.included(base) base.class_eval do include Packable extend ClassMethods packers do |p| p.set :merge_all, :precision => :single, :endian => :big p.set :double , :precision => :double p.set :float , {} p.set :default , :float end end end def write_packed(io, options) io << pack(self.class.pack_option_to_format(options)) end module ClassMethods #:nodoc: ENDIAN_TO_FORMAT = Hash.new{|h, endian| raise ArgumentError, "Endian #{endian} is not valid. It must be one of #{h.keys.join(', ')}."}. merge!(:big => "G", :network => "G", :little => "E", :native => "D").freeze FORMAT_TO_SINGLE_PRECISION = {'G' => 'g', 'E' => 'e', 'D' => 'f'}.freeze PRECISION = Hash.new{|h, precision| raise ArgumentError, "Precision #{precision} is not valid. It must be one of #{h.keys.join(', ')}."}. merge!(:single => 4, :double => 8).freeze def pack_option_to_format(options) format = ENDIAN_TO_FORMAT[options[:endian]] format = FORMAT_TO_SINGLE_PRECISION[format] if options[:precision] == :single format end def read_packed(io, options) s = io.read_exactly(PRECISION[options[:precision]]) s && s.unpack(pack_option_to_format(options)).first end end end end end packable-1.3.14/lib/packable/extensions/proc.rb0000644000004100000410000000063413674353043021404 0ustar www-datawww-datamodule Packable module Extensions #:nodoc: module Proc # A bit of wizardry to return an +UnboundMethod+ which can be bound to any object def unbind Object.send(:define_method, :__temp_bound_method, &self) Object.instance_method(:__temp_bound_method) end # Shortcut for unbind.bind(to) def bind(to) unbind.bind(to) end end end end packable-1.3.14/lib/packable/extensions/io.rb0000644000004100000410000000712413674353043021051 0ustar www-datawww-datarequire 'enumerator' Enumerator = Enumerable::Enumerator unless defined?(Enumerator) module Packable module Extensions #:nodoc: module IO def self.included(base) #:nodoc: base.alias_method_chain :read, :packing base.alias_method_chain :write, :packing base.alias_method_chain :each, :packing end # Methods supported by seekable streams. SEEKABLE_API = %i[pos pos= seek rewind].freeze # Check whether can seek without errors. def seekable? if !defined?(@seekable) @seekable = # The IO class throws an exception at runtime if we try to change # position on a non-regular file. if respond_to?(:stat) stat.file? else # Duck-type the rest of this. SEEKABLE_API.all? { |m| respond_to?(m) } end end @seekable end # Returns the change in io.pos caused by the block. # Has nothing to do with packing, but quite helpful and so simple... def pos_change(&block) delta =- pos yield delta += pos end # Usage: # io >> Class # io >> [Class, options] # io >> :shortcut def >> (options) r = [] class << r attr_accessor :stream def >> (options) self << stream.read(options) end end r.stream = self r >> options end # Returns (or yields) a modified IO object that will always pack/unpack when writing/reading. def packed packedio = clone packedio.set_encoding("ascii-8bit") if packedio.respond_to? :set_encoding class << packedio def << (arg) arg = [arg, :default] unless arg.instance_of?(::Array) pack_and_write(*arg) self end def packed block_given? ? yield(self) : self end alias_method :write, :pack_and_write #bypass test for argument length end block_given? ? yield(packedio) : packedio end def each_with_packing(*options, &block) return each_without_packing(*options, &block) if options.empty? || (Integer === options.first) || (String === options.first) || !seekable? return Enumerator.new(self, :each_with_packing, *options) unless block_given? yield read(*options) until eof? end def write_with_packing(*arg) (arg.length <= 1 || !seekable?) ? write_without_packing(*arg) : pack_and_write(*arg) end def read_with_packing(*arg) return read_without_packing(*arg) if arg.empty? || arg.first.nil? || arg.first.is_a?(Numeric) || !seekable? values = Packable::Packers.to_class_option_list(*arg).map do |klass, options, original| if options[:read_packed] options[:read_packed].call(self) else klass.read_packed(self, options) end end return values.size > 1 ? values : values.first end # returns a string of exactly n bytes, or else raises an EOFError def read_exactly(n) return "" if n.zero? s = read_without_packing(n) raise EOFError if s.nil? || s.length < n s end def pack_and_write(*arg) original_pos = pos Packable::Packers.to_object_option_list(*arg).each do |obj, options| if options[:write_packed] options[:write_packed].bind(obj).call(self) else obj.write_packed(self, options) end end pos - original_pos end end end end packable-1.3.14/lib/packable/extensions/object.rb0000644000004100000410000000046413674353043021710 0ustar www-datawww-datamodule Packable module Extensions #:nodoc: module Object #:nodoc: def self.included(base) #:nodoc: base.class_eval do class << self # include only packers method into Object include PackersClassMethod end end end end end endpackable-1.3.14/lib/packable.rb0000644000004100000410000000074113674353043016241 0ustar www-datawww-datarequire "packable/version" require 'backports/tools/alias_method_chain' require 'backports/rails/module' require_relative 'packable/packers' require_relative 'packable/mixin' [Object, Array, String, Integer, Float, IO, Proc].each do |klass| require_relative 'packable/extensions/' + klass.name.downcase klass.class_eval { include Packable::Extensions.const_get(klass.name) } end StringIO.class_eval { include Packable::Extensions::IO } # Since StringIO doesn't inherit from IO packable-1.3.14/README.rdoc0000644000004100000410000002341213674353043015212 0ustar www-datawww-data= Packable Library - Intro *NOTE:* This library monkeypatches core classes and wa designed for Ruby 1.8 & 1.9. A redesign using refinements would be much better. Minimal support is provided. 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. In case you are wondering why on earth someone would want to do serious (un)packing when YAML & XML are built-in: I wrote this library to read and write FLV files... == Feature summary: === Explicit forms Strings, integers & floats have long forms instead of the cryptic letter notation. For example: ["answer", 42].pack("C3n") can be written as: ["answer", 42].pack({:bytes => 3}, {:bytes => 2, :endian => :big}) This can look a bit too verbose, so let's introduce shortcuts right away: === Shortcuts Most commonly used options have shortcuts and you can define your own. For example: :unsigned_long <===> {:bytes => 4, :signed => false, :endian => :big} === IO IO classes (File & StringIO) can use (un)packing routines. For example: signature, block_len, temperature = my_file >> [String, :bytes=>3] >> Integer >> :float The method +each+ also accepts packing options: StringIO.new("\000\001\000\002\000\003").each(:short).to_a ===> [1,2,3] === Custom classes It's easy to make you own classes (un)packable. All the previous goodies are thus available: File.open("great_flick.flv") do |f| head = f.read(FLV::Header) f.each(FLV::Tag) do |tag| # do something meaningful with each tag... end end === Filters It's also easy to define special shortcuts that will call blocks to (un)pack any class. As an example, this could be useful to add special packing features to String (without monkey patching String::pack). == Installation First, ensure that you're running at least RubyGems 1.2 (check gem --version if you're not sure -- to update: sudo gem update --system). Add GitHub to your gem sources (if you haven't already): sudo gem sources -a http://gems.github.com Get the gem: sudo gem install marcandre-packable That's it! Simply require 'packable' in your code to use it. == Compatibility Designed to work with ruby 1.8 & 1.9. = Documentation == Packing and unpacking The library was designed to be backward compatible, so the usual packing and unpacking methods still work as before. All packable objects can also be packed directly (no need to use an array). For example: 42.pack("n") ===> "\000*" In a similar fashion, unpacking can done using class methods: Integer.unpack("\000*", "n") ===> 42 == Formats Although the standard string formats can still be used, it is possible to pass a list of options (see example in feature summary). These are the options for core types: === Integer [+bytes+] Number of bytes (default is 4) to use. [+endian+] Either :big (or :network, default), :little or :native. [+signed+] Either +true+ (default) or +false+. This will make a difference only when unpacking. === Float [+precision+] Either :single (default) or :double. [+endian+] Either :big (or :network, default), :little or :native. === String [+bytes+] Total length (default is the full length) [+fill+] The string to use for filling when packing a string shorter than the specified bytes option. Default is a space. === Array [+repeat+] This option can be used (when packing only) to repeat the current option. A value of :all will mean for all remaining elements of the array. When unpacking, it is necessary to specify the class in addition to any option, like so: "AB".unpack(Integer, :bytes => 2, :endian => :big, :signed => false) ===> 0x3132 == Shortcuts and default values It's easy to add shortcuts for easier (un)packing: String.packers.set :flv_signature, :bytes => 3, :fill => "FLV" "x".pack(:flv_signature) ===> "xFL" Two shortcut names have special meanings: +default+ and +merge_all+. +default+ specifies the options to use when nothing is specified, while +merge_all+ will be merged with all options. For example: String.packers do |p| p.set :merge_all, :fill => "*" # Unless explicitly specified, :fill will now be "*" p.set :default, :bytes => 8 # If no option is given, this will act as default end "ab".pack ===> "ab******" "ab".pack(:bytes=>4) ===> "ab**" "ab".pack(:fill => "!") ===> "ab" # Not "ab!!" A shortcut can refer to another shortcut, as so: String.packers do |p| p.set :creator, :bytes => 4 p.set :app_type, :creator end "hello".pack(:app_type) ===> "hell" The following shortcuts and defaults are built-in the library: === Integer :merge_all => :bytes=>4, :signed=>true, :endian=>:big :default => :long :long => {} :short => :bytes=>2 :byte => :bytes=>1 :unsigned_long => :bytes=>4, :signed=>false :unsigned_short => :bytes=>2, :signed=>false === Float :merge_all => :precision => :single, :endian => :big :default => :float :double => :precision => :double :float => {} === String :merge_all => :fill => " " == Files and StringIO 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): io >> :flv_signature >> Integer >> [String, {:bytes => 8}] io.read(:flv_signature, Integer, [String, {:bytes => 8}]) io.read(:flv_signature, Integer, String, {:bytes => 8}) [io.read(:flv_signature), io.read(Integer), io.read(String, :bytes => 8)] In a similar fashion, these have the same effect although the return value is different io << "x".pack(:flv_signature) << 66.pack << "Hello".pack(:bytes => 8) # returns io io << ["x", 66, "Hello"].pack(:flv_signature, {} , {:bytes => 8}) # returns io io.write("x", :flv_signature, 66, "Hello", {:bytes => 8}) # returns the # of bytes written io.packed << ["x",:flv_signature] << 66 << ["Hello", {:bytes => 8}] # returns a "packed io" The last example shows how io.packed returns a special IO object (a packing IO) that will pack arguments before writing it. This is to insure compatibility with the usual behavior of IO objects: io << 66 ==> appends "66" io.packed << 66 ==> appends "\000\000\000B" We "cheated" in the previous example; instead of writing io.packed.write(...) we used the shorter form. This works because we're passing more than one argument; for only one argument we must call io.packed.write(66) less the usual +write+ method is called. Since the standard library desn't define the >> operator for IO objects, we are free to use either io.packed or io directly. Note that reading one value only will return that value directly, not an array containing that value: io.read(Integer) ===> 42, not [42] io.read(Integer,Integer) ===> [42,43] io << Integer ===> [42] == Custom classes Including the mixin +Packable+ will make a class (un)packable. Packable relies on +write_packed+ and unpacking on +read_packed+. For example: class MyHeader < Struct.new(:signature, :nb_blocks) include Packable def write_packed(packedio, options) packedio << [signature, {:bytes=>3}] << [nb_blocks, :short] end def self.read_packed(packedio, options) h = MyHeader.new h.signature, h.nb_blocks = packedio >> [String, {:bytes => 3}] >> :short h end end We used the argument name +packedio+ to remind us that these are packed IO objects, i.e. they will write their arguments after packing them instead of converting them to string like normal IO objects. With this definition, +MyHeader+ can be both packed and unpacked: h = MyHeader.new("FLV", 65) h.pack ===> "FLV\000A" StringIO.new("FLV\000A") >> Signature ===> [a copy of h] A default self.read_packed is provided by the +Packable+ mixin, which allows you to define +read_packed+ as an instance method instead of a class method. In that case, +read_packed+ instance method is called with the same arguments and should modify +self+ accordingly (instead of returning a new object). It is not necessary to return +self+. The previous example can thus be shortened: class MyHeader #... def read_packed(packedio, options) self.signature, self.nb_blocks = packedio >> [String, {:bytes => 3}] >> :short end end == Filter 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: String.packers.set :length_encoded do |packer| packer.write { |packedio| packedio << length << self } packer.read { |packedio| packedio.read(packedio.read(Integer)) } end "hello!".pack(:length_encoded) ===> "\000\000\000\006hello!" ["this", "is", "great!"].pack(*[:length_encoded]*3).unpack(*[:length_encoded]*3) ===> ["this", "is", "great!"] Note that the +write+ block will be executed as an instance method (which is why we could use +length+ & +self+), while +read+ is a normal block that must return the newly read object. == Inheritance A final note to say that packers are inherited in some way. For instance one could define a filter for all objects: Object.packers.set :with_class do |packer| packer.write { |io| io << [self.class.name, :length_encoded] << self } packer.read do |io| klass = eval(io.read(:length_encoded)) io.read(klass) end end [42, MyHeader.new("Wow", 1)].pack(:with_class, :with_class).unpack(:with_class, :with_class) ===> [42, MyHeader.new("Wow", 1)] = License packable is licensed under the terms of the MIT License, see the included LICENSE file. Author:: Marc-André Lafortune packable-1.3.14/Gemfile0000644000004100000410000000013513674353043014674 0ustar www-datawww-datasource 'https://rubygems.org' # Specify your gem's dependencies in packable.gemspec gemspec packable-1.3.14/LICENSE.txt0000644000004100000410000000206413674353043015227 0ustar www-datawww-dataCopyright (c) 2012 Marc-Andre Lafortune MIT License Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.