#! /usr/bin/env python
#
# Copyright (C) 2015 Open Information Security Foundation
#
# You can copy, redistribute or modify this Program under the terms of
# the GNU General Public License version 2 as published by the Free
# Software Foundation.
#
# This program 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
# version 2 along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301, USA.

# This script generates DNP3 related source code based on definitions
# of DNP3 objects (currently the object structs).

from __future__ import print_function

import sys
import re
import yaml

import jinja2

IN_PLACE_START = "/* START GENERATED CODE */"
IN_PLACE_END = "/* END GENERATED CODE */"

util_lua_dnp3_objects_c_template = """/* Copyright (C) 2015 Open Information Security Foundation
 *
 * You can copy, redistribute or modify this Program under the terms of
 * the GNU General Public License version 2 as published by the Free
 * Software Foundation.
 *
 * This program 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
 * version 2 along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 * 02110-1301, USA.
 */

/**
 * DO NOT EDIT. THIS FILE IS AUTO-GENERATED.
 *
 * Generated by command:
 *    {{command_line}}
 */

#include "suricata-common.h"

#include "app-layer-dnp3.h"
#include "app-layer-dnp3-objects.h"

#include "lua.h"
#include "lualib.h"
#include "lauxlib.h"

#include "util-lua.h"
#include "util-lua-dnp3-objects.h"

/**
 * \\brief Push an object point item onto the stack.
 */
void DNP3PushPoint(lua_State *luastate, DNP3Object *object,
    DNP3Point *point)
{
    switch (DNP3_OBJECT_CODE(object->group, object->variation)) {
{% for object in objects %}
        case DNP3_OBJECT_CODE({{object.group}}, {{object.variation}}): {
            DNP3ObjectG{{object.group}}V{{object.variation}} *data = point->data;
{% for field in object.fields %}
{% if is_integer_type(field.type) %}
            lua_pushliteral(luastate, "{{field.name}}");
            lua_pushinteger(luastate, data->{{field.name}});
            lua_settable(luastate, -3);
{% elif field["type"] in ["flt32", "flt64"] %}
            lua_pushliteral(luastate, "{{field.name}}");
            lua_pushnumber(luastate, data->{{field.name}});
            lua_settable(luastate, -3);
{% elif field["type"] == "chararray" %}
            lua_pushliteral(luastate, "{{field.name}}");
            LuaPushStringBuffer(luastate, (uint8_t *)data->{{field.name}},
                strlen(data->{{field.name}}));
            lua_settable(luastate, -3);
{% elif field["type"] == "vstr4" %}
            lua_pushliteral(luastate, "{{field.name}}");
            LuaPushStringBuffer(luastate, (uint8_t *)data->{{field.name}},
                strlen(data->{{field.name}}));
            lua_settable(luastate, -3);
{% elif field.type == "bytearray" %}
            lua_pushliteral(luastate, "{{field.name}}");
            lua_pushlstring(luastate, (const char *)data->{{field.name}},
                data->{{field.len_field}});
            lua_settable(luastate, -3);
{% elif field.type == "bstr8" %}
{% for field in field.fields %}
            lua_pushliteral(luastate, "{{field.name}}");
            lua_pushinteger(luastate, data->{{field.name}});
            lua_settable(luastate, -3);
{% endfor %}
{% else %}
{{ raise("Unhandled datatype: %s" % (field.type)) }}
{% endif %}
{% endfor %}
            break;
        }
{% endfor %}
        default:
            break;
    }
}

"""

