require 'ruby_smb/dcerpc/ndr'

RSpec.shared_examples 'a properly aligned field in a NdrStruct' do |field, alignment|
  let(:test_struct_class) do
    klass = Class.new(RubySMB::Dcerpc::Ndr::NdrStruct) do
      default_parameters byte_align: alignment
      ndr_char :a
    end
    klass.send(field.to_sym, :test)
    klass
  end
  let(:test_struct) do
    test_struct_class.new(a: 'A', test: test_value)
  end
  let(:struct_bin) { "A#{"\x00" * (alignment - 1)}#{test_bin}".b }

  it "is always #{alignment}-bytes aligned" do
    (1..8).each do |i|
      test_struct = test_struct_class.new(a: 'A' * i, test: test_value)
      expect(test_struct.test.abs_offset % alignment).to eq(0)
    end
  end
  it 'writes the expected binary stream with the correct padding bytes' do
    expect(test_struct.to_binary_s).to eq(struct_bin)
  end
  it 'reads a padded binary stream' do
    test_struct2 = test_struct_class.read(struct_bin)
    expect(test_struct2).to eq(test_struct)
  end
  it 'reports the expected structure size' do
    expect(test_struct.num_bytes).to eq(struct_bin.size)
  end
end

RSpec.shared_examples 'a properly aligned conformant structure in a NdrStruct' do |field, alignment|
  let(:test_struct_class) do
    klass = Class.new(RubySMB::Dcerpc::Ndr::NdrStruct) do
      default_parameters byte_align: alignment
      ndr_char :a
    end
    klass.send(field.to_sym, :test)
    klass
  end
  let(:test_struct) do
    test_struct_class.new(a: 'A', test: test_value)
  end
  let(:struct_bin) do
    max_count = test_bin.slice!(0, 4)
    offset = max_count.size + 1
    align = (alignment - (offset % alignment)) % alignment
    "#{max_count}A#{"\x00" * align}#{test_bin}".b
  end

  it "is always #{alignment}-bytes aligned" do
    (1..8).each do |i|
      test_struct = test_struct_class.new(a: 'A' * i, test: test_value)
      expect(test_struct.test.abs_offset % alignment).to eq(0)
    end
  end
  it 'writes the expected binary stream with the correct padding bytes' do
    expect(test_struct.to_binary_s).to eq(struct_bin)
  end
  it 'reads a padded binary stream' do
    test_struct2 = test_struct_class.read(struct_bin)
    expect(test_struct2).to eq(test_struct)
  end
  it 'reports the expected structure size' do
    expect(test_struct.num_bytes).to eq(struct_bin.size)
  end
end

RSpec.describe RubySMB::Dcerpc::Ndr::NdrBoolean do
  it 'is a BinData::Uint32le class' do
    expect(described_class).to be < BinData::Uint32le
  end
  it 'has :byte_align parameter set to the expected value' do
    expect(described_class.default_parameters[:byte_align]).to eq(4)
  end

  subject(:boolean) { described_class.new }

  context 'with #new' do
    it 'is false its default value' do
      expect(boolean.new).to eq(false)
    end
    it 'sets true to true' do
      expect(boolean.new(true)).to eq(true)
    end
    it 'sets false to false' do
      expect(boolean.new(false)).to eq(false)
    end
    it 'sets 1 to true' do
      expect(boolean.new(1)).to eq(true)
    end
    it 'sets 0 to false' do
      expect(boolean.new(0)).to eq(false)
    end
    it 'sets any integer > 0 to true' do
      expect(boolean.new(rand(1..1000))).to eq(true)
    end
    it 'raises an ArgumentError error when passing a String' do
      expect { boolean.new("false") }.to raise_error(ArgumentError)
    end
  end

  context 'with #assign' do
    it 'sets true to true' do
      boolean.assign(true)
      expect(boolean).to eq(true)
    end
    it 'sets false to false' do
      boolean.assign(false)
      expect(boolean).to eq(false)
    end
    it 'sets 1 to true' do
      boolean.assign(1)
      expect(boolean).to eq(true)
    end
    it 'sets 0 to false' do
      boolean.assign(0)
      expect(boolean).to eq(false)
    end
    it 'sets any integer > 0 to true' do
      boolean.assign(rand(1..100))
      expect(boolean).to eq(true)
    end
    it 'raises an ArgumentError error when passing a String' do
      expect { boolean.assign("false") }.to raise_error(ArgumentError)
    end
  end

  context 'when reading data' do
    it 'sets false with an uint32 zero value' do
      boolean.read("\x00\x00\x00\x00")
      expect(boolean).to eq(false)
    end
    it 'sets true with an uint32 1 value' do
      boolean.read("\x01\x00\x00\x00")
      expect(boolean).to eq(true)
    end
    it 'sets true with any uint32 value > 0' do
      boolean.read([rand(1..1000)].pack('L'))
      expect(boolean).to eq(true)
    end
    it 'reads itself' do
      expect(boolean.read(described_class.new(true).to_binary_s)).to eq(true)
    end
  end

  context 'with #to_binary_s' do
    it 'returns the expected true binary representation' do
      boolean.assign(true)
      expect(boolean.to_binary_s).to eq("\x01\x00\x00\x00")
    end
    it 'returns the expected false binary representation' do
      boolean.assign(false)
      expect(boolean.to_binary_s).to eq("\x00\x00\x00\x00")
    end
  end

  context 'when testing alignment in a NdrStruct' do
    it_behaves_like 'a properly aligned field in a NdrStruct', described_class.bindata_name.to_sym, 4 do
      let(:test_value) { 1 }
      let(:test_bin) { "\x01\x00\x00\x00".b }
    end
  end
end

RSpec.describe RubySMB::Dcerpc::Ndr::NdrChar do
  it 'is a BinData::String class' do
    expect(described_class).to be < BinData::String
  end
  it 'has :byte_align parameter set to the expected value' do
    expect(described_class.default_parameters[:byte_align]).to eq(1)
  end

  subject(:char) { described_class.new }

  context 'with #new' do
    it 'is "\x00" its default value' do
      expect(char.new).to eq("\x00")
    end
    it 'sets a character' do
      expect(char.new('A')).to eq('A')
    end
    it 'sets the first character when passing a String' do
      expect(char.new('ABC')).to eq('A')
    end
    it 'does not set a value when passing anything else than a string' do
      char.new(['ABC'])
      expect(char).to eq("\x00")
      char.new(67)
      expect(char).to eq("\x00")
    end
  end

  context 'with #assign' do
    it 'sets a character' do
      char.assign('A')
      expect(char).to eq('A')
    end
    it 'sets the first character when passing a String' do
      char.assign('ABC')
      expect(char).to eq('A')
    end
    it 'converts the argument to string and keeps the first character' do
      char.assign(['ABC'])
      expect(char).to eq('[')
      char.assign(67)
      expect(char).to eq('6')
    end
  end

  context 'when reading data' do
    it 'sets a character' do
      char.read("\x41")
      expect(char).to eq('A')
    end
    it 'reads itself' do
      expect(char.read(described_class.new('A').to_binary_s)).to eq('A')
    end
  end

  context 'with #to_binary_s' do
    it 'returns the expected character binary representation' do
      char.assign('A')
      expect(char.to_binary_s).to eq("\x41")
    end
  end

  context 'when testing alignment in a NdrStruct' do
    it_behaves_like 'a properly aligned field in a NdrStruct', described_class.bindata_name.to_sym, 1 do
      let(:test_value) { 'B' }
      let(:test_bin) { 'B'.b }
    end
  end
end

RSpec.describe RubySMB::Dcerpc::Ndr::NdrWideChar do
  it 'is a RubySMB::Field::String16 class' do
    expect(described_class).to be < RubySMB::Field::String16
  end
  it 'has :byte_align parameter set to the expected value' do
    expect(described_class.default_parameters[:byte_align]).to eq(2)
  end

  subject(:char) { described_class.new }

  context 'with #new' do
    it 'is "\x00" its default value' do
      expect(char.new).to eq("\x00".encode('utf-16le'))
    end
    it 'sets a character' do
      expect(char.new('A')).to eq('A'.encode('utf-16le'))
    end
    it 'sets the first character when passing a String' do
      expect(char.new('ABC')).to eq('A'.encode('utf-16le'))
    end
    it 'does not set a value when passing anything else than a string' do
      char.new(['ABC'])
      expect(char).to eq("\x00".encode('utf-16le'))
      char.new(67)
      expect(char).to eq("\x00".encode('utf-16le'))
    end
  end

  context 'with #assign' do
    it 'sets a character' do
      char.assign('A')
      expect(char).to eq('A'.encode('utf-16le'))
    end
    it 'sets the first character when passing a String' do
      char.assign('ABC')
      expect(char).to eq('A'.encode('utf-16le'))
    end
    it 'converts the argument to string and keeps the first character' do
      char.assign(['ABC'])
      expect(char).to eq('['.encode('utf-16le'))
      char.assign(67)
      expect(char).to eq('6'.encode('utf-16le'))
    end
  end

  context 'when reading data' do
    it 'sets a character' do
      char.read("\x41\x00")
      expect(char).to eq('A'.encode('utf-16le'))
    end
    it 'reads itself' do
      expect(char.read(described_class.new('A').to_binary_s)).to eq('A'.encode('utf-16le'))
    end
  end

  context 'with #to_binary_s' do
    it 'returns the expected character binary representation' do
      char.assign('A')
      expect(char.to_binary_s).to eq("\x41\x00")
    end
  end

  context 'when testing alignment in a NdrStruct' do
    it_behaves_like 'a properly aligned field in a NdrStruct', described_class.bindata_name.to_sym, 2 do
      let(:test_value) { 'B' }
      let(:test_bin) { "B\x00".b }
    end
  end
end

RSpec.describe RubySMB::Dcerpc::Ndr::NdrEnum do
  it 'is a BinData::Int16le class' do
    expect(described_class).to be < BinData::Int16le
  end
  it 'has :byte_align parameter set to the expected value' do
    expect(described_class.default_parameters[:byte_align]).to eq(2)
  end

  context 'when testing alignment in a NdrStruct' do
    it_behaves_like 'a properly aligned field in a NdrStruct', described_class.bindata_name.to_sym, 2 do
      let(:test_value) { 1 }
      let(:test_bin) { "\x01\x00".b }
    end
  end
end

{
  NdrUint8: { parent_class: :Uint8, nb_bytes: 1},
  NdrUint16: { parent_class: :Uint16le, nb_bytes: 2},
  NdrUint32: { parent_class: :Uint32le, nb_bytes: 4},
  NdrUint64: { parent_class: :Uint64le, nb_bytes: 8},
}.each do |klass, info|
  full_klass = RubySMB::Dcerpc::Ndr.const_get(klass)
  RSpec.describe(full_klass) do
    it "is a BinData::#{info[:parent_class]} class" do
      expect(described_class).to be < BinData.const_get(info[:parent_class])
    end
    it 'has :byte_align parameter set to the expected value' do
      expect(described_class.default_parameters[:byte_align]).to eq(info[:nb_bytes])
    end

    context 'when testing alignment in a NdrStruct' do
      it_behaves_like 'a properly aligned field in a NdrStruct', full_klass.bindata_name.to_sym, info[:nb_bytes] do
        let(:test_value) { 1 }
        let(:test_bin) { "\x01#{"\x00" * (info[:nb_bytes] - 1)}".b }
      end
    end
  end
end

RSpec.describe RubySMB::Dcerpc::Ndr::NdrFileTime do
  it 'is a RubySMB::Field::FileTime' do
    expect(described_class).to be < RubySMB::Field::FileTime
  end
  it 'has :byte_align parameter set to the expected value' do
    expect(described_class.default_parameters[:byte_align]).to eq(4)
  end

  context 'when testing alignment in a NdrStruct' do
    it_behaves_like 'a properly aligned field in a NdrStruct', described_class.bindata_name.to_sym, 4 do
      let(:test_value) { 132820620350000000 }
      let(:test_bin) { "\x80\v\xCF\x86\xA6\xDF\xD7\x01" }
    end
  end
end

#####################################
#       NDR Constructed Types       #
#####################################


#
# Arrays
#

RSpec.shared_examples "a BinData::Array" do
  it 'is a BinData::Array class' do
    expect(described_class).to be < BinData::Array
  end
  it 'is an empty array by default' do
    expect(described_class.new(type: element_class)).to eq([])
  end

  context 'with elements' do
    it 'contains the expected element types' do
      expect(subject.all? {|e| e.is_a?(element_class)}).to be true
    end
    it 'has the expected size' do
      expect(subject.size).to eq(values.size)
    end

    context 'when setting a value at index greater than the current number of elements' do
      let(:index_offset) { 3 }
      let(:new_element) { element_class.new(values[0]) }
      before :example do
        subject[values.size + index_offset] = new_element
      end
      it 'adds elements until it reaches the new index' do
        expect(subject.size).to eq(values.size + index_offset + 1)
      end
      it 'sets the new elements to the element type default value' do
        (values.size..(index_offset - 1)).each do |i|
          expect(subject[i]).to eq(0)
        end
        expect(subject[values.size + index_offset]).to eq(new_element)
      end
    end

    context 'when getting a value at index greater than the current number of elements' do
      let(:index_offset) { 3 }
      it 'adds elements until it reaches the new index' do
        subject[values.size + index_offset]
        expect(subject.size).to eq(values.size + index_offset + 1)
      end
      it 'sets the new elements to the element type default value' do
        (values.size..index_offset).each do |i|
          expect(subject[i]).to eq(element_class.new)
        end
      end
    end

    context 'when assigning another array' do
      let(:new_array) { [values[0], values[1], values[2]] }
      before :example do
        subject.assign(new_array)
      end
      it 'has the same size than the other array' do
        expect(subject.size).to eq(new_array.size)
      end
      it 'replaces all the elements' do
        new_array.each_with_index do |e, i|
          expect(subject[i]).to eq(e)
        end
      end
    end
  end
end

