#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""
Libxc Migration v2 streams

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

import sys

from struct import calcsize, unpack

from xen.migration.verify import StreamError, RecordError, VerifyBase

# In Python3 long type have been merged into int, 1L syntax is no longer valid
if sys.version_info > (3,):
    long = int

# Image Header
IHDR_FORMAT = "!QIIHHI"

IHDR_MARKER  = 0xffffffffffffffff
IHDR_IDENT   = 0x58454E46 # "XENF" in ASCII
IHDR_VERSION = 2

IHDR_OPT_BIT_ENDIAN = 0
IHDR_OPT_LE = (0 << IHDR_OPT_BIT_ENDIAN)
IHDR_OPT_BE = (1 << IHDR_OPT_BIT_ENDIAN)

IHDR_OPT_RESZ_MASK = 0xfffe

# Domain Header
DHDR_FORMAT = "IHHII"

DHDR_TYPE_x86_pv  = 0x00000001
DHDR_TYPE_x86_hvm = 0x00000002
DHDR_TYPE_x86_pvh = 0x00000003
DHDR_TYPE_arm     = 0x00000004

dhdr_type_to_str = {
    DHDR_TYPE_x86_pv  : "x86 PV",
    DHDR_TYPE_x86_hvm : "x86 HVM",
    DHDR_TYPE_x86_pvh : "x86 PVH",
    DHDR_TYPE_arm     : "ARM",
}

# Records
RH_FORMAT = "II"

REC_TYPE_end                        = 0x00000000
REC_TYPE_page_data                  = 0x00000001
REC_TYPE_x86_pv_info                = 0x00000002
REC_TYPE_x86_pv_p2m_frames          = 0x00000003
REC_TYPE_x86_pv_vcpu_basic          = 0x00000004
REC_TYPE_x86_pv_vcpu_extended       = 0x00000005
REC_TYPE_x86_pv_vcpu_xsave          = 0x00000006
REC_TYPE_shared_info                = 0x00000007
REC_TYPE_tsc_info                   = 0x00000008
REC_TYPE_hvm_context                = 0x00000009
REC_TYPE_hvm_params                 = 0x0000000a
REC_TYPE_toolstack                  = 0x0000000b
REC_TYPE_x86_pv_vcpu_msrs           = 0x0000000c
REC_TYPE_verify                     = 0x0000000d
REC_TYPE_checkpoint                 = 0x0000000e
REC_TYPE_checkpoint_dirty_pfn_list  = 0x0000000f

rec_type_to_str = {
    REC_TYPE_end                        : "End",
    REC_TYPE_page_data                  : "Page data",
    REC_TYPE_x86_pv_info                : "x86 PV info",
    REC_TYPE_x86_pv_p2m_frames          : "x86 PV P2M frames",
    REC_TYPE_x86_pv_vcpu_basic          : "x86 PV vcpu basic",
    REC_TYPE_x86_pv_vcpu_extended       : "x86 PV vcpu extended",
    REC_TYPE_x86_pv_vcpu_xsave          : "x86 PV vcpu xsave",
    REC_TYPE_shared_info                : "Shared info",
    REC_TYPE_tsc_info                   : "TSC info",
    REC_TYPE_hvm_context                : "HVM context",
    REC_TYPE_hvm_params                 : "HVM params",
    REC_TYPE_toolstack                  : "Toolstack",
    REC_TYPE_x86_pv_vcpu_msrs           : "x86 PV vcpu msrs",
    REC_TYPE_verify                     : "Verify",
    REC_TYPE_checkpoint                 : "Checkpoint",
    REC_TYPE_checkpoint_dirty_pfn_list  : "Checkpoint dirty pfn list"
}

# page_data
PAGE_DATA_FORMAT             = "II"
PAGE_DATA_PFN_MASK           = (long(1) << 52) - 1
PAGE_DATA_PFN_RESZ_MASK      = ((long(1) << 60) - 1) & ~((long(1) << 52) - 1)