output_json_dnp3_objects_template = """/* Copyright (C) 2015 Open Information Security Foundation
 *
 * You can copy, redistribute or modify this Program under the terms of
 * the GNU General Public License version 2 as published by the Free
 * Software Foundation.
 *
 * This program 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
 * version 2 along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 * 02110-1301, USA.
 */

/**
 * DO NOT EDIT. THIS FILE IS AUTO-GENERATED.
 *
 * Generated by command:
 *    {{command_line}}
 */

#include "suricata-common.h"

#include "app-layer-dnp3.h"
#include "app-layer-dnp3-objects.h"
#include "output-json-dnp3-objects.h"
#include "output-json.h"

// clang-format off
void OutputJsonDNP3SetItem(JsonBuilder *js, DNP3Object *object,
    DNP3Point *point)
{

    switch (DNP3_OBJECT_CODE(object->group, object->variation)) {
{% for object in objects %}
        case DNP3_OBJECT_CODE({{object.group}}, {{object.variation}}): {
            DNP3ObjectG{{object.group}}V{{object.variation}} *data = point->data;
{% for field in object.fields %}
{% if is_integer_type(field.type) %}
            jb_set_uint(js, "{{field.name}}", data->{{field.name}});
{% elif field.type in ["flt32", "flt64"] %}
            jb_set_float(js, "{{field.name}}", data->{{field.name}});
{% elif field.type == "bytearray" %}
            jb_set_base64(js, "data->{{field.name}}", data->{{field.name}}, data->{{field.len_field}});
{% elif field.type == "vstr4" %}
            jb_set_string(js, "data->{{field.name}}", data->{{field.name}});
{% elif field.type == "chararray" %}
            if (data->{{field.len_field}} > 0) {
                jb_set_string_from_bytes(
                        js, "{{field.name}}", (const uint8_t *)data->{{field.name}}, data->{{field.len_field}});
            } else {
                jb_set_string(js, "{{field.name}}", "");
            }
{% elif field.type == "bstr8" %}
{% for field in field.fields %}
            jb_set_uint(js, "{{field.name}}", data->{{field.name}});
{% endfor %}
{% else %}
{{ raise("Unhandled datatype: %s" % (field.type)) }}
{% endif %}
{% endfor %}
            break;
        }
{% endfor %}
        default:
            SCLogDebug("Unknown object: %d:%d", object->group,
                object->variation);
            break;
    }

}
// clang-format on

"""

def has_freeable_types(fields):
    freeable_types = [
        "bytearray",
    ]
    for field in fields:
        if field["type"] in freeable_types:
            return True
    return False

def is_integer_type(datatype):
    integer_types = [
        "uint64",
        "uint32",
        "uint24",
        "uint16",
        "uint8",
        "int64",
        "int32",
        "int16",
        "int8",
        "dnp3time",
    ]
    return datatype in integer_types

def to_type(datatype):
    type_map = {
        "uint8": "uint8_t",
    }
    if datatype in type_map:
        return type_map[datatype]
    else:
        raise Exception("Unknown datatype: %s" % (datatype))

def generate(template, filename, context):
    print("Generating %s." % (filename))
    try:
        env = jinja2.Environment(trim_blocks=True)
        output = env.from_string(template).render(context)
        with open(filename, "w") as fileobj:
            fileobj.write(output)
    except Exception as err:
        print("Failed to generate %s: %s" % (filename, err))
        sys.exit(1)

def raise_helper(msg):
    raise Exception(msg)

def gen_object_structs(context):
    """ Generate structs for all the define DNP3 objects. """

    template = """

/* Code generated by:
 *     {{command_line}}
 */

{% for object in objects %}
typedef struct DNP3ObjectG{{object.group}}V{{object.variation}}_ {
{% for field in object.fields %}
{% if field.type == "bstr8" %}
{% for field in field.fields %}
    uint8_t {{field.name}}:{{field.width}};
{% endfor %}
{% else %}
{% if field.type == "int16" %}
    int16_t {{field.name}};
{% elif field.type == "int32" %}
    int32_t {{field.name}};
{% elif field.type == "uint8" %}
    uint8_t {{field.name}};
{% elif field.type == "uint16" %}
    uint16_t {{field.name}};
{% elif field.type == "uint24" %}
    uint32_t {{field.name}};
{% elif field.type == "uint32" %}
    uint32_t {{field.name}};
{% elif field.type == "uint64" %}
    uint64_t {{field.name}};
{% elif field.type == "flt32" %}
    float {{field.name}};
{% elif field.type == "flt64" %}
    double {{field.name}};
{% elif field.type == "dnp3time" %}
    uint64_t {{field.name}};
{% elif field.type == "bytearray" %}
    uint8_t *{{field.name}};
{% elif field.type == "vstr4" %}
    char {{field.name}}[5];
{% elif field.type == "chararray" %}
    char {{field.name}}[{{field.size}}];
{% else %}
    {{ raise("Unknown datatype type '%s' for object %d:%d" % (
           field.type, object.group, object.variation)) }}
{% endif %}
{% endif %}
{% endfor %}
{% if object.extra_fields %}
{% for field in object.extra_fields %}
{% if field.type == "uint8" %}
    uint8_t {{field.name}};
{% elif field.type == "uint16" %}
    uint16_t {{field.name}};
{% elif field.type == "uint32" %}
    uint32_t {{field.name}};
{% else %}
    {{ raise("Unknown datatype: %s" % (field.type)) }}
{% endif %}
{% endfor %}
{% endif %}
} DNP3ObjectG{{object.group}}V{{object.variation}};

{% endfor %}
"""

    filename = "src/app-layer-dnp3-objects.h"
    try:
        env = jinja2.Environment(trim_blocks=True)
        code = env.from_string(template).render(context)
        content = open(filename).read()
        content = re.sub(
            "(%s).*(%s)" % (re.escape(IN_PLACE_START), re.escape(IN_PLACE_END)),
            r"\1%s\2" % (code), content, 1, re.M | re.DOTALL)
        open(filename, "w").write(content)
        print("Updated %s." % (filename))
    except Exception as err:
        print("Failed to update %s: %s" % (filename, err), file=sys.stderr)
        sys.exit(1)