RSpec.shared_examples "a NDR Array" do |conformant:, varying:|
  let(:max_count) { subject.to_binary_s.slice(0, 4).unpack('L' * 4)[0] }
  let(:actual_count) do
    index = conformant ? 8 : 4
    subject.to_binary_s.slice(index, 4).unpack('L' * 4)[0]
  end
  let(:offset) do
    index = conformant ? 4 : 0
    subject.to_binary_s.slice(index, 4).unpack('L' * 4)[0]
  end

  if conformant
    it "has :max_count set to 0 (little endian uint32) by default" do
      empty_array = described_class.new(type: element_class)
      expect(empty_array.to_binary_s[0, 4]).to eq("\x00\x00\x00\x00")
    end
  end
  if varying
    it "has :offset and :actual_count set to 0 (little endian uint32) by default" do
      empty_array = described_class.new(type: element_class)
      # offset
      expect(empty_array.to_binary_s[0, 4]).to eq("\x00\x00\x00\x00")
      # actual_count
      expect(empty_array.to_binary_s[4, 4]).to eq("\x00\x00\x00\x00")
    end
  end
  it 'reads itself' do
    expect(described_class.new(type: element_class).read(subject.to_binary_s)).to eq(values)
  end
  it 'has :byte_align parameter set to the largest alignment of the array element type and the size information type, if any' do
    # minimum alignment is 4 bytes since arrays are prefixed with an uint32 size element for 32-bit NDR
    align = [4, element_size].max
    expect(subject.eval_parameter(:byte_align)).to eq(align)
  end

  context 'when checking if its elements have :byte_align parameter set' do
    # element independent
    it 'does not raise error when the :byte_align parameter is set in the element class' do
      test_element = Class.new(BinData::Record) do
        default_parameters byte_align: 4
        endian :little
        uint32 :a
      end
      BinData::RegisteredClasses.register('test_element', test_element)
      expect { described_class.new(type: :test_element, byte_align: 4) }.to_not raise_error
    end
    # element independent
    it 'does not raise error when the :byte_align parameter is set during instantiation' do
      test_element = Class.new(BinData::Record) do
        endian :little
        uint32 :a
      end
      BinData::RegisteredClasses.register('test_element', test_element)
      expect { described_class.new(type: [:test_element, {byte_align: 4}], byte_align: 4) }.to_not raise_error
    end
    context 'with a NDR element' do
      # element independent
      it 'does not raise error when the type element is a symbol' do
        expect { described_class.new(type: :ndr_uint32, byte_align: 4) }.to_not raise_error
      end
      # element independent
      it 'does not raise error when the type element is a class' do
        expect { described_class.new(type: RubySMB::Dcerpc::Ndr::NdrUint32, byte_align: 4) }.to_not raise_error
      end
    end
    # element independent
    it 'raises an ArgumentError when the element has no :byte_align' do
      test_element = Class.new(BinData::Record) do
        endian :little
        uint32 :a
      end
      BinData::RegisteredClasses.register('test_element', test_element)
      expect { described_class.new(type: :test_element, byte_align: 4) }.to raise_error(ArgumentError)
    end
    # element independent
    it 'raises an ArgumentError when the element has a parameter different than :byte_align' do
      test_element = Class.new(BinData::Array) do
        default_parameters type: :uint8
      end
      BinData::RegisteredClasses.register('test_element', test_element)
      expect { described_class.new(type: [:test_element, {other_param: 1}], byte_align: 4) }.to raise_error(ArgumentError)
    end
  end

  context 'with size information' do
    if conformant
      it "sets :max_count to a little endian uint32 value representing the number of elements" do
        expect(max_count).to eq(subject.size)
      end
      it "updates :max_count when adding one element" do
        subject << values[0]
        expect(max_count).to eq(subject.size)
      end
      context 'when setting a value at index greater than the current number of elements' do
        it "sets :max_count to the new number of elements" do
          index_offset = rand(10)
          subject[values.size + index_offset] = values[0]
          expect(max_count).to eq(values.size + index_offset + 1)
        end
      end
      context 'when reading a value at index greater than the current number of elements' do
        it "sets :max_count to the new number of elements" do
          index_offset = rand(10)
          subject[values.size + index_offset]
          expect(max_count).to eq(values.size + index_offset + 1)
        end
      end
      context 'when assigning another array' do
        it "sets :max_count to the new number of elements" do
          new_size = rand(10)
          new_array = new_size.times.map { values[0] }
          subject.assign(new_array)
          expect(max_count).to eq(new_size)
        end
      end
    end

    if varying
      it "sets :actual_count to a little endian uint32 value representing the number of elements" do
        expect(actual_count).to eq(subject.size)
      end
      it "updates :actual_count when adding one element" do
        subject << values[0]
        expect(actual_count).to eq(subject.size)
      end
      it 'has offset always set to 0' do
        expect(offset).to eq(0)
        subject << values[0]
        expect(offset).to eq(0)
      end
      context 'when setting a value at index greater than the current number of elements' do
        it "sets :actual_count to the new number of elements" do
          index_offset = rand(10)
          subject[values.size + index_offset] = values[0]
          expect(actual_count).to eq(values.size + index_offset + 1)
        end
      end
      context 'when reading a value at index greater than the current number of elements' do
        it "sets :actual_count to the new number of elements" do
          index_offset = rand(10)
          subject[values.size + index_offset]
          expect(actual_count).to eq(values.size + index_offset + 1)
        end
      end
      context 'when assigning another array' do
        it "sets :actual_count to the new number of elements" do
          new_size = rand(10)
          new_array = new_size.times.map { values[0] }
          subject.assign(new_array)
          expect(actual_count).to eq(new_size)
        end
      end
    end
  end

  context 'when reading a binary stream' do
    subject { described_class.new(type: element_class).read(binary_stream) }

    it 'has the expected size' do
      expect(subject.size).to eq(values.size)
    end
    it 'sets the new element values' do
      values.each_with_index do |value, i|
        expect(subject[i]).to eq(value)
      end
    end
    it 'has the same binary representation than the original binary stream' do
      expect(subject.to_binary_s).to eq(binary_stream)
    end
    if conformant
      it "sets :max_count to the new number of elements" do
        expect(max_count).to eq(values.size)
      end
    end
    if varying
      it "sets :actual_count to the new number of elements" do
        expect(actual_count).to eq(values.size)
      end
    end
    it 'sets @read_until_index to the number of elements' do
      expect(subject.read_until_index).to eq(values.size)
    end
    context 'with an empty array' do
      let(:empty_binary_str) do
        str = ''
        str << "\x00\x00\x00\x00" if conformant
        str << "\x00\x00\x00\x00\x00\x00\x00\x00" if varying
        str
      end
      subject { described_class.new(type: element_class).read(empty_binary_str) }

      it 'is an empty array' do
        expect(subject).to eq([])
      end
      it 'sets @read_until_index to 0' do
        expect(subject.read_until_index).to eq(0)
      end
      if conformant
        it "sets :max_count to 0" do
          expect(max_count).to eq(0)
        end
      end
      if varying
        it "sets :actual_count to 0" do
          expect(actual_count).to eq(0)
        end
      end
    end
  end

  context 'when getting a binary stream' do
    it 'outputs the expected binary' do
      expect(subject.to_binary_s).to eq(binary_stream)
    end
    context 'with an empty array' do
      let(:empty_binary_str) do
        str = ''
        str << "\x00\x00\x00\x00" if conformant
        str << "\x00\x00\x00\x00\x00\x00\x00\x00" if varying
        str
      end
      subject { described_class.new(type: element_class).assign([]) }

      it 'outputs the expected binary' do
        expect(subject.to_binary_s).to eq(empty_binary_str)
      end
    end
  end

  context 'when calling #do_num_bytes' do
    it 'returns the expected number of bytes' do
      expect(subject.do_num_bytes).to eq(binary_stream.size)
    end
    context 'with NDR structures' do
      let(:array_with_struct) do
        described_class.new([{a: 2, b: 'A'}, {a: 3, b: 'B'}], type: :test_struct)
      end
      before :example do
        test_struct = Class.new(RubySMB::Dcerpc::Ndr::NdrStruct) do
          default_parameters byte_align: 4
          ndr_uint32 :a
          ndr_char   :b
        end
        BinData::RegisteredClasses.register('test_struct', test_struct)
      end

      it 'returns the expected number of bytes, including padding' do
        uint32_size = 4
        char_size = 1
        pad_size = 3
        expected_nb = (uint32_size + char_size) * 2
        expected_nb += pad_size
        expected_nb += 4 if conformant
        expected_nb += 8 if varying
        expect(array_with_struct.do_num_bytes).to eq(expected_nb)
      end
    end
  end

  {
    NdrUint8: { align: 1, data: rand(0xFF), pack: 'C' },
    NdrUint16: { align: 2, data: rand(0xFFFF), pack: 'S<' },
    NdrUint32: { align: 4, data: rand(0xFFFFFFFF), pack: 'L<' },
    NdrUint64: { align: 8, data: rand(0xFFFFFFFFFFFFFFFF), pack: 'Q<'},
    NdrChar: { align: 1, data: 'A', binary: 'A'.b },
    NdrVarString: { align: 4, data: 'AAAAA'.encode('ASCII-8BIT'), binary: "#{[0].pack('L')}#{[5].pack('L')}AAAAA".b},
    NdrVarStringz: { align: 4, data: 'AAAAA'.encode('ASCII-8BIT'), binary: "#{[0].pack('L')}#{[6].pack('L')}AAAAA\x00".b},
    NdrVarWideString: { align: 4, data: 'AAAAA'.encode('UTF-16LE'), binary: "#{[0].pack('L')}#{[5].pack('L')}A\x00A\x00A\x00A\x00A\x00".b},
    NdrVarWideStringz: { align: 4, data: 'AAAAA'.encode('UTF-16LE'), binary: "#{[0].pack('L')}#{[6].pack('L')}A\x00A\x00A\x00A\x00A\x00\x00\x00".b},
  }.each do |klass, info|
    context "when it is embedded in a NDR structure and preceeded by a #{klass}" do
      let(:field_class) { RubySMB::Dcerpc::Ndr.const_get(klass) }
      let(:field_obj) { field_class.new(info[:data]) }
      let(:embedded_struct) do
        byte_align = struct_max_align
        embedded_struct = Class.new(RubySMB::Dcerpc::Ndr::NdrStruct) do
          default_parameters(byte_align: byte_align)
        end
        embedded_struct.send(field_class.bindata_name.to_sym, :a)
        embedded_struct
      end

      context 'directly as an array' do
        before :example do
          embedded_struct.send(described_class.bindata_name.to_sym, :ary, type: element_class)
        end
        let(:array_max_align) { (varying && element_size < 4) ? 4 : element_size }
        let(:struct_max_align) { [array_max_align, info[:align]].max }
        let(:test_instance) { embedded_struct.new(a: field_obj, ary: values) }
        let(:binary_str) do
          binary_str = "".b
          if conformant
            binary_str << binary_stream.slice!(0, 4)
          end
          pad_length = (struct_max_align - (binary_str.size % struct_max_align)) % struct_max_align
          binary_str << "\x00" * pad_length
          if info[:data].is_a?(String)
            binary_str << info[:binary]
          else
            binary_str << "#{[info[:data]].pack(info[:pack])}"
          end
          if varying
            pad_length = (array_max_align - (binary_str.size % array_max_align)) % array_max_align
            binary_str << "\x00" * pad_length
            binary_str << binary_stream.slice!(0, 8)
          end
          pad_length = (element_size - (binary_str.size % element_size)) % element_size
          binary_str << "\x00" * pad_length
          binary_str << bin_values
          binary_str
        end

        context 'when writing the stream of bytes' do
          it 'outputs the expect binary' do
            expect(test_instance.to_binary_s).to eq(binary_str)
          end
        end

        context 'when reading a stream of bytes' do
          it 'outputs the expected structure' do
            expect(embedded_struct.read(binary_str)).to eq({ a: info[:data], ary: values })
          end
        end

        context 'when calling #num_bytes' do
          it 'outputs the expected number of bytes' do
            expect(test_instance.num_bytes).to eq(binary_str.size)
          end
        end
      end

      context 'as a pointer to an array' do
        before :example do
          array_ptr = Class.new(described_class) do
            extend RubySMB::Dcerpc::Ndr::PointerClassPlugin
          end
          BinData::RegisteredClasses.register('test_array_ptr', array_ptr)
          embedded_struct.send(:test_array_ptr, :ary_ptr, type: element_class)
        end
        let(:struct_max_align) { [4, info[:align]].max }
        let(:test_instance) { embedded_struct.new(a: field_obj, ary_ptr: values) }
        let(:binary_str) do
          binary_str = "".b
          if info[:data].is_a?(String)
            binary_str << info[:binary]
          else
            binary_str << "#{[info[:data]].pack(info[:pack])}"
          end
          pad_length = (4 - (binary_str.size % 4)) % 4
          binary_str << "\x00" * pad_length
          ref_id = [RubySMB::Dcerpc::Ndr::INITIAL_REF_ID].pack('L<')
          binary_str << ref_id
          if conformant
            binary_str << binary_stream.slice!(0, 4)
          end
          if varying
            binary_str << binary_stream.slice!(0, 8)
          end
          pad_length = (element_size - (binary_str.size % element_size)) % element_size
          binary_str << "\x00" * pad_length
          binary_str << bin_values
          binary_str
        end

        context 'when writing the stream of bytes' do
          it 'outputs the expect binary' do
            expect(test_instance.to_binary_s).to eq(binary_str)
          end
        end

        context 'when reading a stream of bytes' do
          it 'outputs the expected structure' do
            expect(embedded_struct.read(binary_str)).to eq({ a: info[:data], ary_ptr: values })
          end
        end

        context 'when calling #num_bytes' do
          it 'outputs the expected number of bytes' do
            expect(test_instance.num_bytes).to eq(binary_str.size)
          end
        end
      end

      context 'embedded in another structure' do
        before :example do
          byte_align = struct_max_align
          embedded_struct2 = Class.new(RubySMB::Dcerpc::Ndr::NdrStruct) do
            default_parameters(byte_align: byte_align)
          end
          embedded_struct2.send(field_class.bindata_name.to_sym, :a)
          embedded_struct2.send(described_class.bindata_name.to_sym, :ary, type: element_class)
          BinData::RegisteredClasses.register('embedded_struct_2nd_lvl', embedded_struct2)
          embedded_struct.send(:embedded_struct_2nd_lvl, :struct2)
        end
        let(:array_max_align) { (varying && element_size < 4) ? 4 : element_size }
        let(:struct2_max_align) { [array_max_align, info[:align]].max }
        let(:struct_max_align) { [struct2_max_align, info[:align]].max }
        let(:test_instance) { embedded_struct.new(a: field_obj, struct2: { a: field_obj, ary: values }) }
        let(:binary_str) do
          binary_str = "".b
          if conformant
            binary_str << binary_stream.slice!(0, 4)
          end
          # a
          pad_length = (struct_max_align - (binary_str.size % struct_max_align)) % struct_max_align
          binary_str << "\x00" * pad_length
          if info[:data].is_a?(String)
            binary_str << info[:binary]
          else
            binary_str << "#{[info[:data]].pack(info[:pack])}"
          end
          # struct2.a
          pad_length = (struct2_max_align - (binary_str.size % struct2_max_align)) % struct2_max_align
          binary_str << "\x00" * pad_length
          if info[:data].is_a?(String)
            binary_str << info[:binary]
          else
            binary_str << "#{[info[:data]].pack(info[:pack])}"
          end
          # struct2.ary
          if varying
            pad_length = (array_max_align - (binary_str.size % array_max_align)) % array_max_align
            binary_str << "\x00" * pad_length
            binary_str << binary_stream.slice!(0, 8)
          end
          pad_length = (element_size - (binary_str.size % element_size)) % element_size
          binary_str << "\x00" * pad_length
          binary_str << bin_values
          binary_str
        end

        context 'when writing the stream of bytes' do
          it 'outputs the expect binary' do
            expect(test_instance.to_binary_s).to eq(binary_str)
          end
        end

        context 'when reading a stream of bytes' do
          it 'outputs the expected structure' do
            expect(embedded_struct.read(binary_str)).to eq({ a: field_obj, struct2: { a: field_obj, ary: values } })
          end
        end

        context 'when calling #num_bytes' do
          it 'outputs the expected number of bytes' do
            expect(test_instance.num_bytes).to eq(binary_str.size)
          end
        end
      end

      unless conformant
        {
          NdrUint8:  { size: 1 },
          NdrUint16: { size: 2 },
          NdrUint32: { size: 4 },
          NdrUint64: { size: 8 }
        }.each do |klass2, info2|
          context "directly as an array and followed by a #{klass2}" do
            before :example do
              embedded_struct.send(described_class.bindata_name.to_sym, :ary, type: element_class)
              embedded_struct.send(field_class2.bindata_name.to_sym, :b)
            end
            let(:field_class2) { RubySMB::Dcerpc::Ndr.const_get(klass2) }
            let(:field_obj2) { field_class2.new(rand(0xFF)) }
            let(:array_max_align) { element_size < 4 ? 4 : element_size }
            let(:struct_max_align) { [array_max_align, info[:align], info2[:size]].max }
            let(:test_instance) { embedded_struct.new(a: field_obj, ary: values, b: field_obj2) }
            let(:binary_str) do
              binary_str = "".b
              # a
              if info[:data].is_a?(String)
                binary_str << info[:binary]
              else
                binary_str << "#{[info[:data]].pack(info[:pack])}"
              end
              # ary
              pad_length = (array_max_align - (binary_str.size % array_max_align)) % array_max_align
              binary_str << "\x00" * pad_length
              binary_str << binary_stream.slice!(0, 8)
              pad_length = (element_size - (binary_str.size % element_size)) % element_size
              binary_str << "\x00" * pad_length
              binary_str << bin_values
              # b
              pad_length = (info2[:size] - (binary_str.size % info2[:size])) % info2[:size]
              binary_str << "\x00" * pad_length
              binary_str << field_obj2.to_binary_s
              binary_str
            end

            context 'when writing the stream of bytes' do
              it 'outputs the expect binary' do
                expect(test_instance.to_binary_s).to eq(binary_str)
              end
            end

            context 'when reading a stream of bytes' do
              it 'outputs the expected structure' do
                expect(embedded_struct.read(binary_str)).to eq({ a: info[:data], ary: values, b: field_obj2.to_i })
              end
            end

            context 'when calling #num_bytes' do
              it 'outputs the expected number of bytes' do
                expect(test_instance.num_bytes).to eq(binary_str.size)
              end
            end
          end
        end
      end
    end
  end
end

RSpec.describe RubySMB::Dcerpc::Ndr::NdrFixArray do
  it 'is a BinData::Array class' do
    expect(described_class).to be < BinData::Array
  end
  it 'is an empty array by default' do
    expect(described_class.new(type: :ndr_uint16, byte_align: 2)).to eq([])
  end

  subject { described_class.new(type: :ndr_uint16, byte_align: 2, initial_length: 4) }

  it 'is an array of initial_length default elements' do
    expect(subject).to eq([0, 0, 0, 0])
  end

  context 'when checking if its elements have :byte_align parameter set' do
    it 'does not raise error when the :byte_align parameter is set in the element class' do
      test_element = Class.new(BinData::Record) do
        default_parameters byte_align: 4
        endian :little
        uint32 :a
      end
      BinData::RegisteredClasses.register('test_element', test_element)
      expect { described_class.new(type: :test_element, byte_align: 4) }.to_not raise_error
    end
    it 'does not raise error when the :byte_align parameter is set during instantiation' do
      test_element = Class.new(BinData::Record) do
        endian :little
        uint32 :a
      end
      BinData::RegisteredClasses.register('test_element', test_element)
      expect { described_class.new(type: [:test_element, {byte_align: 4}], byte_align: 4) }.to_not raise_error
    end
    context 'with a NDR element' do
      it 'does not raise error when the type element is a symbol' do
        expect { described_class.new(type: :ndr_uint32, byte_align: 4) }.to_not raise_error
      end
      it 'does not raise error when the type element is a class' do
        expect { described_class.new(type: RubySMB::Dcerpc::Ndr::NdrUint32, byte_align: 4) }.to_not raise_error
      end
    end
    it 'raises an ArgumentError when no :byte_align is provided' do
      test_element = Class.new(BinData::Record) do
        endian :little
        uint32 :a
      end
      BinData::RegisteredClasses.register('test_element', test_element)
      expect { described_class.new(type: :test_element, byte_align: 4) }.to raise_error(ArgumentError)
    end
    it 'raises an ArgumentError when other parameters than :byte_align are provided' do
      test_element = Class.new(BinData::Array) do
        default_parameters type: :uint8
      end
      BinData::RegisteredClasses.register('test_element', test_element)
      expect { described_class.new(type: [:test_element, {other_param: 1}], byte_align: 4) }.to raise_error(ArgumentError)
    end
  end

  context 'with elements' do
    before :example do
      subject.assign([1, 2, 3, 4])
    end
    it 'contains the expected element types' do
      expect(subject.all? {|e| e.is_a?(BinData::Uint16le)}).to be true
    end
    it 'has the expected size' do
      expect(subject.size).to eq(4)
    end

    context 'when setting a value to an element array' do
      context 'in the array bounds' do
        it 'does not raise error and sets the new value' do
          expect { subject[3] = 14 }.to_not raise_error
          expect(subject[3]).to eq(14)
        end
      end
      context 'outside of the array bounds' do
        it 'raises an error' do
          expect { subject[4] = 14 }.to raise_error(ArgumentError)
        end
      end
    end
    context 'when reading the value of an element array' do
      context 'in the array bounds' do
        it 'does not raise error and returns the value' do
          expect { subject[3] }.to_not raise_error
          expect(subject[3]).to eq(4)
        end
      end
      context 'outside of the array bounds' do
        it 'raises an error' do
          expect { subject[4] }.to raise_error(ArgumentError)
        end
      end
    end
    context 'when assigning another array' do
      context 'with the same number of elements' do
        it 'does not raise error and returns the value' do
          expect { subject.assign([5, 6, 7, 8]) }.to_not raise_error
          expect(subject).to eq([5, 6, 7, 8])
        end
      end
      context 'with more elements' do
        it 'raises an error' do
          expect { subject.assign([5, 6, 7, 8, 9]) }.to raise_error(ArgumentError)
        end
      end
      context 'with less elements' do
        it 'raises an error' do
          expect { subject.assign([5, 6, 7]) }.to raise_error(ArgumentError)
        end
      end
    end
    context 'when pushing a new element' do
      it 'raises an error' do
        expect { subject << 88 }.to raise_error(ArgumentError)
      end
    end
    context 'when unshifting a new element' do
      it 'raises an error' do
        expect { subject.unshift(88) }.to raise_error(ArgumentError)
      end
    end
    context 'when concatenating another array' do
      it 'raises an error' do
        expect { subject.concat([3,4]) }.to raise_error(ArgumentError)
      end
    end
    context 'when cheking if :byte_align parameter is set' do
      it 'does not raise error when it is set' do
        expect { described_class.new(type: :ndr_uint16, byte_align: 2, initial_length: 4) }.to_not raise_error
      end

      it 'raises an error when it is not set' do
        expect { described_class.new(type: :uint16le, initial_length: 4) }.to raise_error(ArgumentError)
      end
    end
  end
end

RSpec.describe RubySMB::Dcerpc::Ndr::NdrFixedByteArray do
  it 'is a RubySMB::Dcerpc::Ndr::NdrFixArray class' do
    expect(described_class).to be < RubySMB::Dcerpc::Ndr::NdrFixArray
  end
  it 'has :byte_align parameter set to the expected value' do
    expect(described_class.default_parameters[:byte_align]).to eq(1)
  end

  subject { described_class.new(initial_length: 4) }

  it 'composed of NdrUint8 elements' do
    expect(subject[0]).to be_a( RubySMB::Dcerpc::Ndr::NdrUint8)
  end

  it 'has #initial_length elements' do
    expect(subject.size).to eq(4)
  end

  describe '#assign' do
    it 'assign elements from another array of bytes' do
      ary = [0x33, 0x43, 0x64, 0x22]
      subject.assign(ary)
      expect(subject).to eq(ary)
    end

    it 'assign bytes from string' do
      str = '_Str'
      subject.assign(str)
      expect(subject).to eq(str.bytes)
    end
  end
end

{
  NdrUint8: { size: 1, data: rand(0xFF), pack: 'C' },
  NdrUint16: { size: 2, data: rand(0xFFFF), pack: 'S<' },
  NdrUint32: { size: 4, data: rand(0xFFFFFFFF), pack: 'L<' },
  NdrUint64: { size: 8, data: rand(0xFFFFFFFFFFFFFFFF), pack: 'Q<'},
  NdrChar: {  size: 1, data: 'A', binary: 'A'.b },
  NdrVarString: { size: 4, data: 'AAAAA'.encode('ASCII-8BIT'), binary: "#{[0].pack('L')}#{[5].pack('L')}AAAAA".b},
  NdrVarStringz: { size: 4, data: 'AAAAA'.encode('ASCII-8BIT'), binary: "#{[0].pack('L')}#{[6].pack('L')}AAAAA\x00".b},
  NdrVarWideString: { size: 4, data: 'AAAAA'.encode('UTF-16LE'), binary: "#{[0].pack('L')}#{[5].pack('L')}A\x00A\x00A\x00A\x00A\x00".b},
  NdrVarWideStringz: { size: 4, data: 'AAAAA'.encode('UTF-16LE'), binary: "#{[0].pack('L')}#{[6].pack('L')}A\x00A\x00A\x00A\x00A\x00\x00\x00".b},
}.each do |klass, info|
  RSpec.describe "NDR Array with #{klass}" do
    let(:element_size) { info[:size] }
    let(:element_class) { RubySMB::Dcerpc::Ndr.const_get(klass) }
    let(:values) { rand(3..16).times.map { info[:data] } }
    #let(:values) { [0xFF, 0xFF, 0xFF, 0xFF] }
    let(:bin_values) do
      if info[:data].is_a?(String)
        info[:binary] * values.size
      else
        values.map { |e| [e].pack(info[:pack]) }.join
      end
    end
    let(:binary_stream) do
      pad_length = (element_size - (size_info.size % element_size)) % element_size
      "#{size_info}#{"\x00" * pad_length}#{bin_values}".b
    end
    subject { described_class.new(values, type: element_class) }

    describe RubySMB::Dcerpc::Ndr::NdrConfArray do
      let(:size_info) { [values.size].pack('L<') }
      it_behaves_like 'a BinData::Array'
      it_behaves_like 'a NDR Array', conformant: true, varying: false
    end

    describe RubySMB::Dcerpc::Ndr::NdrVarArray do
      let(:size_info) { "\x00\x00\x00\x00#{[values.size].pack('L<')}" }
      it_behaves_like 'a BinData::Array'
      it_behaves_like 'a NDR Array', conformant: false, varying: true
    end

    describe RubySMB::Dcerpc::Ndr::NdrConfVarArray do
      let(:size_info) { "#{[values.size].pack('L<')}\x00\x00\x00\x00#{[values.size].pack('L<')}" }
      it_behaves_like 'a BinData::Array'
      it_behaves_like 'a NDR Array', conformant: true, varying: true
    end
  end
