/*
 * Copyright (c) 2014-2018, Arm Limited and affiliates.
 * SPDX-License-Identifier: Apache-2.0
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
#include "nsconfig.h"
#include "ns_trace.h"
#include "common_functions.h"
#include "nsdynmemLIB.h"
#include <string.h>
#include "NWK_INTERFACE/Include/protocol.h"
#include "6LoWPAN/IPHC_Decode/cipv6.h"
#include "Common_Protocols/ipv6_constants.h"
#include "6LoWPAN/IPHC_Decode/iphc_decompress.h"

#define TRACE_GROUP "iphc"

/* Analyse a 6LoWPAN datagram for fragmentation, checking header length */
/* Critical fact is that fragments are described with their size+offsets in */
/* terms of the UNCOMPRESSED IP datagram, so when presented with a 6LoWPAN datagram, */
/* we need to work backwards. Another constraint is that compressed headers */
/* must all lie within the first fragment - doesn't concern us here. */
/* Input: a potentially oversized 6LoWPAN-format datagram without fragmentation */
/* Return: size of compressed IPHC headers */
/* Output: uncompressed_size = uncompressed size of IPHC headers */
uint16_t iphc_header_scan(buffer_t *buf, uint16_t *uncompressed_size)
{
    const uint8_t *ptr = buffer_data_pointer(buf);
    const uint8_t *end = buffer_data_end(buf);
    uint16_t uncomp_len = 0;
    const uint8_t *ip_hc;

    *uncompressed_size = 0;

    /* Fragmentation needs us to handle uncompressed case */
    if (ptr < end && ptr[0] == LOWPAN_DISPATCH_IPV6) {
        return 1;
    }

    /* Handle compressed IP header (LOWPAN_IPHC) - LOWPAN_HC1 etc not handled */
IPv6START:
    ip_hc = ptr;
    ptr += 2;
    uncomp_len += 40;

    if (ptr > end) {
        goto truncated;
    }

    if ((ip_hc[0] & LOWPAN_DISPATCH_IPHC_MASK) != LOWPAN_DISPATCH_IPHC) {
        tr_warn("Unexpected 6LoWPAN ID");
        return 0;
    }

    if (ip_hc[1] & HC_CIDE_COMP) {
        ptr++;
    }

    switch (ip_hc[0] & HC_TF_MASK) {
        case HC_TF_ECN_DSCP_FLOW_LABEL:
            ptr += 4;
            break;
        case HC_TF_ECN_FLOW_LABEL:
            ptr += 3;
            break;
        case HC_TF_ECN_DSCP:
            ptr += 1;
            break;
        default:
            break;
    }

    if (!(ip_hc[0] & HC_NEXT_HEADER_MASK)) {
        ptr++;
    }

    if ((ip_hc[0] & HC_HOP_LIMIT_MASK) == HC_HOP_LIMIT_CARRIED_IN_LINE) {
        ptr++;
    }

    switch (ip_hc[1] & HC_SRC_ADR_MODE_MASK) {
        case HC_SRC_ADR_128_BIT:
            if (!(ip_hc[1] & HC_SRCADR_COMP)) {
                ptr += 16;
            }
            break;
        case HC_SRC_ADR_64_BIT:
            ptr += 8;
            break;
        case HC_SRC_ADR_16_BIT:
            ptr += 2;
            break;
        case HC_SRC_ADR_FROM_MAC:
            break;
    }

    if (ip_hc[1] & HC_MULTICAST_COMP) {
        switch (ip_hc[1] & (HC_DSTADR_COMP | HC_DST_ADR_MODE_MASK)) {
            case HC_128BIT_MULTICAST:
                ptr += 16;
                break;
            case HC_48BIT_MULTICAST:
            case HC_48BIT_CONTEXT_MULTICAST:
                ptr += 6;
                break;
            case HC_32BIT_MULTICAST:
                ptr += 4;
                break;
            case HC_8BIT_MULTICAST:
                ptr += 1;
                break;
            default:
                tr_warn("Unknown multicast compression");
                return 0;
        }
    } else {
        switch (ip_hc[1] & HC_DST_ADR_MODE_MASK) {
            case HC_DST_ADR_128_BIT:
                ptr += 16;
                break;
            case HC_DST_ADR_64_BIT:
                ptr += 8;
                break;
            case HC_DST_ADR_16_BIT:
                ptr += 2;
                break;
            case HC_DST_ADR_FROM_MAC:
                break;
        }
    }

    /* Handle following headers (LOWPAN_NHC) */
    bool nhc_next_header = ip_hc[0] & HC_NEXT_HEADER_MASK;
    while (nhc_next_header) {
        uint8_t nhc_id;
        if (ptr >= end) {
            goto truncated;
        }
        nhc_id = *ptr++;
        //tr_debug("IPNH: %02x", next_header);
        if ((nhc_id & NHC_EXT_HEADER_MASK) == NHC_EXT_HEADER) {
            uint8_t len = 0;
            switch (nhc_id & NHC_EXT_ID_MASK) {
                case NHC_EXT_IPV6:
                    goto IPv6START;
                case NHC_EXT_HOP_BY_HOP:
                case NHC_EXT_ROUTING:
                case NHC_EXT_FRAG:
                case NHC_EXT_DEST_OPT:
                case NHC_EXT_MOBILITY:
                    nhc_next_header = nhc_id & NHC_EXT_NH;
                    if (!nhc_next_header) {
                        ptr++;
                    }
                    if (ptr >= end) {
                        goto truncated;
                    }
                    if ((nhc_id & NHC_EXT_ID_MASK) == NHC_EXT_FRAG) {
                        /* See notes in main decompress routine */
                        ptr += 7;
                        uncomp_len += 8;
                    } else {
                        len = *ptr++;
                        /* len is length of following data in bytes */
                        ptr += len;
                        /* Uncompressed headers must be multiple of 8 octets, so
                         * uncompressed length is 2+len ("NH" + "len" + data),
                         * rounded up to a multiple of 8.
                         */
                        uncomp_len += ((2 + len) + 7) & ~ 7;
                    }
                    break;
                default:
                    goto bad_nhc_id;
            }
        } else if ((nhc_id & NHC_UDP_MASK) == NHC_UDP) {
            uncomp_len += 8;
            nhc_next_header = false;
            if (!(nhc_id & NHC_UDP_CKSUM_COMPRESS)) {
                ptr += 2;
            }
            switch (nhc_id & NHC_UDP_PORT_COMPRESS_MASK) {
                case NHC_UDP_PORT_COMPRESS_DST:
                case NHC_UDP_PORT_COMPRESS_SRC:
                    ptr += 3;
                    break;
                case NHC_UDP_PORT_COMPRESS_NONE:
                    ptr += 4;
                    break;
                case NHC_UDP_PORT_COMPRESS_BOTH:
                default:
                    ptr += 1;
                    break;
            }
        } else {
bad_nhc_id:
            tr_warn("Unknown NHC ID: %02x", nhc_id);
            nhc_next_header = false;
        }
    }

    if (ptr > end) {
truncated:
        tr_warn("Truncated packet");
        return 0;
    }

    *uncompressed_size = uncomp_len;
    return ptr - buffer_data_pointer(buf);
}

