/* packet-ifcp.c
 * Routines for iFCP dissection
 * RFC 3821, RFC 3643
 *
 * Copyright 2005   Aboo Valappil     (valappil_aboo@emc.com)
 *           2006 ronnie sahlberg   major refactoring
 *
 *
 * Significantly based on packet-fcip.c by
 *       Copyright 2001, Dinesh G Dutt (ddutt@cisco.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/tfs.h>
#include <wsutil/array.h>

#include "packet-tcp.h"
#include "packet-fc.h"

void proto_register_ifcp(void);
void proto_reg_handoff_ifcp(void);

#define iFCP_ENCAP_HEADER_LEN     28
#define iFCP_MIN_HEADER_LEN       16 /* up to frame len field */

typedef enum {
    iFCP_EOFn    = 0x41,
    iFCP_EOFt    = 0x42,
    iFCP_EOFrt   = 0x44,
    iFCP_EOFdt   = 0x46,
    iFCP_EOFni   = 0x49,
    iFCP_EOFdti  = 0x4E,
    iFCP_EOFrti  = 0x4F,
    iFCP_EOFa    = 0x50
} ifcp_eof_t;

typedef enum {
    iFCP_SOFf    = 0x28,
    iFCP_SOFi4   = 0x29,
    iFCP_SOFi2   = 0x2D,
    iFCP_SOFi3   = 0x2E,
    iFCP_SOFn4   = 0x31,
    iFCP_SOFn2   = 0x35,
    iFCP_SOFn3   = 0x36,
    iFCP_SOFc4   = 0x39
} ifcp_sof_t;

typedef enum {
    FCENCAP_PROTO_FCIP = 1,
    FCENCAP_PROTO_iFCP = 2
} fcencap_proto_t;

static const value_string ifcp_eof_vals[] = {
    {iFCP_EOFn,   "EOFn" },
    {iFCP_EOFt,   "EOFt" },
    {iFCP_EOFrt,  "EOFrt" },
    {iFCP_EOFdt,  "EOFdt" },
    {iFCP_EOFni,  "EOFni" },
    {iFCP_EOFdti, "EOFdti" },
    {iFCP_EOFrti, "EOFrti" },
    {iFCP_EOFa,   "EOFa" },
    {0, NULL},
};

static const value_string ifcp_sof_vals[] = {
    {iFCP_SOFf,  "SOFf" },
    {iFCP_SOFi4, "SOFi4" },
    {iFCP_SOFi2, "SOFi2" },
    {iFCP_SOFi3, "SOFi3" },
    {iFCP_SOFn4, "SOFn4" },
    {iFCP_SOFn2, "SOFn2" },
    {iFCP_SOFn3, "SOFn3" },
    {iFCP_SOFc4, "SOFc4" },
    {0, NULL},
};

static const value_string fcencap_proto_vals[] = {
    {FCENCAP_PROTO_FCIP, "FCIP"},
    {FCENCAP_PROTO_iFCP, "iFCP"},
    {0, NULL},
};

/* RFC 4172 section 5.3.1 shows a chart of the iFCP encapsulated Header Format.
 * It says that bytes 4-7 MUST be zeros.  In reality most vendors are putting
 * some information in these 4 bytes, particularly Nishon.
 */
static const uint8_t ifcp_header_4_bytes[4] = {
    0x02, 0x01, 0xFD, 0xFE
};

static int proto_ifcp;

static int hf_ifcp_protocol;
static int hf_ifcp_protocol_c;
static int hf_ifcp_version;
static int hf_ifcp_version_c;
static int hf_ifcp_encap_flags_c;
static int hf_ifcp_framelen;
static int hf_ifcp_framelen_c;
static int hf_ifcp_tsec;
static int hf_ifcp_tusec;
static int hf_ifcp_encap_crc;
static int hf_ifcp_sof;
static int hf_ifcp_sof_c;
static int hf_ifcp_eof;
static int hf_ifcp_eof_c;
static int hf_ifcp_ls_command_acc;
static int hf_ifcp_flags;
static int hf_ifcp_flags_ses;
static int hf_ifcp_flags_trp;
static int hf_ifcp_flags_spc;
static int hf_ifcp_common_flags;
static int hf_ifcp_common_flags_crcv;