end


#
# Strings
#

RSpec.shared_examples "a NDR String" do |conformant:, char_size:, null_terminated:|
  let(:first_char_offset) { conformant ? 12 : 8}

  it 'has :byte_align parameter set to the expected value' do
    expect(described_class.default_parameters[:byte_align]).to eq(4)
  end

  if conformant
    it_behaves_like "a Conformant Varying String", null_terminated: null_terminated
  else
    it_behaves_like "a Varying String", null_terminated: null_terminated
  end

  it 'is an empty string by default' do
    expect(subject).to eq('')
  end

  it 'has a binary representation of an empty string by default' do
    expect(subject.to_binary_s[first_char_offset..-1]).to be_empty
  end

  it 'reads itself' do
    subject.assign(value)
    expect(subject.read(subject.to_binary_s)).to eq(value)
  end

  context 'when reading another binary stream' do
    before :example do
      subject.read(binary_stream)
    end
    it 'has the expected size' do
      expect(subject.size).to eq(value.size)
    end
    it 'sets the new element values' do
      expect(subject.to_s).to eq(value)
    end
    it 'has the same binary representation than the original binary stream' do
      expect(subject.to_binary_s).to eq(binary_stream)
    end
  end
end

RSpec.shared_examples "a Conformant Varying String" do |null_terminated:|
  minimum_size = null_terminated ? 1 : 0

  it_behaves_like 'a Varying String', offset: 4, null_terminated: null_terminated

  describe '#initialize' do
    it "sets #max_count to 0 (uint32) by default" do
      expect(subject.max_count).to eq(0)
      expect(subject.to_binary_s[0, 4]).to eq([0].pack('L'))
    end
  end

  describe '#do_write' do
    it "writes #max_count corresponding to the number of elements#{' (including the NULL terminator)' if null_terminated}" do
      subject.assign(value)
      expect(subject.to_binary_s[0, 4]).to eq([value.size + minimum_size].pack('L'))
    end
  end

  describe '#do_read' do
    it "sets #max_count to the string size#{' (including the NULL terminator)' if null_terminated}" do
      subject.read(binary_stream)
      expect(subject.max_count).to eq(value.size + minimum_size)
    end
  end

  describe '#assign' do
    context 'with a string' do
      it "sets #max_count to the string length#{' (including the NULL terminator)' if null_terminated}" do
        subject.assign(value)
        expect(subject.max_count).to eq(value.length + minimum_size)
      end
    end

    context 'with a varying string' do
      it "sets #max_count to the string length#{' (including the NULL terminator)' if null_terminated}" do
        case subject
        when RubySMB::Field::Stringz16
          str = RubySMB::Dcerpc::Ndr::NdrVarWideStringz.new(value)
        when RubySMB::Field::String16
          str = RubySMB::Dcerpc::Ndr::NdrVarWideString.new(value)
        when BinData::Stringz
          str = RubySMB::Dcerpc::Ndr::NdrVarStringz.new(value)
        when BinData::String
          str = RubySMB::Dcerpc::Ndr::NdrVarString.new(value)
        end
        subject.assign(str)
        expect(subject.max_count).to eq(value.length + minimum_size)
      end
    end

    context 'with a conformant varying string' do
      it 'sets #max_count to the conformant varying string #max_count value' do
        case subject
        when RubySMB::Field::Stringz16
          str = RubySMB::Dcerpc::Ndr::NdrConfVarWideStringz.new(value)
        when RubySMB::Field::String16
          str = RubySMB::Dcerpc::Ndr::NdrConfVarWideString.new(value)
        when BinData::Stringz
          str = RubySMB::Dcerpc::Ndr::NdrConfVarStringz.new(value)
        when BinData::String
          str = RubySMB::Dcerpc::Ndr::NdrConfVarString.new(value)
        end
        str.max_count = 30
        subject.assign(str)
        expect(subject.max_count).to eq(30)
      end
    end
  end

  describe '#do_num_bytes' do
    it 'give the correct total size in bytes' do
      subject.assign(value)
      expect(subject.do_num_bytes).to eq(binary_stream.size)
    end
  end
end

RSpec.shared_examples "a Varying String" do |offset: 0, null_terminated:|
  minimum_size = null_terminated ? 1 : 0

  describe '#initialize' do
    it 'sets #actual_count to 0 (uint32) by default' do
      expect(subject.actual_count).to eq(0)
      expect(subject.to_binary_s[offset + 4, 4]).to eq([0].pack('L'))
    end
  end

  describe '#do_write' do
    it 'always writes 0 for "offset"' do
      expect(subject.to_binary_s[offset, 4]).to eq("\x00\x00\x00\x00".b)
      subject.assign('Test')
      expect(subject.to_binary_s[offset, 4]).to eq("\x00\x00\x00\x00".b)
    end

    it "writes #actual_count corresponding to the number of elements#{' (including the NULL terminator)' if null_terminated}" do
      subject.assign(value)
      expect(subject.to_binary_s[offset + 4, 4]).to eq([value.size + minimum_size].pack('L'))
    end
  end

  describe '#do_read' do
    it "sets #actual_count to the string size#{' (including the NULL terminator)' if null_terminated}" do
      subject.read(binary_stream)
      expect(subject.actual_count).to eq(value.size + minimum_size)
    end
  end

  describe '#assign' do
    it "sets #actual_count to the string length#{' (including the NULL terminator)' if null_terminated}" do
      subject.assign(value)
      expect(subject.actual_count).to eq(value.length + minimum_size)
    end
  end

  describe '#do_num_bytes' do
    it 'give the correct total size in bytes' do
      subject.assign(value)
      expect(subject.do_num_bytes).to eq(binary_stream.size)
    end
  end
end

RSpec.describe RubySMB::Dcerpc::Ndr::NdrVarString do
  subject { described_class.new }
  it 'is a BinData::String class' do
    expect(described_class).to be < BinData::String
    expect(described_class).not_to be < RubySMB::Field::String16
  end
  it_behaves_like 'a NDR String', conformant: false, char_size: 1, null_terminated: false do
    let(:binary_stream) {
      "\x00\x00\x00\x00"\
      "\x04\x00\x00\x00"\
      "\x41\x42\x43\x44".b
    }
    let(:value) { 'ABCD' }
  end
  context 'with an empty string' do
    it_behaves_like 'a NDR String', conformant: false, char_size: 1, null_terminated: false do
      let(:binary_stream) {
        "\x00\x00\x00\x00"\
        "\x00\x00\x00\x00".b
      }
      let(:value) { '' }
    end
  end
end

RSpec.describe RubySMB::Dcerpc::Ndr::NdrVarStringz do
  subject { described_class.new }
  it 'is a BinData::Stringz class' do
    expect(described_class).to be < BinData::Stringz
    expect(described_class).not_to be < RubySMB::Field::Stringz16
  end
  it_behaves_like 'a NDR String', conformant: false, char_size: 1, null_terminated: true do
    let(:binary_stream) {
      "\x00\x00\x00\x00"\
      "\x05\x00\x00\x00"\
      "\x41\x42\x43\x44\x00".b
    }
    let(:value) { 'ABCD' }
  end
  context 'with an empty string' do
    it_behaves_like 'a NDR String', conformant: false, char_size: 1, null_terminated: true do
      let(:binary_stream) {
        "\x00\x00\x00\x00"\
        "\x01\x00\x00\x00"\
        "\x00".b
      }
      let(:value) { '' }
    end
  end
end

RSpec.describe RubySMB::Dcerpc::Ndr::NdrVarWideString do
  subject { described_class.new }
  it 'is a RubySMB::Field::String16 class' do
    expect(described_class).to be < RubySMB::Field::String16
  end
  it_behaves_like 'a NDR String', conformant: false, char_size: 2, null_terminated: false do
    let(:binary_stream) {
      "\x00\x00\x00\x00"\
      "\x04\x00\x00\x00"\
      "\x41\x00\x42\x00\x43\x00\x44\x00".b
    }
    let(:value) { 'ABCD'.encode('utf-16le') }
  end
  context 'with an empty string' do
    it_behaves_like 'a NDR String', conformant: false, char_size: 2, null_terminated: false do
      let(:binary_stream) {
        "\x00\x00\x00\x00"\
        "\x00\x00\x00\x00".b
      }
      let(:value) { '' }
    end
  end
end

RSpec.describe RubySMB::Dcerpc::Ndr::NdrVarWideStringz do
  subject { described_class.new }
  it 'is a RubySMB::Field::Stringz16 class' do
    expect(described_class).to be < RubySMB::Field::Stringz16
  end
  it_behaves_like 'a NDR String', conformant: false, char_size: 2, null_terminated: true do
    let(:binary_stream) {
      "\x00\x00\x00\x00"\
      "\x05\x00\x00\x00"\
      "\x41\x00\x42\x00\x43\x00\x44\x00\x00\x00".b
    }
    let(:value) { 'ABCD'.encode('utf-16le') }
  end
  context 'with an empty string' do
    it_behaves_like 'a NDR String', conformant: false, char_size: 2, null_terminated: true do
      let(:binary_stream) {
        "\x00\x00\x00\x00"\
        "\x01\x00\x00\x00"\
        "\x00\x00".b
      }
      let(:value) { '' }
    end
  end
end

RSpec.describe RubySMB::Dcerpc::Ndr::NdrConfVarString do
  subject { described_class.new }
  it 'is a BinData::String class' do
    expect(described_class).to be < BinData::String
    expect(described_class).not_to be < RubySMB::Field::String16
  end
  it_behaves_like 'a NDR String', conformant: true, char_size: 1, null_terminated: false do
    let(:binary_stream) {
      "\x04\x00\x00\x00"\
      "\x00\x00\x00\x00"\
      "\x04\x00\x00\x00"\
      "\x41\x42\x43\x44".b
    }
    let(:value) { 'ABCD' }
  end
  context 'with an empty string' do
    it_behaves_like 'a NDR String', conformant: true, char_size: 1, null_terminated: false do
      let(:binary_stream) {
        "\x00\x00\x00\x00"\
        "\x00\x00\x00\x00"\
        "\x00\x00\x00\x00".b
      }
      let(:value) { '' }
    end
  end
end

RSpec.describe RubySMB::Dcerpc::Ndr::NdrConfVarStringz do
  subject { described_class.new }
  it 'is a BinData::Stringz class' do
    expect(described_class).to be < BinData::Stringz
    expect(described_class).not_to be < RubySMB::Field::Stringz16
  end
  it_behaves_like 'a NDR String', conformant: true, char_size: 1, null_terminated: true do
    let(:binary_stream) {
      "\x05\x00\x00\x00"\
      "\x00\x00\x00\x00"\
      "\x05\x00\x00\x00"\
      "\x41\x42\x43\x44\x00".b
    }
    let(:value) { 'ABCD' }
  end
  context 'with an empty string' do
    it_behaves_like 'a NDR String', conformant: true, char_size: 1, null_terminated: true do
      let(:binary_stream) {
        "\x01\x00\x00\x00"\
        "\x00\x00\x00\x00"\
        "\x01\x00\x00\x00"\
        "\x00".b
      }
      let(:value) { '' }
    end
  end
end

RSpec.describe RubySMB::Dcerpc::Ndr::NdrConfVarWideString do
  subject { described_class.new }
  it 'is a RubySMB::Field::String16 class' do
    expect(described_class).to be < RubySMB::Field::String16
  end
  it_behaves_like 'a NDR String', conformant: true, char_size: 2, null_terminated: false do
    let(:binary_stream) {
      "\x04\x00\x00\x00"\
      "\x00\x00\x00\x00"\
      "\x04\x00\x00\x00"\
      "\x41\x00\x42\x00\x43\x00\x44\x00".b
    }
    let(:value) { 'ABCD'.encode('utf-16le') }
  end
  context 'with an empty string' do
    it_behaves_like 'a NDR String', conformant: true, char_size: 2, null_terminated: false do
      let(:binary_stream) {
        "\x00\x00\x00\x00"\
        "\x00\x00\x00\x00"\
        "\x00\x00\x00\x00".b
      }
      let(:value) { '' }
    end
  end
end

RSpec.describe RubySMB::Dcerpc::Ndr::NdrConfVarWideStringz do
  subject { described_class.new }
  it 'is a RubySMB::Field::Stringz16 class' do
    expect(described_class).to be < RubySMB::Field::Stringz16
  end
  it_behaves_like 'a NDR String', conformant: true, char_size: 2, null_terminated: true do
    let(:binary_stream) {
      "\x05\x00\x00\x00"\
      "\x00\x00\x00\x00"\
      "\x05\x00\x00\x00"\
      "\x41\x00\x42\x00\x43\x00\x44\x00\x00\x00".b
    }
    let(:value) { 'ABCD'.encode('utf-16le') }
  end
  context 'with an empty string' do
    it_behaves_like 'a NDR String', conformant: true, char_size: 1, null_terminated: true do
      let(:binary_stream) {
        "\x01\x00\x00\x00"\
        "\x00\x00\x00\x00"\
        "\x01\x00\x00\x00"\
        "\x00\x00".b
      }
      let(:value) { '' }
    end
  end
end


#
# Structures
#

