Factor out Cstruct

This commit is contained in:
Brian Candler
2011-05-01 18:12:46 +01:00
parent f205d19591
commit 63fd7d18b6
3 changed files with 232 additions and 207 deletions

185
lib/cstruct.rb Normal file
View File

@@ -0,0 +1,185 @@
# This class allows defining of C-style structures, and converting
# object instances to and from a packed binary representation.
#
# A new structure is created by subclassing Cstruct, and then using the
# 'field' metaprogramming macro to define each field:
#
# class Foo < Cstruct
# field :bar, :char
# field :baz, :long
#
# # custom packing
# field :qux, :pattern => "Z16", :default => EMPTY_STRING
#
# # user-defined types
# define_type :str16, :pattern => "Z16", :default => EMPTY_STRING
# field :qux2, :str16
# field :qux3, :str16
# end
#
# You can then instantiate the structure by calling 'new'. You may pass in
# a hash of values to initialize the structure.
#
# msg = Foo.new(:bar => 123)
# msg.bar = 456 # accessor methods
# str = msg.to_s # convert to binary
# msg2 = Foo.parse(str) # convert from binary
# msg2 = Foo.new(msg) # copy an existing object
class Cstruct
EMPTY_STRING = "".freeze #:nodoc:
EMPTY_ARRAY = [].freeze #:nodoc:
TYPE_INFO = {} #:nodoc
# The size of the structure (in bytes)
def self.bytesize
@bytesize
end
# Define a new type for use with 'field'. You supply the
# symbolic name for the type, and a set of options.
# :pattern => "str" # format string for Array#pack / String#unpack
# :default => val # default (if not 0)
# :size => 16 # size of this entry
# :align => 4 # align to 4-byte boundary (must be power-of-two)
# :align => true # align to [size]-byte boundary
#
# If you do not specify :size then it is calculated by packing an
# instance of the default value.
def self.define_type(name, opt)
TYPE_INFO[name] = opt
end
# Return a type info hash given a type id. Raises IndexError if not found.
def self.find_type(type)
case type
when nil, Hash
type
else
TYPE_INFO.fetch(type)
end
end
define_type :uchar, :pattern => "C"
define_type :uint16, :pattern => "S", :align => true
define_type :uint32, :pattern => "L", :align => true
define_type :char, :pattern => "c"
define_type :int16, :pattern => "s", :align => true
define_type :int32, :pattern => "l", :align => true
define_type :ushort, :pattern => "S_", :align => true
define_type :uint, :pattern => "I", :align => true
define_type :ulong, :pattern => "L_", :align => true
define_type :short, :pattern => "s_", :align => true
define_type :int, :pattern => "i", :align => true
define_type :long, :pattern => "l_", :align => true
define_type :ns, :pattern => "n", :align => true
define_type :nl, :pattern => "N", :align => true
SIZEOF_SIZE_T = Integer(`echo __SIZEOF_SIZE_T__ | gcc -E -P -`) rescue 1.size
define_type :size_t,
case SIZEOF_SIZE_T
when 8
{:pattern => "Q", :align => true}
when 4
{:pattern => "L", :align => true}
else
raise "Bad size_t (#{SIZEOF_SIZE_T.inspect})"
end
# these can be used at end of structure only
define_type :binary, :pattern => "a*", :default => EMPTY_STRING
# cstring has \x00 terminator when sent over wire
define_type :cstring, :pattern => "Z*", :default => EMPTY_STRING
def initialize(h=nil)
if h.instance_of?(self.class)
@attrs = h.to_hash.dup
else
@attrs = self.class::DEFAULTS.dup
h.each { |k,v| self[k] = v } if h
end
end
def to_hash
@attrs
end
def each(&blk)
@attrs.each(&blk)
end
# Set a field by name. Currently can use either symbol or string as key.
def []=(k,v)
send "#{k}=", v
end
# Retrieve a field by name. Must use symbol as key.
def [](k)
@attrs[k]
end
def self.inherited(subclass) #:nodoc:
subclass.const_set(:FIELDS, [])
subclass.const_set(:FORMAT, "")
subclass.const_set(:DEFAULTS, {})
subclass.instance_variable_set(:@bytesize, 0)
end
# Define a field for this message, which creates accessor methods and
# sets up data required to pack and unpack the structure.
# field :foo, :uchar
# field :foo, :uchar, :default=>0xff # use this default value
def self.field(name, type, opt={})
info = find_type(type)
pattern = info[:pattern]
default = opt.fetch(:default) { info.fetch(:default, 0) }
# Apply padding for structure alignment if necessary
size = info[:size] || [default].pack(pattern).bytesize
if align = info[:align]
align = size if align == true
field_pad alignto(@bytesize, align) - @bytesize
end
@bytesize += size
self::FIELDS << name
self::FORMAT << pattern
self::DEFAULTS[name] = default
define_method name do
@attrs.fetch name
end
define_method "#{name}=" do |val|
@attrs.store name, val
end
end
# Skip pad byte(s) - default 1
def self.field_pad(count=1)
if count > 0
self::FORMAT << "x#{count}"
@bytesize += count
end
end
# Returns the packed binary representation of this structure
def to_s
self.class::FIELDS.map { |key| self[key] }.pack(self.class::FORMAT)
end
def inspect
"#<#{self.class} #{@attrs.inspect}>"
end
# Convert a binary representation of this structure into an object instance
def self.parse(data, obj=new)
data.unpack(self::FORMAT).zip(self::FIELDS).each do |val, key|
obj[key] = val
end
obj
end
# Round up a number to multiple of m, where m is a power of two
def self.alignto(val, m)
(val + (m-1)) & ~(m-1)
end
end

