# This file is part of Scapy
# Scapy is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# any later version.
#
# Scapy is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Scapy. If not, see <http://www.gnu.org/licenses/>.

# Copyright (C) 2016 Gauthier Sebaux

# scapy.contrib.description = ProfinetIO Remote Procedure Call (RPC)
# scapy.contrib.status = loads

"""
PNIO RPC endpoints
"""

import struct
from uuid import UUID

from scapy.packet import Packet, bind_layers
from scapy.config import conf
from scapy.fields import BitField, ByteField, BitEnumField, ConditionalField, \
    FieldLenField, FieldListField, IntField, IntEnumField, \
    LenField, MACField, PadField, PacketField, PacketListField, \
    ShortEnumField, ShortField, StrFixedLenField, StrLenField, \
    UUIDField, XByteField, XIntField, XShortEnumField, XShortField
from scapy.contrib.dce_rpc import DceRpc, EndiannessField, DceRpcPayload
from scapy.compat import bytes_hex
from scapy.volatile import RandUUID

# Block Packet
BLOCK_TYPES_ENUM = {
    0x0000: "AlarmNotification_High",
    0x0001: "AlarmNotification_Low",
    0x0008: "IODWriteReqHeader",
    0x0009: "IODReadReqHeader",
    0x0010: "DiagnosisData",
    0x0012: "ExpectedIdentificationData",
    0x0013: "RealIdentificationData",
    0x0014: "SubsituteValue",
    0x0015: "RecordInputDataObjectElement",
    0x0016: "RecordOutputDataObjectElement",
    0x0018: "ARData",
    0x0019: "LogBookData",
    0x001a: "APIData",
    0x001b: "SRLData",
    0x0020: "I&M0",
    0x0021: "I&M1",
    0x0022: "I&M2",
    0x0023: "I&M3",
    0x0024: "I&M4",
    0x0030: "I&M0FilterDataSubmodule",
    0x0031: "I&M0FilterDataModule",
    0x0032: "I&M0FilterDataDevice",
    0x0101: "ARBlockReq",
    0x0102: "IOCRBlockReq",
    0x0103: "AlarmCRBlockReq",
    0x0104: "ExpectedSubmoduleBlockReq",
    0x0105: "PrmServerBlockReq",
    0x0106: "MCRBlockReq",
    0x0107: "ARRPCBlockReq",
    0x0108: "ARVendorBlockReq",
    0x0109: "IRInfoBlock",
    0x010a: "SRInfoBlock",
    0x010b: "ARFSUBlock",
    0x0110: "IODBlockReq_connect_end",
    0x0111: "IODBlockReq_plug",
    0x0112: "IOXBlockReq_connect",
    0x0113: "IOXBlockReq_plug",
    0x0114: "ReleaseBlockReq",
    0x0116: "IOXBlockReq_companion",
    0x0117: "IOXBlockReq_rt_class_3",
    0x0118: "IODBlockReq_connect_begin",
    0x0119: "SubmoduleListBlock",
    0x0200: "PDPortDataCheck",
    0x0201: "PdevData",
    0x0202: "PDPortDataAdjust",
    0x0203: "PDSyncData",
    0x0204: "IsochronousModeData",
    0x0205: "PDIRData",
    0x0206: "PDIRGlobalData",
    0x0207: "PDIRFrameData",
    0x0208: "PDIRBeginEndData",
    0x0209: "AdjustDomainBoundary",
    0x020a: "SubBlock_check_Peers",
    0x020b: "SubBlock_check_LineDelay",
    0x020c: "SubBlock_check_MAUType",
    0x020e: "AdjustMAUType",
    0x020f: "PDPortDataReal",
    0x0210: "AdjustMulticastBoundary",
    0x0211: "PDInterfaceMrpDataAdjust",
    0x0212: "PDInterfaceMrpDataReal",
    0x0213: "PDInterfaceMrpDataCheck",
    0x0214: "PDPortMrpDataAdjust",
    0x0215: "PDPortMrpDataReal",
    0x0216: "MrpManagerParams",
    0x0217: "MrpClientParams",
    0x0219: "MrpRingStateData",
    0x021b: "AdjustLinkState",
    0x021c: "CheckLinkState",
    0x021e: "CheckSyncDifference",
    0x021f: "CheckMAUTypeDifference",
    0x0220: "PDPortFODataReal",
    0x0221: "FiberOpticManufacturerSpecific",
    0x0222: "PDPortFODataAdjust",
    0x0223: "PDPortFODataCheck",
    0x0224: "AdjustPeerToPeerBoundary",
    0x0225: "AdjustDCPBoundary",
    0x0226: "AdjustPreambleLength",
    0x0228: "FiberOpticDiagnosisInfo",
    0x022a: "PDIRSubframeData",
    0x022b: "SubframeBlock",
    0x022d: "PDTimeData",
    0x0230: "PDNCDataCheck",
    0x0231: "MrpInstanceDataAdjustBlock",
    0x0232: "MrpInstanceDataRealBlock",
    0x0233: "MrpInstanceDataCheckBlock",
    0x0240: "PDInterfaceDataReal",
    0x0250: "PDInterfaceAdjust",
    0x0251: "PDPortStatistic",
    0x0400: "MultipleBlockHeader",
    0x0401: "COContainerContent",
    0x0500: "RecordDataReadQuery",
    0x0600: "FSHelloBlock",
    0x0601: "FSParameterBlock",
    0x0608: "PDInterfaceFSUDataAdjust",
    0x0609: "ARFSUDataAdjust",
    0x0700: "AutoConfiguration",
    0x0701: "AutoConfigurationCommunication",
    0x0702: "AutoConfigurationConfiguration",
    0x0703: "AutoConfigurationIsochronous",
    0x0A00: "UploadBLOBQuery",
    0x0A01: "UploadBLOB",
    0x0A02: "NestedDiagnosisInfo",
    0x0F00: "MaintenanceItem",
    0x0F01: "UploadRecord",
    0x0F02: "iParameterItem",
    0x0F03: "RetrieveRecord",
    0x0F04: "RetrieveAllRecord",
    0x8001: "AlarmAckHigh",
    0x8002: "AlarmAckLow",
    0x8008: "IODWriteResHeader",
    0x8009: "IODReadResHeader",
    0x8101: "ARBlockRes",
    0x8102: "IOCRBlockRes",
    0x8103: "AlarmCRBlockRes",
    0x8104: "ModuleDiffBlock",
    0x8105: "PrmServerBlockRes",
    0x8106: "ARServerBlockRes",
    0x8107: "ARRPCBlockRes",
    0x8108: "ARVendorBlockRes",
    0x8110: "IODBlockRes_connect_end",
    0x8111: "IODBlockRes_plug",
    0x8112: "IOXBlockRes_connect",
    0x8113: "IOXBlockRes_plug",
    0x8114: "ReleaseBlockRes",
    0x8116: "IOXBlockRes_companion",
    0x8117: "IOXBlockRes_rt_class_3",
    0x8118: "IODBlockRes_connect_begin",
}