static int ett_ifcp;
static int ett_ifcp_sof;
static int ett_ifcp_eof;
static int ett_ifcp_flags;
static int ett_ifcp_common_flags;
static int ett_ifcp_protocol;
static int ett_ifcp_version;
static int ett_ifcp_frame_len;

static bool ifcp_desegment    = true;

static dissector_handle_t ifcp_handle;
static dissector_handle_t fc_handle;


/* This function checks the first 16 bytes of the "header" that it looks sane
 * and returns true if this looks like iFCP and false if it doesn't.
 */
static bool
ifcp_header_test(tvbuff_t *tvb, int offset)
{
    uint16_t flen, flen1;

    /* we can only do this test if we have 16 bytes or more */
    if(tvb_captured_length_remaining(tvb, offset)<iFCP_MIN_HEADER_LEN){
        return false;
    }

    /*
    * As per the iFCP standard, the following tests must PASS:
    * 1)  Frame Length field validation -- 15 < Frame Length < 545;
    * 2)  Comparison of Frame Length field to its ones complement; and
    * 3)  A valid EOF is found in the word preceding the start of the next
    *     iFCP header as indicated by the Frame Length field, to be tested
    *     as follows:
    *     1)  Bits 24-31 and 16-23 contain identical legal EOF values (the
    *         list of legal EOF values is in the FC Frame Encapsulation
    *         [21]); and
    *     2)  Bits 8-15 and 0-7 contain the ones complement of the EOF
    *         value found in bits 24-31.
    *
    * As per the iFCP standard, in addition, at least 3 of the following
    * set of tests must be performed to identify that we've located the
    * start of an iFCP frame.
    * a)  Protocol# ones complement field (1 test);
    * b)  Version ones complement field (1 test);
    * c)  Replication of encapsulation word 0 in word 1 (1 test);
    * d)  Reserved field and its ones complement (2 tests);
    * e)  Flags field and its ones complement (2 tests);
    *    f)  CRC field is equal to zero (1 test); (DON'T DO THIS TEST!)
    * g)  SOF fields and ones complement fields (4 tests);
    * h)  Format and values of FC header (1 test);
    * i)  CRC of FC Frame (2 tests);
    * j)  FC Frame Encapsulation header information in the next iFCP Frame
    *     (1 test).
    *
    * At least 3 of the 16 tests listed above SHALL be performed. Failure
    * of any of the above tests actually performed SHALL indicate an
    * encapsulation error and the FC Frame SHALL NOT be forwarded on to
    * the FC Entity.
    */


    /*
     * Tests a, b and c
     */
    if(tvb_memeql(tvb, offset, ifcp_header_4_bytes, 4) != 0){
        return false;
        }

    /* check the frame length */
    flen=tvb_get_ntohs(tvb, offset+12)&0x03FF;
    if((flen < 15) || (flen > 545)){
        return false;
    }

    /* check the complement of the frame length */
    flen1=tvb_get_ntohs(tvb, offset+14)&0x03FF;
    if(flen!=((~flen1)&0x03FF)){
        return false;
    }


    /* this should be good enough for our heuristics */
    return true;
}


#define IFCP_FLAGS_SES      0x04
#define IFCP_FLAGS_TRP      0x02
#define IFCP_FLAGS_SPC      0x01

static const true_false_string ifcp_flags_ses_tfs = {
    "This is a SESSION CONTROL FRAME",
    "This is a normal frame"
};

static const true_false_string ifcp_flags_trp_tfs = {
    "Address TRANSPARENT Mode Enabled",
    "Address TRANSLATION Mode Enabled"
};

static const true_false_string ifcp_flags_spc_tfs = {
    "This frame requires SPECIAL PROCESSING",
    "This is a normal frame"
};

static int
dissect_ifcpflags(tvbuff_t *tvb, int offset, proto_tree *parent_tree)
{
    static int * const flags[] = {
        &hf_ifcp_flags_ses,
        &hf_ifcp_flags_trp,
        &hf_ifcp_flags_spc,
        NULL
    };
    proto_tree_add_bitmask(parent_tree, tvb, offset, hf_ifcp_flags,
                           ett_ifcp_flags, flags, ENC_BIG_ENDIAN);

    offset++;
    return offset;
}