View File

@@ -1,91 +1,56 @@
require 'cstruct'
require 'netlink/constants'
require 'ipaddr'
module Netlink
EMPTY_STRING = "".freeze #:nodoc:
EMPTY_ARRAY = [].freeze #:nodoc:
NLMSGHDR_PACK = "LSSLL".freeze # :nodoc:
NLMSGHDR_SIZE = [0,0,0,0,0].pack(NLMSGHDR_PACK).bytesize # :nodoc:
EMPTY_STRING = Cstruct::EMPTY_STRING #:nodoc:
EMPTY_ARRAY = Cstruct::EMPTY_ARRAY #:nodoc:
# This is the base class from which all Netlink messages are derived.
# To define a new Netlink message, make a subclass and then call the
# "field" metaprogramming method to define the parts of the message, in
# order. The "code" metaprogramming method defines which incoming message
# types are to be built using this structure.
#
# You can then instantiate the message by calling 'new' as usual. It is
# usually most convenient to pass in a hash of values when creating a message.
#
# class Foo < Message
# code 10, 11, 12
# field :foo, :char, :default=>255
# field :bar, :long
# end
# msg = Foo.new(:bar => 123) # or Foo.new("bar" => 123)
# msg.bar = 456
# msg2 = Foo.new(:qux => 999) # error: no method "qux="
# msg2 = Foo.new(msg) # cloning an existing message
#
# Use RtattrMessage instead for messages which are followed by variable rtattrs.
class Message
TYPE_INFO = {} #:nodoc
class Message < Cstruct
# Map of numeric message type code => message class
CODE_TO_MESSAGE = {}
# The size of the structure (in bytes)
def self.bytesize
@bytesize
# Define which message type code(s) to build using this structure
def self.code(*codes)
codes.each { |code| CODE_TO_MESSAGE[code] = self }
end
# Define a new type for use with field and rtattr. You supply the
# symbolic name for the type, and a set of options. field supports only:
# :pattern => "str" # format string for Array#pack / String#unpack
# :default => val # default (if not 0)
# rtattr optionally also supports:
# :pack => lambda # code to convert value to binary string
# :unpack => lambda # code to convert binary string to value
def self.define_type(name, opt)
TYPE_INFO[name] = opt
NLMSG_ALIGNTO_1 = NLMSG_ALIGNTO-1 #:nodoc:
NLMSG_ALIGNTO_1_MASK = ~NLMSG_ALIGNTO_1 #:nodoc:
# Round up a length to a multiple of NLMSG_ALIGNTO bytes
def self.nlmsg_align(n)
(n + NLMSG_ALIGNTO_1) & NLMSG_ALIGNTO_1_MASK
end
# Return a type info hash given a type id. Raises IndexError if not found.
def self.find_type(type)
case type
when nil, Hash
type
else
TYPE_INFO.fetch(type)
PADDING = ("\000" * NLMSG_ALIGNTO).freeze #:nodoc:
# Pad a string up to a multiple of NLMSG_ALIGNTO bytes. Returns str.
def self.nlmsg_pad(str)
str << PADDING[0, nlmsg_align(str.bytesize) - str.bytesize]
end
end
define_type :uchar, :pattern => "C"
define_type :uint16, :pattern => "S", :align => true
define_type :uint32, :pattern => "L", :align => true
define_type :char, :pattern => "c"
define_type :int16, :pattern => "s", :align => true
define_type :int32, :pattern => "l", :align => true
define_type :ushort, :pattern => "S_", :align => true
define_type :uint, :pattern => "I", :align => true
define_type :ulong, :pattern => "L_", :align => true
define_type :short, :pattern => "s_", :align => true
define_type :int, :pattern => "i", :align => true
define_type :long, :pattern => "l_", :align => true
define_type :ns, :pattern => "n", :align => true
define_type :nl, :pattern => "N", :align => true
SIZE_T_SIZE = Integer(`echo __SIZEOF_SIZE_T__ | gcc -E -P -`) rescue 1.size
define_type :size_t,
case SIZE_T_SIZE
when 8
{:pattern => "Q", :align => true}
when 4
{:pattern => "L", :align => true}
else
raise "Bad size_t"
end
define_type :binary, :pattern => "a*", :default => EMPTY_STRING
# cstring has \x00 terminator when sent over wire
define_type :cstring, :pattern => "Z*", :default => EMPTY_STRING
# Extends Message to support variable Rtattr attributes. Use 'field'
# to define the fixed parts of the message, and 'rtattr' to define the
# permitted rtattrs. We assume that any particular rtattr is not repeated,
# so we store them in the same underlying hash and create simple accessors
# for them.
#
# As well as using :pattern for simple pack/unpack, you can also
# specify :pack and :unpack lambdas to do higher-level conversion
# of field values.
class RtattrMessage < Message
# L2 addresses are presented as ASCII hex. You may optionally include
# colons, hyphens or dots.
# IFInfo.new(:address => "00:11:22:33:44:55") # this is OK
@@ -144,131 +109,6 @@ module Netlink
end
}
def initialize(h=nil)
if h.instance_of?(self.class)
@attrs = h.to_hash.dup
else
@attrs = self.class::DEFAULTS.dup
h.each { |k,v| self[k] = v } if h
end
end
def to_hash
@attrs
end
def each(&blk)
@attrs.each(&blk)
end
# Set a field by name. Currently can use either symbol or string as key.
def []=(k,v)
send "#{k}=", v
end
# Retrieve a field by name. Must use symbol as key.
def [](k)
@attrs[k]
end
def self.inherited(subclass) #:nodoc:
subclass.const_set(:FIELDS, [])
subclass.const_set(:FORMAT, "")
subclass.const_set(:DEFAULTS, {})
subclass.instance_variable_set(:@bytesize, 0)
end
# Map of numeric message type code => message class
CODE_TO_MESSAGE = {}
# Define which message type code(s) to build using this structure
def self.code(*codes)
codes.each { |code| CODE_TO_MESSAGE[code] = self }
end
# Define a field for this message, which creates accessor methods and
# sets up data required to pack and unpack the structure.
# field :foo, :uchar
# field :foo, :uchar, :default=>0xff # use this default value
def self.field(name, type, opt={})
info = find_type(type)
pattern = info[:pattern]
default = opt.fetch(:default) { info.fetch(:default, 0) }
# Apply padding for structure alignment if necessary
size = info[:size] || [default].pack(pattern).bytesize
if align = info[:align]
align = size if align == true
field_pad alignto(@bytesize, align) - @bytesize
end
@bytesize += size
self::FIELDS << name
self::FORMAT << pattern
self::DEFAULTS[name] = default
define_method name do
@attrs.fetch name
end
define_method "#{name}=" do |val|
@attrs.store name, val
end
end
# Skip pad byte(s) - default 1
def self.field_pad(count=1)
if count > 0
self::FORMAT << "x#{count}"
@bytesize += count
end
end
# Returns the packed binary representation of this message (without
# header, and not padded to NLMSG_ALIGNTO bytes)
def to_s
self.class::FIELDS.map { |key| self[key] }.pack(self.class::FORMAT)
end
def inspect
"#<#{self.class} #{@attrs.inspect}>"
end
# Convert a binary representation of this message into an object instance
def self.parse(data)
res = new
data.unpack(self::FORMAT).zip(self::FIELDS).each do |val, key|
res[key] = val
end
res
end
NLMSG_ALIGNTO_1 = NLMSG_ALIGNTO-1 #:nodoc:
NLMSG_ALIGNTO_1_MASK = ~NLMSG_ALIGNTO_1 #:nodoc:
# Round up a number to multiple of m, where m is a power of two
def self.alignto(val, m)
(val + (m-1)) & ~(m-1)
end
# Round up a length to a multiple of NLMSG_ALIGNTO bytes
def self.align(n)
(n + NLMSG_ALIGNTO_1) & NLMSG_ALIGNTO_1_MASK
end
PADDING = ("\000" * NLMSG_ALIGNTO).freeze #:nodoc:
# Pad a string up to a multiple of NLMSG_ALIGNTO bytes. Returns str.
def self.pad(str)
str << PADDING[0, align(str.bytesize) - str.bytesize]
end
end
# Extends Message to support variable Rtattr attributes. Use 'field'
# to define the fixed parts of the message, and 'rtattr' to define the
# permitted rtattrs. We assume that any particular rtattr is not repeated,
# so we store them in the same underlying hash and create simple accessors
# for them.
class RtattrMessage < Message
RTATTR_PACK = "S_S_".freeze #:nodoc:
RTATTR_SIZE = [0,0].pack(RTATTR_PACK).bytesize #:nodoc:
@@ -294,10 +134,10 @@ module Netlink
# Return the byte offset to the first rtattr
def self.attr_offset
@attr_offset ||= Message.align(@bytesize)
@attr_offset ||= Message.nlmsg_align(@bytesize)
end
# Returns the packed binary representation of this message.
# Returns the packed binary representation of the entire message.
# The main message is processed *after* the rtattrs; this is so that
# the address family can be set automatically while processing any
# optional l3 address rtattrs.
@@ -305,7 +145,7 @@ module Netlink
data = ""
self.class::RTATTRS.each do |code, (name, info)|
if val = @attrs[name]
Message.pad(data) # assume NLMSG_ALIGNTO == NLA_ALIGNTO
Message.nlmsg_pad(data) # assume NLMSG_ALIGNTO == NLA_ALIGNTO
if pack = info[:pack]
val = pack[val,self]
elsif pattern = info[:pattern]
@@ -314,7 +154,7 @@ module Netlink
data << [val.bytesize+RTATTR_SIZE, code].pack(RTATTR_PACK) << val
end
end
data.empty? ? super : Message.pad(super) + data
data.empty? ? super : Message.nlmsg_pad(super) + data
end
# Convert a binary representation of this message into an object instance.
@@ -351,17 +191,17 @@ module Netlink
raise "Truncated rtattr body!" if ptr + len > data.bytesize
raise "Invalid rtattr len!" if len < RTATTR_SIZE
yield code, data[ptr+RTATTR_SIZE, len-RTATTR_SIZE]
ptr = Message.align(ptr + len) # assume NLMSG_ALIGNTO == NLA_ALIGNTO
ptr = Message.nlmsg_align(ptr + len) # assume NLMSG_ALIGNTO == NLA_ALIGNTO
end
end
end
# struct nlmsgerr
# struct nlmsgerr (netlink.h)
class Err < Message
code NLMSG_ERROR
field :error, :int
#field :msg, :pattern => NLMSGHDR_PACK
#field :msg, :pattern => NLMSGHDR_PACK (can't, returns multiple values)
field :msg_len, :uint32
field :msg_type, :uint16
field :msg_flags, :uint16

View File

@@ -101,7 +101,7 @@ module Netlink
msgs.each_with_index do |msg, index|
if index < msgs.size - 1
data << build_message(type, msg, flags|NLM_F_MULTI, next_seq, pid)
Message.pad(data)
Message.nlmsg_pad(data)
else
data << build_message(type, msg, flags, next_seq, pid)
end
@@ -215,7 +215,7 @@ module Netlink
data = mesg[ptr+NLMSGHDR_SIZE, len-NLMSGHDR_SIZE]
STDERR.puts " data=#{data.inspect}" if $DEBUG && !data.empty?
yield type, flags, seq, pid, data
ptr = ptr + Message.align(len)
ptr = ptr + Message.nlmsg_align(len)
break unless flags & Netlink::NLM_F_MULTI
end
end