lib/ronin/support/binary/ctypes/struct_type.rb
# 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 `struct` type.
#
# @api private
#
# @since 1.0.0
#
class StructType < AggregateType
#
# Represents a member within a struct.
#
# @api private
#
# @since 1.0.0
#
class Member
# The offset, in bytes, of the member from the beginning of the
# struct.
#
# @return [Integer]
attr_reader :offset
# The type of the member.
#
# @return [Type]
attr_reader :type
#
# Initializes the member.
#
# @param [Integer] offset
# The offset, in bytes, of the member from the beginning of the
# struct.
#
# @param [Type] type
# The type of the member.
#
def initialize(offset,type)
@offset = offset
@type = type
end
#
# Calculates padding for the given type at the given offset.
#
# @param [Integer] offset
#
# @param [Type] type
#
# @return [Integer]
#
# @api private
#
# @see https://en.wikipedia.org/wiki/Data_structure_alignment#Computing_padding
#
def self.padding_for(offset,type)
(type.alignment - (offset % type.alignment)) % type.alignment
end
#
# Calculates the pack string for the given type and optional
# padding.
#
# @param [Type] type
#
# @param [Integer] padding
#
# @return [String]
# The pack string for the member.
#
# @api private
#
def self.pack_string_for(type,padding=0)
if padding > 0 then "#{'x' * padding}#{type.pack_string}"
else type.pack_string
end
end
#
# The size, in bytes, of the member within the struct.
#
# @return [Integer]
#
def size
@type.size
end
#
# The alignment, in bytes, of the member within the struct.
#
# @return [Integer]
#
def alignment
@type.alignment
end
end
# The members of the struct type.
#
# @return [Hash{Symbol => Member}]
attr_reader :members
# The size of the struct type.
#
# @return [Integer, Float::INFINITY]
attr_reader :size
# The alignment, in bytes, for the struct type, so that all members
# within the struct type are themselves aligned.
#
# @return [Integer]
attr_reader :alignment
#
# Initializes the struct type.
#
# @param [Hash{Symbol => Member}] members
# The members for the struct type.
#
# @param [Integer, nil] alignment
# Optional custom alignment the struct type.
#
# @param [String, nil] pack_string
# The String for `Array#pack` or `String#unpack`.
#
def initialize(members, size: , alignment: , pack_string: )
@members = members
@size = size
@alignment = alignment
super(pack_string: pack_string)
end
#
# Builds the struct type from the given fields.
#
# @param [Hash{Symbol => Type}] fields
# The field names and types for the struct type.
#
# @param [Integer, nil] alignment
# Optional custom alignment the struct type.
#
# @param [Boolean] padding
# Controls whether to pad struct members for alignment.
#
# @return [StructType]
# The new struct type.
#
def self.build(fields, alignment: nil, padding: true)
members = {}
max_alignment = 0
pack_string = String.new(encoding: Encoding::ASCII_8BIT)
offset = 0
fields.each do |name,type|
pad = if padding then Member.padding_for(offset,type)
else 0
end
members[name] = Member.new(offset + pad,type)
max_alignment = type.alignment if type.alignment > max_alignment
if pack_string
if type.pack_string
pack_string << Member.pack_string_for(type,pad)
else
pack_string = nil
end
end
# omit infinite sizes from the struct size
unless type.size == Float::INFINITY
offset += type.size + pad
end
end
return new(members, size: offset,
alignment: alignment || max_alignment,
pack_string: pack_string)
end
#
# Creates a new Hash of the struct's uninitialized members.
#
# @return [Hash{Symbol => Object}]
# The uninitialized values for the new struct's members.
#
def uninitialized_value
@members.transform_values do |member|
member.type.uninitialized_value
end
end
#
# The number of members within the struct.
#
# @return [Integer]
#
def length
@members.length
end
#
# Creates a copy of the struct type with a different {#alignment}.
#
# @param [Integer] new_alignment
# The new alignment for the new struct type.
#
# @return [ScalarType]
# The new struct type.
#
def align(new_alignment)
self.class.new(@members, size: @size,
alignment: new_alignment,
pack_string: pack_string)
end
#
# Packs the hash of values into the struct'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)
if @pack_string
super(hash)
else
buffer = String.new("\0" * @size, encoding: Encoding::ASCII_8BIT)
hash.each do |name,value|
member = @members.fetch(name) do
raise(ArgumentError,"unknown struct member (#{name.inspect}), must be: #{@members.keys.map(&:inspect).join(', ')}")
end
data = member.type.pack(value)
if member.size == Float::INFINITY
buffer[member.offset..] = data
else
buffer[member.offset,member.size] = data
end
end
return buffer
end
end
#
# Unpacks binary data into a Hash of values using the struct's binary
# layout.
#
# @param [String] data
# The binary data to unpack.
#
# @return [Hash{Symbol => Integer, Float, String, nil}]
# The unpacked hash.
#
def unpack(data)
if @pack_string
super(data)
else
hash = {}
@members.each do |name,member|
slice = if member.size == Float::INFINITY
data.byteslice(member.offset..)
else
data.byteslice(member.offset,member.size)
end
hash[name] = member.type.unpack(slice)
end
return hash
end
end
#
# Enqueues a Hash of values onto the flat list of values.
#
# @param [Array] values
# The flat array of values.
#
# @param [Hash] hash
# The hash to enqueue.
#
# @api private
#
def enqueue_value(values,hash)
unknown_keys = hash.keys - @members.keys
unless unknown_keys.empty?
raise(ArgumentError,"unknown struct members: #{unknown_keys.map(&:inspect).join(', ')}")
end
@members.each do |name,member|
value = hash[name] || member.type.uninitialized_value
member.type.enqueue_value(values,value)
end
end
#
# Dequeues a Hash from the flat list of values.
#
# @param [Array] values
# The flat array of values.
#
# @return [Hash]
# The dequeued hash.
#
# @api private
#
def dequeue_value(values)
hash = {}
@members.each do |name,member|
hash[name] = member.type.dequeue_value(values)
end
return hash
end
end
end
end
end
end