/* packet-hcrt.c
 *
 * Routines for Hotline Command-Response Transaction (HCrt)
 * Protocol specifications (draft) are available here
 * https://github.com/ShepardSiegel/hotline/tree/master/doc
 *
 * Copyright 2013 Dario Lombardo (lomato@gmail.com)
 *
 * Wireshark - Network traffic analyzer
 * By Gerald Combs <gerald@wireshark.org>
 * Copyright 1998 Gerald Combs
 *
 * SPDX-License-Identifier: GPL-2.0-or-later
 */

#include "config.h"
#include <epan/packet.h>
#include <epan/prefs.h>
#include <epan/expert.h>

static int proto_hcrt = -1;

#define HCRT_UDP_PORTS_DEFAULT "47000"

static guint ethertype_pref = 0xf052;

static int hf_hcrt_header = -1;
static int hf_hcrt_message_tag = -1;
static int hf_hcrt_message_type = -1;
static int hf_hcrt_am = -1;
static int hf_hcrt_do = -1;
static int hf_hcrt_1st_dword_enable = -1;
static int hf_hcrt_last_dword_enable = -1;
static int hf_hcrt_resp_code = -1;
static int hf_hcrt_adl = -1;
static int hf_hcrt_last = -1;
static int hf_hcrt_body = -1;
static int hf_hcrt_addr_32 = -1;
static int hf_hcrt_addr_64 = -1;
static int hf_hcrt_data_32 = -1;
static int hf_hcrt_data_64 = -1;
static int hf_hcrt_command_nop = -1;

static gint ett_hcrt = -1;
static gint ett_hcrt_msg = -1;
static gint ett_hcrt_hdr = -1;
static gint ett_hcrt_body = -1;

static expert_field ei_hcrt_error = EI_INIT;

void proto_reg_handoff_hcrt(void);
void proto_register_hcrt(void);

static dissector_handle_t hcrt_handle;

#define HCRT_HDR_LEN 4

#define HCRT_NOP      0x0
#define HCRT_WRITE    0x1
#define HCRT_READ     0x2
#define HCRT_RESPONSE 0x3

#define ADDR_MODE_32 1
#define ADDR_MODE_64 2

/* Message types */
static const value_string hcrt_message_types[] = {
    {0x00, "NOP"},
    {0x01, "Write"},
    {0x02, "Read"},
    {0x03, "Response"},
    {0, NULL}
};

/* Addressing modes */
static const value_string hcrt_ams[] = {
    {0x0, "32 bit"},
    {0x1, "64 bit"},
    {0, NULL}
};

/* Discovery operations */
static const true_false_string hcrt_dos = {
    "DO",
    "not DO",
};

static const value_string dword_enable_vals[] = {
    {0xF, "4B"},
    {0xC, "2B (MS)"},
    {0x3, "2B (LS)"},
    {0x8, "1B (B3)"},
    {0x4, "1B (B2)"},
    {0x2, "1B (B1)"},
    {0x1, "1B (B0)"},
    {0, NULL}
};

static const value_string response_codes[] = {
    {0x0, "OK"},
    {0x1, "Timeout"},
    {0x2, "Error"},
    {0x3, "Reserved"},
    {0x4, "Reserved"},
    {0x5, "Reserved"},
    {0x6, "Reserved"},
    {0x7, "Reserved"},
    {0x8, "Reserved"},
    {0x9, "Reserved"},
    {0xa, "Reserved"},
    {0xb, "Reserved"},
    {0xc, "Reserved"},
    {0xd, "Reserved"},
    {0xe, "Reserved"},
    {0xf, "Reserved"},
    {0, NULL}
};


