require 'spec_helper'
require 'ronin/binary/template'

describe Binary::Template do
  describe "TYPES" do
    subject { described_class::TYPES }

    it("uint8      => C") { subject[:uint8].should       == 'C' }
    it("uint16     => S") { subject[:uint16].should      == 'S' }
    it("uint32     => L") { subject[:uint32].should      == 'L' }
    it("uint64     => Q") { subject[:uint64].should      == 'Q' }
    it("int8       => C") { subject[:int8].should        == 'c' }
    it("int16      => S") { subject[:int16].should       == 's' }
    it("int32      => L") { subject[:int32].should       == 'l' }
    it("int64      => Q") { subject[:int64].should       == 'q' }
    if RUBY_VERSION < '1.9.'
    it("uint16_le  => v") { subject[:uint16_le].should   == 'v' }
    it("uint32_le  => V") { subject[:uint32_le].should   == 'V' }
    it("uint16_be  => n") { subject[:uint16_be].should   == 'n' }
    it("uint32_be  => N") { subject[:uint32_be].should   == 'N' }
    end
    it("uchar      => Z")  { subject[:uchar].should      == 'Z' }
    it("ushort     => S!") { subject[:ushort].should     == 'S!'}
    it("uint       => I!") { subject[:uint].should       == 'I!'}
    it("ulong      => L!") { subject[:ulong].should      == 'L!'}
    it("ulong_long => Q")  { subject[:ulong_long].should == 'Q' }
    it("char       => Z")  { subject[:char].should       == 'Z' }
    it("short      => s!") { subject[:short].should      == 's!'}
    it("int        => i!") { subject[:int].should        == 'i!'}
    it("long       => l!") { subject[:long].should       == 'l!'}
    it("long_long  => q")  { subject[:long_long].should  == 'q' }
    it("utf8       => U")  { subject[:utf8].should       == 'U' }
    it("float      => F") { subject[:float].should       == 'F' }
    it("double     => D") { subject[:double].should      == 'D' }
    it("float_le   => e") { subject[:float_le].should    == 'e' }
    it("double_le  => E") { subject[:double_le].should   == 'E' }
    it("float_be   => g") { subject[:float_be].should    == 'g' }
    it("double_ge  => G") { subject[:double_be].should   == 'G' }
    it("ubyte      => C") { subject[:ubyte].should       == 'C' }
    it("byte       => c") { subject[:byte].should        == 'c' }
    it("string     => Z*") { subject[:string].should     == 'Z*'}

    if RUBY_VERSION > '1.9.'
      context "Ruby 1.9" do
        it("uint16_le     => S<") { subject[:uint16_le].should      == 'S<' }
        it("uint32_le     => L<") { subject[:uint32_le].should      == 'L<' }
        it("uint64_le     => Q<") { subject[:uint64_le].should      == 'Q<' }
        it("int16_le      => S<") { subject[:int16_le].should       == 's<' }
        it("int32_le      => L<") { subject[:int32_le].should       == 'l<' }
        it("int64_le      => Q<") { subject[:int64_le].should       == 'q<' }
        it("uint16_be     => S>") { subject[:uint16_be].should      == 'S>' }
        it("uint32_be     => L>") { subject[:uint32_be].should      == 'L>' }
        it("uint64_be     => Q>") { subject[:uint64_be].should      == 'Q>' }
        it("int16_be      => S>") { subject[:int16_be].should       == 's>' }
        it("int32_be      => L>") { subject[:int32_be].should       == 'l>' }
        it("int64_be      => Q>") { subject[:int64_be].should       == 'q>' }
        it("ushort_le     => S!<") { subject[:ushort_le].should     == 'S!<'}
        it("uint_le       => I!<") { subject[:uint_le].should       == 'I!<'}
        it("ulong_le      => L!<") { subject[:ulong_le].should      == 'L!<'}
        it("ulong_long_le => L!<") { subject[:ulong_long_le].should == 'Q<' }
        it("short_le      => S!<") { subject[:short_le].should      == 's!<'}
        it("int_le        => I!<") { subject[:int_le].should        == 'i!<'}
        it("long_le       => L!<") { subject[:long_le].should       == 'l!<'}
        it("long_long_le  => L!<") { subject[:long_long_le].should  == 'q<' }
        it("ushort_be     => S!>") { subject[:ushort_be].should     == 'S!>'}
        it("uint_be       => I!>") { subject[:uint_be].should       == 'I!>'}
        it("ulong_be      => L!>") { subject[:ulong_be].should      == 'L!>'}
        it("ulong_long_be => L!>") { subject[:ulong_long_be].should == 'Q>' }
        it("short_be      => S!>") { subject[:short_be].should      == 's!>'}
        it("int_be        => I!>") { subject[:int_be].should        == 'i!>'}
        it("long_be       => L!>") { subject[:long_be].should       == 'l!>'}
        it("long_long_be  => L!>") { subject[:long_long_be].should  == 'q>' }
      end
    end
  end

  describe "translate" do
    subject { described_class }

    context "when given :endian" do
      it "should translate endian-types" do
        subject.translate(:uint, :endian => :little).should == :uint_le
      end

      it "should not translate non-endian-types" do
        subject.translate(:string, :endian => :little).should == :string
      end

      it "should raise an ArgumentError for unknown endianness" do
        lambda {
          subject.translate(:uint, :endian => :foo)
        }.should raise_error(ArgumentError)
      end
    end
  end

  describe "compile" do
    let(:type) { :uint }
    let(:code) { subject::TYPES[type] }

    subject { described_class }

    it "should translate types to their pack codes" do
      subject.compile([type]).should == code
    end

    it "should support specifying the length of a field" do
      subject.compile([[type, 10]]).should == "#{code}10"
    end

    it "should raise ArgumentError for unknown types" do
      lambda {
        subject.compile([:foo])
      }.should raise_error(ArgumentError)
    end
  end

  describe "#initialize" do
    subject { described_class.new [:uint32, :string] }

    it "should store the types" do
      subject.fields.should == [
        :uint32,
        :string
      ]
    end

    it "should raise ArgumentError for unknown types" do
      lambda {
        described_class.new [:foo]
      }.should raise_error(ArgumentError)
    end
  end

  ENDIANNESS = if [0x1234].pack('S') == "\x34\x12"
                 :little
               else
                 :big
               end

  let(:byte) { 0x41 }
  let(:char) { 'A' }

  let(:bytes) { [104, 101, 108, 108, 111] }
  let(:chars) { bytes.map(&:chr).join }
  let(:string) { chars }

  let(:uint8)  { 0xff }
  let(:uint16) { 0xffff }
  let(:uint32) { 0xffffffff }
  let(:uint64) { 0xffffffffffffffff }

  let(:int8)  { -1 }
  let(:int16) { -1 }
  let(:int32) { -1 }
  let(:int32) { -1 }
  let(:int64) { -1 }

  describe "#pack" do
    context ":byte" do
      subject { described_class.new [:byte] }

      it "should pack a signed byte" do
        subject.pack(byte).should == char
      end
    end

    context "[:byte, n]" do
      let(:n) { string.length }
      subject { described_class.new [[:byte, n]] }

      it "should pack multiple signed characters" do
        subject.pack(*bytes).should == chars
      end
    end

    context ":char" do
      subject { described_class.new [:char] }

      it "should pack a signed character" do
        subject.pack(char).should == char
      end
    end

    context "[:char, n]" do
      let(:n) { string.length }
      subject { described_class.new [[:char, n]] }

      it "should pack multiple signed characters" do
        subject.pack(*chars).should == string
      end

      context "padding" do
        let(:padding) { 10 }
        subject { described_class.new [[:char, n + padding]] }

        it "should pad the string with '\\0' characters" do
          subject.pack(*chars).should == (string + ("\0" * padding))
        end
      end
    end

    context ":uint8" do
      subject { described_class.new [:uint8] }

      it "should pack an unsigned 8bit integer" do
        subject.pack(uint8).should == "\xff"
      end
    end

    context ":uint16" do
      subject { described_class.new [:uint16] }

      it "should pack an unsigned 16bit integer" do
        subject.pack(uint16).should == "\xff\xff"
      end
    end

    context ":uint32" do
      subject { described_class.new [:uint32] }

      it "should pack an unsigned 32bit integer" do
        subject.pack(uint32).should == "\xff\xff\xff\xff"
      end
    end

    context ":uint64" do
      subject { described_class.new [:uint64] }

      it "should pack an unsigned 64bit integer" do
        subject.pack(uint64).should == "\xff\xff\xff\xff\xff\xff\xff\xff"
      end
    end

    context ":int8" do
      subject { described_class.new [:int8] }

      it "should pack an signed 8bit integer" do
        subject.pack(int8).should == "\xff"
      end
    end

    context ":int16" do
      subject { described_class.new [:int16] }

      it "should pack an unsigned 16bit integer" do
        subject.pack(int16).should == "\xff\xff"
      end
    end

    context ":int32" do
      subject { described_class.new [:int32] }

      it "should pack an unsigned 32bit integer" do
        subject.pack(int32).should == "\xff\xff\xff\xff"
      end
    end

    context ":int64" do
      subject { described_class.new [:int64] }

      it "should pack an unsigned 64bit integer" do
        subject.pack(int64).should == "\xff\xff\xff\xff\xff\xff\xff\xff"
      end
    end

    context ":string" do
      subject { described_class.new [:string] }

      it "should pack a string" do
        subject.pack(string).should == "#{string}\0"
      end
    end
  end

  describe "#unpack" do
    context ":byte" do
      subject { described_class.new [:byte] }

      it "should unpack a signed byte" do
        subject.unpack(char).should == [byte]
      end
    end

    context "[:byte, n]" do
      let(:n) { string.length }
      subject { described_class.new [[:byte, n]] }

      it "should pack multiple signed characters" do
        subject.unpack(chars).should == bytes
      end
    end

    context ":char" do
      subject { described_class.new [:char] }

      it "should unpack a signed character" do
        subject.unpack(char).should == [char]
      end
    end

    context "[:char, n]" do
      let(:n) { string.length }
      subject { described_class.new [[:char, n]] }

      it "should unpack multiple signed characters" do
        subject.unpack(string).should == [chars]
      end

      context "padding" do
        let(:padding) { 10 }
        subject { described_class.new [[:char, n + padding]] }

        it "should strip '\\0' padding characters" do
          subject.unpack(string + ("\0" * padding)).should == [chars]
        end
      end
    end

    context ":uint8" do
      subject { described_class.new [:uint8] }

      it "should unpack an unsigned 8bit integer" do
        subject.unpack("\xff").should == [uint8]
      end
    end

    context ":uint16" do
      subject { described_class.new [:uint16] }

      it "should unpack an unsigned 16bit integer" do
        subject.unpack("\xff\xff").should == [uint16]
      end
    end

    context ":uint32" do
      subject { described_class.new [:uint32] }

      it "should unpack an unsigned 32bit integer" do
        subject.unpack("\xff\xff\xff\xff").should == [uint32]
      end
    end

    context ":uint64" do
      subject { described_class.new [:uint64] }

      it "should unpack an unsigned 64bit integer" do
        subject.unpack("\xff\xff\xff\xff\xff\xff\xff\xff").should == [uint64]
      end
    end

    context ":int8" do
      subject { described_class.new [:int8] }

      it "should unpack an signed 8bit integer" do
        subject.unpack("\xff").should == [int8]
      end
    end

    context ":int16" do
      subject { described_class.new [:int16] }

      it "should unpack an unsigned 16bit integer" do
        subject.unpack("\xff\xff").should == [int16]
      end
    end

    context ":int32" do
      subject { described_class.new [:int32] }

      it "should unpack an unsigned 32bit integer" do
        subject.unpack("\xff\xff\xff\xff").should == [int32]
      end
    end

    context ":int64" do
      subject { described_class.new [:int64] }

      it "should unpack an unsigned 64bit integer" do
        subject.unpack("\xff\xff\xff\xff\xff\xff\xff\xff").should == [int64]
      end
    end

    context ":string" do
      subject { described_class.new [:string] }

      it "should unpack a string" do
        subject.unpack("#{string}\0").should == [string]
      end
    end
  end

  describe "#to_s" do
    subject { described_class.new [:uint32, :string] }

    it "should return the pack format String" do
      subject.to_s.should == "LZ*"
    end
  end

  describe "#inspect" do
    let(:template) { described_class.new [:uint32, :string] }

    subject { template.inspect }

    it "should inspect the class" do
      subject.should include(described_class.name)
    end

    it "should inspect the template" do
      subject.should include(template.fields.inspect)
    end
  end
end