# IODWriteReq & IODWriteMultipleReq Packets
IOD_WRITE_REQ_INDEX = {
    0x8000: "ExpectedIdentificationData_subslot",
    0x8001: "RealIdentificationData_subslot",
    0x800a: "Diagnosis_channel_subslot",
    0x800b: "Diagnosis_all_subslot",
    0x800c: "Diagnosis_Maintenance_subslot",
    0x8010: "Maintenance_required_in_channel_subslot",
    0x8011: "Maintenance_demanded_in_channel_subslot",
    0x8012: "Maintenance_required_in_all_channels_subslot",
    0x8013: "Maintenance_demanded_in_all_channels_subslot",
    0x801e: "SubstitueValue_subslot",
    0x8020: "PDIRSubframeData_subslot",
    0x8028: "RecordInputDataObjectElement_subslot",
    0x8029: "RecordOutputDataObjectElement_subslot",
    0x802a: "PDPortDataReal_subslot",
    0x802b: "PDPortDataCheck_subslot",
    0x802c: "PDIRData_subslot",
    0x802d: "Expected_PDSyncData_subslot",
    0x802f: "PDPortDataAdjust_subslot",
    0x8030: "IsochronousModeData_subslot",
    0x8031: "Expected_PDTimeData_subslot",
    0x8050: "PDInterfaceMrpDataReal_subslot",
    0x8051: "PDInterfaceMrpDataCheck_subslot",
    0x8052: "PDInterfaceMrpDataAdjust_subslot",
    0x8053: "PDPortMrpDataAdjust_subslot",
    0x8054: "PDPortMrpDataReal_subslot",
    0x8060: "PDPortFODataReal_subslot",
    0x8061: "PDPortFODataCheck_subslot",
    0x8062: "PDPortFODataAdjust_subslot",
    0x8070: "PdNCDataCheck_subslot",
    0x8071: "PDInterfaceAdjust_subslot",
    0x8072: "PDPortStatistic_subslot",
    0x8080: "PDInterfaceDataReal_subslot",
    0x8090: "Expected_PDInterfaceFSUDataAdjust",
    0x80a0: "Energy_saving_profile_record_0",
    0x80b0: "CombinedObjectContainer",
    0x80c0: "Sequence_events_profile_record_0",
    0xaff0: "I&M0",
    0xaff1: "I&M1",
    0xaff2: "I&M2",
    0xaff3: "I&M3",
    0xaff4: "I&M4",
    0xc000: "Expect edIdentificationData_slot",
    0xc001: "RealId entificationData_slot",
    0xc00a: "Diagno sis_channel_slot",
    0xc00b: "Diagnosis_all_slot",
    0xc00c: "Diagnosis_Maintenance_slot",
    0xc010: "Maintenance_required_in_channel_slot",
    0xc011: "Maintenance_demanded_in_channel_slot",
    0xc012: "Maintenance_required_in_all_channels_slot",
    0xc013: "Maintenance_demanded_in_all_channels_slot",
    0xe000: "ExpectedIdentificationData_AR",
    0xe001: "RealIdentificationData_AR",
    0xe002: "ModuleDiffBlock_AR",
    0xe00a: "Diagnosis_channel_AR",
    0xe00b: "Diagnosis_all_AR",
    0xe00c: "Diagnosis_Maintenance_AR",
    0xe010: "Maintenance_required_in_channel_AR",
    0xe011: "Maintenance_demanded_in_channel_AR",
    0xe012: "Maintenance_required_in_all_channels_AR",
    0xe013: "Maintenance_demanded_in_all_channels_AR",
    0xe040: "WriteMultiple",
    0xe050: "ARFSUDataAdjust_AR",
    0xf000: "RealIdentificationData_API",
    0xf00a: "Diagnosis_channel_API",
    0xf00b: "Diagnosis_all_API",
    0xf00c: "Diagnosis_Maintenance_API",
    0xf010: "Maintenance_required_in_channel_API",
    0xf011: "Maintenance_demanded_in_channel_API",
    0xf012: "Maintenance_required_in_all_channels_API",
    0xf013: "Maintenance_demanded_in_all_channels_API",
    0xf020: "ARData_API",
    0xf80c: "Diagnosis_Maintenance_device",
    0xf820: "ARData",
    0xf821: "APIData",
    0xf830: "LogBookData",
    0xf831: "PdevData",
    0xf840: "I&M0FilterData",
    0xf841: "PDRealData",
    0xf842: "PDExpectedData",
    0xf850: "AutoConfiguration",
    0xf860: "GSD_upload",
    0xf861: "Nested_Diagnosis_info",
    0xfbff: "Trigger_index_CMSM",
}

