#!/usr/bin/env python3.7
# -*- coding: utf-8 -*-

"""
Libxl Migration v2 streams

Record structures as per docs/specs/libxl-migration-stream.pandoc, and
verification routines.
"""

import sys

from struct import calcsize, unpack, unpack_from
from xen.migration.verify import StreamError, RecordError, VerifyBase
from xen.migration.libxc import VerifyLibxc

# Header
HDR_FORMAT = "!QII"

HDR_IDENT = 0x4c6962786c466d74 # "LibxlFmt" in ASCII
HDR_VERSION = 2

HDR_OPT_BIT_ENDIAN = 0
HDR_OPT_BIT_LEGACY = 1

HDR_OPT_LE     = (0 << HDR_OPT_BIT_ENDIAN)
HDR_OPT_BE     = (1 << HDR_OPT_BIT_ENDIAN)
HDR_OPT_LEGACY = (1 << HDR_OPT_BIT_LEGACY)

HDR_OPT_RESZ_MASK = 0xfffc

# Records
RH_FORMAT = "II"

REC_TYPE_end                    = 0x00000000
REC_TYPE_libxc_context          = 0x00000001
REC_TYPE_emulator_xenstore_data = 0x00000002
REC_TYPE_emulator_context       = 0x00000003
REC_TYPE_checkpoint_end         = 0x00000004
REC_TYPE_checkpoint_state       = 0x00000005

rec_type_to_str = {
    REC_TYPE_end                    : "End",
    REC_TYPE_libxc_context          : "Libxc context",
    REC_TYPE_emulator_xenstore_data : "Emulator xenstore data",
    REC_TYPE_emulator_context       : "Emulator context",
    REC_TYPE_checkpoint_end         : "Checkpoint end",
    REC_TYPE_checkpoint_state       : "Checkpoint state",
}

# emulator_* header
EMULATOR_HEADER_FORMAT = "II"

EMULATOR_ID_unknown       = 0x00000000
EMULATOR_ID_qemu_trad     = 0x00000001
EMULATOR_ID_qemu_upstream = 0x00000002

emulator_id_to_str = {
    EMULATOR_ID_unknown       : "Unknown",
    EMULATOR_ID_qemu_trad     : "Qemu Traditional",
    EMULATOR_ID_qemu_upstream : "Qemu Upstream",
}


#
# libxl format
#

LIBXL_QEMU_SIGNATURE = "DeviceModelRecord0002"
LIBXL_QEMU_RECORD_HDR = "=%dsI" % (len(LIBXL_QEMU_SIGNATURE), )