static bool decompress_mc_addr(const lowpan_context_list_t *context_list, uint8_t *addr, const uint8_t **in_ptr, const uint8_t *outer_iid, uint8_t context, uint8_t mode)
{
    const uint8_t *in = *in_ptr;
    (void) outer_iid;
    switch (mode) {
        case HC_128BIT_MULTICAST:
            memcpy(addr, in, 16);
            *in_ptr = in + 16;
            return true;
        case HC_48BIT_MULTICAST:
            addr[0] = 0xff;
            addr[1] = *in++;
            memset(&addr[2], 0, 9);
            memcpy(&addr[11], in, 5);
            *in_ptr = in + 5;
            return true;
        case HC_32BIT_MULTICAST:
            addr[0] = 0xff;
            addr[1] = *in++;
            memset(&addr[2], 0, 11);
            memcpy(&addr[13], in, 3);
            *in_ptr = in + 3;
            return true;
        case HC_8BIT_MULTICAST:
            memcpy(addr, ADDR_LINK_LOCAL_ALL_NODES, 15);
            addr[15] = *in++;
            *in_ptr = in;
            return true;
        case HC_48BIT_CONTEXT_MULTICAST: {
            lowpan_context_t *ctx = lowpan_context_get_by_id(context_list, context);
            if (!ctx) {
                return false;
            }
            addr[0] = 0xff;
            addr[1] = *in++;
            addr[2] = *in++;
            addr[3] = ctx->length;
            memcpy(&addr[4], ctx->prefix, 8);
            memcpy(&addr[12], in, 4);
            *in_ptr = in + 4;
            return true;
        }
        default:
            return false;
    }
}