static void dissect_hcrt_body(tvbuff_t* tvb, proto_tree* tree , guint* offset,
    int type, int addr_mode, int adl, int body_len)
{
    proto_item* ti_body;
    proto_tree* hcrt_body_tree;
    gint i;

    ti_body = proto_tree_add_item(tree, hf_hcrt_body, tvb, *offset, body_len, ENC_NA);
    hcrt_body_tree = proto_item_add_subtree(ti_body, ett_hcrt_body);

    switch (type) {
        case HCRT_NOP:
            proto_tree_add_item(hcrt_body_tree, hf_hcrt_command_nop, tvb, *offset,
                body_len, ENC_NA);
            break;
        case HCRT_WRITE:
            if (addr_mode == ADDR_MODE_32) {
                /* Address (32) */
                proto_tree_add_item(hcrt_body_tree, hf_hcrt_addr_32, tvb, *offset,
                    4, ENC_LITTLE_ENDIAN);

                /* Data */
                for (i = 1; i <= adl; i++) {
                    proto_tree_add_item(hcrt_body_tree, hf_hcrt_data_32, tvb,
                        *offset + i * 4, 4, ENC_LITTLE_ENDIAN);
                }
            } else {
                /* Address (64) */
                proto_tree_add_item(hcrt_body_tree, hf_hcrt_addr_64, tvb, *offset,
                    8, ENC_LITTLE_ENDIAN);

                /* Data */
                for (i = 1; i <= adl; i++)
                    proto_tree_add_item(hcrt_body_tree, hf_hcrt_data_64, tvb,
                        *offset + i * 8, 8, ENC_LITTLE_ENDIAN);
            }
            break;
        case HCRT_READ:
            if (addr_mode == ADDR_MODE_32) {
                /* Address (32) */
                proto_tree_add_item(hcrt_body_tree, hf_hcrt_addr_32, tvb, *offset, 4,
                    ENC_LITTLE_ENDIAN);
            } else {
                /* Address (64) */
                proto_tree_add_item(hcrt_body_tree, hf_hcrt_addr_64, tvb, *offset, 8,
                    ENC_LITTLE_ENDIAN);
            }
            break;
        case HCRT_RESPONSE:
            if (body_len > 0) {
                proto_tree_add_item(hcrt_body_tree, hf_hcrt_command_nop, tvb, *offset,
                    body_len, ENC_NA);
            }
            break;
        default:
            DISSECTOR_ASSERT_NOT_REACHED();
            break;
    }

    (*offset) += body_len;
}

/* Returns true if this is the last message */
static gboolean dissect_hcrt_header(tvbuff_t* tvb, packet_info* pinfo, proto_tree* tree,
    guint* offset, guint8 b0_first, guint8 b0_current)
{
    proto_item* ti_hdr;
    proto_tree* hcrt_hdr_tree;
    gboolean last;
    guint8 type;

    ti_hdr = proto_tree_add_item(tree, hf_hcrt_header, tvb, *offset, 4, ENC_NA);
    hcrt_hdr_tree = proto_item_add_subtree(ti_hdr, ett_hcrt_hdr);

    if (b0_first != b0_current) {
        expert_add_info_format(pinfo, hcrt_hdr_tree, &ei_hcrt_error,
            "Invalid Byte 0 in Header. Must be equal in all HCrt messages. "
                "Expected: %.2X, got: %.2X", b0_first, b0_current);
    }

    type = (b0_current & 0x30) >> 4;

    /* == Byte 0 == */
    /* TAG */
    proto_tree_add_item(hcrt_hdr_tree, hf_hcrt_message_tag, tvb,
        *offset, 1, ENC_NA);
    /* Message Type */
    proto_tree_add_item(hcrt_hdr_tree, hf_hcrt_message_type, tvb,
        *offset, 1, ENC_NA);
    /* Addressing Mode */
    proto_tree_add_item(hcrt_hdr_tree, hf_hcrt_am, tvb,
        *offset, 1, ENC_NA);
    /* Discovery Operation */
    proto_tree_add_item(hcrt_hdr_tree, hf_hcrt_do, tvb,
        *offset, 1, ENC_NA);
    (*offset)++;

    /* == Byte 1 == */
    if (type != HCRT_RESPONSE) {
        /* 1st DWORD enable */
        proto_tree_add_item(hcrt_hdr_tree, hf_hcrt_1st_dword_enable, tvb,
            *offset, 1, ENC_NA);
    } else {
        /* Response Code */
        proto_tree_add_item(hcrt_hdr_tree, hf_hcrt_resp_code, tvb,
            *offset, 1, ENC_NA);
    }

    if (type != HCRT_RESPONSE) {
        /* Last DWORD enable */
        proto_tree_add_item(hcrt_hdr_tree, hf_hcrt_last_dword_enable, tvb,
            *offset, 1, ENC_NA);
    }
    (*offset)++;

    /* == Byte 2 & 3 == */
    /* ADL */
    proto_tree_add_item(hcrt_hdr_tree, hf_hcrt_adl, tvb, *offset, 2, ENC_LITTLE_ENDIAN);
    /* Last */
    proto_tree_add_item(hcrt_hdr_tree, hf_hcrt_last, tvb, *offset, 2, ENC_LITTLE_ENDIAN);

    /* last */
    last = (tvb_get_letohs(tvb, *offset) & 0x8000) != 0;
    (*offset) += 2;
    return last;
}