#define IFCP_COMMON_FLAGS_CRCV      0x04

static void
dissect_commonflags(tvbuff_t *tvb, int offset, proto_tree *parent_tree)
{
    static int * const flags[] = {
        &hf_ifcp_common_flags_crcv,
        NULL
    };

    proto_tree_add_bitmask(parent_tree, tvb, offset, hf_ifcp_common_flags,
                           ett_ifcp_common_flags, flags, ENC_BIG_ENDIAN);
}

static int
dissect_ifcp_pdu(tvbuff_t *tvb, packet_info *pinfo, proto_tree *parent_tree, void* data _U_)
{
    int         offset = 0, frame_len = 0;
    uint8_t     sof    = 0, eof = 0;
    proto_item *ti;
    proto_tree *tree           = NULL;
    tvbuff_t   *next_tvb;
    uint8_t     protocol;
    proto_tree *protocol_tree  = NULL;
    proto_tree *version_tree   = NULL;
    proto_tree *frame_len_tree = NULL;
    proto_tree *sof_tree       = NULL;
    proto_tree *eof_tree       = NULL;
    fc_data_t   fc_data;

    /* verify we have a full header  (do we need to do this? */
    if(tvb_captured_length(tvb)<iFCP_ENCAP_HEADER_LEN){
        return 0;
    }

    col_set_str(pinfo->cinfo, COL_PROTOCOL, "iFCP");

    frame_len = (tvb_get_ntohs (tvb, offset+12) & 0x03FF)*4;


        if (parent_tree) {
            if (tvb_bytes_exist (tvb, offset, frame_len-4)) {
                sof = tvb_get_uint8 (tvb, offset+iFCP_ENCAP_HEADER_LEN);
                eof = tvb_get_uint8 (tvb, offset+frame_len - 4);

                ti = proto_tree_add_protocol_format (parent_tree, proto_ifcp, tvb, offset,
                                                     iFCP_ENCAP_HEADER_LEN,
                                                     "iFCP (%s/%s)",
                                                     val_to_str(pinfo->pool, sof, ifcp_sof_vals,
                                                                 "0x%x"),
                                                     val_to_str(pinfo->pool, eof, ifcp_eof_vals,
                                                                 "0x%x"));
            } else {
                sof = tvb_get_uint8 (tvb, offset+iFCP_ENCAP_HEADER_LEN);

                ti = proto_tree_add_protocol_format (parent_tree, proto_ifcp, tvb, offset,
                                                     iFCP_ENCAP_HEADER_LEN,
                                                     "iFCP (%s/%s)",
                                                     val_to_str(pinfo->pool, sof, ifcp_sof_vals,
                                                                 "0x%x"),
                                                     "NA");
            }
            tree = proto_item_add_subtree (ti, ett_ifcp);
        }



    /* The Common FC Encap header */
    /* protocol */
    ti=proto_tree_add_item_ret_uint8(tree, hf_ifcp_protocol, tvb, offset, 1, ENC_BIG_ENDIAN, &protocol);
    protocol_tree=proto_item_add_subtree(ti, ett_ifcp_protocol);

    offset++;

    /* version */
    ti=proto_tree_add_item(tree, hf_ifcp_version, tvb, offset, 1, ENC_BIG_ENDIAN);
    version_tree=proto_item_add_subtree(ti, ett_ifcp_version);
    offset++;

    /* protocol complement */
    proto_tree_add_item(protocol_tree, hf_ifcp_protocol_c, tvb, offset, 1, ENC_BIG_ENDIAN);
    offset++;

    /* version complement */
    proto_tree_add_item(version_tree, hf_ifcp_version_c, tvb, offset, 1, ENC_BIG_ENDIAN);
    offset++;

    /* 4 reserved bytes */
    offset+=4;

    /* iFCP specific fields */
    if(protocol==FCENCAP_PROTO_iFCP){
        /* LS_COMMAND_ACC */
        proto_tree_add_item(tree, hf_ifcp_ls_command_acc, tvb, offset, 1, ENC_BIG_ENDIAN);
        offset++;

        /* iFCP Flags */
        offset=dissect_ifcpflags(tvb, offset, tree);

        /* SOF */
        ti=proto_tree_add_item(tree, hf_ifcp_sof, tvb, offset, 1, ENC_BIG_ENDIAN);
        sof_tree=proto_item_add_subtree(ti, ett_ifcp_sof);
        offset++;

        /* EOF */
        ti=proto_tree_add_item(tree, hf_ifcp_eof, tvb, offset, 1, ENC_BIG_ENDIAN);
        eof_tree=proto_item_add_subtree(ti, ett_ifcp_eof);
        offset++;
    } else {
        offset+=4;
        sof_tree=tree; /* better than nothing */
        eof_tree=tree;
    }

    /* Common Flags */
    dissect_commonflags(tvb, offset, tree);

    /* frame len */
    ti=proto_tree_add_item(tree, hf_ifcp_framelen, tvb, offset, 2, ENC_BIG_ENDIAN);
    frame_len_tree=proto_item_add_subtree(ti, ett_ifcp_frame_len);
    offset+=2;

    /* complement of flags and frame len */
    proto_tree_add_item(frame_len_tree, hf_ifcp_encap_flags_c, tvb, offset, 1, ENC_BIG_ENDIAN);
    proto_tree_add_item(frame_len_tree, hf_ifcp_framelen_c, tvb, offset, 2, ENC_BIG_ENDIAN);
    offset+=2;

    /* timestamp seconds */
    proto_tree_add_item(tree, hf_ifcp_tsec, tvb, offset, 4, ENC_BIG_ENDIAN);
    offset+=4;

    /* timestamp fractions */
    proto_tree_add_item(tree, hf_ifcp_tusec, tvb, offset, 4, ENC_BIG_ENDIAN);
    offset+=4;

    /* crc */
    proto_tree_add_item(tree, hf_ifcp_encap_crc, tvb, offset, 4, ENC_BIG_ENDIAN);
    offset+=4;


    /* FC SOF/-SOF */
    proto_tree_add_item(sof_tree, hf_ifcp_sof, tvb, offset, 1, ENC_BIG_ENDIAN);
    offset++;
    proto_tree_add_item(sof_tree, hf_ifcp_sof, tvb, offset, 1, ENC_BIG_ENDIAN);
    offset++;
    proto_tree_add_item(sof_tree, hf_ifcp_sof_c, tvb, offset, 1, ENC_BIG_ENDIAN);
    offset++;
    proto_tree_add_item(sof_tree, hf_ifcp_sof_c, tvb, offset, 1, ENC_BIG_ENDIAN);
    offset++;

    /* FC EOF/-EOF */
    if(tvb_bytes_exist(tvb, frame_len-4, 4)) {
        proto_tree_add_item(eof_tree, hf_ifcp_eof, tvb, frame_len-4, 1, ENC_BIG_ENDIAN);
        proto_tree_add_item(eof_tree, hf_ifcp_eof, tvb, frame_len-3, 1, ENC_BIG_ENDIAN);
        proto_tree_add_item(eof_tree, hf_ifcp_eof_c, tvb, frame_len-2, 1, ENC_BIG_ENDIAN);
        proto_tree_add_item(eof_tree, hf_ifcp_eof_c, tvb, frame_len-1, 1, ENC_BIG_ENDIAN);
        }


    /* Call the FC Dissector if this is carrying an FC frame */
    /* Set the SOF/EOF flags in the packet_info header */
    fc_data.sof_eof = 0;

    switch(sof){
    case iFCP_SOFi3:
    case iFCP_SOFi2:
    case iFCP_SOFi4:
        fc_data.sof_eof = FC_DATA_SOF_FIRST_FRAME;
        break;
    case iFCP_SOFf:
        fc_data.sof_eof = FC_DATA_SOF_SOFF;
        break;
    default:
        if(sof){
            if (eof != iFCP_EOFn) {
                fc_data.sof_eof |= FC_DATA_EOF_LAST_FRAME;
                    } else if (eof != iFCP_EOFt) {
                fc_data.sof_eof |= FC_DATA_EOF_INVALID;
            }
        }
    }

    next_tvb=tvb_new_subset_length(tvb, offset, frame_len-offset-4);
    fc_data.ethertype = ETHERTYPE_UNK;

    if(fc_handle){
        call_dissector_with_data(fc_handle, next_tvb, pinfo, parent_tree, &fc_data);
    } else {
        call_data_dissector(next_tvb, pinfo, parent_tree);
    }

    return tvb_captured_length(tvb);
}