RSpec.describe RubySMB::Dcerpc::Ndr::NdrStruct do

  describe 'Struct.method_missing' do
    context 'When validating conformant arrays' do
      let(:super_result) { double('Super method_missing result') }
      let(:super_result_array) { [super_result] }
      before :example do
        allow(super_result).to receive(:has_parameter?).and_return(true)
        allow(BinData::Record).to receive(:method_missing).and_return(super_result_array)
        allow(described_class).to receive(:validate_conformant_array)
        allow(described_class).to receive(:default_parameters).and_return({byte_align: 4})
      end
      it 'calls the superclass method_missing and returns the result' do
        expect(described_class.method_missing(1, 2)).to eq(super_result_array)
      end
      it 'performs conformant array validation if the field is an array of BinData::SanitizedField' do
        allow(super_result).to receive(:is_a?).with(BinData::SanitizedField).and_return(true)
        described_class.method_missing(1, 2)
        expect(described_class).to have_received(:validate_conformant_array).with(super_result_array)
      end
      it 'does not perform conformant array validation if the field is not an array of BinData::SanitizedField' do
        allow(super_result).to receive(:is_a?).with(BinData::SanitizedField).and_return(false)
        described_class.method_missing(1, 2)
        expect(described_class).to_not have_received(:validate_conformant_array).with(super_result_array)
      end
    end

    context 'when cheking if the fields have :byte_align parameter' do
      it 'does not raise error when it is set' do
        expect {
          Class.new(described_class) do
            default_parameters byte_align: 4
            endian :little
            ndr_uint32 :a
          end.new
        }.to_not raise_error
      end

      it 'raises an error when it is not set' do
        expect {
          Class.new(described_class) do
            endian :little
            ndr_uint32 :a
          end.new
        }.to raise_error(ArgumentError)
      end

      it 'does not raise error when the field is a BinData:Bit*' do
        expect {
          Class.new(described_class) do
            default_parameters byte_align: 4
            endian :little
            bit2 :a
          end.new
        }.to_not raise_error
      end
    end
  end

  describe 'Struct.validate_conformant_array' do
    context 'with a conformant array' do
      it 'does not raise error if the array is the last member' do
        expect {
          Class.new(described_class) do
            default_parameters byte_align: 4
            endian :little

            ndr_uint32     :a
            ndr_conf_array :b, type: :ndr_uint16
          end
        }.to_not raise_error
      end
      it 'raises error if the array is not the last member' do
        expect {
          Class.new(described_class) do
            default_parameters byte_align: 4
            endian :little

            ndr_conf_array :b, type: :ndr_uint16
            ndr_uint32     :a
          end
        }.to raise_error(ArgumentError)
      end
      it 'does not raise error if the array is not the last member of a non NdrStruct' do
        expect {
          Class.new(BinData::Record) do
            default_parameters byte_align: 4
            endian :little

            ndr_conf_array :b, type: :ndr_uint16
            ndr_uint32     :a
          end
        }.to_not raise_error
      end
    end

    context 'with a conformant varying array' do
      it 'does not raise error if the array is the last member' do
        expect {
          Class.new(described_class) do
            default_parameters byte_align: 4
            endian :little

            ndr_uint32         :a
            ndr_conf_var_array :b, type: :ndr_uint16
          end
        }.to_not raise_error
      end
      it 'raises error if the array is not the last member' do
        expect {
          Class.new(described_class) do
            default_parameters byte_align: 4
            endian :little

            ndr_conf_var_array :b, type: :ndr_uint16
            ndr_uint32         :a
          end
        }.to raise_error(ArgumentError)
      end
    end

    context 'with a conformant varying string' do
      it 'does not raise error if the string is the last member' do
        expect {
          Class.new(described_class) do
            default_parameters byte_align: 4
            endian :little

            ndr_uint32          :a
            ndr_conf_var_string :b
          end
        }.to_not raise_error
      end
      it 'raises error if the string is not the last member' do
        expect {
          Class.new(described_class) do
            default_parameters byte_align: 4
            endian :little

            ndr_conf_var_string :b
            ndr_uint32          :a
          end
        }.to raise_error(ArgumentError)
      end
    end

    context 'with an embedded structure containing a conformant array' do
      after :example do
        BinData::RegisteredClasses.unregister('test_struct')
      end

      context 'when the embedded structure is the last member' do
        it 'does not raise error' do
          expect {
            struct_with_array = Class.new(described_class) do
              default_parameters byte_align: 4
              endian :little

              ndr_uint32         :a
              ndr_conf_var_array :b, type: :ndr_uint16
            end
            BinData::RegisteredClasses.register('test_struct', struct_with_array)
            Class.new(described_class) do
              default_parameters byte_align: 4
              endian :little

              ndr_uint32  :a
              test_struct :b
            end
          }.to_not raise_error
        end
      end

      context 'when the embedded structure is not the last member' do
        it 'raises error' do
          expect {
            struct_with_array = Class.new(described_class) do
              default_parameters byte_align: 4
              endian :little

              ndr_uint32         :a
              ndr_conf_var_array :b, type: :ndr_uint16
            end
            BinData::RegisteredClasses.register('test_struct', struct_with_array)
            Class.new(described_class) do
              default_parameters byte_align: 4
              endian :little

              test_struct :b
              ndr_uint32  :a
            end
          }.to raise_error(ArgumentError)
        end
      end
    end
  end

  it 'is a BinData::Record class' do
    expect(described_class).to be < BinData::Record
  end

  context 'with only primitives' do
    let(:struct) do
      Class.new(described_class) do
        default_parameters byte_align: 4
        endian  :little

        ndr_uint8   :a
        ndr_uint16  :b
        ndr_uint32  :c
        ndr_char    :d
        ndr_boolean :e
      end
    end

    it 'initializes the members to their default value' do
      expect(struct.new).to eq(a: 0, b: 0, c: 0, d: "\x00", e: false)
    end
    it 'reads itself' do
      values = {a: 44, b: 444, c: 4444, d: "T", e: true}
      struct_instance = struct.new(values)
      expect(struct.read(struct_instance.to_binary_s)).to eq(values)
    end
    context 'with values' do
      subject do
        struct.new(a: 1, b: 2, c: 3, d: "A", e: true)
      end
      it 'returns the expected member values' do
        expect(subject).to eq(a: 1, b: 2, c: 3, d: "A", e: true)
      end
      it 'outputs the expected binary representation' do
        binary =  "\x01"             # a
        binary << "\x00"             # pad
        binary << "\x02\x00"         # b
        binary << "\x03\x00\x00\x00" # c
        binary << "\x41"             # d
        binary << "\x00\x00\x00"     # pad
        binary << "\x01\x00\x00\x00" # e
        expect(subject.to_binary_s).to eq(binary.b)
      end
    end
  end

  context 'with pointers' do
    let(:struct) do
      Class.new(described_class) do
        default_parameters byte_align: 4
        endian  :little

        ndr_uint8      :a
        ndr_uint16_ptr :b
        ndr_uint32     :c
        ndr_char_ptr   :d
        ndr_boolean    :e
      end
    end

    it 'initializes the members to their default value' do
      expect(struct.new).to eq(a: 0, b: :null, c: 0, d: :null, e: false)
    end
    it 'reads itself' do
      values = {a: 44, b: 444, c: 4444, d: "T", e: true}
      struct_instance = struct.new(values)
      expect(struct.read(struct_instance.to_binary_s)).to eq(values)
    end
    context 'with values' do
      subject do
        struct.new(a: 1, b: 2, c: 3, d: "A", e: true)
      end
      it 'returns the expected member values' do
        expect(subject).to eq(a: 1, b: 2, c: 3, d: "A", e: true)
      end
      it 'outputs the expected binary representation' do
        binary =  "\x01"             # a
        binary << "\x00\x00\x00"     # pad
        binary << "\x01\x00\x00\x00" # b.ref_id
        binary << "\x03\x00\x00\x00" # c
        binary << "\x02\x00\x00\x00" # d.ref_id
        binary << "\x01\x00\x00\x00" # e
        binary << "\x02\x00"         # deferred b
        binary << "\x41"             # deferred d
        expect(subject.to_binary_s).to eq(binary.b)
      end
    end
  end

  context 'with fixed arrays' do
    let(:struct) do
      Class.new(described_class) do
        default_parameters byte_align: 4
        endian  :little

        ndr_uint32    :a
        ndr_fix_array :b, type: :ndr_uint32, initial_length: 3, byte_align: 4
        ndr_uint32    :c
      end
    end

    it 'initializes the members to their default value' do
      expect(struct.new).to eq(a: 0, b: [0, 0, 0], c: 0)
    end
    it 'reads itself' do
      values = {a: 44, b: [1,2,3], c: 4444}
      struct_instance = struct.new(values)
      expect(struct.read(struct_instance.to_binary_s)).to eq(values)
    end
    context 'with values' do
      subject do
        struct.new(a: 4, b: [1,2,3], c: 5)
      end
      it 'returns the expected member values' do
        expect(subject).to eq(a: 4, b: [1,2,3], c: 5)
      end
      it 'outputs the expected binary representation' do
        binary =  "\x04\x00\x00\x00" # a
        binary << "\x01\x00\x00\x00" # b[0]
        binary << "\x02\x00\x00\x00" # b[1]
        binary << "\x03\x00\x00\x00" # b[2]
        binary << "\x05\x00\x00\x00" # c
        expect(subject.to_binary_s).to eq(binary.b)
      end
    end
  end

  context 'with fixed arrays containing pointers' do
    let(:struct) do
      Class.new(described_class) do
        default_parameters byte_align: 4
        endian  :little

        ndr_uint32    :a
        ndr_fix_array :b, type: :ndr_uint32_ptr, initial_length: 3, byte_align: 4
        ndr_uint32    :c
      end
    end

    it 'initializes the members to their default value' do
      expect(struct.new).to eq(a: 0, b: [:null, :null, :null], c: 0)
    end
    it 'reads itself' do
      values = {a: 44, b: [1,2,3], c: 4444}
      struct_instance = struct.new(values)
      expect(struct.read(struct_instance.to_binary_s)).to eq(values)
    end
    context 'with values' do
      subject do
        struct.new(a: 4, b: [1,2,3], c: 5)
      end
      it 'returns the expected member values' do
        expect(subject).to eq(a: 4, b: [1,2,3], c: 5)
      end
      it 'outputs the expected binary representation' do
        binary =  "\x04\x00\x00\x00" # a
        binary << "\x01\x00\x00\x00" # b[0].ref_id
        binary << "\x02\x00\x00\x00" # b[1].ref_id
        binary << "\x03\x00\x00\x00" # b[2].ref_id
        binary << "\x05\x00\x00\x00" # c
        binary << "\x01\x00\x00\x00" # b[0]
        binary << "\x02\x00\x00\x00" # b[1]
        binary << "\x03\x00\x00\x00" # b[2]
        expect(subject.to_binary_s).to eq(binary.b)
      end
    end
  end

  context 'with varying arrays' do
    let(:struct) do
      Class.new(described_class) do
        default_parameters byte_align: 4
        endian  :little

        ndr_uint32    :a
        ndr_var_array :b, type: :ndr_uint32
        ndr_uint32    :c
      end
    end

    it 'initializes the members to their default value' do
      expect(struct.new).to eq(a: 0, b: [], c: 0)
    end
    it 'reads itself' do
      values = {a: 44, b: [1,2,3], c: 4444}
      struct_instance = struct.new(values)
      expect(struct.read(struct_instance.to_binary_s)).to eq(values)
    end
    context 'with values' do
      subject do
        struct.new(a: 4, b: [1,2,3], c: 5)
      end
      it 'returns the expected member values' do
        expect(subject).to eq(a: 4, b: [1,2,3], c: 5)
      end
      it 'outputs the expected binary representation' do
        binary =  "\x04\x00\x00\x00" # a
        binary << "\x00\x00\x00\x00" # array offset
        binary << "\x03\x00\x00\x00" # array actual_count
        binary << "\x01\x00\x00\x00" # b[0]
        binary << "\x02\x00\x00\x00" # b[1]
        binary << "\x03\x00\x00\x00" # b[2]
        binary << "\x05\x00\x00\x00" # c
        expect(subject.to_binary_s).to eq(binary.b)
      end
    end
  end

  context 'with varying arrays containing pointers' do
    let(:struct) do
      Class.new(described_class) do
        default_parameters byte_align: 4
        endian  :little

        ndr_uint32    :a
        ndr_var_array :b, type: :ndr_uint32_ptr
        ndr_uint32    :c
      end
    end

    it 'initializes the members to their default value' do
      expect(struct.new).to eq(a: 0, b: [], c: 0)
    end
    it 'reads itself' do
      values = {a: 44, b: [1,2,3], c: 4444}
      struct_instance = struct.new(values)
      expect(struct.read(struct_instance.to_binary_s)).to eq(values)
    end
    context 'with values' do
      subject do
        struct.new(a: 4, b: [1,2,3], c: 5)
      end
      it 'returns the expected member values' do
        expect(subject).to eq(a: 4, b: [1,2,3], c: 5)
      end
      it 'outputs the expected binary representation' do
        binary =  "\x04\x00\x00\x00" # a
        binary << "\x00\x00\x00\x00" # array offset
        binary << "\x03\x00\x00\x00" # array actual_count
        binary << "\x01\x00\x00\x00" # b[0].ref_id
        binary << "\x02\x00\x00\x00" # b[1].ref_id
        binary << "\x03\x00\x00\x00" # b[2].ref_id
        binary << "\x05\x00\x00\x00" # c
        binary << "\x01\x00\x00\x00" # deferred b[0]
        binary << "\x02\x00\x00\x00" # deferred b[1]
        binary << "\x03\x00\x00\x00" # deferred b[2]
        expect(subject.to_binary_s).to eq(binary.b)
      end
    end
  end

  context 'with conformant arrays' do
    let(:struct) do
      Class.new(described_class) do
        default_parameters byte_align: 4
        endian  :little

        ndr_uint32     :a
        ndr_uint32     :b
        ndr_conf_array :c, type: :ndr_uint32
      end
    end

    it 'initializes the members to their default value' do
      expect(struct.new).to eq(a: 0, b: 0, c: [])
    end
    it 'reads itself' do
      values = {a: 44, b: 4444, c: [1,2,3]}
      struct_instance = struct.new(values)
      expect(struct.read(struct_instance.to_binary_s)).to eq(values)
    end
    context 'with values' do
      subject do
        struct.new(a: 4, b: 5, c: [1,2,3])
      end
      it 'returns the expected member values' do
        expect(subject).to eq(a: 4, b: 5, c: [1,2,3])
      end
      it 'outputs the expected binary representation' do
        binary =  "\x03\x00\x00\x00" # array max_count
        binary << "\x04\x00\x00\x00" # a
        binary << "\x05\x00\x00\x00" # b
        binary << "\x01\x00\x00\x00" # c[0]
        binary << "\x02\x00\x00\x00" # c[1]
        binary << "\x03\x00\x00\x00" # c[2]
        expect(subject.to_binary_s).to eq(binary.b)
      end
    end
  end

  context 'with conformant arrays containing pointers' do
    let(:struct) do
      Class.new(described_class) do
        default_parameters byte_align: 4
        endian  :little

        ndr_uint32     :a
        ndr_uint32     :b
        ndr_conf_array :c, type: :ndr_uint32_ptr
      end
    end

    it 'initializes the members to their default value' do
      expect(struct.new).to eq(a: 0, b: 0, c: [])
    end
    it 'reads itself' do
      values = {a: 44, b: 4444, c: [1,2,3]}
      struct_instance = struct.new(values)
      expect(struct.read(struct_instance.to_binary_s)).to eq(values)
    end
    context 'with values' do
      subject do
        struct.new(a: 4, b: 5, c: [1,2,3])
      end
      it 'returns the expected member values' do
        expect(subject).to eq(a: 4, b: 5, c: [1,2,3])
      end
      it 'outputs the expected binary representation' do
        binary =  "\x03\x00\x00\x00" # array max_count
        binary << "\x04\x00\x00\x00" # a
        binary << "\x05\x00\x00\x00" # b
        binary << "\x01\x00\x00\x00" # c[0].ref_id
        binary << "\x02\x00\x00\x00" # c[1].ref_id
        binary << "\x03\x00\x00\x00" # c[2].ref_id
        binary << "\x01\x00\x00\x00" # c[0]
        binary << "\x02\x00\x00\x00" # c[1]
        binary << "\x03\x00\x00\x00" # c[2]
        expect(subject.to_binary_s).to eq(binary.b)
      end
    end
  end

  context 'with a conformant varying array' do
    let(:struct) do
      Class.new(described_class) do
        default_parameters byte_align: 4
        endian  :little

        ndr_uint32         :a
        ndr_uint32         :b
        ndr_conf_var_array :c, type: :ndr_uint32
      end
    end

    it 'initializes the members to their default value' do
      expect(struct.new).to eq(a: 0, b: 0, c: [])
    end
    it 'reads itself' do
      values = {a: 44, b: 4444, c: [1,2,3]}
      struct_instance = struct.new(values)
      expect(struct.read(struct_instance.to_binary_s)).to eq(values)
    end
    context 'with values' do
      subject do
        struct.new(a: 4, b: 5, c: [1,2,3])
      end
      it 'returns the expected member values' do
        expect(subject).to eq(a: 4, b: 5, c: [1,2,3])
      end
      it 'outputs the expected binary representation' do
        binary =  "\x03\x00\x00\x00" # array max_count
        binary << "\x04\x00\x00\x00" # a
        binary << "\x05\x00\x00\x00" # b
        binary << "\x00\x00\x00\x00" # array offset
        binary << "\x03\x00\x00\x00" # array actual_count
        binary << "\x01\x00\x00\x00" # c[0]
        binary << "\x02\x00\x00\x00" # c[1]
        binary << "\x03\x00\x00\x00" # c[2]
        expect(subject.to_binary_s).to eq(binary.b)
      end
    end
  end

  context 'with a conformant varying array containing pointers' do
    let(:struct) do
      Class.new(described_class) do
        default_parameters byte_align: 4
        endian  :little

        ndr_uint32         :a
        ndr_uint32         :b
        ndr_conf_var_array :c, type: :ndr_uint32_ptr
      end
    end

    it 'initializes the members to their default value' do
      expect(struct.new).to eq(a: 0, b: 0, c: [])
    end
    it 'reads itself' do
      values = {a: 44, b: 4444, c: [1,2,3]}
      struct_instance = struct.new(values)
      expect(struct.read(struct_instance.to_binary_s)).to eq(values)
    end
    context 'with values' do
      subject do
        struct.new(a: 4, b: 5, c: [1,2,3])
      end
      it 'returns the expected member values' do
        expect(subject).to eq(a: 4, b: 5, c: [1,2,3])
      end
      it 'outputs the expected binary representation' do
        binary =  "\x03\x00\x00\x00" # array max_count
        binary << "\x04\x00\x00\x00" # a
        binary << "\x05\x00\x00\x00" # c
        binary << "\x00\x00\x00\x00" # array offset
        binary << "\x03\x00\x00\x00" # array actual_count
        binary << "\x01\x00\x00\x00" # b[0].ref_id
        binary << "\x02\x00\x00\x00" # b[1].ref_id
        binary << "\x03\x00\x00\x00" # b[2].ref_id
        binary << "\x01\x00\x00\x00" # deferred b[0]
        binary << "\x02\x00\x00\x00" # deferred b[1]
        binary << "\x03\x00\x00\x00" # deferred b[2]
        expect(subject.to_binary_s).to eq(binary.b)
      end
    end
  end

  context 'with an embedded structure containing a fixed array' do
    after :example do
      BinData::RegisteredClasses.unregister('test_struct')
    end

    let(:struct) do
      struct_with_array = Class.new(described_class) do
        default_parameters byte_align: 4
        endian :little

        ndr_uint16    :a
        ndr_fix_array :b, type: :ndr_uint32, initial_length: 3, byte_align: 4
        ndr_uint16    :c
      end
      BinData::RegisteredClasses.register('test_struct', struct_with_array)
      Class.new(described_class) do
        default_parameters byte_align: 4
        endian  :little

        ndr_uint16  :a
        ndr_uint16  :b
        test_struct :c
      end
    end

    it 'initializes the members to their default value' do
      expect(struct.new).to eq(a: 0, b: 0, c: {a: 0, b: [0, 0 ,0], c: 0})
    end
    it 'reads itself' do
      values = {a: 44, b: 4444, c: {a: 5555, b: [1, 2, 3], c: 77}}
      struct_instance = struct.new(values)
      expect(struct.read(struct_instance.to_binary_s)).to eq(values)
    end
    context 'with values' do
      subject do
        struct.new(a: 5, b: 6, c: {a: 7, b: [1, 2, 3], c: 8})
      end
      it 'returns the expected member values' do
        expect(subject).to eq(a: 5, b: 6, c: {a: 7, b: [1, 2, 3], c: 8})
      end
      it 'outputs the expected binary representation' do
        binary =  "\x05\x00" # a
        binary << "\x06\x00" # b
        binary << "\x07\x00" # c.a
        binary << "\x00\x00" # pad
        binary << "\x01\x00\x00\x00" # c.b[0]
        binary << "\x02\x00\x00\x00" # c.b[1]
        binary << "\x03\x00\x00\x00" # c.b[2]
        binary << "\x08\x00" # c.c
        expect(subject.to_binary_s).to eq(binary.b)
      end
    end
  end

  context 'with an embedded structure containing a fixed array of pointers' do
    after :example do
      BinData::RegisteredClasses.unregister('test_struct')
    end

    let(:struct) do
      struct_with_array = Class.new(described_class) do
        default_parameters byte_align: 4
        endian :little

        ndr_uint16    :a
        ndr_fix_array :b, type: :ndr_uint32_ptr, initial_length: 3, byte_align: 4
        ndr_uint16    :c
      end
      BinData::RegisteredClasses.register('test_struct', struct_with_array)
      Class.new(described_class) do
        default_parameters byte_align: 4
        endian  :little

        ndr_uint16  :a
        ndr_uint16  :b
        test_struct :c
      end
    end

    it 'initializes the members to their default value' do
      expect(struct.new).to eq(a: 0, b: 0, c: {a: 0, b: [:null, :null ,:null], c: 0})
    end
    it 'reads itself' do
      values = {a: 44, b: 4444, c: {a: 5555, b: [1, 2, 3], c: 77}}
      struct_instance = struct.new(values)
      expect(struct.read(struct_instance.to_binary_s)).to eq(values)
    end
    context 'with values' do
      subject do
        struct.new(a: 5, b: 6, c: {a: 7, b: [1, 2, 3], c: 8})
      end
      it 'returns the expected member values' do
        expect(subject).to eq(a: 5, b: 6, c: {a: 7, b: [1, 2, 3], c: 8})
      end
      it 'outputs the expected binary representation' do
        binary =  "\x05\x00"         # a
        binary << "\x06\x00"         # b
        binary << "\x07\x00"         # c.a
        binary << "\x00\x00"         # pad
        binary << "\x01\x00\x00\x00" # c.b[0].ref_id
        binary << "\x02\x00\x00\x00" # c.b[1].ref_id
        binary << "\x03\x00\x00\x00" # c.b[2].ref_id
        binary << "\x08\x00"         # c.c
        binary << "\x00\x00"         # pad
        binary << "\x01\x00\x00\x00" # deferred c.b[0]
        binary << "\x02\x00\x00\x00" # deferred c.b[1]
        binary << "\x03\x00\x00\x00" # deferred c.b[2]
        expect(subject.to_binary_s).to eq(binary.b)
      end
    end
  end

  context 'with an embedded structure containing a varying array' do
    after :example do
      BinData::RegisteredClasses.unregister('test_struct')
    end

    let(:struct) do
      struct_with_array = Class.new(described_class) do
        default_parameters byte_align: 4
        endian :little

        ndr_uint16    :a
        ndr_var_array :b, type: :ndr_uint32
        ndr_uint16    :c
      end
      BinData::RegisteredClasses.register('test_struct', struct_with_array)
      Class.new(described_class) do
        default_parameters byte_align: 4
        endian  :little

        ndr_uint16  :a
        ndr_uint16  :b
        test_struct :c
      end
    end

    it 'initializes the members to their default value' do
      expect(struct.new).to eq(a: 0, b: 0, c: {a: 0, b: [], c: 0})
    end
    it 'reads itself' do
      values = {a: 44, b: 4444, c: {a: 5555, b: [1, 2, 3], c: 77}}
      struct_instance = struct.new(values)
      expect(struct.read(struct_instance.to_binary_s)).to eq(values)
    end
    context 'with values' do
      subject do
        struct.new(a: 5, b: 6, c: {a: 7, b: [1, 2, 3], c: 8})
      end
      it 'returns the expected member values' do
        expect(subject).to eq(a: 5, b: 6, c: {a: 7, b: [1, 2, 3], c: 8})
      end
      it 'outputs the expected binary representation' do
        binary =  "\x05\x00"         # a
        binary << "\x06\x00"         # b
        binary << "\x07\x00"         # c.a
        binary << "\x00\x00"         # pad
        binary << "\x00\x00\x00\x00" # array offset
        binary << "\x03\x00\x00\x00" # array actual_count
        binary << "\x01\x00\x00\x00" # c.b[0]
        binary << "\x02\x00\x00\x00" # c.b[1]
        binary << "\x03\x00\x00\x00" # c.b[2]
        binary << "\x08\x00"         # c.c
        expect(subject.to_binary_s).to eq(binary.b)
      end
    end
  end

  context 'with an embedded structure containing a varying array of pointers' do
    after :example do
      BinData::RegisteredClasses.unregister('test_struct')
    end

    let(:struct) do
      struct_with_array = Class.new(described_class) do
        default_parameters byte_align: 4
        endian :little

        ndr_uint16    :a
        ndr_var_array :b, type: :ndr_uint32_ptr
        ndr_uint16    :c
      end
      BinData::RegisteredClasses.register('test_struct', struct_with_array)
      Class.new(described_class) do
        default_parameters byte_align: 4
        endian  :little

        ndr_uint16  :a
        ndr_uint16  :b
        test_struct :c
      end
    end

    it 'initializes the members to their default value' do
      expect(struct.new).to eq(a: 0, b: 0, c: {a: 0, b: [], c: 0})
    end
    it 'reads itself' do
      values = {a: 44, b: 4444, c: {a: 5555, b: [1, 2, 3], c: 77}}
      struct_instance = struct.new(values)
      expect(struct.read(struct_instance.to_binary_s)).to eq(values)
    end
    context 'with values' do
      subject do
        struct.new(a: 5, b: 6, c: {a: 7, b: [1, 2, 3], c: 8})
      end
      it 'returns the expected member values' do
        expect(subject).to eq(a: 5, b: 6, c: {a: 7, b: [1, 2, 3], c: 8})
      end
      it 'outputs the expected binary representation' do
        binary =  "\x05\x00"         # a
        binary << "\x06\x00"         # b
        binary << "\x07\x00"         # c.a
        binary << "\x00\x00"         # pad
        binary << "\x00\x00\x00\x00" # array offset
        binary << "\x03\x00\x00\x00" # array actual_count
        binary << "\x01\x00\x00\x00" # c.b[0].ref_id
        binary << "\x02\x00\x00\x00" # c.b[1].ref_id
        binary << "\x03\x00\x00\x00" # c.b[2].ref_id
        binary << "\x08\x00"         # c.c
        binary << "\x00\x00"         # pad
        binary << "\x01\x00\x00\x00" # deferred c.b[0]
        binary << "\x02\x00\x00\x00" # deferred c.b[1]
        binary << "\x03\x00\x00\x00" # deferred c.b[2]
        expect(subject.to_binary_s).to eq(binary.b)
      end
    end
  end

  context 'with an embedded structure containing a conformant array' do
    after :example do
      BinData::RegisteredClasses.unregister('test_struct')
    end

    let(:struct) do
      struct_with_array = Class.new(described_class) do
        default_parameters byte_align: 4
        endian :little

        ndr_uint16     :a
        ndr_uint16     :b
        ndr_conf_array :c, type: :ndr_uint32
      end
      BinData::RegisteredClasses.register('test_struct', struct_with_array)
      Class.new(described_class) do
        default_parameters byte_align: 4
        endian  :little

        ndr_uint16  :a
        ndr_uint16  :b
        test_struct :c
      end
    end

    it 'initializes the members to their default value' do
      expect(struct.new).to eq(a: 0, b: 0, c: {a: 0, b: 0, c: []})
    end
    it 'reads itself' do
      values = {a: 44, b: 4444, c: {a: 5555, b: 77, c: [1, 2, 3]}}
      struct_instance = struct.new(values)
      expect(struct.read(struct_instance.to_binary_s)).to eq(values)
    end
    context 'with values' do
      subject do
        struct.new(a: 5, b: 6, c: {a: 7, b: 8, c: [1, 2, 3]})
      end
      it 'returns the expected member values' do
        expect(subject).to eq(a: 5, b: 6, c: {a: 7, b:8, c: [1, 2, 3]})
      end
      it 'outputs the expected binary representation' do
        binary = "\x03\x00\x00\x00"  # array max_count
        binary << "\x05\x00"         # a
        binary << "\x06\x00"         # b
        binary << "\x07\x00"         # c.a
        binary << "\x08\x00"         # c.b
        binary << "\x01\x00\x00\x00" # c.c[0]
        binary << "\x02\x00\x00\x00" # c.c[1]
        binary << "\x03\x00\x00\x00" # c.c[2]
        expect(subject.to_binary_s).to eq(binary.b)
      end
    end
  end

  context 'with an embedded structure containing a conformant array of pointers' do
    after :example do
      BinData::RegisteredClasses.unregister('test_struct')
    end

    let(:struct) do
      struct_with_array = Class.new(described_class) do
        default_parameters byte_align: 4
        endian :little

        ndr_uint16     :a
        ndr_uint16     :b
        ndr_conf_array :c, type: :ndr_uint32_ptr
      end
      BinData::RegisteredClasses.register('test_struct', struct_with_array)
      Class.new(described_class) do
        default_parameters byte_align: 4
        endian  :little

        ndr_uint16  :a
        ndr_uint16  :b
        test_struct :c
      end
    end

    it 'initializes the members to their default value' do
      expect(struct.new).to eq(a: 0, b: 0, c: {a: 0, b: 0, c: []})
    end
    it 'reads itself' do
      values = {a: 44, b: 4444, c: {a: 5555, b: 77, c: [1, 2, 3]}}
      struct_instance = struct.new(values)
      expect(struct.read(struct_instance.to_binary_s)).to eq(values)
    end
    context 'with values' do
      subject do
        struct.new(a: 5, b: 6, c: {a: 7, b: 8, c: [1, 2, 3]})
      end
      it 'returns the expected member values' do
        expect(subject).to eq(a: 5, b: 6, c: {a: 7, b:8, c: [1, 2, 3]})
      end
      it 'outputs the expected binary representation' do
        binary =  "\x03\x00\x00\x00" # array max_count
        binary << "\x05\x00"         # a
        binary << "\x06\x00"         # b
        binary << "\x07\x00"         # c.a
        binary << "\x08\x00"         # c.b
        binary << "\x01\x00\x00\x00" # c.b[0].ref_id
        binary << "\x02\x00\x00\x00" # c.b[1].ref_id
        binary << "\x03\x00\x00\x00" # c.b[2].ref_id
        binary << "\x01\x00\x00\x00" # deferred c.b[0]
        binary << "\x02\x00\x00\x00" # deferred c.b[1]
        binary << "\x03\x00\x00\x00" # deferred c.b[2]
        expect(subject.to_binary_s).to eq(binary.b)
      end
    end
  end

  context 'with an embedded structure containing a conformant varying array' do
    after :example do
      BinData::RegisteredClasses.unregister('test_struct')
    end

    let(:struct) do
      struct_with_array = Class.new(described_class) do
        default_parameters byte_align: 4
        endian :little

        ndr_uint16         :a
        ndr_uint16         :b
        ndr_conf_var_array :c, type: :ndr_uint32
      end
      BinData::RegisteredClasses.register('test_struct', struct_with_array)
      Class.new(described_class) do
        default_parameters byte_align: 4
        endian  :little

        ndr_uint16  :a
        ndr_uint16  :b
        test_struct :c
      end
    end

    it 'initializes the members to their default value' do
      expect(struct.new).to eq(a: 0, b: 0, c: {a: 0, b: 0, c: []})
    end
    it 'reads itself' do
      values = {a: 44, b: 4444, c: {a: 5555, b: 77, c: [1, 2, 3]}}
      struct_instance = struct.new(values)
      expect(struct.read(struct_instance.to_binary_s)).to eq(values)
    end
    context 'with values' do
      subject do
        struct.new(a: 5, b: 6, c: {a: 7, b: 8, c: [1, 2, 3]})
      end
      it 'returns the expected member values' do
        expect(subject).to eq(a: 5, b: 6, c: {a: 7, b:8, c: [1, 2, 3]})
      end
      it 'outputs the expected binary representation' do
        binary =  "\x03\x00\x00\x00" # array max_count
        binary << "\x05\x00"         # a
        binary << "\x06\x00"         # b
        binary << "\x07\x00"         # c.a
        binary << "\x08\x00"         # c.b
        binary << "\x00\x00\x00\x00" # array offset
        binary << "\x03\x00\x00\x00" # array actual_count
        binary << "\x01\x00\x00\x00" # c.c[0]
        binary << "\x02\x00\x00\x00" # c.c[1]
        binary << "\x03\x00\x00\x00" # c.c[2]
        expect(subject.to_binary_s).to eq(binary.b)
      end
    end
  end

  context 'with an embedded structure containing a conformant varying array of pointers' do
    after :example do
      BinData::RegisteredClasses.unregister('test_struct')
    end

    let(:struct) do
      struct_with_array = Class.new(described_class) do
        default_parameters byte_align: 4
        endian :little

        ndr_uint16         :a
        ndr_uint16         :b
        ndr_conf_var_array :c, type: :ndr_uint32_ptr
      end
      BinData::RegisteredClasses.register('test_struct', struct_with_array)
      Class.new(described_class) do
        default_parameters byte_align: 4
        endian  :little

        ndr_uint16  :a
        ndr_uint16  :b
        test_struct :c
      end
    end

    it 'initializes the members to their default value' do
      expect(struct.new).to eq(a: 0, b: 0, c: {a: 0, b: 0, c: []})
    end
    it 'reads itself' do
      values = {a: 44, b: 4444, c: {a: 5555, b: 77, c: [1, 2, 3]}}
      struct_instance = struct.new(values)
      expect(struct.read(struct_instance.to_binary_s)).to eq(values)
    end
    context 'with values' do
      subject do
        struct.new(a: 5, b: 6, c: {a: 7, b: 8, c: [1, 2, 3]})
      end
      it 'returns the expected member values' do
        expect(subject).to eq(a: 5, b: 6, c: {a: 7, b:8, c: [1, 2, 3]})
      end
      it 'outputs the expected binary representation' do
        binary =  "\x03\x00\x00\x00" # array max_count
        binary << "\x05\x00"         # a
        binary << "\x06\x00"         # b
        binary << "\x07\x00"         # c.a
        binary << "\x08\x00"         # c.b
        binary << "\x00\x00\x00\x00" # array offset
        binary << "\x03\x00\x00\x00" # array actual_count
        binary << "\x01\x00\x00\x00" # c.b[0].ref_id
        binary << "\x02\x00\x00\x00" # c.b[1].ref_id
        binary << "\x03\x00\x00\x00" # c.b[2].ref_id
        binary << "\x01\x00\x00\x00" # deferred c.b[0]
        binary << "\x02\x00\x00\x00" # deferred c.b[1]
        binary << "\x03\x00\x00\x00" # deferred c.b[2]
        expect(subject.to_binary_s).to eq(binary.b)
      end
    end
  end

  context 'with a pointer to a conformant arrays' do
    let(:struct) do
      struct_ptr = Class.new(RubySMB::Dcerpc::Ndr::NdrConfArray) do
        default_parameters(type: :ndr_uint32)
        extend RubySMB::Dcerpc::Ndr::PointerClassPlugin
      end
      BinData::RegisteredClasses.register('test_conf_array_ptr', struct_ptr)
      Class.new(described_class) do
        default_parameters byte_align: 4
        endian  :little

        ndr_uint32          :a
        ndr_uint32          :b
        test_conf_array_ptr :c
      end
    end

    it 'initializes the members to their default value' do
      expect(struct.new).to eq(a: 0, b: 0, c: :null)
    end
    it 'reads itself' do
      values = {a: 44, b: 4444, c: [1,2,3]}
      struct_instance = struct.new(values)
      expect(struct.read(struct_instance.to_binary_s)).to eq(values)
    end
    context 'with values' do
      subject do
        struct.new(a: 4, b: 5, c: [1,2,3])
      end
      it 'returns the expected member values' do
        expect(subject).to eq(a: 4, b: 5, c: [1,2,3])
      end
      it 'outputs the expected binary representation' do
        binary =  "\x04\x00\x00\x00" # a
        binary << "\x05\x00\x00\x00" # b
        binary << "\x01\x00\x00\x00" # c.ref_id
        binary << "\x03\x00\x00\x00" # array max_count
        binary << "\x01\x00\x00\x00" # c[0]
        binary << "\x02\x00\x00\x00" # c[1]
        binary << "\x03\x00\x00\x00" # c[2]
        expect(subject.to_binary_s).to eq(binary.b)
      end
    end
  end

  context 'with a pointer to an embedded structure containing a conformant arrays' do
    after :example do
      BinData::RegisteredClasses.unregister('test_struct')
    end

    let(:struct) do
      struct_with_array = Class.new(described_class) do
        default_parameters byte_align: 4
        endian :little

        ndr_uint32         :a
        ndr_conf_var_array :b, type: :ndr_uint32
      end
      struct_ptr = Class.new(struct_with_array) do
        extend RubySMB::Dcerpc::Ndr::PointerClassPlugin
      end
      BinData::RegisteredClasses.register('test_struct_ptr', struct_ptr)
      Class.new(BinData::Record) do
        default_parameters byte_align: 4
        endian  :little

        ndr_uint32      :a
        ndr_uint32      :b
        test_struct_ptr :c
      end
    end

    it 'initializes the members to their default value' do
      expect(struct.new).to eq(a: 0, b: 0, c: :null)
    end
    it 'reads itself' do
      values = {a: 44, b: 4444, c: {a: 5555, b: [1, 2, 3, 4]}}
      struct_instance = struct.new(values)
      expect(struct.read(struct_instance.to_binary_s)).to eq(values)
    end
    context 'with values' do
      subject do
        struct.new(a: 5, b: 6, c: {a: 7, b: [1, 2, 3, 4]})
      end
      it 'returns the expected member values' do
        expect(subject).to eq(a: 5, b: 6, c: {a: 7, b: [1, 2, 3, 4]})
      end
      it 'outputs the expected binary representation' do
        binary =  "\x05\x00\x00\x00" # a
        binary << "\x06\x00\x00\x00" # b
        binary << "\x01\x00\x00\x00" # c.ref_id
        binary << "\x04\x00\x00\x00" # array max_count
        binary << "\x07\x00\x00\x00" # c.a
        binary << "\x00\x00\x00\x00" # array offset
        binary << "\x04\x00\x00\x00" # array actual_count
        binary << "\x01\x00\x00\x00" # c.b[0]
        binary << "\x02\x00\x00\x00" # c.b[1]
        binary << "\x03\x00\x00\x00" # c.b[2]
        binary << "\x04\x00\x00\x00" # c.b[3]
        expect(subject.to_binary_s).to eq(binary.b)
      end
    end
  end

  context 'with varying strings' do
    let(:struct) do
      Class.new(described_class) do
        default_parameters byte_align: 4
        endian  :little

        ndr_uint8      :a
        ndr_var_string :b
        ndr_uint32     :c
        ndr_var_string :d
        ndr_boolean    :e
      end
    end

    it 'initializes the members to their default value' do
      expect(struct.new).to eq(a: 0, b: '', c: 0, d: '', e: false)
    end
    it 'reads itself' do
      values = {a: 44, b: 'Test1', c: 4444, d: 'Test22', e: true}
      struct_instance = struct.new(values)
      expect(struct.read(struct_instance.to_binary_s)).to eq(values)
    end
    context 'with values' do
      subject do
        struct.new(a: 1, b: 'Test1', c: 3, d: 'Test22', e: true)
      end
      it 'returns the expected member values' do
        expect(subject).to eq(a: 1, b: 'Test1', c: 3, d: 'Test22', e: true)
      end
      it 'outputs the expected binary representation' do
        binary =  "\x01"             # a
        binary << "\x00\x00\x00"     # pad
        binary << "\x00\x00\x00\x00" # b offset
        binary << "\x05\x00\x00\x00" # b actual_count
        binary << 'Test1'            # b
        binary << "\x00\x00\x00"     # pad
        binary << "\x03\x00\x00\x00" # c
        binary << "\x00\x00\x00\x00" # b offset
        binary << "\x06\x00\x00\x00" # b actual_count
        binary << 'Test22'           # d
        binary << "\x00\x00"         # pad
        binary << "\x01\x00\x00\x00" # e
        expect(subject.to_binary_s).to eq(binary.b)
      end
    end
  end

  context 'with varying null terminated strings' do
    let(:struct) do
      Class.new(described_class) do
        default_parameters byte_align: 4
        endian  :little

        ndr_uint8       :a
        ndr_var_stringz :b
        ndr_uint32      :c
        ndr_var_stringz :d
        ndr_boolean     :e
      end
    end

    it 'initializes the members to their default value' do
      expect(struct.new).to eq(a: 0, b: '', c: 0, d: '', e: false)
    end
    it 'reads itself' do
      values = {a: 44, b: 'Test1', c: 4444, d: 'Test22', e: true}
      struct_instance = struct.new(values)
      expect(struct.read(struct_instance.to_binary_s)).to eq(values)
    end
    context 'with values' do
      subject do
        struct.new(a: 1, b: 'Test1', c: 3, d: 'Test22', e: true)
      end
      it 'returns the expected member values' do
        expect(subject).to eq(a: 1, b: 'Test1', c: 3, d: 'Test22', e: true)
      end
      it 'outputs the expected binary representation' do
        binary =  "\x01"             # a
        binary << "\x00\x00\x00"     # pad
        binary << "\x00\x00\x00\x00" # b offset
        binary << "\x06\x00\x00\x00" # b actual_count
        binary << "Test1\x00"        # b
        binary << "\x00\x00"         # pad
        binary << "\x03\x00\x00\x00" # c
        binary << "\x00\x00\x00\x00" # b offset
        binary << "\x07\x00\x00\x00" # b actual_count
        binary << "Test22\x00"       # d
        binary << "\x00"             # pad
        binary << "\x01\x00\x00\x00" # e
        expect(subject.to_binary_s).to eq(binary.b)
      end
    end
  end

  context 'with varying wide strings' do
    let(:struct) do
      Class.new(described_class) do
        default_parameters byte_align: 4
        endian  :little

        ndr_uint8           :a
        ndr_var_wide_string :b
        ndr_uint32          :c
        ndr_var_wide_string :d
        ndr_boolean         :e
      end
    end

    it 'initializes the members to their default value' do
      expect(struct.new).to eq(a: 0, b: '', c: 0, d: '', e: false)
    end
    it 'reads itself' do
      values = {a: 44, b: 'Test1'.encode('utf-16le'), c: 4444, d: 'Test22'.encode('utf-16le'), e: true}
      struct_instance = struct.new(values)
      expect(struct.read(struct_instance.to_binary_s)).to eq(values)
    end
    context 'with values' do
      subject do
        struct.new(a: 1, b: 'Test1', c: 3, d: 'Test22', e: true)
      end
      it 'returns the expected member values' do
        expect(subject).to eq(a: 1, b: 'Test1'.encode('utf-16le'), c: 3, d: 'Test22'.encode('utf-16le'), e: true)
      end
      it 'outputs the expected binary representation' do
        binary =  "\x01"                        # a
        binary << "\x00\x00\x00"                # pad
        binary << "\x00\x00\x00\x00"            # b offset
        binary << "\x05\x00\x00\x00"            # b actual_count
        binary << 'Test1'.encode('utf-16le').b  # b
        binary << "\x00\x00"                    # pad
        binary << "\x03\x00\x00\x00"            # c
        binary << "\x00\x00\x00\x00"            # b offset
        binary << "\x06\x00\x00\x00"            # b actual_count
        binary << 'Test22'.encode('utf-16le').b # d
        binary << "\x01\x00\x00\x00"            # e
        expect(subject.to_binary_s).to eq(binary.b)
      end
    end
  end

  context 'with varying null terminated wide strings' do
    let(:struct) do
      Class.new(described_class) do
        default_parameters byte_align: 4
        endian  :little

        ndr_uint8            :a
        ndr_var_wide_stringz :b
        ndr_uint32           :c
        ndr_var_wide_stringz :d
        ndr_boolean          :e
      end
    end

    it 'initializes the members to their default value' do
      expect(struct.new).to eq(a: 0, b: '', c: 0, d: '', e: false)
    end
    it 'reads itself' do
      values = {a: 44, b: 'Test1'.encode('utf-16le'), c: 4444, d: 'Test22'.encode('utf-16le'), e: true}
      struct_instance = struct.new(values)
      expect(struct.read(struct_instance.to_binary_s)).to eq(values)
    end
    context 'with values' do
      subject do
        struct.new(a: 1, b: 'Test1', c: 3, d: 'Test22', e: true)
      end
      it 'returns the expected member values' do
        expect(subject).to eq(a: 1, b: 'Test1'.encode('utf-16le'), c: 3, d: 'Test22'.encode('utf-16le'), e: true)
      end
      it 'outputs the expected binary representation' do
        binary =  "\x01"                            # a
        binary << "\x00\x00\x00"                    # pad
        binary << "\x00\x00\x00\x00"                # b offset
        binary << "\x06\x00\x00\x00"                # b actual_count
        binary << "Test1\x00".encode('utf-16le').b  # b
        binary << "\x03\x00\x00\x00"                # c
        binary << "\x00\x00\x00\x00"                # b offset
        binary << "\x07\x00\x00\x00"                # b actual_count
        binary << "Test22\x00".encode('utf-16le').b # d
        binary << "\x00\x00"                        # pad
        binary << "\x01\x00\x00\x00"                # e
        expect(subject.to_binary_s).to eq(binary.b)
      end
    end
  end

  context 'with conformant varying strings' do
    let(:struct) do
      Class.new(described_class) do
        default_parameters byte_align: 4
        endian  :little

        ndr_uint8           :a
        ndr_uint32          :b
        ndr_boolean         :c
        ndr_conf_var_string :d
      end
    end

    it 'initializes the members to their default value' do
      expect(struct.new).to eq(a: 0, b: 0, c: false, d: '')
    end
    it 'reads itself' do
      values = {a: 44, b: 4444, c: true, d: 'Test22'}
      struct_instance = struct.new(values)
      expect(struct.read(struct_instance.to_binary_s)).to eq(values)
    end
    context 'with values' do
      subject do
        struct.new(a: 1, b: 3, c: true, d: 'Test22')
      end
      it 'returns the expected member values' do
        expect(subject).to eq(a: 1, b: 3, c: true, d: 'Test22')
      end
      it 'outputs the expected binary representation' do
        binary =  "\x06\x00\x00\x00" # b max_count
        binary << "\x01"             # a
        binary << "\x00\x00\x00"     # pad
        binary << "\x03\x00\x00\x00" # b
        binary << "\x01\x00\x00\x00" # c
        binary << "\x00\x00\x00\x00" # b offset
        binary << "\x06\x00\x00\x00" # b actual_count
        binary << 'Test22'           # d
        expect(subject.to_binary_s).to eq(binary.b)
      end
    end
  end

  context 'with conformant varying null-terminated strings' do
    let(:struct) do
      Class.new(described_class) do
        default_parameters byte_align: 4
        endian  :little

        ndr_uint8            :a
        ndr_uint32           :b
        ndr_boolean          :c
        ndr_conf_var_stringz :d
      end
    end

    it 'initializes the members to their default value' do
      expect(struct.new).to eq(a: 0, b: 0, c: false, d: '')
    end
    it 'reads itself' do
      values = {a: 44, b: 4444, c: true, d: 'Test22'}
      struct_instance = struct.new(values)
      expect(struct.read(struct_instance.to_binary_s)).to eq(values)
    end
    context 'with values' do
      subject do
        struct.new(a: 1, b: 3, c: true, d: 'Test22')
      end
      it 'returns the expected member values' do
        expect(subject).to eq(a: 1, b: 3, c: true, d: 'Test22')
      end
      it 'outputs the expected binary representation' do
        binary =  "\x07\x00\x00\x00" # b max_count
        binary << "\x01"             # a
        binary << "\x00\x00\x00"     # pad
        binary << "\x03\x00\x00\x00" # b
        binary << "\x01\x00\x00\x00" # c
        binary << "\x00\x00\x00\x00" # b offset
        binary << "\x07\x00\x00\x00" # b actual_count
        binary << "Test22\x00"       # d
        expect(subject.to_binary_s).to eq(binary.b)
      end
    end
  end

  context 'with conformant varying wide strings' do
    let(:struct) do
      Class.new(described_class) do
        default_parameters byte_align: 4
        endian  :little

        ndr_uint8                :a
        ndr_uint32               :b
        ndr_boolean              :c
        ndr_conf_var_wide_string :d
      end
    end

    it 'initializes the members to their default value' do
      expect(struct.new).to eq(a: 0, b: 0, c: false, d: '')
    end
    it 'reads itself' do
      values = {a: 44, b: 4444, c: true, d: 'Test22'.encode('utf-16le')}
      struct_instance = struct.new(values)
      expect(struct.read(struct_instance.to_binary_s)).to eq(values)
    end
    context 'with values' do
      subject do
        struct.new(a: 1, b: 3, c: true, d: 'Test22')
      end
      it 'returns the expected member values' do
        expect(subject).to eq(a: 1, b: 3, c: true, d: 'Test22'.encode('utf-16le'))
      end
      it 'outputs the expected binary representation' do
        binary =  "\x06\x00\x00\x00"            # b max_count
        binary << "\x01"                        # a
        binary << "\x00\x00\x00"                # pad
        binary << "\x03\x00\x00\x00"            # b
        binary << "\x01\x00\x00\x00"            # c
        binary << "\x00\x00\x00\x00"            # b offset
        binary << "\x06\x00\x00\x00"            # b actual_count
        binary << 'Test22'.encode('utf-16le').b # d
        expect(subject.to_binary_s).to eq(binary.b)
      end
    end
  end

  context 'with conformant varying null-terminated wide strings' do
    let(:struct) do
      Class.new(described_class) do
        default_parameters byte_align: 4
        endian  :little

        ndr_uint8                 :a
        ndr_uint32                :b
        ndr_boolean               :c
        ndr_conf_var_wide_stringz :d
      end
    end

    it 'initializes the members to their default value' do
      expect(struct.new).to eq(a: 0, b: 0, c: false, d: '')
    end
    it 'reads itself' do
      values = {a: 44, b: 4444, c: true, d: 'Test22'.encode('utf-16le')}
      struct_instance = struct.new(values)
      expect(struct.read(struct_instance.to_binary_s)).to eq(values)
    end
    context 'with values' do
      subject do
        struct.new(a: 1, b: 3, c: true, d: 'Test22')
      end
      it 'returns the expected member values' do
        expect(subject).to eq(a: 1, b: 3, c: true, d: 'Test22'.encode('utf-16le'))
      end
      it 'outputs the expected binary representation' do
        binary =  "\x07\x00\x00\x00"                # b max_count
        binary << "\x01"                            # a
        binary << "\x00\x00\x00"                    # pad
        binary << "\x03\x00\x00\x00"                # b
        binary << "\x01\x00\x00\x00"                # c
        binary << "\x00\x00\x00\x00"                # b offset
        binary << "\x07\x00\x00\x00"                # b actual_count
        binary << "Test22\x00".encode('utf-16le').b # d
        expect(subject.to_binary_s).to eq(binary.b)
      end
    end
  end