/* Return true if this is the last message */
static gboolean dissect_hcrt_message(tvbuff_t* tvb, packet_info* pinfo, proto_tree* tree,
    guint* offset, guint8 b0_first, int i)
{
    gboolean last;
    guint adl;
    guint addr_mode;
    guint body_len;
    proto_tree* hcrt_msg_tree;
    guint8 b0_current;
    int type;

    /* Save byte 0 of current packet */
    b0_current = tvb_get_guint8(tvb, *offset);

    /* Get details from header */
    adl = tvb_get_letohs(tvb, *offset + 2) & 0x0FFF;
    addr_mode = (1 + ((b0_current & 0x40) >> 6));
    type = (b0_current & 0x30) >> 4;

    switch (type) {
        case HCRT_NOP:
            body_len = 4 * addr_mode * adl;
            break;
        case HCRT_WRITE:
            body_len = 4 * addr_mode * (adl + 1);
            break;
        case HCRT_READ:
            body_len = 4 * addr_mode;
            break;
        case HCRT_RESPONSE:
            body_len = 4 * addr_mode * adl;
            break;
        default:
            DISSECTOR_ASSERT_NOT_REACHED();
            break;
    }

    hcrt_msg_tree = proto_tree_add_subtree_format(tree, tvb, *offset,
        HCRT_HDR_LEN + body_len, ett_hcrt_msg, NULL, "Message %d", i);

    last = dissect_hcrt_header(tvb, pinfo, hcrt_msg_tree, offset, b0_first, b0_current);
    dissect_hcrt_body(tvb, hcrt_msg_tree, offset, type, addr_mode, adl, body_len);

    return last;
}

static int dissect_hcrt(tvbuff_t* tvb, packet_info* pinfo, proto_tree* tree, void* data _U_)
{
    guint8 type;
    proto_item* ti;
    proto_tree* hcrt_tree;
    guint offset;
    int i = 1;
    guint8 b0_first;
    guint8 tag;
    guint adl;

    col_set_str(pinfo->cinfo, COL_PROTOCOL, "HCrt");
    col_clear(pinfo->cinfo, COL_INFO);

    /* Save byte 0 of first message. Will be checked against byte 0 of other messages */
    b0_first = tvb_get_guint8(tvb, 0);

    tag  = b0_first & 0x0F;
    type = (b0_first & 0x30) >> 4;
    adl  = tvb_get_letohs(tvb, 2) & 0x0FFF;

    col_add_fstr(pinfo->cinfo, COL_INFO, "Type: %s, Tag: 0x%X, ADL: %u",
        val_to_str(type, hcrt_message_types, "Unknown (0x%02x)"), tag, adl);

    if (adl == 1) {
        if (type == HCRT_READ || type == HCRT_WRITE) {
            col_append_fstr(pinfo->cinfo, COL_INFO, ", Address: 0x%.8X", tvb_get_letohl(tvb, 4));
        }
        if (type == HCRT_WRITE) {
            col_append_fstr(pinfo->cinfo, COL_INFO, ", Data: 0x%.8X", tvb_get_letohl(tvb, 8));
        }
    }

    offset = 0;
    ti = proto_tree_add_item(tree, proto_hcrt, tvb, 0, -1, ENC_NA);
    hcrt_tree = proto_item_add_subtree(ti, ett_hcrt);

    while (!dissect_hcrt_message(tvb, pinfo, hcrt_tree, &offset, b0_first, i)) {
        i++;
    }
    return tvb_captured_length(tvb);
}