# ARBlockReq Packets
AR_TYPE = {
    0x0001: "IOCARSingle",
    0x0006: "IOSAR",
    0x0010: "IOCARSingle_RT_CLASS_3",
    0x0020: "IOCARSR",
}

# IOCRBlockReq Packets
IOCR_TYPE = {
    0x0001: "InputCR",
    0x0002: "OutputCR",
    0x0003: "MulticastProviderCR",
    0x0004: "MulticastConsumerCR",
}

IOCR_BLOCK_REQ_IOCR_PROPERTIES = {
    0x1: "RT_CLASS_1",
    0x2: "RT_CLASS_2",
    0x3: "RT_CLASS_3",
    0x4: "RT_CLASS_UDP",
}


# List of all valid activity UUIDs for the DceRpc layer with PROFINET RPC
# endpoint.
#
# Because these are used in overloaded_fields, it must be a ``UUID``, not a
# string.
RPC_INTERFACE_UUID = {
    "UUID_IO_DeviceInterface": UUID("dea00001-6c97-11d1-8271-00a02442df7d"),
    "UUID_IO_ControllerInterface":
        UUID("dea00002-6c97-11d1-8271-00a02442df7d"),
    "UUID_IO_SupervisorInterface":
        UUID("dea00003-6c97-11d1-8271-00a02442df7d"),
    "UUID_IO_ParameterServerInterface":
        UUID("dea00004-6c97-11d1-8271-00a02442df7d"),
}


# Generic Block Packet
class BlockHeader(Packet):
    """Abstract packet to centralize block headers fields"""
    fields_desc = [
        ShortEnumField("block_type", None, BLOCK_TYPES_ENUM),
        ShortField("block_length", None),
        ByteField("block_version_high", 1),
        ByteField("block_version_low", 0),
    ]

    def __new__(cls, name, bases, dct):
        raise NotImplementedError()


class Block(Packet):
    """A generic block packet for PNIO RPC"""
    fields_desc = [
        BlockHeader,
        StrLenField("load", "", length_from=lambda pkt: pkt.block_length - 2),
    ]
    # default block_type
    block_type = 0

    def post_build(self, p, pay):
        # update the block_length if needed
        if self.block_length is None:
            # block_length and block_type are not part of the length count
            length = len(p) - 4
            p = p[:2] + struct.pack("!H", length) + p[4:]

        return Packet.post_build(self, p, pay)

    def extract_padding(self, s):
        # all fields after block_length are included in the length and must be
        # subtracted from the pdu length l
        length = self.payload_length()
        return s[:length], s[length:]

    def payload_length(self):
        """ A function for each block, to determine the length of
        the payload """
        return 0  # default, no payload