static unsigned
get_ifcp_pdu_len(packet_info *pinfo _U_, tvbuff_t *tvb, int offset, void *data _U_)
{
    unsigned pdu_len;

    if(!ifcp_header_test(tvb, offset)){
        return 0;
    }

    pdu_len=(tvb_get_ntohs(tvb, offset+12)&0x03FF)*4;
    return pdu_len;
}

static int
dissect_ifcp(tvbuff_t *tvb, packet_info *pinfo, proto_tree *parent_tree, void* data)
{
    tcp_dissect_pdus(tvb, pinfo, parent_tree, ifcp_desegment, iFCP_MIN_HEADER_LEN, get_ifcp_pdu_len, dissect_ifcp_pdu, data);
    return tvb_captured_length(tvb);
}


/* This is called for those sessions where we have explicitly said
 * this to be iFCP using "Decode As..."
 * In this case we will not check the port number for sanity and just
 * do as the user said.
 */
static int
dissect_ifcp_handle(tvbuff_t *tvb, packet_info *pinfo, proto_tree *tree, void* data)
{
    return dissect_ifcp(tvb, pinfo, tree, data);
}

static bool
dissect_ifcp_heur(tvbuff_t *tvb, packet_info *pinfo, proto_tree *tree, void *data)
{
    if(!ifcp_header_test(tvb, 0)){
        return false;
    }

    dissect_ifcp(tvb, pinfo, tree, data);

    /* our heuristics are so strong that if the heuristics above passed
     * and the dissection of the pdu did not cause any exceptions
     * then we can set this as our conversation dissector
     */
    if(ifcp_handle){
        conversation_t* ifcp_conv;

        ifcp_conv=find_or_create_conversation(pinfo);
        /* XXX why does this not work? it doesn't result in dissect_ifcp_handle being called    look into later*/
        conversation_set_dissector(ifcp_conv, ifcp_handle);
    }

    return true;
}