static bool decompress_addr(const lowpan_context_list_t *context_list, uint8_t *addr, const uint8_t **in_ptr, bool is_dst, const uint8_t *outer_iid, uint8_t context, uint8_t mode)
{
    if (!is_dst) {
        /* Get SRC bits and move into DST position, without multicast bit */
        mode = (mode >> 4) & (HC_DSTADR_COMP | HC_DST_ADR_MODE_MASK);
        context >>= 4;
    } else {
        mode &= (HC_MULTICAST_COMP | HC_DSTADR_COMP | HC_DST_ADR_MODE_MASK);
        context &= 0xf;
    }

    if (mode & HC_MULTICAST_COMP) {
        return decompress_mc_addr(context_list, addr, in_ptr, outer_iid, context, mode & ~ HC_MULTICAST_COMP);
    }

    switch (mode & HC_DST_ADR_MODE_MASK) {
        case HC_DST_ADR_128_BIT:
            if (mode & HC_DSTADR_COMP) {
                /* Special case - different for src and dst */
                if (is_dst) {
                    return false;
                } else {
                    memset(addr, 0, 16); // unspecified (::)
                    return true;
                }
            }
            memcpy(addr, *in_ptr, 16);
            *in_ptr += 16;
            return true;
        case HC_DST_ADR_64_BIT:
            memcpy(addr + 8, *in_ptr, 8);
            *in_ptr += 8;
            break;
        case HC_DST_ADR_16_BIT:
            memcpy(addr + 8, ADDR_SHORT_ADR_SUFFIC, 6);
            addr[14] = (*in_ptr)[0];
            addr[15] = (*in_ptr)[1];
            *in_ptr += 2;
            break;
        case HC_DST_ADR_FROM_MAC:
            memcpy(addr + 8, outer_iid, 8);
            break;
    }

    if (mode & HC_DSTADR_COMP) {
        lowpan_context_t *ctx = lowpan_context_get_by_id(context_list, context);
        if (!ctx) {
            return false;
        }
        /* Copy a minimum of 64 bits to get required zero fill up to IID -
         * we rely on the context storage core having zero-padding in
         * the prefix field for short contexts.
         */
        bitcopy(addr, ctx->prefix, ctx->length < 64 ? 64 : ctx->length);
        return true;
    } else {
        memcpy(addr, ADDR_LINK_LOCAL_PREFIX, 8);
        return true;
    }
}

typedef struct iphc_decompress_state {
    const lowpan_context_list_t *const context_list;
    const uint8_t *in;
    const uint8_t *const end;
    uint8_t *out;
    uint8_t *nh_ptr;
    const uint8_t *outer_src_iid;
    const uint8_t *outer_dst_iid;
} iphc_decompress_state_t;