# Specific Block Packets
#     IODControlRe{q,s}
class IODControlReq(Block):
    """IODControl request block"""
    fields_desc = [
        BlockHeader,
        StrFixedLenField("padding", "", length=2),
        UUIDField("ARUUID", None),
        ShortField("SessionKey", 0),
        XShortField("AlarmSequenceNumber", 0),
        # ControlCommand
        BitField("ControlCommand_reserved", 0, 9),
        BitField("ControlCommand_PrmBegin", 0, 1),
        BitField("ControlCommand_ReadyForRT_CLASS_3", 0, 1),
        BitField("ControlCommand_ReadyForCompanion", 0, 1),
        BitField("ControlCommand_Done", 0, 1),
        BitField("ControlCommand_Release", 0, 1),
        BitField("ControlCommand_ApplicationReady", 0, 1),
        BitField("ControlCommand_PrmEnd", 0, 1),
        XShortField("ControlBlockProperties", 0)
    ]

    def post_build(self, p, pay):
        # Try to find the right block type
        if self.block_type is None:
            if self.ControlCommand_PrmBegin:
                p = struct.pack("!H", 0x0118) + p[2:]
            elif self.ControlCommand_ReadyForRT_CLASS_3:
                p = struct.pack("!H", 0x0117) + p[2:]
            elif self.ControlCommand_ReadyForCompanion:
                p = struct.pack("!H", 0x0116) + p[2:]
            elif self.ControlCommand_Release:
                p = struct.pack("!H", 0x0114) + p[2:]
            elif self.ControlCommand_ApplicationReady:
                if self.AlarmSequenceNumber > 0:
                    p = struct.pack("!H", 0x0113) + p[2:]
                else:
                    p = struct.pack("!H", 0x0112) + p[2:]
            elif self.ControlCommand_PrmEnd:
                if self.AlarmSequenceNumber > 0:
                    p = struct.pack("!H", 0x0111) + p[2:]
                else:
                    p = struct.pack("!H", 0x0110) + p[2:]
        return Block.post_build(self, p, pay)

    def get_response(self):
        """Generate the response block of this request.
        Careful: it only sets the fields which can be set from the request
        """
        res = IODControlRes()
        for field in ["ARUUID", "SessionKey", "AlarmSequenceNumber"]:
            res.setfieldval(field, self.getfieldval(field))

        res.block_type = self.block_type + 0x8000
        return res


class IODControlRes(Block):
    """IODControl response block"""
    fields_desc = [
        BlockHeader,
        StrFixedLenField("padding", "", length=2),
        UUIDField("ARUUID", None),
        ShortField("SessionKey", 0),
        XShortField("AlarmSequenceNumber", 0),
        # ControlCommand
        BitField("ControlCommand_reserved", 0, 9),
        BitField("ControlCommand_PrmBegin", 0, 1),
        BitField("ControlCommand_ReadyForRT_CLASS_3", 0, 1),
        BitField("ControlCommand_ReadyForCompanion", 0, 1),
        BitField("ControlCommand_Done", 1, 1),
        BitField("ControlCommand_Release", 0, 1),
        BitField("ControlCommand_ApplicationReady", 0, 1),
        BitField("ControlCommand_PrmEnd", 0, 1),
        XShortField("ControlBlockProperties", 0)
    ]

    # default block_type value
    block_type = 0x8110
    # The block_type can be among 0x8110 to 0x8118 except 0x8115
    # The right type is however determine by the type of the request
    # (same type as the request + 0x8000)


#     IODWriteRe{q,s}
class IODWriteReq(Block):
    """IODWrite request block"""
    fields_desc = [
        BlockHeader,
        ShortField("seqNum", 0),
        UUIDField("ARUUID", None),
        XIntField("API", 0),
        XShortField("slotNumber", 0),
        XShortField("subslotNumber", 0),
        StrFixedLenField("padding", "", length=2),
        XShortEnumField("index", 0, IOD_WRITE_REQ_INDEX),
        LenField("recordDataLength", None, fmt="I"),
        StrFixedLenField("RWPadding", "", length=24),
    ]
    # default block_type value
    block_type = 0x0008

    def payload_length(self):
        return self.recordDataLength

    def get_response(self):
        """Generate the response block of this request.
        Careful: it only sets the fields which can be set from the request
        """
        res = IODWriteRes()
        for field in ["seqNum", "ARUUID", "API", "slotNumber",
                      "subslotNumber", "index"]:
            res.setfieldval(field, self.getfieldval(field))
        return res


class IODWriteRes(Block):
    """IODWrite response block"""
    fields_desc = [
        BlockHeader,
        ShortField("seqNum", 0),
        UUIDField("ARUUID", None),
        XIntField("API", 0),
        XShortField("slotNumber", 0),
        XShortField("subslotNumber", 0),
        StrFixedLenField("padding", "", length=2),
        XShortEnumField("index", 0, IOD_WRITE_REQ_INDEX),
        LenField("recordDataLength", None, fmt="I"),
        XShortField("additionalValue1", 0),
        XShortField("additionalValue2", 0),
        IntEnumField("status", 0, ["OK"]),
        StrFixedLenField("RWPadding", "", length=16),
    ]
    # default block_type value
    block_type = 0x8008