end


#
# Pointers
#
{
  NdrUint8Ptr: { parent_class: :NdrUint8, data: 2, binary: "\x02", size: 1 },
  NdrUint16Ptr: { parent_class: :NdrUint16, data: 3, binary: [3].pack('S'), size: 2 },
  NdrUint32Ptr: { parent_class: :NdrUint32, data: 5, binary: [5].pack('L'), size: 4 },
  NdrUint64Ptr: { parent_class: :NdrUint64, data: 7, binary: [7].pack('Q'), size: 8 },
  NdrCharPtr: { parent_class: :NdrChar, data: 'C', binary: 'C', size: 1 },
  NdrBooleanPtr: { parent_class: :NdrBoolean, data: true, binary: [1].pack('L'), size: 4 },
  NdrStringPtr: {
    parent_class: :NdrConfVarString,
    data: 'Test1',
    binary: "#{[5].pack('L')}#{[0].pack('L')}#{[5].pack('L')}Test1", size: 4
  },
  NdrStringzPtr: {
    parent_class: :NdrConfVarStringz,
    data: 'Test2',
    binary: "#{[6].pack('L')}#{[0].pack('L')}#{[6].pack('L')}Test2\x00", size: 4
  },
  NdrWideStringPtr: {
    parent_class: :NdrConfVarWideString,
    data: 'Test3'.encode('utf-16le'),
    binary: "#{[5].pack('L')}#{[0].pack('L')}#{[5].pack('L')}#{'Test3'.encode('utf-16le').b}", size: 4
  },
  NdrWideStringzPtr: {
    parent_class: :NdrConfVarWideStringz,
    data: 'Test4'.encode('utf-16le'),
    binary: "#{[6].pack('L')}#{[0].pack('L')}#{[6].pack('L')}#{'Test4'.encode('utf-16le').b}\x00\x00", size: 4
  },
  NdrByteArrayPtr: {
    parent_class: :NdrConfVarArray,
    data: [1,2,3,4],
    binary: "#{[4].pack('L')}#{[0].pack('L')}#{[4].pack('L')}\x01\x02\x03\x04", size: 4
  },
  NdrFileTimePtr: {
    parent_class: :NdrFileTime,
    data: 132682503830000000,
    binary: [132682503830000000].pack('Q'), size: 4
  }
}.each do |ndr_class, info|
  RSpec.describe(RubySMB::Dcerpc::Ndr.const_get(ndr_class)) do
    subject { described_class.new }
    let(:class_with_ref_to) do
      struct = Class.new(BinData::Record) do
        endian :little
        ndr_uint32 :a
      end
      struct.send(described_class.bindata_name.to_sym, :b)
      struct.send(described_class.bindata_name.to_sym, :c, ref_to: :b)
      struct
    end
    let(:ref_to_instance) { class_with_ref_to.new(a: 1, b: info[:data]) }
    let(:ref_id) { [RubySMB::Dcerpc::Ndr::INITIAL_REF_ID].pack('L') }

    it 'is a RubySMB::Dcerpc::Ndr::PointerClassPlugin class' do
      expect(described_class).to be_a(RubySMB::Dcerpc::Ndr::PointerClassPlugin)
    end
    it 'is not a Top Level class' do
      expect(described_class).not_to be_a(RubySMB::Dcerpc::Ndr::TopLevelPlugin)
    end
    it "is a RubySMB::Dcerpc::Ndr::#{info[:parent_class]} class" do
      parent_class = if info[:parent_class].is_a?(Symbol)
                       RubySMB::Dcerpc::Ndr.const_get(info[:parent_class])
                     else
                       info[:parent_class]
                     end
      expect(described_class).to be < parent_class
    end

    it { is_expected.to respond_to :ref_id }

    describe '#initialize_instance' do
      it 'sets #ref_id to 0 by default' do
        expect(subject.ref_id).to eq(0)
      end
      it 'does not reset #ref_id to 0 when it has already been set to a value' do
        subject.ref_id = 5
        subject.initialize_instance
        expect(subject.ref_id).to eq(5)
      end
      it 'sets #ref_id to INITIAL_REF_ID by default when :initial_value parameter is provided' do
        test_instance = described_class.new(nil, {initial_value: info[:data]})
        expect(test_instance.ref_id).to eq(RubySMB::Dcerpc::Ndr::INITIAL_REF_ID)
      end
    end

    describe '#extend_top_level_class' do
      it 'extends to TopLevelPlugin' do
        expect(subject).to be_a(RubySMB::Dcerpc::Ndr::TopLevelPlugin)
      end
      it 'sets the pointer to top level pointer' do
        expect(subject.is_top_level_ptr).to be true
      end
      context 'when embedded in another structure' do
        let(:struct_class) do
          struct_class = Class.new(RubySMB::Dcerpc::Ndr::NdrStruct) do
            default_parameters byte_align: 4
            endian  :little
          end
          struct_class.send(described_class.bindata_name.to_sym, :c)
          struct_class
        end
        let(:top_level_class) do
          Class.new(BinData::Record) do
            endian  :little
            ndr_uint32  :a
            test_struct :b
          end
        end
        before :example do
          BinData::RegisteredClasses.register('test_struct', struct_class)
        end

        it 'extends the parents to TopLevelPlugin' do
          struct = top_level_class.new(a: 2, b: {c: info[:data]})
          expect(struct).to be_a(RubySMB::Dcerpc::Ndr::TopLevelPlugin)
          expect(struct.b).to be_a(RubySMB::Dcerpc::Ndr::TopLevelPlugin)
        end
        it 'only sets the top level structure as a top level pointer' do
          struct = top_level_class.new(a: 2, b: {c: info[:data]})
          expect(struct.is_top_level_ptr).to be true
          expect(struct.b.is_top_level_ptr).to be false
          expect(struct.b.c.is_top_level_ptr).to be false
        end
      end
    end

    describe '#snapshot' do
      it 'outputs :null by default' do
        expect(subject.snapshot).to eq(:null)
      end
      it 'outputs the referent when it refers to another Top-Level pointer' do
        expect(ref_to_instance.snapshot).to eq({a:1, b:info[:data], c:info[:data]})
      end
      # BinData does not support initial_value parameter for arrays
      unless ndr_class == :NdrByteArrayPtr
        it 'outputs the :initial_value parameter value when nothing has been assigned yet' do
          test_ptr = described_class.new(nil, {initial_value: info[:data]})
          expect(test_ptr.snapshot).to eq(info[:data])
        end
      end
    end

    describe '#do_write' do
      it 'outputs 32-bit zero binary representation when it is a null pointer' do
        expect(subject.to_binary_s).to eq("\x00\x00\x00\x00".b)
      end
      it 'outputs the initial referent ID followed by the representation of the referent' do
        expect(subject.new(info[:data]).to_binary_s).to eq("#{ref_id}#{info[:binary]}".b)
      end
      it 'outputs the referent ID of the Top-Level pointer it is refering to' do
        align = (4 - (info[:binary].size % 4)) % 4
        pad = "\x00" * align
        expect(ref_to_instance.to_binary_s).to eq(
          "#{[1].pack('L')}#{ref_id}#{info[:binary]}#{pad}#{ref_id}"
        )
        expect(ref_to_instance.c.to_binary_s).to eq(ref_id)
      end
      it 'outputs the initial referent ID and the referent representaiton if it is not embedded in another constructed structure' do
        expect(subject.new(info[:data]).to_binary_s).to eq("#{ref_id}#{info[:binary]}".b)
      end
      # BinData does not support initial_value parameter for arrays
      unless ndr_class == :NdrByteArrayPtr
        it 'outputs the :initial_value parameter value when nothing has been assigned yet' do
          test_ptr = described_class.new(nil, {initial_value: info[:data]})
          expect(test_ptr.new.to_binary_s).to eq("#{ref_id}#{info[:binary]}".b)
        end
      end
      context 'when embedded in another constructed structure'do
        let(:embedding_struct) do
          RubySMB::Dcerpc::Ndr::NdrConfArray.new([info[:data], info[:data]], type: described_class)
        end
        let(:output_str) do
          ref_id2 = [RubySMB::Dcerpc::Ndr::INITIAL_REF_ID + 1].pack('L')
          output_str = "#{[2].pack('L')}#{ref_id}#{ref_id2}".b
          align = (info[:size] - (output_str.size % info[:size])) % info[:size]
          output_str << "\x00".b * align
          output_str << info[:binary]
          align = (info[:size] - (output_str.size % info[:size])) % info[:size]
          output_str << "\x00".b * align
          output_str << info[:binary]
        end

        it 'does not change the embedded structure binary representation' do
          expect(embedding_struct[0].to_binary_s).to eq("#{ref_id}#{info[:binary]}".b)
          expect(embedding_struct[1].to_binary_s).to eq("#{ref_id}#{info[:binary]}".b)
        end
        it 'returns the correct aligned binary stream' do
          expect(embedding_struct.to_binary_s).to eq(output_str)
        end
        context 'when calling #to_binary_s on embedded structures' do
          # There was a bug where calling to_binary_s updated the ref_id
          # persistently, which resulted in wrong output
          it 'returns the correct aligned binary stream' do
            embedding_struct[0].to_binary_s
            embedding_struct[1].to_binary_s
            expect(embedding_struct.to_binary_s).to eq(output_str)
          end
        end
      end
    end

    describe '#do_read' do
      it 'reads a 32-bit zero binary representation as a null pointer' do
        expect(subject.read("\x00\x00\x00\x00".b)).to eq(:null)
      end
      it 'reads the referent ID followed by the representation of the referent' do
        expect(subject.read("#{ref_id}#{info[:binary]}")).to eq(info[:data])
        expect(subject.ref_id).to eq(RubySMB::Dcerpc::Ndr::INITIAL_REF_ID)
      end
      it 'reads the referent ID of the Top-Level pointer it is refering to' do
        align = (4 - (info[:binary].size % 4)) % 4
        pad = "\x00" * align
        binary_str = "#{[1].pack('L')}"\
                     "#{ref_id}"\
                     "#{info[:binary]}"\
                     "#{pad}"\
                     "#{ref_id}"
        test_instance = class_with_ref_to.read(binary_str)
        expect(test_instance.c.snapshot).to eq(info[:data])
        expect(test_instance.c.ref_id).to eq(test_instance.b.ref_id)
      end
      it 'reads the initial referent ID and the referent representaiton if it is not embedded in another constructed structure' do
        expect(subject.read("#{ref_id}#{info[:binary]}".b)).to eq(info[:data])
        expect(subject.ref_id).to eq(RubySMB::Dcerpc::Ndr::INITIAL_REF_ID)
      end
      context 'when embedded in another constructed structure'do
        let(:test_struct) { described_class.new(info[:data].dup) }
        let(:embedding_struct) do
          subject.assign(info[:data])
          RubySMB::Dcerpc::Ndr::NdrConfArray.new([subject, test_struct], type: described_class)
        end
        let(:binary_str) do
          ref_id2 = [RubySMB::Dcerpc::Ndr::INITIAL_REF_ID + 1].pack('L')
          binary_str = "#{[2].pack('L')}#{ref_id}#{ref_id2}".b
          align = (info[:size] - (binary_str.size % info[:size])) % info[:size]
          binary_str << "\x00".b * align
          binary_str << info[:binary]
          align = (info[:size] - (binary_str.size % info[:size])) % info[:size]
          binary_str << "\x00".b * align
          binary_str << info[:binary]
          binary_str
        end
        it 'correctly defers the referent after the embedding structure in the stream' do
          obj = embedding_struct.read(binary_str)
          expect(obj).to eq([subject, test_struct])
        end
      end
    end

    describe '#assign' do
      it 'sets #ref_id to 0 when assigning :null' do
        subject.assign(:null)
        expect(subject.ref_id).to eq(0)
      end
      it 'assigns the value to the referent when it refers to another Top-Level pointer' do
        ref_to_instance.c = info[:data]
        expect(ref_to_instance.b).to eq(info[:data])
      end
      it 'sets #ref_id to the initial reference ID value' do
        subject.assign(info[:data])
        expect(subject.ref_id).to eq(RubySMB::Dcerpc::Ndr::INITIAL_REF_ID)
      end
      it 'does not change #ref_id when it has already been set' do
        subject.ref_id = 20
        subject.assign(info[:data])
        expect(subject.ref_id).to eq(20)
      end
    end

    describe '#alias?' do
      it 'returns true if it refers to another Top-Level pointer' do
        expect(ref_to_instance.c.is_alias?).to be true
      end
    end

    describe '#fetch_alias_referent' do
      it 'returns the referent' do
        expect(ref_to_instance.c.fetch_alias_referent).to eq(info[:data])
      end
      context 'when the referent is in a NDR structure' do
        let(:class_with_ref_to) do
          test_struct_class = Class.new(RubySMB::Dcerpc::Ndr::NdrStruct) do
            default_parameters(byte_align: 4)
            ndr_uint32     :a
          end
          test_struct_class.send(described_class.bindata_name.to_sym, :ptr1)
          BinData::RegisteredClasses.register('test_struct', test_struct_class)
          struct = Class.new(BinData::Record) do
            test_struct :struct1
          end
          struct.send(described_class.bindata_name.to_sym, :ptr2, ref_to: :ptr1)
          struct
        end
        let(:ref_to_instance) { class_with_ref_to.new(struct1: { a: 2, ptr1: info[:data] }) }
        it 'returns the referent' do
          expect(ref_to_instance.ptr2.fetch_alias_referent).to eq(info[:data])
        end
      end
    end

    describe '#do_num_bytes' do
      it 'returns 4 if it is a null pointer' do
        expect(subject.do_num_bytes).to eq(4)
      end
      it 'returns 4 if it refers to another Top-Level pointer' do
        expect(ref_to_instance.c.do_num_bytes).to eq(4)
      end
      it "returns #{4 + info[:binary].size} a first-instance pointer" do
        subject.assign(info[:data])
        expect(subject.do_num_bytes).to eq(4 + info[:binary].size)
      end
    end
  end