class VerifyLibxl(VerifyBase):
    """ Verify a Libxl v2 stream """

    def __init__(self, info, read):
        VerifyBase.__init__(self, info, read)


    def verify(self):
        """ Verity a libxl stream """

        self.verify_hdr()

        while self.verify_record() != REC_TYPE_end:
            pass


    def verify_hdr(self):
        """ Verify a Header """
        ident, version, options = self.unpack_exact(HDR_FORMAT)

        if ident != HDR_IDENT:
            raise StreamError("Bad image id: Expected 0x%x, got 0x%x" %
                              (HDR_IDENT, ident))

        if version != HDR_VERSION:
            raise StreamError("Unknown image version: Expected %d, got %d" %
                              (HDR_VERSION, version))

        if options & HDR_OPT_RESZ_MASK:
            raise StreamError("Reserved bits set in image options field: 0x%x" %
                              (options & HDR_OPT_RESZ_MASK))

        if ( (sys.byteorder == "little") and
             ((options & HDR_OPT_BIT_ENDIAN) != HDR_OPT_LE) ):
            raise StreamError(
                "Stream is not native endianess - unable to validate")

        endian = ["little", "big"][options & HDR_OPT_LE]

        if options & HDR_OPT_LEGACY:
            self.info("Libxl Header: %s endian, legacy converted" % (endian, ))
        else:
            self.info("Libxl Header: %s endian" % (endian, ))


    def verify_record(self):
        """ Verify an individual record """
        rtype, length = self.unpack_exact(RH_FORMAT)

        if rtype not in rec_type_to_str:
            raise StreamError("Unrecognised record type %x" % (rtype, ))

        self.info("Libxl Record: %s, length %d" %
                  (rec_type_to_str[rtype], length))

        contentsz = (length + 7) & ~7
        content = self.rdexact(contentsz)

        padding = content[length:]
        if padding != b"\x00" * len(padding):
            raise StreamError("Padding containing non0 bytes found")

        if rtype not in record_verifiers:
            raise RuntimeError(
                "No verification function for libxl record '%s'" %
                rec_type_to_str[rtype])
        else:
            record_verifiers[rtype](self, content[:length])

        return rtype


    def verify_record_end(self, content):
        """ End record """

        if len(content) != 0:
            raise RecordError("End record with non-zero length")


    def verify_record_libxc_context(self, content):
        """ Libxc context record """

        if len(content) != 0:
            raise RecordError("Libxc context record with non-zero length")

        # Verify the libxc stream, as we can't seek forwards through it
        VerifyLibxc(self.info, self.read).verify()


    def verify_record_emulator_xenstore_data(self, content):
        """ Emulator Xenstore Data record """
        minsz = calcsize(EMULATOR_HEADER_FORMAT)

        if len(content) < minsz:
            raise RecordError("Length must be at least %d bytes, got %d" %
                              (minsz, len(content)))

        emu_id, emu_idx = unpack(EMULATOR_HEADER_FORMAT, content[:minsz])

        if emu_id not in emulator_id_to_str:
            raise RecordError("Unrecognised emulator id 0x%x" % (emu_id, ))

        self.info("Emulator Xenstore Data (%s, idx %d)" %
                  (emulator_id_to_str[emu_id], emu_idx))

        # Chop off the emulator header
        content = content[minsz:]

        if len(content):

            if content[-1] != '\x00':
                raise RecordError("Data not NUL terminated")

            # Split without the final NUL, to get an even number of parts
            parts = content[:-1].split("\x00")

            if (len(parts) % 2) != 0:
                raise RecordError("Expected an even number of strings, got %d" %
                                  (len(parts), ))

            for key, val in zip(parts[0::2], parts[1::2]):
                self.info("  '%s' = '%s'" % (key, val))


    def verify_record_emulator_context(self, content):
        """ Emulator Context record """
        minsz = calcsize(EMULATOR_HEADER_FORMAT)

        if len(content) < minsz:
            raise RecordError("Length must be at least %d bytes, got %d" %
                              (minsz, len(content)))

        emu_id, emu_idx = unpack(EMULATOR_HEADER_FORMAT, content[:minsz])

        if emu_id not in emulator_id_to_str:
            raise RecordError("Unrecognised emulator id 0x%x" % (emu_id, ))

        self.info("  Index %d, type %s" % (emu_idx, emulator_id_to_str[emu_id]))


    def verify_record_checkpoint_end(self, content):
        """ Checkpoint end record """

        if len(content) != 0:
            raise RecordError("Checkpoint end record with non-zero length")

    def verify_record_checkpoint_state(self, content):
        """ Checkpoint state """
        if len(content) == 0:
            raise RecordError("Checkpoint state record with zero length")


record_verifiers = {
    REC_TYPE_end:
        VerifyLibxl.verify_record_end,
    REC_TYPE_libxc_context:
        VerifyLibxl.verify_record_libxc_context,
    REC_TYPE_emulator_xenstore_data:
        VerifyLibxl.verify_record_emulator_xenstore_data,
    REC_TYPE_emulator_context:
        VerifyLibxl.verify_record_emulator_context,
    REC_TYPE_checkpoint_end:
        VerifyLibxl.verify_record_checkpoint_end,
    REC_TYPE_checkpoint_state:
        VerifyLibxl.verify_record_checkpoint_state,
}