F_PARAMETERS_BLOCK_ID = [
    "No_F_WD_Time2_No_F_iPar_CRC", "No_F_WD_Time2_F_iPar_CRC",
    "F_WD_Time2_No_F_iPar_CRC", "F_WD_Time2_F_iPar_CRC",
    "reserved_4", "reserved_5", "reserved_6", "reserved_7"
]


class FParametersBlock(Packet):
    """F-Parameters configuration block"""
    name = "F-Parameters Block"
    fields_desc = [
        # F_Prm_Flag1
        BitField("F_Prm_Flag1_Reserved_7", 0, 1),
        BitField("F_CRC_Seed", 0, 1),
        BitEnumField("F_CRC_Length", 0, 2,
                     ["CRC-24", "depreciated", "CRC-32", "reserved"]),
        BitEnumField("F_SIL", 2, 2, ["SIL_1", "SIL_2", "SIL_3", "No_SIL"]),
        BitField("F_Check_iPar", 0, 1),
        BitField("F_Check_SeqNr", 0, 1),

        # F_Prm_Flag2
        BitEnumField("F_Par_Version", 1, 2,
                     ["V1", "V2", "reserved_2", "reserved_3"]),
        BitEnumField("F_Block_ID", 0, 3, F_PARAMETERS_BLOCK_ID),
        BitField("F_Prm_Flag2_Reserved", 0, 2),
        BitField("F_Passivation", 0, 1),

        XShortField("F_Source_Add", 0),
        XShortField("F_Dest_Add", 0),
        ShortField("F_WD_Time", 0),
        ConditionalField(
            cond=lambda p: p.getfieldval("F_Block_ID") & 0b110 == 0b010,
            fld=ShortField("F_WD_Time_2", 0)),
        ConditionalField(
            cond=lambda p: p.getfieldval("F_Block_ID") & 0b101 == 0b001,
            fld=XIntField("F_iPar_CRC", 0)),
        XShortField("F_Par_CRC", 0)
    ]
    overload_fields = {
        IODWriteReq: {
            "index": 0x100,  # commonly used index for F-Parameters block
        }
    }


bind_layers(IODWriteReq, FParametersBlock, index=0x0100)
bind_layers(FParametersBlock, conf.padding_layer)


#     IODWriteMultipleRe{q,s}
class PadFieldWithLen(PadField):
    """PadField which handles the i2len function to include padding"""
    def i2len(self, pkt, val):
        """get the length of the field, including the padding length"""
        fld_len = self._fld.i2len(pkt, val)
        return fld_len + self.padlen(fld_len)


class IODWriteMultipleReq(Block):
    """IODWriteMultiple request"""
    fields_desc = [
        BlockHeader,
        ShortField("seqNum", 0),
        UUIDField("ARUUID", None),
        XIntField("API", 0xffffffff),
        XShortField("slotNumber", 0xffff),
        XShortField("subslotNumber", 0xffff),
        StrFixedLenField("padding", "", length=2),
        XShortEnumField("index", 0, IOD_WRITE_REQ_INDEX),
        FieldLenField("recordDataLength", None, fmt="I", length_of="blocks"),
        StrFixedLenField("RWPadding", "", length=24),
        FieldListField("blocks", [],
                       PadFieldWithLen(PacketField("", None, IODWriteReq), 4),
                       length_from=lambda pkt: pkt.recordDataLength)
    ]
    # default values
    block_type = 0x0008
    index = 0xe040
    API = 0xffffffff
    slotNumber = 0xffff
    subslotNumber = 0xffff

    def post_build(self, p, pay):
        # patch the update of block_length, as requests field must not be
        # included. block_length is always 60
        if self.block_length is None:
            p = p[:2] + struct.pack("!H", 60) + p[4:]

        # Remove the final padding added in requests
        fld, val = self.getfield_and_val("blocks")
        if fld.i2count(self, val) > 0:
            length = len(val[-1])
            pad = fld.field.padlen(length)
            if pad > 0:
                p = p[:-pad]
                # also reduce the recordDataLength accordingly
                if self.recordDataLength is None:
                    val = struct.unpack("!I", p[36:40])[0]
                    val -= pad
                    p = p[:36] + struct.pack("!I", val) + p[40:]

        return Packet.post_build(self, p, pay)

    def get_response(self):
        """Generate the response block of this request.
        Careful: it only sets the fields which can be set from the request
        """
        res = IODWriteMultipleRes()
        for field in ["seqNum", "ARUUID", "API", "slotNumber",
                      "subslotNumber", "index"]:
            res.setfieldval(field, self.getfieldval(field))

        # append all block response
        res_blocks = []
        for block in self.getfieldval("blocks"):
            res_blocks.append(block.get_response())
        res.setfieldval("blocks", res_blocks)
        return res