end

RSpec.describe RubySMB::Dcerpc::Ndr::TopLevelPlugin do
  let(:ref_id) { RubySMB::Dcerpc::Ndr::INITIAL_REF_ID }
  let(:struct_with_ptr) do
    Class.new(RubySMB::Dcerpc::Ndr::NdrStruct) do
      default_parameters byte_align: 4
      endian :little

      ndr_uint32     :b
      ndr_char_ptr   :ptr2
      ndr_uint32_ptr :ptr3
    end
  end
  let(:random_struct) do
    Class.new(RubySMB::Dcerpc::Ndr::NdrStruct) do
      default_parameters byte_align: 4
      endian              :little

      ndr_uint32 :rand1
      ndr_uint32 :rand2
    end
  end

  before :example do
    BinData::RegisteredClasses.register('struct_with_ptr', struct_with_ptr)
    BinData::RegisteredClasses.register('random_struct', random_struct)
  end

  context 'with a BinData structure' do
    let(:test_struct) do
      Class.new(BinData::Record) do
        endian          :little

        ndr_uint32      :a
        ndr_char_ptr    :ptr1
        struct_with_ptr :d
        ndr_uint32_ptr  :ptr4
      end
    end
    let(:snapshot) { { a: 55, ptr1: 'M', d: { b: 66, ptr2: 'A', ptr3: 33 }, ptr4: 44 } }
    let(:binary) do
      "#{[55].pack('L')}"\
      "#{[ref_id].pack('L')}"\
      "M"\
      "\x00\x00\x00"\
      "#{[66].pack('L')}"\
      "#{[ref_id + 1].pack('L')}"\
      "#{[ref_id + 2].pack('L')}"\
      "A"\
      "\x00\x00\x00"\
      "#{[33].pack('L')}"\
      "#{[ref_id + 3].pack('L')}"\
      "#{[44].pack('L')}".b
    end
    subject { test_struct.new(snapshot) }

    it 'outputs the correct binary stream' do
      expect(subject.to_binary_s).to eq(binary)
    end
  end

  context 'with a BinData structure containing pointers with :initial_value set' do
    let(:test_struct) do
      Class.new(BinData::Record) do
        endian          :little

        ndr_uint32      :a
        ndr_char_ptr    :ptr1, initial_value: 'M'
        struct_with_ptr :d
        ndr_uint32_ptr  :ptr4, initial_value: 44
      end
    end
    let(:snapshot) { { a: 55, d: { b: 66, ptr2: 'A', ptr3: 33 } } }
    let(:binary) do
      "#{[55].pack('L')}"\
      "#{[ref_id].pack('L')}"\
      "M"\
      "\x00\x00\x00"\
      "#{[66].pack('L')}"\
      "#{[ref_id + 1].pack('L')}"\
      "#{[ref_id + 2].pack('L')}"\
      "A"\
      "\x00\x00\x00"\
      "#{[33].pack('L')}"\
      "#{[ref_id + 3].pack('L')}"\
      "#{[44].pack('L')}".b
    end
    subject { test_struct.new(snapshot) }

    it 'outputs the correct binary stream' do
      expect(subject.to_binary_s).to eq(binary)
    end
  end

  context 'with a BinData::Choice' do
    let(:test_struct) do
      Class.new(BinData::Record) do
        endian          :little

        ndr_uint32      :a
        ndr_char_ptr    :ptr1
        choice :choice1, selection: :a, byte_align: 4 do
          struct_with_ptr 55
        end
        ndr_uint32_ptr  :ptr4
      end
    end
    let(:snapshot) { { a: 55, ptr1: 'M', choice1: { b: 66, ptr2: 'A', ptr3: 33 }, ptr4: 44 } }
    let(:binary) do
      "#{[55].pack('L')}"\
      "#{[ref_id].pack('L')}"\
      "M"\
      "\x00\x00\x00"\
      "#{[66].pack('L')}"\
      "#{[ref_id + 1].pack('L')}"\
      "#{[ref_id + 2].pack('L')}"\
      "A"\
      "\x00\x00\x00"\
      "#{[33].pack('L')}"\
      "#{[ref_id + 3].pack('L')}"\
      "#{[44].pack('L')}".b
    end
    subject { test_struct.new(snapshot) }

    it 'outputs the correct binary stream' do
      expect(subject.to_binary_s).to eq(binary)
    end
  end

  context 'with a NDR array and an alias pointer' do
    let(:test_struct) do
      Class.new(BinData::Record) do
        endian          :little

        ndr_uint32      :a
        ndr_char_ptr    :ptr1
        ndr_conf_array  :array1, type: :random_struct
        struct_with_ptr :d
        ndr_uint32_ptr  :ptr4, ref_to: :ptr3
      end
    end
    let(:snapshot) do
      {
        a: 0xF1,
        ptr1: 'A',
        array1: [
          { rand1: 0xF2, rand2: 0xF3 },
          { rand1: 0xF4, rand2: 0xF5 }
        ],
        d: { b: 0xF6, ptr2: 'B', ptr3: 0xF7 }
      }
    end
    let(:binary) do
      "#{[0xF1].pack('L<')}"\
      "#{[ref_id].pack('L<')}"\
      "A"\
      "\x00\x00\x00"\
      "#{[2].pack('L<')}"\
      "#{[0xF2].pack('L<')}"\
      "#{[0xF3].pack('L<')}"\
      "#{[0xF4].pack('L<')}"\
      "#{[0xF5].pack('L<')}"\
      "#{[0xF6].pack('L<')}"\
      "#{[ref_id + 1].pack('L<')}"\
      "#{[ref_id + 2].pack('L<')}"\
      "B"\
      "\x00\x00\x00"\
      "#{[0xF7].pack('L<')}"\
      "#{[ref_id + 2].pack('L<')}"
    end
    subject { test_struct.new(snapshot) }

    it 'outputs the correct binary stream' do
      expect(subject.to_binary_s).to eq(binary)
    end

    context 'when the alias pointer is positioned after the referent pointer' do
      let(:test_struct2) do
        Class.new(BinData::Record) do
          endian          :little

          ndr_uint32      :a
          ndr_char_ptr    :ptr1
          ndr_conf_array  :array1, type: :random_struct
          # :ptr3 is part of :d structure, which appears after :ptr4
          ndr_uint32_ptr  :ptr4, ref_to: :ptr3
          struct_with_ptr :d
        end
      end

      context 'with #snapshot' do
        it 'raises an exception' do
          expect { test_struct2.new(snapshot).snapshot }.to raise_error(ArgumentError)
        end
      end

      context 'with #to_binary_s' do
        it 'raises an exception' do
          expect { test_struct2.new(snapshot).to_binary_s }.to raise_error(ArgumentError)
        end
      end
    end

    context 'when the alias pointer refers to a non-existing pointer' do
      let(:test_struct2) do
        Class.new(BinData::Record) do
          endian          :little

          ndr_uint32      :a
          ndr_char_ptr    :ptr1
          ndr_conf_array  :array1, type: :random_struct
          ndr_uint32_ptr  :ptr4, ref_to: :ptr5
          struct_with_ptr :d
        end
      end

      it 'raises an exception' do
        expect { test_struct2.new(snapshot).snapshot }.to raise_error(ArgumentError)
      end
    end
  end
