# StructFu, a nifty way to leverage Ruby's built in Struct class
# to create meaningful binary data. 

module StructFu
	# Normally, self.size and self.length will refer to the Struct
	# size as an array. It's a hassle to redefine, so this introduces some
	# shorthand to get at the size of the resultant string.
	def sz
		self.to_s.size
	end

	alias len sz

	# Typecast is used mostly by packet header classes, such as IPHeader,
	# TCPHeader, and the like. It takes an argument, and casts it to the
	# expected type for that element. 
	def typecast(i)
		c = caller[0].match(/.*`([^']+)='/)[1]
		self[c.intern].read i
	end

	# Used like typecast(), but specifically for casting Strings to StructFu::Strings.
	def body=(i)
		if i.kind_of? ::String
			typecast(i)
		elsif i.kind_of? StructFu
			self[:body] = i
		elsif i.nil?
			self[:body] = StructFu::String.new.read("")
		else
			raise ArgumentError, "Can't cram a #{i.class} into a StructFu :body"
		end
	end

	# Ints all have a value, an endianness, and a default value.
	# Note that the signedness of Int values are implicit as
	# far as the subclasses are concerned; to_i and to_f will 
	# return Integer/Float versions of the input value, instead
	# of attempting to unpack the pack value. (This can be a useful
	# hint to other functions).
	#
	# ==== Header Definition
	#
	#   Fixnum  :value
	#   Symbol  :endian
	#   Fixnum  :width
	#   Fixnum  :default
	class Int < Struct.new(:value, :endian, :width, :default)
		alias :v= :value=
		alias :v :value
		alias :e= :endian=
		alias :e :endian
		alias :w= :width=
		alias :w :width
		alias :d= :default=
		alias :d :default

		# This is a parent class definition and should not be used directly.
		def to_s
			raise StandardError, "StructFu::Int#to_s accessed, must be redefined."
		end

		# Returns the Int as an Integer.
		def to_i
			(self.v || self.d).to_i
		end

		# Returns the Int as a Float.
		def to_f
			(self.v || self.d).to_f
		end
		
		def initialize(value=nil, endian=nil, width=nil, default=nil)
			super(value,endian,width,default=0)
		end

		# Reads either an Integer or a packed string, and populates the value accordingly.
		def read(i)
			self.v = i.kind_of?(Integer) ? i.to_i : i.to_s.unpack(@packstr).first
			self
		end

	end

	# Int8 is a one byte value.
	class Int8 < Int

		def initialize(v=nil)
			super(v,nil,w=1)
			@packstr = "C"
		end

		# Returns a one byte value as a packed string.
		def to_s
		 [(self.v || self.d)].pack("C")
		end

	end

	# Int16 is a two byte value.
	class Int16 < Int
		def initialize(v=nil, e=:big)
			super(v,e,w=2)
			@packstr = (self.e == :big) ? "n" : "v"
		end

		# Returns a two byte value as a packed string.
		def to_s
			[(self.v || self.d)].pack(@packstr)
	 	end

	end
  
	# Int16be is a two byte value in big-endian format.
	class Int16be < Int16
	end

	# Int16le is a two byte value in little-endian format.
	class Int16le < Int16
		def initialize(v=nil, e=:little)
			super(v,e)
			@packstr = (self.e == :big) ? "n" : "v"
		end
	end

	# Int32 is a four byte value.
	class Int32 < Int
		def initialize(v=nil, e=:big)
			super(v,e,w=4)
			@packstr = (self.e == :big) ? "N" : "V"
		end

		# Returns a four byte value as a packed string.
		def to_s
			[(self.v || self.d)].pack(@packstr)
	 	end

	end

	# Int32be is a four byte value in big-endian format.
	class Int32be < Int32
	end

	# Int32le is a four byte value in little-endian format.
	class Int32le < Int32
		def initialize(v=nil, e=:little)
			super(v,e)
		end
	end

	# Strings are just like regular strings, except it comes with a read() function
	# so that it behaves like other StructFu elements.
	class String < ::String
		def read(str)
			str = str.to_s
			self.replace str
			self
		end
	end

	# Provides a primitive for creating strings, preceeded by
	# an Int type of length. By default, a string of length zero with
	# a one-byte length is presumed.  
	#
	# Note that IntStrings aren't used for much, but it seemed like a good idea at the time.
	class IntString < Struct.new(:int, :string, :mode)

		def initialize(string='',int=Int8,mode=nil)
			unless int.respond_to?(:ancestors) && int.ancestors.include?(StructFu::Int)
				raise StandardError, "Invalid length (#{int.inspect}) associated with this String."
			else
				super(int.new,string,mode)
				calc
			end
		end

		# Calculates the size of a string, and sets it as the value.
		def calc
			int.v = string.to_s.size
			self.to_s
		end

		# Returns the object as a string, depending on the mode set upon object creation.
		def to_s
			if mode == :parse
				"#{int}" + [string].pack("a#{len}")
			elsif mode == :fix
				self.int.v = string.size
				"#{int}#{string}"
			else
				"#{int}#{string}"
			end
		end

		# By redefining #string=, we can ensure the correct value
		# is calculated upon assignment. If you'd prefer to have
		# an incorrect value, use the syntax, obj[:string]="value"
		# instead. Note, by using the alternate form, you must
		# #calc before you can trust the int's value. Think of the
		# = assignment as "set to equal," while the []= assignment
		# as "boxing in" the value. Maybe.
		def string=(s)
			self[:string] = s
			calc
		end

		# Shorthand for querying a length. Note that the usual "length"
		# and "size" refer to the number of elements of this struct.
		def len
			self[:int].value
		end

		# Override the size, if you must.
		def len=(i)
			self[:int].value=i
		end

		# Read takes a string, assumes an int width as previously
		# defined upon initialization, but makes no guarantees
		# the int value isn't lying. You're on your own to test
		# for that (or use parse() with a :mode set).
		def read(s)
			unless s[0,int.width].size == int.width
				raise StandardError, "String is too short for type #{int.class}"
			else
				int.read(s[0,int.width])
				self[:string] = s[int.width,s.size]
			end
			self.to_s
		end

		# parse() is like read(), except that it interprets the string, either
		# based on the declared length, or the actual length. Which strategy
		# is used is dependant on which :mode is set (with self.mode).
		#
		# :parse : Read the length, and then read in that many bytes of the string. 
		#          The string may be truncated or padded out with nulls, as dictated by the value.
		# :fix   : Skip the length, read the rest of the string, then set the length to what it ought to be.
		# else   : If neither of these modes are set, just perfom a normal read(). This is the default.
		def parse(s)
			unless s[0,int.width].size == int.width
				raise StandardError, "String is too short for type #{int.class}"
			else
				case mode 
				when :parse
					int.read(s[0,int.width])
					self[:string] = s[int.width,int.value]
					if string.size < int.value
						self[:string] += ("\x00" * (int.value - self[:string].size))
					end
				when :fix
					self.string = s[int.width,s.size]
				else
					return read(s)
				end
			end
			self.to_s
		end

	end

end

class Struct

	# Monkeypatch for Struct to include some string safety -- anything that uses
	# Struct is going to presume binary strings anyway.
	def force_binary(str)
		str.force_encoding "binary" if str.respond_to? :force_encoding
	end

end

# vim: nowrap sw=2 sts=0 ts=2 ff=unix ft=ruby