def gen_object_decoders(context):
    """ Generate decoders for all defined DNP3 objects. """

    template = """

/* Code generated by:
 *     {{command_line}}
 */

{% for object in objects %}
{% if object.packed %}
static int DNP3DecodeObjectG{{object.group}}V{{object.variation}}(const uint8_t **buf, uint32_t *len,
    uint8_t prefix_code, uint32_t start, uint32_t count,
    DNP3PointList *points)
{
    DNP3ObjectG{{object.group}}V{{object.variation}} *object = NULL;
    uint32_t bytes = (count / 8) + 1;
    uint32_t prefix = 0;
    uint32_t point_index = start;

    if (!DNP3ReadPrefix(buf, len, prefix_code, &prefix)) {
        goto error;
    }

    for (uint32_t i = 0; i < bytes; i++) {

        uint8_t octet;

        if (!DNP3ReadUint8(buf, len, &octet)) {
            goto error;
        }

        for (int j = 0; j < 8 && count; j = j + {{object.fields[0].width}}) {

            object = SCCalloc(1, sizeof(*object));
            if (unlikely(object == NULL)) {
                goto error;
            }

{% if object.fields[0].width == 1 %}
            object->{{object.fields[0].name}} = (octet >> j) & 0x1;
{% elif object.fields[0].width == 2 %}
            object->{{object.fields[0].name}} = (octet >> j) & 0x3;
{% else %}
#error "Unhandled field width: {{object.fields[0].width}}"
{% endif %}

            if (!DNP3AddPoint(points, object, point_index, prefix_code, prefix)) {
                goto error;
            }

            object = NULL;
            count--;
            point_index++;
        }

    }

    return 1;
error:
    if (object != NULL) {
        SCFree(object);
    }
    return 0;
}

{% else %}
static int DNP3DecodeObjectG{{object.group}}V{{object.variation}}(const uint8_t **buf, uint32_t *len,
    uint8_t prefix_code, uint32_t start, uint32_t count,
    DNP3PointList *points)
{
    DNP3ObjectG{{object.group}}V{{object.variation}} *object = NULL;
    uint32_t prefix = 0;
    uint32_t point_index = start;
{% if object._track_offset %}
    uint32_t offset;
{% endif %}
{% if object.constraints %}

{% for (key, val) in object.constraints.items() %}
{% if key == "require_size_prefix" %}
    if (!DNP3PrefixIsSize(prefix_code)) {
        goto error;
    }
{% elif key == "require_prefix_code" %}
    if (prefix_code != {{val}}) {
        goto error;
    }
{% else %}
{{ raise("Unhandled constraint: %s" % (key)) }}
{% endif %}
{% endfor %}
{% endif %}

    if (*len < count/8) {
        goto error;
    }
    while (count--) {

        object = SCCalloc(1, sizeof(*object));
        if (unlikely(object == NULL)) {
            goto error;
        }

        if (!DNP3ReadPrefix(buf, len, prefix_code, &prefix)) {
            goto error;
        }
{% if object._track_offset %}

        offset = *len;
{% endif %}

{% for field in object.fields %}
{% if field.type == "int16" %}
        if (!DNP3ReadUint16(buf, len, (uint16_t *)&object->{{field.name}})) {
            goto error;
        }
{% elif field.type == "int32" %}
        if (!DNP3ReadUint32(buf, len, (uint32_t *)&object->{{field.name}})) {
            goto error;
        }
{% elif field.type == "uint8" %}
        if (!DNP3ReadUint8(buf, len, &object->{{field.name}})) {
            goto error;
        }
{% elif field.type == "uint16" %}
        if (!DNP3ReadUint16(buf, len, &object->{{field.name}})) {
            goto error;
        }
{% elif field.type == "uint24" %}
        if (!DNP3ReadUint24(buf, len, &object->{{field.name}})) {
            goto error;
        }
{% elif field.type == "uint32" %}
        if (!DNP3ReadUint32(buf, len, &object->{{field.name}})) {
            goto error;
        }
{% elif field.type == "uint64" %}
        if (!DNP3ReadUint64(buf, len, &object->{{field.name}})) {
            goto error;
        }
{% elif field.type == "flt32" %}
        if (!DNP3ReadFloat32(buf, len, &object->{{field.name}})) {
            goto error;
        }
{% elif field.type == "flt64" %}
        if (!DNP3ReadFloat64(buf, len, &object->{{field.name}})) {
            goto error;
        }
{% elif field.type == "dnp3time" %}
        if (!DNP3ReadUint48(buf, len, &object->{{field.name}})) {
            goto error;
        }
{% elif field.type == "vstr4" %}
        if (*len < 4) {
            goto error;
        }
        memcpy(object->{{field.name}}, *buf, 4);
        object->{{field.name}}[4] = '\\\\0';
        *buf += 4;
        *len -= 4;
{% elif field.type == "bytearray" %}
{% if field.len_from_prefix %}
        if (prefix < (offset - *len)) {
            goto error;
        }
        object->{{field.len_field}} = (uint16_t) (prefix - (offset - *len));
{% endif %}
        if (object->{{field.len_field}} > 0) {
            if (*len < object->{{field.len_field}}) {
                /* Not enough data. */
                goto error;
            }
            object->{{field.name}} = SCCalloc(1, object->{{field.len_field}});
            if (unlikely(object->{{field.name}} == NULL)) {
                goto error;
            }
            memcpy(object->{{field.name}}, *buf, object->{{field.len_field}});
            *buf += object->{{field.len_field}};
            *len -= object->{{field.len_field}};
        }
{% elif field.type == "chararray" %}
{% if field.len_from_prefix %}
        if (prefix - (offset - *len) >= {{field.size}} || prefix < (offset - *len)) {
            goto error;
        }
{% if field.size == 255 %}
        object->{{field.len_field}} = (uint8_t) (prefix - (offset - *len));
{% else %}
        object->{{field.len_field}} = (uint16_t) (prefix - (offset - *len));
{% endif %}
{% endif %}
        if (object->{{field.len_field}} > 0) {
            if (*len < object->{{field.len_field}}) {
                /* Not enough data. */
                goto error;
            }
            memcpy(object->{{field.name}}, *buf, object->{{field.len_field}});
            *buf += object->{{field.len_field}};
            *len -= object->{{field.len_field}};
        }
        object->{{field.name}}[object->{{field.len_field}}] = '\\\\0';
{% elif field.type == "bstr8" %}
        {
            uint8_t octet;
            if (!DNP3ReadUint8(buf, len, &octet)) {
                goto error;
            }
{% set ns = namespace(shift=0) %}
{% for field in field.fields %}
{% if field.width == 1 %}
            object->{{field.name}} = (octet >> {{ns.shift}}) & 0x1;
{% elif field.width == 2 %}
            object->{{field.name}} = (octet >> {{ns.shift}}) & 0x3;
{% elif field.width == 4 %}
            object->{{field.name}} = (octet >> {{ns.shift}}) & 0xf;
{% elif field.width == 7 %}
            object->{{field.name}} = (octet >> {{ns.shift}}) & 0x7f;
{% else %}
{{ raise("Unhandled width of %d." % (field.width)) }}
{% endif %}
{% set ns.shift = ns.shift + field.width %}
{% endfor %}
        }
{% else %}
{{ raise("Unhandled datatype '%s' for object %d:%d." % (field.type,
       object.group, object.variation)) }}
{% endif %}
{% endfor %}

        if (!DNP3AddPoint(points, object, point_index, prefix_code, prefix)) {
            goto error;
        }

        object = NULL;
        point_index++;
    }

    return 1;
error:
    if (object != NULL) {
{% for field in object.fields %}
{% if field.type == "bytearray" %}
        if (object->{{field.name}} != NULL) {
            SCFree(object->{{field.name}});
        }
{% endif %}
{% endfor %}
        SCFree(object);
    }

    return 0;
}

{% endif %}
{% endfor %}

void DNP3FreeObjectPoint(int group, int variation, void *point)
{
    switch(DNP3_OBJECT_CODE(group, variation)) {
{% for object in objects %}
{% if f_has_freeable_types(object.fields) %}
        case DNP3_OBJECT_CODE({{object.group}}, {{object.variation}}): {
            DNP3ObjectG{{object.group}}V{{object.variation}} *object = (DNP3ObjectG{{object.group}}V{{object.variation}} *) point;
{% for field in object.fields %}
{% if field.type == "bytearray" %}
            if (object->{{field.name}} != NULL) {
                SCFree(object->{{field.name}});
            }
{% endif %}
{% endfor %}
            break;
        }
{% endif %}
{% endfor %}
        default:
            break;
    }
    SCFree(point);
}

/**
 * \\\\brief Decode a DNP3 object.
 *
 * \\\\retval 0 on success. On failure a positive integer corresponding
 *     to a DNP3 application layer event will be returned.
 */
int DNP3DecodeObject(int group, int variation, const uint8_t **buf,
    uint32_t *len, uint8_t prefix_code, uint32_t start,
    uint32_t count, DNP3PointList *points)
{
    int rc = 0;

    switch (DNP3_OBJECT_CODE(group, variation)) {
{% for object in objects %}
        case DNP3_OBJECT_CODE({{object.group}}, {{object.variation}}):
            rc = DNP3DecodeObjectG{{object.group}}V{{object.variation}}(buf, len, prefix_code, start, count,
                points);
            break;
{% endfor %}
        default:
            return DNP3_DECODER_EVENT_UNKNOWN_OBJECT;
    }

    return rc ? 0 : DNP3_DECODER_EVENT_MALFORMED;
}

"""

    try:
        filename = "src/app-layer-dnp3-objects.c"
        env = jinja2.Environment(trim_blocks=True, lstrip_blocks=True)
        code = env.from_string(template).render(context)
        content = open(filename).read()
        content = re.sub(
            "(%s).*(%s)" % (re.escape(IN_PLACE_START), re.escape(IN_PLACE_END)),
            r"\1%s\n\2" % (code), content, 1, re.M | re.DOTALL)
        open(filename, "w").write(content)
        print("Updated %s." % (filename))
    except Exception as err:
        print("Failed to update %s: %s" % (filename, err), file=sys.stderr)
        sys.exit(1)