end

RSpec.describe ::BinData::NdrPointerArgProcessor do
  let(:embedding_struct) do
    Class.new(BinData::Record) do
      ndr_class :a
    end
  end

  before :example do
    ndr_class = Class.new(ref_class) do
      #default_parameters byte_align: 4
      arg_processor :ndr_pointer
    end
    BinData::RegisteredClasses.register('ndr_class', ndr_class)
  end

  context 'with a NDR structure as referent' do
    let(:ref_class) { RubySMB::Dcerpc::Ndr::NdrUint32 }
    it 'does not raise error' do
      expect { embedding_struct }.to_not raise_error
    end
  end
  context 'with a BinData structure as referent' do
    let(:ref_class) { BinData::Uint32le }

    it 'raises an error' do
      expect { embedding_struct }.to raise_error(ArgumentError)
    end
    context 'with byte_align parameter' do
      let(:embedding_struct) do
        Class.new(BinData::Record) do
          ndr_class :a, byte_align: 4
        end
      end
      it 'raises an error' do
        expect { embedding_struct }.to raise_error(ArgumentError)
      end
    end
    context 'with referent_byte_align parameter' do
      let(:embedding_struct) do
        Class.new(BinData::Record) do
          ndr_class :a, referent_byte_align: 4
        end
      end
      it 'does not raise error' do
        expect { embedding_struct }.to_not raise_error
      end
    end
  end
end

RSpec.describe RubySMB::Dcerpc::Ndr::NdrContextHandle do
  it 'is a BinData::Primitive class' do
    expect(described_class).to be < BinData::Primitive
  end

  subject { described_class.new }

  it { is_expected.to respond_to :context_handle_attributes }
  it { is_expected.to respond_to :context_handle_uuid }

  context 'with #get' do
    it 'returns a hash with the default values' do
      expect(subject.get).to eq({context_handle_attributes: 0, context_handle_uuid: '00000000-0000-0000-0000-000000000000'})
    end
    it 'returns a hash with the expected values' do
      subject.assign(context_handle_attributes: 4, context_handle_uuid: '57800405-0301-3330-5566-040023007000')
      expect(subject.get).to eq({context_handle_attributes: 4, context_handle_uuid: '57800405-0301-3330-5566-040023007000'})
    end
  end

  context 'with #set' do
    context 'with a hash' do
      it 'sets the expected values' do
        subject.set({context_handle_attributes: 4, context_handle_uuid: '57800405-0301-3330-5566-040023007000'})
        expect(subject).to eq({context_handle_attributes: 4, context_handle_uuid: '57800405-0301-3330-5566-040023007000'})
      end
    end
    context 'with a NdrContextHandle object' do
      it 'sets the expected values' do
        object = described_class.new
        object.set({context_handle_attributes: 4, context_handle_uuid: '57800405-0301-3330-5566-040023007000'})
        subject.set(object)
        expect(subject).to eq({context_handle_attributes: 4, context_handle_uuid: '57800405-0301-3330-5566-040023007000'})
      end
    end
    context 'with a binary string' do
      it 'sets the expected values' do
        subject.set("\x04\x00\x00\x00\x05\x04\x80\x57\x01\x03\x30\x33\x55\x66\x04\x00\x23\x00\x70\x00")
        expect(subject).to eq({context_handle_attributes: 4, context_handle_uuid: '57800405-0301-3330-5566-040023007000'})
      end
    end
  end
end