class IODWriteMultipleRes(Block):
    """IODWriteMultiple response"""
    fields_desc = [
        BlockHeader,
        ShortField("seqNum", 0),
        UUIDField("ARUUID", None),
        XIntField("API", 0xffffffff),
        XShortField("slotNumber", 0xffff),
        XShortField("subslotNumber", 0xffff),
        StrFixedLenField("padding", "", length=2),
        XShortEnumField("index", 0, IOD_WRITE_REQ_INDEX),
        FieldLenField("recordDataLength", None, fmt="I", length_of="blocks"),
        XShortField("additionalValue1", 0),
        XShortField("additionalValue2", 0),
        IntEnumField("status", 0, ["OK"]),
        StrFixedLenField("RWPadding", "", length=16),
        FieldListField("blocks", [], PacketField("", None, IODWriteRes),
                       length_from=lambda pkt: pkt.recordDataLength)
    ]
    # default values
    block_type = 0x8008
    index = 0xe040

    def post_build(self, p, pay):
        # patch the update of block_length, as requests field must not be
        # included. block_length is always 60
        if self.block_length is None:
            p = p[:2] + struct.pack("!H", 60) + p[4:]

        return Packet.post_build(self, p, pay)


#     ARBlockRe{q,s}
class ARBlockReq(Block):
    """Application relationship block request"""
    fields_desc = [
        BlockHeader,
        XShortEnumField("ARType", 1, AR_TYPE),
        UUIDField("ARUUID", None),
        ShortField("SessionKey", 0),
        MACField("CMInitiatorMacAdd", None),
        UUIDField("CMInitiatorObjectUUID", None),
        # ARProperties
        BitField("ARProperties_PullModuleAlarmAllowed", 0, 1),
        BitEnumField("ARProperties_StartupMode", 0, 1,
                     ["Legacy", "Advanced"]),
        BitField("ARProperties_reserved_3", 0, 6),
        BitField("ARProperties_reserved_2", 0, 12),
        BitField("ARProperties_AcknowledgeCompanionAR", 0, 1),
        BitEnumField("ARProperties_CompanionAR", 0, 2,
                     ["Single_AR", "First_AR", "Companion_AR", "reserved"]),
        BitEnumField("ARProperties_DeviceAccess", 0, 1,
                     ["ExpectedSubmodule", "Controlled_by_IO_device_app"]),
        BitField("ARProperties_reserved_1", 0, 3),
        BitEnumField("ARProperties_ParametrizationServer", 0, 1,
                     ["External_PrmServer", "CM_Initator"]),
        BitField("ARProperties_SupervisorTakeoverAllowed", 0, 1),
        BitEnumField("ARProperties_State", 1, 3, {1: "Active"}),
        ShortField("CMInitiatorActivityTimeoutFactor", 1000),
        ShortField("CMInitiatorUDPRTPort", 0x8892),
        FieldLenField("StationNameLength", None, fmt="H",
                      length_of="CMInitiatorStationName"),
        StrLenField("CMInitiatorStationName", "",
                    length_from=lambda pkt: pkt.StationNameLength),
    ]
    # default block_type value
    block_type = 0x0101

    def get_response(self):
        """Generate the response block of this request.
        Careful: it only sets the fields which can be set from the request
        """
        res = ARBlockRes()
        for field in ["ARType", "ARUUID", "SessionKey"]:
            res.setfieldval(field, self.getfieldval(field))
        return res


class ARBlockRes(Block):
    """Application relationship block response"""
    fields_desc = [
        BlockHeader,
        XShortEnumField("ARType", 1, AR_TYPE),
        UUIDField("ARUUID", None),
        ShortField("SessionKey", 0),
        MACField("CMResponderMacAdd", None),
        ShortField("CMResponderUDPRTPort", 0x8892),
    ]
    # default block_type value
    block_type = 0x8101


#     IOCRBlockRe{q,s}
class IOCRAPIObject(Packet):
    """API item descriptor used in API description of IOCR blocks"""
    name = "API item"
    fields_desc = [
        XShortField("SlotNumber", 0),
        XShortField("SubslotNumber", 0),
        ShortField("FrameOffset", 0),
    ]

    def extract_padding(self, s):
        return None, s  # No extra payload


class IOCRAPI(Packet):
    """API description used in IOCR block"""
    name = "API"
    fields_desc = [
        XIntField("API", 0),
        FieldLenField("NumberOfIODataObjects", None,
                      count_of="IODataObjects"),
        PacketListField("IODataObjects", [], IOCRAPIObject,
                        count_from=lambda p: p.NumberOfIODataObjects),
        FieldLenField("NumberOfIOCS", None,
                      count_of="IOCSs"),
        PacketListField("IOCSs", [], IOCRAPIObject,
                        count_from=lambda p: p.NumberOfIOCS),
    ]

    def extract_padding(self, s):
        return None, s  # No extra payload