def preprocess_object(obj):

    valid_keys = [
        "group",
        "variation",
        "constraints",
        "extra_fields",
        "fields",
        "packed",
    ]

    valid_field_keys = [
        "type",
        "name",
        "width",
        "len_from_prefix",
        "len_field",
        "fields",
        "size",
    ]

    if "unimplemented" in obj:
        print("Object not implemented: %s:%s: %s" % (
            str(obj["group"]), str(obj["variation"]), obj["unimplemented"]))
        return None

    for key, val in obj.items():

        if key not in valid_keys:
            print("Invalid key '%s' in object %d:%d" % (
                key, obj["group"], obj["variation"]), file=sys.stderr)
            sys.exit(1)

    for field in obj["fields"]:

        for key in field.keys():
            if key not in valid_field_keys:
                print("Invalid key '%s' in object %d:%d" % (
                    key, obj["group"], obj["variation"]), file=sys.stderr)
                sys.exit(1)

        if "len_from_prefix" in field and field["len_from_prefix"]:
            obj["_track_offset"] = True
            break

        if field["type"] == "bstr8":
            width = 0
            for subfield in field["fields"]:
                width += int(subfield["width"])
            assert(width == 8)

    return obj

def main():

    # Require Jinja2 2.10 or greater.
    jv = jinja2.__version__.split(".")
    if int(jv[0]) < 2 or (int(jv[0]) == 2 and int(jv[1]) < 10):
        print("error: jinja2 v2.10 or great required")
        return 1

    definitions = yaml.load(open("scripts/dnp3-gen/dnp3-objects.yaml"))
    print("Loaded %s objects." % (len(definitions["objects"])))
    definitions["objects"] = map(preprocess_object, definitions["objects"])

    # Filter out unimplemented objects.
    definitions["objects"] = [
        obj for obj in definitions["objects"] if obj != None]

    context = {
        "raise": raise_helper,
        "objects": definitions["objects"],
        "is_integer_type": is_integer_type,
        "f_to_type": to_type,
        "f_has_freeable_types": has_freeable_types,
        "command_line": " ".join(sys.argv),
    }

    gen_object_structs(context)
    gen_object_decoders(context)
    generate(util_lua_dnp3_objects_c_template,
             "src/util-lua-dnp3-objects.c",
             context)
    generate(output_json_dnp3_objects_template,
             "src/output-json-dnp3-objects.c",
             context)

if __name__ == "__main__":
    sys.exit(main())