RSpec.describe 'Alignment' do
  RSpec.shared_examples 'an aligned structure' do |align: 4, field_value:, field_binary:|
    let(:struct_obj) do
      struct_class.new(aligned_field: field_value)
    end
    it "is #{align}-bytes aligned" do
      expect(struct_obj.aligned_field.rel_offset % align).to eq(0)
    end
    it 'includes padding in its binary representation' do
      expect(struct_obj.to_binary_s).to eq("A#{"\x00" * (align - 1)}#{field_binary}".b)
    end
  end

  let(:params) { {} }
  let(:struct_class) do
    Class.new(BinData::Record) do
      endian  :little
      string  :one_byte, value: 'A'
    end
  end

  context 'in a BinData::Struct' do
    let(:params) { {} }
    before :example do
      struct_class.send(described_class.bindata_name.to_sym, :aligned_field, params)
    end

    describe RubySMB::Dcerpc::Ndr::NdrBoolean do
      it_behaves_like(
        'an aligned structure',
        field_value: true,
        field_binary: [1].pack('L')
      )
    end

    describe RubySMB::Dcerpc::Ndr::NdrWideChar do
      it_behaves_like(
        'an aligned structure',
        align: 2,
        field_value: 'B'.encode('utf-16le'),
        field_binary: "B\x00"
      )
    end

    describe RubySMB::Dcerpc::Ndr::NdrEnum do
      it_behaves_like(
        'an aligned structure',
        align: 2,
        field_value: 3,
        field_binary: [3].pack('S')
      )
    end

    describe RubySMB::Dcerpc::Ndr::NdrFixArray do
      context 'with an array of NdrUint32' do
        it_behaves_like(
          'an aligned structure',
          field_value: [1, 2, 3],
          field_binary: [1, 2, 3].pack('LLL')
        ) do
          let(:params) { {type: :ndr_uint32, initial_length: 3, byte_align: 4} }
        end
      end
      context 'with an array of NdrUint64' do
        it_behaves_like(
          'an aligned structure',
          align: 8,
          field_value: [1, 2],
          field_binary: [1, 2].pack('QQ')
        ) do
          let(:params) { {type: :ndr_uint64, initial_length: 2, byte_align: 8} }
        end
      end
    end

    describe RubySMB::Dcerpc::Ndr::NdrConfArray do
      context 'with an array of NdrUint32' do
        it_behaves_like(
          'an aligned structure',
          field_value: [1, 2, 3],
          field_binary: [3].pack('L') + [1, 2, 3].pack('LLL')
        ) do
          let(:params) { {type: :ndr_uint32 } }
        end
      end
      context 'with an array of NdrUint64' do
        it_behaves_like(
          'an aligned structure',
          align: 8,
          field_value: [1, 2],
          field_binary: [2].pack('L') + [0].pack('L') + [1, 2].pack('QQ')
        ) do
          let(:params) { {type: :ndr_uint64 } }
        end
      end
    end

    describe RubySMB::Dcerpc::Ndr::NdrVarArray do
      context 'with an array of NdrUint32' do
        it_behaves_like(
          'an aligned structure',
          field_value: [1 ,2 ,3],
          field_binary: [0].pack('L') + [3].pack('L') + [1, 2, 3].pack('LLL')
        ) do
          let(:params) { {type: :ndr_uint32 } }
        end
      end
      context 'with an array of NdrUint64' do
        it_behaves_like(
          'an aligned structure',
          align: 8,
          field_value: [1, 2],
          field_binary: [0].pack('L') + [2].pack('L') + [1, 2].pack('QQ')
        ) do
          let(:params) { {type: :ndr_uint64 } }
        end
      end
    end

    describe RubySMB::Dcerpc::Ndr::NdrConfVarArray do
      context 'with an array of NdrUint32' do
        it_behaves_like(
          'an aligned structure',
          field_value: [1, 2, 3],
          field_binary: [3].pack('L') + [0].pack('L') + [3].pack('L') + [1, 2, 3].pack('LLL')
        ) do
          let(:params) { {type: :ndr_uint32 } }
        end
      end
      context 'with an array of NdrUint64' do
        it_behaves_like(
          'an aligned structure',
          align: 8,
          field_value: [1, 2],
          field_binary: [2].pack('L') + [0].pack('L') + [2].pack('L') + [0].pack('L') + [1, 2].pack('QQ')
        ) do
          let(:params) { {type: :ndr_uint64 } }
        end
      end
    end

    describe RubySMB::Dcerpc::Ndr::NdrVarString do
      it_behaves_like(
        'an aligned structure',
        field_value: "Test1",
        field_binary: [0].pack('L') + [5].pack('L') + "Test1"
      )
    end

    describe RubySMB::Dcerpc::Ndr::NdrVarStringz do
      it_behaves_like(
        'an aligned structure',
        field_value: "Test1",
        field_binary: [0].pack('L') + [6].pack('L') + "Test1\x00"
      )
    end

    describe RubySMB::Dcerpc::Ndr::NdrVarWideString do
      it_behaves_like(
        'an aligned structure',
        field_value: "Test1".encode('utf-16le'),
        field_binary: [0].pack('L') + [5].pack('L') + "Test1".encode('utf-16le').force_encoding('ASCII')
      )
    end

    describe RubySMB::Dcerpc::Ndr::NdrVarWideStringz do
      it_behaves_like(
        'an aligned structure',
        field_value: "Test1".encode('utf-16le'),
        field_binary: [0].pack('L') + [6].pack('L') + "Test1\x00".encode('utf-16le').force_encoding('ASCII')
      )
    end

    describe RubySMB::Dcerpc::Ndr::NdrConfVarString do
      it_behaves_like(
        'an aligned structure',
        field_value: "Test1",
        field_binary: [5].pack('L') + [0].pack('L') + [5].pack('L') + "Test1"
      )
    end

    describe RubySMB::Dcerpc::Ndr::NdrConfVarStringz do
      it_behaves_like(
        'an aligned structure',
        field_value: "Test1",
        field_binary: [6].pack('L') + [0].pack('L') + [6].pack('L') + "Test1\x00"
      )
    end

    describe RubySMB::Dcerpc::Ndr::NdrConfVarWideString do
      it_behaves_like(
        'an aligned structure',
        field_value: "Test1".encode('utf-16le'),
        field_binary: [5].pack('L') + [0].pack('L') + [5].pack('L') + "Test1".encode('utf-16le').force_encoding('ASCII')
      )
    end

    describe RubySMB::Dcerpc::Ndr::NdrConfVarWideStringz do
      it_behaves_like(
        'an aligned structure',
        field_value: "Test1".encode('utf-16le'),
        field_binary: [6].pack('L') + [0].pack('L') + [6].pack('L') + "Test1\x00".encode('utf-16le').force_encoding('ASCII')
      )
    end

    {
      NdrUint8Ptr: { data: 2, binary: "\x02" },
      NdrUint16Ptr: { data: 3, binary: [3].pack('S') },
      NdrUint32Ptr: { data: 5, binary: [5].pack('L') },
      NdrUint64Ptr: { data: 7, binary: [7].pack('Q') },
      NdrCharPtr: { data: 'C', binary: 'C' },
      NdrBooleanPtr: { data: true, binary: [1].pack('L')},
      NdrStringPtr: { data: 'Test1', binary: "#{[5].pack('L')}#{[0].pack('L')}#{[5].pack('L')}Test1" },
      NdrStringzPtr: { data: 'Test2', binary: "#{[6].pack('L')}#{[0].pack('L')}#{[6].pack('L')}Test2\x00" },
      NdrWideStringPtr: { data: 'Test3'.encode('utf-16le'), binary: "#{[5].pack('L')}#{[0].pack('L')}#{[5].pack('L')}#{'Test3'.encode('utf-16le').force_encoding('ASCII')}" },
      NdrWideStringzPtr: { data: 'Test4'.encode('utf-16le'), binary: "#{[6].pack('L')}#{[0].pack('L')}#{[6].pack('L')}#{'Test4'.encode('utf-16le').force_encoding('ASCII')}\x00\x00" },
      NdrByteArrayPtr: { data: [1,2,3,4], binary: "#{[4].pack('L')}#{[0].pack('L')}#{[4].pack('L')}\x01\x02\x03\x04" },
      NdrFileTimePtr: { data: 132682503830000000, binary: [132682503830000000].pack('Q') }
    }.each do |ndr_class, info|
      describe(RubySMB::Dcerpc::Ndr.const_get(ndr_class)) do
        it_behaves_like(
          'an aligned structure',
          field_value: info[:data],
          field_binary: [RubySMB::Dcerpc::Ndr::INITIAL_REF_ID].pack('L')+ info[:binary]
        )
      end
    end

    describe RubySMB::Dcerpc::Ndr::NdrContextHandle do
      it_behaves_like(
        'an aligned structure',
        field_value: { context_handle_attributes: 4, context_handle_uuid: '57800405-0301-3330-5566-040023007000' },
        field_binary: "\x04\x00\x00\x00\x05\x04\x80\x57\x01\x03\x30\x33\x55\x66\x04\x00\x23\x00\x70\x00"
      )
    end
  end

  context 'in a NDR structure' do
    describe 'Structure of mixed integers' do
      before :example do
        ndr_struct_class = Class.new(RubySMB::Dcerpc::Ndr::NdrStruct) do
          default_parameters byte_align: 4
          endian  :little

          ndr_uint32  :int_value1, byte_align: 4
          ndr_uint8   :int_value2
        end
        BinData::RegisteredClasses.register('ndr_struct_class', ndr_struct_class)
        struct_class.send(:ndr_struct_class, :aligned_field, params)
      end

      it_behaves_like(
        'an aligned structure',
        align: 4,
        field_value: { int_value1: 5, int_value2: 3 },
        field_binary: [5].pack('L') + "\x03"
      ) do
        # :byte_align is set according to the type of the largest element in
        # the structure (uint32):
        let(:params) { {byte_align: 4 } }
      end
    end
  end

  context 'in a conformant array' do
    let(:params) { {} }
    before :example do
      struct_class.send(:ndr_conf_array, :aligned_field, { :type => [ described_class, params ] } )
    end

    describe RubySMB::Dcerpc::Ndr::NdrBoolean do
      it_behaves_like(
        'an aligned structure',
        field_value: [true, false, true],
        field_binary: [3].pack('L') + [1, 0, 1].pack('LLL')
      )
    end

    describe RubySMB::Dcerpc::Ndr::NdrWideChar do
      it_behaves_like(
        'an aligned structure',
        field_value: 'ABC'.encode('utf-16le').chars,
        field_binary: [3].pack('L') + "A\x00B\x00C\x00"
      )
    end

    describe RubySMB::Dcerpc::Ndr::NdrEnum do
      it_behaves_like(
        'an aligned structure',
        field_value: [1, 2, 3],
        field_binary: [3].pack('L') + [1, 2, 3].pack('SSS')
      )
    end

    describe RubySMB::Dcerpc::Ndr::NdrFixArray do
      context 'with an array of NdrUint32' do
        it_behaves_like(
          'an aligned structure',
          field_value: [ [1, 2], [3, 4] ],
          field_binary: [2].pack('L') + [1, 2].pack('LL') + [3, 4].pack('LL')
        ) do
          let(:params) { {type: :ndr_uint32, initial_length: 2, byte_align: 4} }
        end
      end
      context 'with an array of NdrUint64' do
        it_behaves_like(
          'an aligned structure',
          align: 8,
          field_value: [ [1, 2], [3, 4] ],
          field_binary: [2].pack('L') + [0].pack('L') + [1, 2].pack('QQ') + [3, 4].pack('QQ')
        ) do
          let(:params) { {type: :ndr_uint64, initial_length: 2, byte_align: 8} }
        end
      end
    end

    describe RubySMB::Dcerpc::Ndr::NdrConfArray do
      context 'with an array of NdrUint32' do
        it_behaves_like(
          'an aligned structure',
          field_value: [ [1, 2], [3, 4] ],
          field_binary: [2].pack('L') + [2].pack('L') + [1, 2].pack('LL') + [2].pack('L') + [3, 4].pack('LL')
        ) do
          let(:params) { {type: :ndr_uint32} }
        end
      end
      context 'with an array of NdrUint64' do
        it_behaves_like(
          'an aligned structure',
          align: 8,
          field_value: [ [1, 2], [3, 4] ],
          field_binary: [2].pack('L') + [0].pack('L') + [2].pack('L') + [0].pack('L') + [1, 2].pack('QQ') + [2].pack('L') + [0].pack('L') + [3, 4].pack('QQ')
        ) do
          let(:params) { {type: :ndr_uint64} }
        end
      end
    end

    describe RubySMB::Dcerpc::Ndr::NdrVarArray do
      context 'with an array of NdrUint32' do
        it_behaves_like(
          'an aligned structure',
          field_value: [ [1, 2], [3, 4] ],
          field_binary: [2].pack('L') + [0].pack('L') + [2].pack('L') + [1, 2].pack('LL') + [0].pack('L') + [2].pack('L') + [3, 4].pack('LL')
        ) do
          let(:params) { {type: :ndr_uint32} }
        end
      end
      context 'with an array of NdrUint64' do
        it_behaves_like(
          'an aligned structure',
          align: 8,
          field_value: [ [1, 2], [3, 4] ],
          field_binary: [2].pack('L') + [0].pack('L') + [0].pack('L') + [2].pack('L') + [1, 2].pack('QQ') + [0].pack('L') + [2].pack('L') + [3, 4].pack('QQ')
        ) do
          let(:params) { {type: :ndr_uint64} }
        end
      end
    end

    describe RubySMB::Dcerpc::Ndr::NdrConfVarArray do
      context 'with an array of NdrUint32' do
        it_behaves_like(
          'an aligned structure',
          field_value: [ [1, 2], [3, 4] ],
          field_binary: [2].pack('L') + [2].pack('L') + [0].pack('L') + [2].pack('L') + [1, 2].pack('LL') + [2].pack('L') + [0].pack('L') + [2].pack('L') + [3, 4].pack('LL')
        ) do
          let(:params) { {type: :ndr_uint32 } }
        end
      end
      context 'with an array of NdrUint64' do
        it_behaves_like(
          'an aligned structure',
          align: 8,
          field_value: [ [1, 2], [3, 4] ],
          field_binary: [2].pack('L') + [0].pack('L') + [2].pack('L') + [0].pack('L') + [2].pack('L') + [0].pack('L') + [1, 2].pack('QQ') + [2].pack('L') + [0].pack('L') + [2].pack('L') + [0].pack('L') + [3, 4].pack('QQ')
        ) do
          let(:params) { {type: :ndr_uint64} }
        end
      end
    end

    describe RubySMB::Dcerpc::Ndr::NdrVarString do
      it_behaves_like(
        'an aligned structure',
        field_value: [ "Test1", "Test2" ],
        field_binary: [2].pack('L') + [0].pack('L') + [5].pack('L') + "Test1" + [0].pack('L') + [5].pack('L') + "Test2"
      )
    end

    describe RubySMB::Dcerpc::Ndr::NdrVarStringz do
      it_behaves_like(
        'an aligned structure',
        field_value: [ "Test1", "Test2" ],
        field_binary: [2].pack('L') + [0].pack('L') + [6].pack('L') + "Test1\x00" + [0].pack('L') + [6].pack('L') + "Test2\x00"
      )
    end

    describe RubySMB::Dcerpc::Ndr::NdrVarWideString do
      it_behaves_like(
        'an aligned structure',
        field_value: [ "Test1".encode('utf-16le'), "Test2".encode('utf-16le') ],
        field_binary: [2].pack('L') + [0].pack('L') + [5].pack('L') + "Test1".encode('utf-16le').force_encoding('ASCII') + [0].pack('L') + [5].pack('L') + "Test2".encode('utf-16le').force_encoding('ASCII')
      )
    end

    describe RubySMB::Dcerpc::Ndr::NdrVarWideStringz do
      it_behaves_like(
        'an aligned structure',
        field_value: [ "Test1".encode('utf-16le'), "Test2".encode('utf-16le') ],
        field_binary: [2].pack('L') + [0].pack('L') + [6].pack('L') + "Test1\x00".encode('utf-16le').force_encoding('ASCII') + [0].pack('L') + [6].pack('L') + "Test2\x00".encode('utf-16le').force_encoding('ASCII')
      )
    end

    describe RubySMB::Dcerpc::Ndr::NdrConfVarString do
      it_behaves_like(
        'an aligned structure',
        field_value: [ "Test1", "Test2" ],
        field_binary: [2].pack('L') + [5].pack('L') + [0].pack('L') + [5].pack('L') + "Test1" + [5].pack('L') + [0].pack('L') + [5].pack('L') + "Test2"
      )
    end

    describe RubySMB::Dcerpc::Ndr::NdrConfVarStringz do
      it_behaves_like(
        'an aligned structure',
        field_value: [ "Test1", "Test2" ],
        field_binary: [2].pack('L') + [6].pack('L') + [0].pack('L') + [6].pack('L') + "Test1\x00" + [6].pack('L') + [0].pack('L') + [6].pack('L') + "Test2\x00"
      )
    end

    describe RubySMB::Dcerpc::Ndr::NdrConfVarWideString do
      it_behaves_like(
        'an aligned structure',
        field_value: [ "Test1".encode('utf-16le'), "Test2".encode('utf-16le') ],
        field_binary: [2].pack('L') + [5].pack('L') + [0].pack('L') + [5].pack('L') + "Test1".encode('utf-16le').force_encoding('ASCII') + [5].pack('L') + [0].pack('L') + [5].pack('L') + "Test2".encode('utf-16le').force_encoding('ASCII')
      )
    end

    describe RubySMB::Dcerpc::Ndr::NdrConfVarWideStringz do
      it_behaves_like(
        'an aligned structure',
        field_value: [ "Test1".encode('utf-16le'), "Test2".encode('utf-16le') ],
        field_binary: [2].pack('L') + [6].pack('L') + [0].pack('L') + [6].pack('L') + "Test1\x00".encode('utf-16le').force_encoding('ASCII') + [6].pack('L') + [0].pack('L') + [6].pack('L') + "Test2\x00".encode('utf-16le').force_encoding('ASCII')
      )
    end

    {
      NdrUint8Ptr: { data: 2, binary: "\x02", size: 1 },
      NdrUint16Ptr: { data: 3, binary: [3].pack('S'), size: 2 },
      NdrUint32Ptr: { data: 5, binary: [5].pack('L'), size: 4 },
      NdrUint64Ptr: { data: 7, binary: [7].pack('Q'), size: 8 },
      NdrCharPtr: { data: 'C', binary: 'C', size: 1 },
      NdrBooleanPtr: { data: true, binary: [1].pack('L'), size: 4 },
      NdrStringPtr: {
        data: 'Test1',
        binary: "#{[5].pack('L')}#{[0].pack('L')}#{[5].pack('L')}Test1",
        size: 4
      },
      NdrStringzPtr: {
        data: 'Test2',
        binary: "#{[6].pack('L')}#{[0].pack('L')}#{[6].pack('L')}Test2\x00",
        size: 4
      },
      NdrWideStringPtr: {
        data: 'Test3'.encode('utf-16le'),
        binary: "#{[5].pack('L')}#{[0].pack('L')}#{[5].pack('L')}#{'Test3'.encode('utf-16le').b}",
        size: 4
      },
      NdrWideStringzPtr: {
        data: 'Test4'.encode('utf-16le'),
        binary: "#{[6].pack('L')}#{[0].pack('L')}#{[6].pack('L')}#{'Test4'.encode('utf-16le').b}\x00\x00",
        size: 4
      },
      NdrByteArrayPtr: {
        data: [1,2,3,4],
        binary: "#{[4].pack('L')}#{[0].pack('L')}#{[4].pack('L')}\x01\x02\x03\x04",
        size: 4
      },
      NdrFileTimePtr: { data: 132682503830000000, binary: [132682503830000000].pack('Q'), size: 4 }
    }.each do |ndr_class, info|
      describe(RubySMB::Dcerpc::Ndr.const_get(ndr_class)) do
        ref_id = [RubySMB::Dcerpc::Ndr::INITIAL_REF_ID].pack('L')
        ref_id2 = [RubySMB::Dcerpc::Ndr::INITIAL_REF_ID + 1].pack('L')
        binary_str = [2].pack('L') + ref_id + ref_id2
        # Embedding structure starts with 'A' + pad (4 bytes)
        current_size = 4 + binary_str.size
        align = (info[:size] - (current_size % info[:size])) % info[:size]
        binary_str << "\x00".b * align
        current_size += align
        binary_str << info[:binary]
        current_size += info[:binary].size
        align = (info[:size] - (current_size % info[:size])) % info[:size]
        binary_str << "\x00".b * align
        binary_str << info[:binary]
        it_behaves_like(
          'an aligned structure',
          field_value: [ info[:data], info[:data] ],
          field_binary: binary_str
        )
      end
    end

    describe RubySMB::Dcerpc::Ndr::NdrContextHandle do
      it_behaves_like(
        'an aligned structure',
        field_value: [
          { context_handle_attributes: 4, context_handle_uuid: '57800405-0301-3330-5566-040023007000' },
          { context_handle_attributes: 2, context_handle_uuid: '57800405-0301-3330-5566-040023007000' }
        ],
        field_binary: [2].pack('L') + "\x04\x00\x00\x00\x05\x04\x80\x57\x01\x03\x30\x33\x55\x66\x04\x00\x23\x00\x70\x00\x02\x00\x00\x00\x05\x04\x80\x57\x01\x03\x30\x33\x55\x66\x04\x00\x23\x00\x70\x00"
      )
    end
  end
end

RSpec.describe RubySMB::Dcerpc::Ndr::TypeSerialization1CommonTypeHeader do
  it 'is a BinData::Record class' do
    expect(described_class).to be < BinData::Record
  end
  it 'has :byte_align parameter set to the expected value' do
    expect(described_class.default_parameters[:byte_align]).to eq(8)
  end

  subject { described_class.new }

  it { is_expected.to respond_to :version }
  it { is_expected.to respond_to :endianness }
  it { is_expected.to respond_to :common_header_length }
  it { is_expected.to respond_to :filler }

  context 'with #version' do
    it 'is a BinData::Uint8' do
      expect(subject.version).to be_a BinData::Uint8
    end
    it 'returns 1 by default' do
      expect(subject.version).to eq(1)
    end
  end

  context 'with #endianness' do
    it 'is a BinData::Uint8' do
      expect(subject.endianness).to be_a BinData::Uint8
    end
    it 'returns 0x10 by default' do
      expect(subject.endianness).to eq(0x10)
    end
  end

  context 'with #common_header_length' do
    it 'is a BinData::Uint16le' do
      expect(subject.common_header_length).to be_a BinData::Uint16le
    end
    it 'returns 8 by default' do
      expect(subject.common_header_length).to eq(8)
    end
  end

  context 'with #filler' do
    it 'is a BinData::Uint32le' do
      expect(subject.filler).to be_a BinData::Uint32le
    end
    it 'returns 0xCCCCCCCC by default' do
      expect(subject.filler).to eq(0xCCCCCCCC)
    end
  end

  it 'reads itself' do
    values = {version: 4, endianness: 0x33, common_header_length: 44, filler: 0xFFFFFFFF}
    struct_instance = described_class.new(values)
    expect(described_class.read(struct_instance.to_binary_s)).to eq(values)
  end
end

RSpec.describe RubySMB::Dcerpc::Ndr::TypeSerialization1PrivateHeader do
  it 'is a BinData::Record class' do
    expect(described_class).to be < BinData::Record
  end
  it 'has :byte_align parameter set to the expected value' do
    expect(described_class.default_parameters[:byte_align]).to eq(8)
  end

  subject { described_class.new }

  it { is_expected.to respond_to :object_buffer_length }
  it { is_expected.to respond_to :filler }

  context 'with #object_buffer_length' do
    it 'is a BinData::Uint32le' do
      expect(subject.object_buffer_length).to be_a BinData::Uint32le
    end
    it 'calls its parent\'s #field_length method to set its default value' do
      test_struct = Class.new(BinData::Record) do
        endian :little
        type_serialization1_private_header :header

        def field_length(obj);end
      end.new
      expect(test_struct).to receive(:field_length).and_return(4)
      expect(test_struct.header.object_buffer_length).to eq(4)
    end
    it 'sets the default value to 0 when its parent doesn\'t implemet #field_length' do
      expect(subject.object_buffer_length).to eq(0)
    end
  end

  context 'with #filler' do
    it 'is a BinData::Uint32le' do
      expect(subject.filler).to be_a BinData::Uint32le
    end
    it 'returns 0x00000000 by default' do
      expect(subject.filler).to eq(0x00000000)
    end
  end

  it 'reads itself' do
    values = {object_buffer_length: 4, filler: 0xFFFFFFFF}
    struct_instance = described_class.new(values)
    expect(described_class.read(struct_instance.to_binary_s)).to eq(values)
  end
end

RSpec.describe RubySMB::Dcerpc::Ndr::TypeSerialization1 do
  it 'is a BinData::Record class' do
    expect(described_class).to be < BinData::Record
  end
  it 'has :byte_align parameter set to the expected value' do
    expect(described_class.default_parameters[:byte_align]).to eq(8)
  end

  subject { described_class.new }

  it { is_expected.to respond_to :common_header }

  context 'with #common_header' do
    it 'is a TypeSerialization1CommonTypeHeader structure' do
      expect(subject.common_header).to be_a RubySMB::Dcerpc::Ndr::TypeSerialization1CommonTypeHeader
    end
  end

  it 'reads itself' do
    values = {
      common_header: {version: 4, endianness: 0x33, common_header_length: 44, filler: 0xFFFFFFFF}
    }
    struct_instance = described_class.new(values)
    expect(described_class.read(struct_instance.to_binary_s)).to eq(values)
  end

  context 'when inherited' do
    let(:test_struct) do
      Class.new(RubySMB::Dcerpc::Ndr::NdrStruct) do
        default_parameters byte_align: 8
        endian :little

        rpc_unicode_string :full_name
        ndr_uint32 :user_id
      end
    end
    before :example do
      BinData::RegisteredClasses.register('test_struct', test_struct)
    end
    after :example do
      BinData::RegisteredClasses.unregister('test_struct')
    end

    subject do
      Class.new(described_class) do
        default_parameter byte_align: 8
        endian :little

        test_struct :data1
        uint32 :value1, initial_value: 5
        uint32 :value2, initial_value: 6
        test_struct :data2
        uint32 :value3, initial_value: 7
        test_struct :data3
      end.new
    end

    it { is_expected.to respond_to :common_header }
    it { is_expected.to respond_to :private_header1 }
    it { is_expected.to respond_to :data1 }
    it { is_expected.to respond_to :value1 }
    it { is_expected.to respond_to :value2 }
    it { is_expected.to respond_to :private_header2 }
    it { is_expected.to respond_to :data2 }
    it { is_expected.to respond_to :value3 }
    it { is_expected.to respond_to :private_header3 }
    it { is_expected.to respond_to :data3 }

    it 'sets the expected default values' do
      data_obj = test_struct.new
      expect(subject.data1).to eq(data_obj)
      expect(subject.value1).to eq(5)
      expect(subject.value2).to eq(6)
      expect(subject.data2).to eq(data_obj)
      expect(subject.value3).to eq(7)
      expect(subject.data3).to eq(data_obj)
    end

    it 'sets the correct private header\'s object_buffer_length field value' do
      data1 = test_struct.new(full_name: 'test string')
      subject.data1 = data1
      expect(subject.private_header1.object_buffer_length).to eq(data1.num_bytes)
      data2 = test_struct.new(full_name: 'another test string')
      subject.data2 = data2
      expect(subject.private_header2.object_buffer_length).to eq(data2.num_bytes)
      data3 = test_struct.new(full_name: 'more string!!!')
      subject.data3 = data3
      expect(subject.private_header3.object_buffer_length).to eq(data3.num_bytes)
    end
  end
end