static bool decompress_ipv6(iphc_decompress_state_t *restrict ds)
{
    const uint8_t *iphc = ds->in;
    ds->in += 2;

    uint8_t cid;

    if (iphc[1] & HC_CIDE_COMP) {
        cid = *ds->in++;
    } else {
        cid = 0;
    }

    /* First, Traffic Class and Flow Label */
    uint8_t tc = 0;
    uint8_t tf = iphc[0] & HC_TF_MASK;

    /* Extract ECN */
    if (tf != HC_TF_ELIDED) {
        tc = *ds->in >> 6;
    }

    /* Extract DSCP */
    if (tf == HC_TF_ECN_DSCP || tf == HC_TF_ECN_DSCP_FLOW_LABEL) {
        tc |= *ds->in++ << 2;
    }

    *ds->out++ = 0x60 | (tc >> 4);

    if (tf == HC_TF_ECN_FLOW_LABEL || tf == HC_TF_ECN_DSCP_FLOW_LABEL) {
        *ds->out++ = (tc << 4) | (*ds->in++ & 0x0f);
        *ds->out++ = *ds->in++;
        *ds->out++ = *ds->in++;
    } else {
        *ds->out++ = tc << 4;
        *ds->out++ = 0;
        *ds->out++ = 0;
    }

    /* Compute payload length */
    ds->out = common_write_16_bit(ds->end - (ds->out + 36), ds->out);

    /* Next Header */
    if (iphc[0] & HC_NEXT_HEADER_MASK) {
        /* Reserve space for Next Header - will be filled later */
        ds->nh_ptr = ds->out;
        *ds->out++ = IPV6_NH_NONE;
    } else {
        ds->nh_ptr = NULL;
        *ds->out++ = *ds->in++;
    }

    /* Hop Limit */
    switch (iphc[0] & HC_HOP_LIMIT_MASK) {
        case HC_HOP_LIMIT_1:
            *ds->out++ = 1;
            break;
        case HC_HOP_LIMIT_64:
            *ds->out++ = 64;
            break;
        case HC_HOP_LIMIT_255:
            *ds->out++ = 255;
            break;
        case HC_HOP_LIMIT_CARRIED_IN_LINE:
        default:
            *ds->out++ = *ds->in++;
            break;
    }

    if (!decompress_addr(ds->context_list, ds->out, &ds->in, false, ds->outer_src_iid, cid, iphc[1])) {
        tr_warn("SRC Address decompress fail");
        return false;
    }
    ds->outer_src_iid = ds->out + 8;
    ds->out += 16;
    if (!decompress_addr(ds->context_list, ds->out, &ds->in, true, ds->outer_dst_iid, cid, iphc[1])) {
        tr_warn("DST Address decompress fail");
        return false;
    }
    ds->outer_dst_iid = ds->out + 8;
    ds->out += 16;

    return true;
}

static bool decompress_exthdr(iphc_decompress_state_t *ds)
{
    uint8_t nh;

    switch (*ds->in & NHC_EXT_ID_MASK) {
        case NHC_EXT_HOP_BY_HOP:
            nh = IPV6_NH_HOP_BY_HOP;
            break;
        case NHC_EXT_ROUTING:
            nh = IPV6_NH_ROUTING;
            break;
        case NHC_EXT_FRAG:
            nh = IPV6_NH_FRAGMENT;
            break;
        case NHC_EXT_DEST_OPT:
            nh = IPV6_NH_DEST_OPT;
            break;
        case NHC_EXT_MOBILITY:
            nh = IPV6_NH_MOBILITY;
            break;
        case NHC_EXT_IPV6:
            *ds->nh_ptr = IPV6_NH_IPV6;
            ds->in++;
            return decompress_ipv6(ds);
        default:
            return false;
    }

    *ds->nh_ptr = nh;

    if (*ds->in++ & NHC_EXT_NH) {
        /* Reserve space for Next Header - will be filled later */
        ds->nh_ptr = ds->out;
        *ds->out++ = IPV6_NH_NONE;
    } else {
        ds->nh_ptr = NULL;
        *ds->out++ = *ds->in++;
    }

    uint8_t clen;
    if (nh == IPV6_NH_FRAGMENT) {
        /* Fragmentation header is awkward, and RFC 6282 isn't terribly clear */
        /* Second byte is reserved. It isn't a length field. */
        *ds->out++ = *ds->in++;
        clen = 6;
    } else {
        clen = *ds->in++;                   /* Compressed data len */
        *ds->out++ = (clen + 2 - 1) >> 3;   /* Uncompressed header length byte (8-octet units, excluding first) */
    }

    /* Copy main option data */
    memcpy(ds->out, ds->in, clen);
    ds->out += clen;
    ds->in += clen;

    /* If not aligned, add a PAD1 or PADN */
    if ((clen + 2) & 7) {
        uint8_t pad = 8 - ((clen + 2) & 7);
        if (pad == 1) {
            *ds->out++ = IPV6_OPTION_PAD1;
        } else {
            *ds->out++ = IPV6_OPTION_PADN;
            *ds->out++ = (pad -= 2);
            while (pad) {
                *ds->out++ = 0, pad--;
            }
        }
    }

    return true;
}