void
proto_register_ifcp (void)
{
    /* Setup list of header fields  See Section 1.6.1 for details*/
    static hf_register_info hf[] = {
        { &hf_ifcp_protocol,
          {"Protocol", "ifcp.encap.proto", FT_UINT8, BASE_DEC, VALS(fcencap_proto_vals), 0,
           NULL, HFILL }},
        { &hf_ifcp_protocol_c,
          {"Protocol (1's Complement)", "ifcp.encap.protoc", FT_UINT8, BASE_DEC, NULL, 0,
           NULL, HFILL}},
        { &hf_ifcp_version,
          {"Version", "ifcp.encap.version", FT_UINT8, BASE_DEC, NULL, 0,
           NULL, HFILL}},
        { &hf_ifcp_version_c,
          {"Version (1's Complement)", "ifcp.encap.versionc", FT_UINT8, BASE_DEC, NULL, 0,
           NULL, HFILL}},
        { &hf_ifcp_encap_flags_c,
          {"iFCP Encapsulation Flags (1's Complement)", "ifcp.encap_flagsc", FT_UINT8, BASE_HEX, NULL, 0xFC,
           NULL, HFILL}},
        { &hf_ifcp_framelen,
          {"Frame Length (in Words)", "ifcp.encap.framelen", FT_UINT16, BASE_DEC, NULL, 0x03FF,
           NULL, HFILL}},
        { &hf_ifcp_framelen_c,
          {"Frame Length (1's Complement)", "ifcp.encap.framelenc", FT_UINT16, BASE_DEC, NULL, 0x03FF,
           NULL, HFILL}},
        { &hf_ifcp_tsec,
          {"Time (secs)", "ifcp.encap.tsec", FT_UINT32, BASE_DEC, NULL, 0,
           NULL, HFILL}},
        { &hf_ifcp_tusec,
          {"Time (fraction)", "ifcp.encap.tusec", FT_UINT32, BASE_DEC, NULL, 0,
           NULL, HFILL}},
        { &hf_ifcp_encap_crc,
          {"CRC", "ifcp.encap.crc", FT_UINT32, BASE_HEX, NULL, 0,
           NULL, HFILL}},
        { &hf_ifcp_sof,
          {"SOF", "ifcp.sof", FT_UINT8, BASE_HEX, VALS (ifcp_sof_vals), 0,
           NULL, HFILL}},
        { &hf_ifcp_eof,
          {"EOF", "ifcp.eof", FT_UINT8, BASE_HEX, VALS (ifcp_eof_vals), 0,
           NULL, HFILL}},
        { &hf_ifcp_sof_c,
          {"SOF Complement", "ifcp.sof_c", FT_UINT8, BASE_HEX, NULL , 0,
           NULL, HFILL}},
        { &hf_ifcp_eof_c,
          {"EOF Complement", "ifcp.eof_c", FT_UINT8, BASE_HEX, NULL , 0,
           NULL, HFILL}},
        { &hf_ifcp_ls_command_acc,
          {"Ls Command Acc", "ifcp.ls_command_acc", FT_UINT8, BASE_HEX, NULL, 0,
           NULL, HFILL}},
        { &hf_ifcp_common_flags,
          {"Flags", "ifcp.common_flags", FT_UINT8, BASE_HEX , NULL, 0xfc,
           NULL, HFILL }},
        { &hf_ifcp_common_flags_crcv,
          {"CRC", "ifcp.common_flags.crcv", FT_BOOLEAN, 8, TFS(&tfs_valid_not_valid), IFCP_COMMON_FLAGS_CRCV,
           "Is the CRC field valid?", HFILL }},
        { &hf_ifcp_flags,
          {"iFCP Flags", "ifcp.flags", FT_UINT8, BASE_HEX , NULL, 0,
           NULL, HFILL }},
        { &hf_ifcp_flags_ses,
          {"SES", "ifcp.flags.ses", FT_BOOLEAN, 8, TFS(&ifcp_flags_ses_tfs), IFCP_FLAGS_SES,
           "Is this a Session control frame", HFILL }},
        { &hf_ifcp_flags_trp,
          {"TRP", "ifcp.flags.trp", FT_BOOLEAN, 8, TFS(&ifcp_flags_trp_tfs), IFCP_FLAGS_TRP,
           "Is address transparent mode enabled", HFILL }},
        { &hf_ifcp_flags_spc,
          {"SPC", "ifcp.flags.spc", FT_BOOLEAN, 8, TFS(&ifcp_flags_spc_tfs), IFCP_FLAGS_SPC,
           "Is frame part of link service", HFILL }},
    };

    static int *ett[] = {
        &ett_ifcp,
        &ett_ifcp_sof,
        &ett_ifcp_eof,
        &ett_ifcp_protocol,
        &ett_ifcp_version,
    &ett_ifcp_frame_len,
        &ett_ifcp_flags,
        &ett_ifcp_common_flags,
    };

    module_t *ifcp_module;

    /* Register the protocol name and description */
    proto_ifcp = proto_register_protocol("iFCP", "iFCP", "ifcp");

    proto_register_field_array(proto_ifcp, hf, array_length(hf));
    proto_register_subtree_array(ett, array_length(ett));

    ifcp_module = prefs_register_protocol(proto_ifcp, NULL);
    prefs_register_bool_preference(ifcp_module,
                                   "desegment",
                                   "Reassemble iFCP messages spanning multiple TCP segments",
                                   "Whether the iFCP dissector should reassemble messages spanning multiple TCP segments."
                                   " To use this option, you must also enable \"Allow subdissectors to reassemble TCP streams\" in the TCP protocol settings.",
                                   &ifcp_desegment);
    prefs_register_obsolete_preference(ifcp_module, "target_port");

    ifcp_handle = register_dissector("ifcp", dissect_ifcp_handle, proto_ifcp);
}

void
proto_reg_handoff_ifcp (void)
{
    heur_dissector_add("tcp", dissect_ifcp_heur, "iFCP over TCP", "ifcp_tcp", proto_ifcp, HEURISTIC_ENABLE);

    dissector_add_for_decode_as_with_preference("tcp.port", ifcp_handle);

    fc_handle = find_dissector_add_dependency("fc_ifcp", proto_ifcp);
}

/*
 * 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:
 */