# flags from xen/public/domctl.h: XEN_DOMCTL_PFINFO_* shifted by 32 bits
PAGE_DATA_TYPE_SHIFT         = 60
PAGE_DATA_TYPE_LTABTYPE_MASK = (long(0x7) << PAGE_DATA_TYPE_SHIFT)
PAGE_DATA_TYPE_LTAB_MASK     = (long(0xf) << PAGE_DATA_TYPE_SHIFT)
PAGE_DATA_TYPE_LPINTAB       = (long(0x8) << PAGE_DATA_TYPE_SHIFT) # Pinned pagetable

PAGE_DATA_TYPE_NOTAB         = (long(0x0) << PAGE_DATA_TYPE_SHIFT) # Regular page
PAGE_DATA_TYPE_L1TAB         = (long(0x1) << PAGE_DATA_TYPE_SHIFT) # L1 pagetable
PAGE_DATA_TYPE_L2TAB         = (long(0x2) << PAGE_DATA_TYPE_SHIFT) # L2 pagetable
PAGE_DATA_TYPE_L3TAB         = (long(0x3) << PAGE_DATA_TYPE_SHIFT) # L3 pagetable
PAGE_DATA_TYPE_L4TAB         = (long(0x4) << PAGE_DATA_TYPE_SHIFT) # L4 pagetable
PAGE_DATA_TYPE_BROKEN        = (long(0xd) << PAGE_DATA_TYPE_SHIFT) # Broken
PAGE_DATA_TYPE_XALLOC        = (long(0xe) << PAGE_DATA_TYPE_SHIFT) # Allocate-only
PAGE_DATA_TYPE_XTAB          = (long(0xf) << PAGE_DATA_TYPE_SHIFT) # Invalid

# x86_pv_info
X86_PV_INFO_FORMAT        = "BBHI"

X86_PV_P2M_FRAMES_FORMAT  = "II"

# x86_pv_vcpu_{basic,extended,xsave,msrs}
X86_PV_VCPU_HDR_FORMAT    = "II"

# tsc_info
TSC_INFO_FORMAT           = "IIQII"

# hvm_params
HVM_PARAMS_ENTRY_FORMAT   = "QQ"
HVM_PARAMS_FORMAT         = "II"

