ronin-rb/ronin-support

View on GitHub
lib/ronin/support/binary/ctypes/union_type.rb

Summary

Maintainability
A
1 hr
Test Coverage
# frozen_string_literal: true
#
# Copyright (c) 2006-2023 Hal Brodigan (postmodern.mod3 at gmail.com)
#
# ronin-support is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# ronin-support is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with ronin-support.  If not, see <https://www.gnu.org/licenses/>.
#

require 'ronin/support/binary/ctypes/aggregate_type'

module Ronin
  module Support
    module Binary
      module CTypes
        #
        # Represents a `union` type.
        #
        # @api private
        #
        # @since 1.0.0
        #
        class UnionType < AggregateType

          #
          # Represents a member within a union.
          #
          # @api private
          #
          # @since 1.0.0
          #
          class Member

            # The type of the member.
            #
            # @return [Type]
            attr_reader :type

            #
            # Initializes the member.
            #
            # @param [Type] type
            #   The type of the member.
            #
            def initialize(type)
              @type = type
            end

            #
            # The offset of the member within the union.
            #
            # @return [0]
            #
            # @note This method is mainly for compatibility with
            # {StructType::Member#offset}.
            #
            def offset
              0
            end

            #
            # The size, in bytes, of the member within the union.
            #
            # @return [Integer]
            #
            def size
              @type.size
            end

            #
            # The alignment, in bytes, of the member within the union.
            #
            # @return [Integer]
            #
            def alignment
              @type.alignment
            end

          end

          # The members of the union type.
          #
          # @return [Hash{Symbol => Member}]
          attr_reader :members

          # The size of the union type.
          #
          # @return [Integer, Float::INFINITY]
          attr_reader :size

          # The alignment, in bytes, for the union type, so that all members
          # within the union type are themselves aligned.
          #
          # @return [Integer]
          attr_reader :alignment

          #
          # Initializes the union type.
          #
          # @param [Hash{Symbol => Member}] members
          #   The members for the union type.
          #
          # @param [Integer] size
          #   The size of the union type.
          #
          # @param [Integer] alignment
          #   The alignment of the union type.
          #
          def initialize(members, size: , alignment: )
            @members   = members
            @size      = size
            @alignment = alignment

            super(pack_string: nil)
          end

          #
          # Builds the union type from the given fields.
          #
          # @param [Hash{Symbol => Type}] fields
          #   The field names and types for the union type.
          #
          # @param [Integer, nil] alignment
          #   Custom alignment to use instead of the union's alignment.
          #
          # @return [UnionType]
          #   The new union type.
          #
          def self.build(fields, alignment: nil)
            members       = {}
            max_size      = 0
            max_alignment = 0

            fields.each do |name,type|
              members[name] = Member.new(type)

              # omit infinite sizes from the union size
              if (type.size > max_size) && (type.size != Float::INFINITY)
                max_size = type.size
              end

              max_alignment = type.alignment if type.alignment > max_alignment
            end

            return new(members, size:      max_size,
                                alignment: alignment || max_alignment)
          end

          #
          # Creates a new Hash of the union's uninitialized members.
          #
          # @return [Hash]
          #   The uninitialized values for the new union's members.
          #
          def uninitialized_value
            @members.transform_values do |member|
              member.type.uninitialized_value
            end
          end

          #
          # The number of members within the union.
          #
          # @return [Integer]
          #
          def length
            @members.length
          end

          #
          # Creates a copy of the union type with a different {#alignment}.
          #
          # @param [Integer] new_alignment
          #   The new alignment for the new union type.
          #
          # @return [ScalarType]
          #   The new union type.
          #
          def align(new_alignment)
            self.class.new(@members, size:        @size,
                                     alignment:   new_alignment)
          end

          #
          # Packs a hash of values into the member's binary layout.
          #
          # @param [Hash{Symbol => Integer, Float, String}] hash
          #   The hash of values to pack.
          #
          # @return [String]
          #   The packed binary data.
          #
          def pack(hash)
            unknown_keys = hash.keys - @members.keys

            unless unknown_keys.empty?
              raise(ArgumentError,"unknown union members: #{unknown_keys.map(&:inspect).join(', ')}")
            end

            buffer = if @size == Float::INFINITY
                       String.new(encoding: Encoding::ASCII_8BIT)
                     else
                       String.new("\0" * @size, encoding: Encoding::ASCII_8BIT)
                     end

            hash.each do |name,value|
              member = @members.fetch(name)
              data   = member.type.pack(value)

              if data.bytesize <= @size
                # if the packed data fits into buffer, overlay it
                buffer[0,data.bytesize] = data
              else
                # otherwise replace the buffer with the larger data
                buffer = data
              end
            end

            return buffer
          end

          #
          # Unpacks binary data into a Hash of values using the union's binary
          # layout.
          #
          # @param [String] data
          #   The binary data to unpack.
          #
          # @return [Hash{Symbol => Integer, Float, String, nil}]
          #   The unpacked hash.
          #
          def unpack(data)
            hash = {}

            @members.each do |name,member|
              slice = if member.size == Float::INFINITY
                        data
                      else
                        data.byteslice(0,member.size)
                      end

              hash[name] = member.type.unpack(slice)
            end

            return hash
          end

        end
      end
    end
  end
end