# -*- coding: binary -*-
#
# This is a Ruby port of the Python manifest parsing code posted to:
# 	http://stackoverflow.com/questions/3085153/how-to-parse-the-manifest-mbdb-file-in-an-ios-4-0-itunes-backup/3130860#3130860
#

module Rex
module Parser
class AppleBackupManifestDB

	attr_accessor :entry_offsets
	attr_accessor :entries
	attr_accessor :mbdb, :mbdx
	attr_accessor :mbdb_data, :mbdx_data
	attr_accessor :mbdb_offset, :mbdx_offset

	def initialize(mbdb_data, mbdx_data)
		self.entries = {}
		self.entry_offsets = {}
		self.mbdb_data = mbdb_data
		self.mbdx_data = mbdx_data
		parse_mbdb
		parse_mbdx
	end

	def self.from_files(mbdb_file, mbdx_file)
		mbdb_data = ""
		::File.open(mbdb_file, "rb") {|fd| mbdb_data = fd.read(fd.stat.size) }
		mbdx_data = ""
		::File.open(mbdx_file, "rb") {|fd| mbdx_data = fd.read(fd.stat.size) }

		self.new(mbdb_data, mbdx_data)
	end

	def parse_mbdb
		raise ArgumentError, "Not valid MBDB data" if self.mbdb_data[0,4] != "mbdb"
		self.mbdb_offset = 4
		self.mbdb_offset = self.mbdb_offset + 2 # Maps to \x05 \x00 (unknown)

		while self.mbdb_offset < self.mbdb_data.length
			info = {}
			info[:start_offset] = self.mbdb_offset
			info[:domain]       = mbdb_read_string
			info[:filename]     = mbdb_read_string
			info[:linktarget]   = mbdb_read_string
			info[:datahash]     = mbdb_read_string
			info[:unknown1]     = mbdb_read_string
			info[:mode]         = mbdb_read_int(2)
			info[:unknown2]     = mbdb_read_int(4)
			info[:unknown3]     = mbdb_read_int(4)
			info[:uid]          = mbdb_read_int(4)
			info[:gid]          = mbdb_read_int(4)
			info[:mtime]        = Time.at(mbdb_read_int(4))
			info[:atime]        = Time.at(mbdb_read_int(4))
			info[:ctime]        = Time.at(mbdb_read_int(4))
			info[:length]       = mbdb_read_int(8)
			info[:flag]         = mbdb_read_int(1)
			property_count      = mbdb_read_int(1)
			info[:properties]   = {}
			1.upto(property_count) do |i|
				k = mbdb_read_string
				v = mbdb_read_string
				info[:properties][k] = v
			end
			self.entry_offsets[ info[:start_offset] ] = info
		end
		self.mbdb_data = ""
	end

	def parse_mbdx
		raise ArgumentError, "Not a valid MBDX file" if self.mbdx_data[0,4] != "mbdx"

		self.mbdx_offset = 4
		self.mbdx_offset = self.mbdx_offset + 2 # Maps to \x02 \x00 (unknown)

		file_count = mbdx_read_int(4)

		while self.mbdx_offset < self.mbdx_data.length
			file_id = self.mbdx_data[self.mbdx_offset, 20].unpack("C*").map{|c| "%02x" % c}.join
			self.mbdx_offset += 20
			entry_offset = mbdx_read_int(4) + 6
			mode = mbdx_read_int(2)
			entry = entry_offsets[ entry_offset ]
			# May be corrupted if there is no matching entry, but what to do about it?
			next if not entry
			self.entries[file_id] = entry.merge({:mbdx_mode => mode, :file_id => file_id})
		end
		self.mbdx_data = ""
	end

	def mbdb_read_string
		raise RuntimeError, "Corrupted MBDB file" if self.mbdb_offset > self.mbdb_data.length
		len = self.mbdb_data[self.mbdb_offset, 2].unpack("n")[0]
		self.mbdb_offset += 2
		return '' if len == 65535
		val = self.mbdb_data[self.mbdb_offset, len]
		self.mbdb_offset += len
		return val
	end

	def mbdb_read_int(size)
		val = 0
		size.downto(1) do |i|
			val = (val << 8) + self.mbdb_data[self.mbdb_offset, 1].unpack("C")[0]
			self.mbdb_offset += 1
		end
		val
	end

	def mbdx_read_string
		raise RuntimeError, "Corrupted MBDX file" if self.mbdx_offset > self.mbdx_data.length
		len = self.mbdx_data[self.mbdx_offset, 2].unpack("n")[0]
		self.mbdx_offset += 2
		return '' if len == 65535
		val = self.mbdx_data[self.mbdx_offset, len]
		self.mbdx_offset += len
		return val
	end

	def mbdx_read_int(size)
		val = 0
		size.downto(1) do |i|
			val = (val << 8) + self.mbdx_data[self.mbdx_offset, 1].unpack("C")[0]
			self.mbdx_offset += 1
		end
 		val
	end
end


end
end