class VerifyLibxc(VerifyBase):
    """ Verify a Libxc v2 stream """

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

        self.squashed_pagedata_records = 0


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

        self.verify_ihdr()
        self.verify_dhdr()

        while self.verify_record() != REC_TYPE_end:
            pass


    def verify_ihdr(self):
        """ Verify an Image Header """
        marker, ident, version, options, res1, res2 = \
            self.unpack_exact(IHDR_FORMAT)

        if marker != IHDR_MARKER:
            raise StreamError("Bad image marker: Expected 0x%x, got 0x%x"
                              % (IHDR_MARKER, marker))

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

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

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

        if res1 != 0 or res2 != 0:
            raise StreamError("Reserved bits set in image header: 0x%04x:0x%08x"
                              % (res1, res2))

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

        endian = ["little", "big"][options & IHDR_OPT_LE]
        self.info("Libxc Image Header: %s endian" % (endian, ))


    def verify_dhdr(self):
        """ Verify a domain header """

        gtype, page_shift, res1, major, minor = \
            self.unpack_exact(DHDR_FORMAT)

        if gtype not in dhdr_type_to_str:
            raise StreamError("Unrecognised domain type 0x%x" % (gtype, ))

        if res1 != 0:
            raise StreamError("Reserved bits set in domain header 0x%04x"
                              % (res1, ))

        if page_shift != 12:
            raise StreamError("Page shift expected to be 12.  Got %d"
                              % (page_shift, ))

        if major == 0:
            self.info("Domain Header: legacy converted %s"
                      % (dhdr_type_to_str[gtype], ))
        else:
            self.info("Domain Header: %s from Xen %d.%d"
                      % (dhdr_type_to_str[gtype], major, minor))


    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 0x%x" % (rtype, ))

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

        if rtype != REC_TYPE_page_data:

            if self.squashed_pagedata_records > 0:
                self.info("Squashed %d Page Data records together"
                          % (self.squashed_pagedata_records, ))
                self.squashed_pagedata_records = 0

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

        else:
            self.squashed_pagedata_records += 1

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

        if rtype not in record_verifiers:
            raise RuntimeError("No verification function for libxc 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_page_data(self, content):
        """ Page Data record """
        minsz = calcsize(PAGE_DATA_FORMAT)

        if len(content) <= minsz:
            raise RecordError("PAGE_DATA record must be at least %d bytes long"
                              % (minsz, ))

        count, res1 = unpack(PAGE_DATA_FORMAT, content[:minsz])

        if res1 != 0:
            raise StreamError("Reserved bits set in PAGE_DATA record 0x%04x"
                              % (res1, ))

        pfnsz = count * 8
        if (len(content) - minsz) < pfnsz:
            raise RecordError("PAGE_DATA record must contain a pfn record for "
                              "each count")

        pfns = list(unpack("=%dQ" % (count,), content[minsz:minsz + pfnsz]))

        nr_pages = 0
        for idx, pfn in enumerate(pfns):

            if pfn & PAGE_DATA_PFN_RESZ_MASK:
                raise RecordError("Reserved bits set in pfn[%d]: 0x%016x",
                                  idx, pfn & PAGE_DATA_PFN_RESZ_MASK)

            if pfn >> PAGE_DATA_TYPE_SHIFT in (5, 6, 7, 8):
                raise RecordError("Invalid type value in pfn[%d]: 0x%016x",
                                  idx, pfn & PAGE_DATA_TYPE_LTAB_MASK)

            # We expect page data for each normal page or pagetable
            if PAGE_DATA_TYPE_NOTAB <= (pfn & PAGE_DATA_TYPE_LTABTYPE_MASK) \
                    <= PAGE_DATA_TYPE_L4TAB:
                nr_pages += 1

        pagesz = nr_pages * 4096
        if len(content) != minsz + pfnsz + pagesz:
            raise RecordError("Expected %u + %u + %u, got %u"
                              % (minsz, pfnsz, pagesz, len(content)))


    def verify_record_x86_pv_info(self, content):
        """ x86 PV Info record """

        expectedsz = calcsize(X86_PV_INFO_FORMAT)
        if len(content) != expectedsz:
            raise RecordError("x86_pv_info: expected length of %d, got %d"
                              % (expectedsz, len(content)))

        width, levels, res1, res2 = unpack(X86_PV_INFO_FORMAT, content)

        if width not in (4, 8):
            raise RecordError("Expected width of 4 or 8, got %d" % (width, ))

        if levels not in (3, 4):
            raise RecordError("Expected levels of 3 or 4, got %d" % (levels, ))

        if res1 != 0 or res2 != 0:
            raise StreamError("Reserved bits set in X86_PV_INFO: 0x%04x 0x%08x"
                              % (res1, res2))

        bitness = {4:32, 8:64}[width]
        self.info("  %sbit guest, %d levels of pagetables" % (bitness, levels))


    def verify_record_x86_pv_p2m_frames(self, content):
        """ x86 PV p2m frames record """

        if len(content) < 8:
            raise RecordError("x86_pv_p2m_frames: record length must be at"
                              " least 8 bytes long")

        if len(content) % 8 != 0:
            raise RecordError("Length expected to be a multiple of 8, not %d"
                              % (len(content), ))

        start, end = unpack("=II", content[:8])
        self.info("  Start pfn 0x%x, End 0x%x" % (start, end))


    def verify_record_x86_pv_vcpu_generic(self, content, name):
        """ Generic for all REC_TYPE_x86_pv_vcpu_{basic,extended,xsave,msrs} """
        minsz = calcsize(X86_PV_VCPU_HDR_FORMAT)

        if len(content) < minsz:
            raise RecordError("X86_PV_VCPU_%s record length must be at least %d"
                              " bytes long" % (name, minsz))

        if len(content) == minsz:
            self.info("Warning: X86_PV_VCPU_%s record with zero content"
                      % (name, ))

        vcpuid, res1 = unpack(X86_PV_VCPU_HDR_FORMAT, content[:minsz])

        if res1 != 0:
            raise StreamError(
                "Reserved bits set in x86_pv_vcpu_%s record 0x%04x"
                              % (name, res1))

        self.info("  vcpu%d %s context, %d bytes"
                  % (vcpuid, name, len(content) - minsz))


    def verify_record_shared_info(self, content):
        """ shared info record """

        if len(content) != 4096:
            raise RecordError("Length expected to be 4906 bytes, not %d"
                              % (len(content), ))


    def verify_record_tsc_info(self, content):
        """ tsc info record """

        sz = calcsize(TSC_INFO_FORMAT)

        if len(content) != sz:
            raise RecordError("Length should be %u bytes" % (sz, ))

        mode, khz, nsec, incarn, res1 = unpack(TSC_INFO_FORMAT, content)

        if res1 != 0:
            raise StreamError("Reserved bits set in TSC_INFO: 0x%08x"
                              % (res1, ))

        self.info("  Mode %u, %u kHz, %u ns, incarnation %d"
                  % (mode, khz, nsec, incarn))


    def verify_record_hvm_context(self, content):
        """ hvm context record """

        if len(content) == 0:
            raise RecordError("Zero length HVM context")


    def verify_record_hvm_params(self, content):
        """ hvm params record """

        sz = calcsize(HVM_PARAMS_FORMAT)

        if len(content) < sz:
            raise RecordError("Length should be at least %u bytes" % (sz, ))

        count, rsvd = unpack(HVM_PARAMS_FORMAT, content[:sz])

        if rsvd != 0:
            raise RecordError("Reserved field not zero (0x%04x)" % (rsvd, ))

        if count == 0:
            self.info("Warning: HVM_PARAMS record with zero content")

        sz += count * calcsize(HVM_PARAMS_ENTRY_FORMAT)

        if len(content) != sz:
            raise RecordError("Length should be %u bytes" % (sz, ))


    def verify_record_toolstack(self, _):
        """ toolstack record """
        raise DeprecationWarning("Found Toolstack record in stream")


    def verify_record_verify(self, content):
        """ verify record """

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


    def verify_record_checkpoint(self, content):
        """ checkpoint record """

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

    def verify_record_checkpoint_dirty_pfn_list(self, content):
        """ checkpoint dirty pfn list """
        raise RecordError("Found checkpoint dirty pfn list record in stream")


record_verifiers = {
    REC_TYPE_end:
        VerifyLibxc.verify_record_end,
    REC_TYPE_page_data:
        VerifyLibxc.verify_record_page_data,

    REC_TYPE_x86_pv_info:
        VerifyLibxc.verify_record_x86_pv_info,
    REC_TYPE_x86_pv_p2m_frames:
        VerifyLibxc.verify_record_x86_pv_p2m_frames,

    REC_TYPE_x86_pv_vcpu_basic:
        lambda s, x:
        VerifyLibxc.verify_record_x86_pv_vcpu_generic(s, x, "basic"),
    REC_TYPE_x86_pv_vcpu_extended:
        lambda s, x:
        VerifyLibxc.verify_record_x86_pv_vcpu_generic(s, x, "extended"),
    REC_TYPE_x86_pv_vcpu_xsave:
        lambda s, x:
        VerifyLibxc.verify_record_x86_pv_vcpu_generic(s, x, "xsave"),
    REC_TYPE_x86_pv_vcpu_msrs:
        lambda s, x:
        VerifyLibxc.verify_record_x86_pv_vcpu_generic(s, x, "msrs"),

    REC_TYPE_shared_info:
        VerifyLibxc.verify_record_shared_info,
    REC_TYPE_tsc_info:
        VerifyLibxc.verify_record_tsc_info,

    REC_TYPE_hvm_context:
        VerifyLibxc.verify_record_hvm_context,
    REC_TYPE_hvm_params:
        VerifyLibxc.verify_record_hvm_params,
    REC_TYPE_toolstack:
        VerifyLibxc.verify_record_toolstack,
    REC_TYPE_verify:
        VerifyLibxc.verify_record_verify,
    REC_TYPE_checkpoint:
        VerifyLibxc.verify_record_checkpoint,
    REC_TYPE_checkpoint_dirty_pfn_list:
        VerifyLibxc.verify_record_checkpoint_dirty_pfn_list,
    }