class IOCRBlockReq(Block):
    """IO Connection Relationship block request"""
    fields_desc = [
        BlockHeader,
        XShortEnumField("IOCRType", 1, IOCR_TYPE),
        XShortField("IOCRReference", 1),
        XShortField("LT", 0x8892),
        # IOCRProperties
        BitField("IOCRProperties_reserved3", 0, 8),
        BitField("IOCRProperties_reserved2", 0, 11),
        BitField("IOCRProperties_reserved1", 0, 9),
        BitEnumField("IOCRProperties_RTClass", 0, 4,
                     IOCR_BLOCK_REQ_IOCR_PROPERTIES),
        ShortField("DataLength", 40),
        XShortField("FrameID", 0x8000),
        ShortField("SendClockFactor", 32),
        ShortField("ReductionRatio", 32),
        ShortField("Phase", 1),
        ShortField("Sequence", 0),
        XIntField("FrameSendOffset", 0xffffffff),
        ShortField("WatchdogFactor", 10),
        ShortField("DataHoldFactor", 10),
        # IOCRTagHeader
        BitEnumField("IOCRTagHeader_IOUserPriority", 6, 3,
                     {6: "IOCRPriority"}),
        BitField("IOCRTagHeader_reserved", 0, 1),
        BitField("IOCRTagHeader_IOCRVLANID", 0, 12),
        MACField("IOCRMulticastMACAdd", None),
        FieldLenField("NumberOfAPIs", None, fmt="H", count_of="APIs"),
        PacketListField("APIs", [], IOCRAPI,
                        count_from=lambda p: p.NumberOfAPIs)
    ]
    # default block_type value
    block_type = 0x0102

    def get_response(self):
        """Generate the response block of this request.
        Careful: it only sets the fields which can be set from the request
        """
        res = IOCRBlockRes()
        for field in ["IOCRType", "IOCRReference", "FrameID"]:
            res.setfieldval(field, self.getfieldval(field))
        return res


class IOCRBlockRes(Block):
    """IO Connection Relationship block response"""
    fields_desc = [
        BlockHeader,
        XShortEnumField("IOCRType", 1, IOCR_TYPE),
        XShortField("IOCRReference", 1),
        XShortField("FrameID", 0x8000),
    ]
    # default block_type value
    block_type = 0x8102


#     ExpectedSubmoduleBlockReq
class ExpectedSubmoduleDataDescription(Packet):
    """Description of the data of a submodule"""
    name = "Data Description"
    fields_desc = [
        XShortEnumField("DataDescription", 0, {1: "Input", 2: "Output"}),
        ShortField("SubmoduleDataLength", 0),
        ByteField("LengthIOCS", 0),
        ByteField("LengthIOPS", 0),
    ]

    def extract_padding(self, s):
        return None, s  # No extra payload


class ExpectedSubmodule(Packet):
    """Description of a submodule in an API of an expected submodule"""
    name = "Submodule"
    fields_desc = [
        XShortField("SubslotNumber", 0),
        XIntField("SubmoduleIdentNumber", 0),
        # Submodule Properties
        XByteField("SubmoduleProperties_reserved_2", 0),
        BitField("SubmoduleProperties_reserved_1", 0, 2),
        BitField("SubmoduleProperties_DiscardIOXS", 0, 1),
        BitField("SubmoduleProperties_ReduceOutputSubmoduleDataLength", 0, 1),
        BitField("SubmoduleProperties_ReduceInputSubmoduleDataLength", 0, 1),
        BitField("SubmoduleProperties_SharedInput", 0, 1),
        BitEnumField("SubmoduleProperties_Type", 0, 2,
                     ["NO_IO", "INPUT", "OUTPUT", "INPUT_OUTPUT"]),
        PacketListField(
            "DataDescription", [], ExpectedSubmoduleDataDescription,
            count_from=lambda p: 2 if p.SubmoduleProperties_Type == 3 else 1
        ),
    ]

    def extract_padding(self, s):
        return None, s  # No extra payload


class ExpectedSubmoduleAPI(Packet):
    """Description of an API in the expected submodules blocks"""
    name = "API"
    fields_desc = [
        XIntField("API", 0),
        XShortField("SlotNumber", 0),
        XIntField("ModuleIdentNumber", 0),
        XShortField("ModuleProperties", 0),
        FieldLenField("NumberOfSubmodules", None, fmt="H",
                      count_of="Submodules"),
        PacketListField("Submodules", [], ExpectedSubmodule,
                        count_from=lambda p: p.NumberOfSubmodules),
    ]

    def extract_padding(self, s):
        return None, s  # No extra payload


class ExpectedSubmoduleBlockReq(Block):
    """Expected submodule block request"""
    fields_desc = [
        BlockHeader,
        FieldLenField("NumberOfAPIs", None, fmt="H", count_of="APIs"),
        PacketListField("APIs", [], ExpectedSubmoduleAPI,
                        count_from=lambda p: p.NumberOfAPIs)
    ]
    # default block_type value
    block_type = 0x0104

    def get_response(self):
        """Generate the response block of this request.
        Careful: it only sets the fields which can be set from the request
        """
        return None  # no response associated (should be modulediffblock)