static bool decompress_udp(iphc_decompress_state_t *ds)
{
    uint8_t nhc = *ds->in++;

    /* Ports */
    if ((nhc & NHC_UDP_PORT_COMPRESS_MASK) == NHC_UDP_PORT_COMPRESS_BOTH) {
        *ds->out++ = 0xf0;
        *ds->out++ = 0xb0 | (*ds->in >> 4);
        *ds->out++ = 0xf0;
        *ds->out++ = 0xb0 | (*ds->in++ & 0x0f);
    } else {
        *ds->out++ = (nhc & NHC_UDP_PORT_COMPRESS_MASK) == NHC_UDP_PORT_COMPRESS_SRC ? 0xf0 : *ds->in++;
        *ds->out++ = *ds->in++;
        *ds->out++ = (nhc & NHC_UDP_PORT_COMPRESS_MASK) == NHC_UDP_PORT_COMPRESS_DST ? 0xf0 : *ds->in++;
        *ds->out++ = *ds->in++;
    }

    /* Length */
    ds->out = common_write_16_bit(ds->end - (ds->out - 4), ds->out);

    /* Don't currently allow checksum compression */
    if (nhc & NHC_UDP_CKSUM_COMPRESS) {
        return false;
    }
    *ds->out++ = *ds->in++;
    *ds->out++ = *ds->in++;

    *ds->nh_ptr = IPV6_NH_UDP;
    ds->nh_ptr = NULL;

    return true;
}

/* Input: A 6LoWPAN frame, starting with an IPHC header, with outer layer 802.15.4 MAC addresses in src+dst */
/* Output:  An IPv6 frame */
buffer_t *iphc_decompress(const lowpan_context_list_t *context_list, buffer_t *buf)
{
    uint8_t src_iid[8], dst_iid[8];
    uint8_t *iphc = NULL;

    /* Pre-scan to get compressed and uncompressed header size */
    uint16_t ip_size;
    uint16_t hc_size = iphc_header_scan(buf, &ip_size);
    if (hc_size == 0) {
        tr_warn("IPHC size 0");
        goto decomp_error;
    }

    /* Copy compressed header into temporary buffer */
    iphc = ns_dyn_mem_temporary_alloc(hc_size);
    if (!iphc) {
        tr_warn("IPHC header alloc fail %d", hc_size);
        goto decomp_error;
    }
    memcpy(iphc, buffer_data_pointer(buf), hc_size);

    /* Reserve buffer room for the uncompressed header */
    buffer_data_strip_header(buf, hc_size);
    buf = buffer_headroom(buf, ip_size);
    if (!buf) {
        tr_warn("IPHC headroom get fail %d", ip_size);
        goto decomp_error;
    }
    buffer_data_reserve_header(buf, ip_size);

    if (!addr_iid_from_outer(src_iid, &buf->src_sa) || !addr_iid_from_outer(dst_iid, &buf->dst_sa)) {
        tr_warn("Bad outer addr");
        goto decomp_error;
    }

    {
        iphc_decompress_state_t ds = {
            .context_list = context_list,
            .in = iphc,
            .nh_ptr = NULL,
            .out = buffer_data_pointer(buf),
            .end = buffer_data_end(buf),
            .outer_src_iid = src_iid,
            .outer_dst_iid = dst_iid,
        };

        /* Always start with the IP header */
        if (!decompress_ipv6(&ds)) {
            tr_warn("IPV6 decompres fail");
            goto decomp_error;
        }

        /* After the first IP header, we switch on the NHC byte */
        /* Know we're finished when there's no NH byte waiting to be filled */
        while (ds.nh_ptr) {
            bool ok = false;
            if ((ds.in[0] & NHC_UDP_MASK) == NHC_UDP) {
                ok = decompress_udp(&ds);
            } else if ((ds.in[0] & NHC_EXT_HEADER_MASK) == NHC_EXT_HEADER) {
                ok = decompress_exthdr(&ds);
            }
            if (!ok) {
                tr_warn("Unknow NH");
                goto decomp_error;
            }
        }

        if (ds.out != buffer_data_pointer(buf) + ip_size) {
            tr_err("IPHC decompression bug");
            goto decomp_error;
        }
    }

    ns_dyn_mem_free(iphc);
    return buf;

decomp_error:
    tr_warn("IPHC decompression error");
    ns_dyn_mem_free(iphc);
    return buffer_free(buf);
}