void proto_register_hcrt(void)
{
    expert_module_t* expert_hcrt;
    module_t* hcrt_module;

    static hf_register_info hf[] = {
        { &hf_hcrt_header,
            { "Header", "hcrt.hdr",
            FT_NONE, BASE_NONE,
            NULL, 0x00,
            NULL, HFILL }
        },
        { &hf_hcrt_message_tag,
            { "Tag", "hcrt.tag",
            FT_UINT8, BASE_HEX,
            NULL, 0x0F,
            NULL, HFILL }
        },
        { &hf_hcrt_message_type,
            { "Type", "hcrt.type",
            FT_UINT8, BASE_DEC,
            VALS(hcrt_message_types), 0x30,
            NULL, HFILL }
        },
        { &hf_hcrt_am,
            { "Addressing Mode", "hcrt.am",
            FT_UINT8, BASE_DEC,
            VALS(hcrt_ams), 0x40,
            NULL, HFILL }
        },
        { &hf_hcrt_do,
            { "Discovery Operation", "hcrt.do",
            FT_BOOLEAN, 8,
            TFS(&hcrt_dos), 0x80,
            NULL, HFILL }
        },
        { &hf_hcrt_1st_dword_enable,
            { "1st DWORD enable", "hcrt.first_dword_enable",
            FT_UINT8, BASE_HEX,
            VALS(dword_enable_vals), 0xF0,
            NULL, HFILL }
        },
        { &hf_hcrt_last_dword_enable,
            { "Last DWORD enable", "hcrt.last_dword_enable",
            FT_UINT8, BASE_HEX,
            VALS(dword_enable_vals), 0x0F,
            NULL, HFILL }
        },
        { &hf_hcrt_resp_code,
            { "Response code", "hcrt.response_code",
            FT_UINT8, BASE_HEX,
            VALS(response_codes), 0xF0,
            NULL, HFILL }
        },
        { &hf_hcrt_adl,
            { "ADL", "hcrt.adl",
            FT_UINT16, BASE_DEC,
            NULL, 0x0FFF,
            NULL, HFILL }
        },
        { &hf_hcrt_last,
            { "Last message", "hcrt.last",
            FT_BOOLEAN, 16,
            NULL, 0x8000,
            NULL, HFILL }
        },
        { &hf_hcrt_body,
            { "Body", "hcrt.body",
            FT_NONE, BASE_NONE,
            NULL, 0x00,
            NULL, HFILL }
        },
        { &hf_hcrt_addr_32,
            { "Address", "hcrt.address32",
            FT_UINT32, BASE_HEX,
            NULL, 0x0,
            NULL, HFILL }
        },
        { &hf_hcrt_addr_64,
            { "Address", "hcrt.address64",
            FT_UINT64, BASE_HEX,
            NULL, 0x0,
            NULL, HFILL }
        },
        { &hf_hcrt_data_32,
            { "Data", "hcrt.data32",
            FT_UINT32, BASE_HEX,
            NULL, 0x0,
            NULL, HFILL }
        },
        { &hf_hcrt_data_64,
            { "Data", "hcrt.data64",
            FT_UINT64, BASE_HEX,
            NULL, 0x0,
            NULL, HFILL }
        },
        { &hf_hcrt_command_nop,
            { "Command", "hcrt.command_nop",
            FT_BYTES, BASE_NONE,
            NULL, 0x0,
            NULL, HFILL }
        }
    };

    static ei_register_info ei[] = {
        { &ei_hcrt_error, { "hcrt.error", PI_MALFORMED, PI_ERROR, "Unusual error code", EXPFILL }}
    };

    /* Setup protocol subtree array */
    static gint* ett[] = {
        &ett_hcrt,
        &ett_hcrt_msg,
        &ett_hcrt_hdr,
        &ett_hcrt_body,
    };

    proto_hcrt = proto_register_protocol ("Hotline Command-Response Transaction protocol", "HCrt", "hcrt");

    proto_register_field_array(proto_hcrt, hf, array_length(hf));
    proto_register_subtree_array(ett, array_length(ett));
    expert_hcrt = expert_register_protocol(proto_hcrt);
    expert_register_field_array(expert_hcrt, ei, array_length(ei));

    hcrt_module = prefs_register_protocol(proto_hcrt, proto_reg_handoff_hcrt);
    prefs_register_uint_preference(hcrt_module,
        "dissector_ethertype",
        "Ethernet type",
        "The ethernet type used for L2 communications",
        10, &ethertype_pref);

    hcrt_handle = register_dissector("hcrt", dissect_hcrt, proto_hcrt);
}

void proto_reg_handoff_hcrt(void)
{
    static gboolean hcrt_prefs_initialized = FALSE;
    static gint hcrt_ethertype;

    if (!hcrt_prefs_initialized) {
        /* Also register as a dissector that can be selected by a TCP port number via
        "decode as" */
        dissector_add_for_decode_as_with_preference("tcp.port", hcrt_handle);
        dissector_add_uint_range_with_preference("udp.port", HCRT_UDP_PORTS_DEFAULT, hcrt_handle);
        hcrt_prefs_initialized = TRUE;
    } else {
        dissector_delete_uint("ethertype", hcrt_ethertype, hcrt_handle);
    }

    hcrt_ethertype = ethertype_pref;

    dissector_add_uint("ethertype", hcrt_ethertype, hcrt_handle);
}

/*
 * Editor modelines  -  https://www.wireshark.org/tools/modelines.html
 *
 * Local variables:
 * c-basic-offset: 4
 * tab-width: 8
 * indent-tabs-mode: nil
 * End:
 *
 * vi: set shiftwidth=4 tabstop=8 expandtab:
 * :indentSize=4:tabSize=8:noTabs=true:
 */