# PROFINET IO DCE/RPC PDU
PNIO_RPC_BLOCK_ASSOCIATION = {
    # requests
    "0101": ARBlockReq,
    "0102": IOCRBlockReq,
    "0104": ExpectedSubmoduleBlockReq,
    "0110": IODControlReq,
    "0111": IODControlReq,
    "0112": IODControlReq,
    "0113": IODControlReq,
    "0114": IODControlReq,
    "0116": IODControlReq,
    "0117": IODControlReq,
    "0118": IODControlReq,

    # responses
    "8101": ARBlockRes,
    "8102": IOCRBlockRes,
    "8110": IODControlRes,
    "8111": IODControlRes,
    "8112": IODControlRes,
    "8113": IODControlRes,
    "8114": IODControlRes,
    "8116": IODControlRes,
    "8117": IODControlRes,
    "8118": IODControlRes,
}


def _guess_block_class(_pkt, *args, **kargs):
    cls = Block  # Default block type

    # Special cases
    if _pkt[:2] == b'\x00\x08':  # IODWriteReq
        if _pkt[34:36] == b'\xe0@':  # IODWriteMultipleReq
            cls = IODWriteMultipleReq
        else:
            cls = IODWriteReq

    elif _pkt[:2] == b'\x80\x08':    # IODWriteRes
        if _pkt[34:36] == b'\xe0@':  # IODWriteMultipleRes
            cls = IODWriteMultipleRes
        else:
            cls = IODWriteRes

    # Common cases
    else:
        btype = bytes_hex(_pkt[:2]).decode("utf8")
        if btype in PNIO_RPC_BLOCK_ASSOCIATION:
            cls = PNIO_RPC_BLOCK_ASSOCIATION[btype]

    return cls(_pkt, *args, **kargs)


def dce_rpc_endianess(pkt):
    """determine the symbol for the endianness of a the DCE/RPC"""
    try:
        endianness = pkt.underlayer.endianness
    except AttributeError:
        # handle the case where a PNIO class is
        # built without its DCE-RPC under-layer
        # i.e there is no endianness indication
        return "!"
    if endianness == 0:  # big endian
        return ">"
    elif endianness == 1:  # little endian
        return "<"
    else:
        return "!"


class NDRData(Packet):
    """Base NDRData to centralize some fields. It can't be instantiated"""
    fields_desc = [
        EndiannessField(
            FieldLenField("args_length", None, fmt="I", length_of="blocks"),
            endianess_from=dce_rpc_endianess),
        EndiannessField(
            FieldLenField("max_count", None, fmt="I", length_of="blocks"),
            endianess_from=dce_rpc_endianess),
        EndiannessField(
            IntField("offset", 0),
            endianess_from=dce_rpc_endianess),
        EndiannessField(
            FieldLenField("actual_count", None, fmt="I", length_of="blocks"),
            endianess_from=dce_rpc_endianess),
        PacketListField("blocks", [], _guess_block_class,
                        length_from=lambda p: p.args_length)
    ]

    def __new__(cls, name, bases, dct):
        raise NotImplementedError()


class PNIOServiceReqPDU(Packet):
    """PNIO PDU for RPC Request"""
    fields_desc = [
        EndiannessField(
            FieldLenField("args_max", None, fmt="I", length_of="blocks"),
            endianess_from=dce_rpc_endianess),
        NDRData,
    ]
    overload_fields = {
        DceRpc: {
            # random object_uuid in the appropriate range
            "object_uuid": RandUUID("dea00000-6c97-11d1-8271-******"),
            # interface uuid to send to a device
            "interface_uuid": RPC_INTERFACE_UUID["UUID_IO_DeviceInterface"],
            # Request DCE/RPC type
            "type": 0,
        },
    }

    @classmethod
    def can_handle(cls, pkt, rpc):
        """heuristic guess_payload_class"""
        # type = 0 => request
        if rpc.getfieldval("type") == 0 and \
                str(rpc.object_uuid).startswith("dea00000-6c97-11d1-8271-"):
            return True
        return False


DceRpcPayload.register_possible_payload(PNIOServiceReqPDU)


class PNIOServiceResPDU(Packet):
    """PNIO PDU for RPC Response"""
    fields_desc = [
        EndiannessField(IntEnumField("status", 0, ["OK"]),
                        endianess_from=dce_rpc_endianess),
        NDRData,
    ]
    overload_fields = {
        DceRpc: {
            # random object_uuid in the appropriate range
            "object_uuid": RandUUID("dea00000-6c97-11d1-8271-******"),
            # interface uuid to send to a host
            "interface_uuid": RPC_INTERFACE_UUID[
                "UUID_IO_ControllerInterface"],
            # Request DCE/RPC type
            "type": 2,
        },
    }

    @classmethod
    def can_handle(cls, pkt, rpc):
        """heuristic guess_payload_class"""
        # type = 2 => response
        if rpc.getfieldval("type") == 2 and \
                str(rpc.object_uuid).startswith("dea00000-6c97-11d1-8271-"):
            return True
        return False


DceRpcPayload.register_possible_payload(PNIOServiceResPDU)
