// See the file "COPYING" in the main distribution directory for copyright.

#include "zeek/analyzer/protocol/mime/MIME.h"

#include <openssl/evp.h>

#include "zeek/Base64.h"
#include "zeek/NetVar.h"
#include "zeek/Reporter.h"
#include "zeek/analyzer/protocol/mime/consts.bif.h"
#include "zeek/analyzer/protocol/mime/events.bif.h"
#include "zeek/digest.h"
#include "zeek/file_analysis/Manager.h"

// Here are a few things to do:
//
// 1. Add a Zeek internal function 'stop_deliver_data_of_entity' so
// that the engine does not decode and deliver further data for the
// entity (which may speed up the engine by avoiding copying).
//
// 2. Better support for structured header fields, in particular,
// headers of form: <name>=<value>; <param_1>=<param_val_1>;
// <param_2>=<param_val_2>; ... (so that

namespace zeek::analyzer::mime {

static const data_chunk_t null_data_chunk = {0, nullptr};

int mime_header_only = 0;
int mime_decode_data = 1;
int mime_submit_data = 1;

enum MIME_HEADER_FIELDS : uint8_t {
    MIME_CONTENT_TYPE,
    MIME_CONTENT_TRANSFER_ENCODING,
    MIME_FIELD_OTHER,
};

enum MIME_CONTENT_SUBTYPE : uint8_t {
    CONTENT_SUBTYPE_MIXED,       // for multipart
    CONTENT_SUBTYPE_ALTERNATIVE, // for multipart
    CONTENT_SUBTYPE_DIGEST,      // for multipart

    CONTENT_SUBTYPE_RFC822,        // for message
    CONTENT_SUBTYPE_PARTIAL,       // for message
    CONTENT_SUBTYPE_EXTERNAL_BODY, // for message

    CONTENT_SUBTYPE_PLAIN, // for text

    CONTENT_SUBTYPE_OTHER,
};

enum MIME_CONTENT_ENCODING : uint8_t {
    CONTENT_ENCODING_7BIT,
    CONTENT_ENCODING_8BIT,
    CONTENT_ENCODING_BINARY,
    CONTENT_ENCODING_QUOTED_PRINTABLE,
    CONTENT_ENCODING_BASE64,
    CONTENT_ENCODING_OTHER,
};

enum MIME_BOUNDARY_DELIMITER : uint8_t {
    NOT_MULTIPART_BOUNDARY,
    MULTIPART_BOUNDARY,
    MULTIPART_CLOSING_BOUNDARY,
};

static const char* MIMEHeaderName[] = {
    "content-type",
    "content-transfer-encoding",
    nullptr,
};

static const char* MIMEContentTypeName[] = {
    "MULTIPART",
    "MESSAGE",
    "TEXT",
    nullptr,
};

static const char* MIMEContentSubtypeName[] = {
    "MIXED",       // for multipart
    "ALTERNATIVE", // for multipart
    "DIGEST",      // for multipart

    "RFC822",        // for message
    "PARTIAL",       // for message
    "EXTERNAL-BODY", // for message

    "PLAIN", // for text

    nullptr, // other
};

static const char* MIMEContentEncodingName[] = {
    "7BIT", "8BIT", "BINARY", "QUOTED-PRINTABLE", "BASE64", nullptr,
};

bool is_null_data_chunk(data_chunk_t b) { return b.data == nullptr; }

bool is_lws(char ch) { return ch == 9 || ch == 32; }

StringValPtr to_string_val(int length, const char* data) { return make_intrusive<StringVal>(length, data); }

StringValPtr to_string_val(const char* data, const char* end_of_data) {
    return make_intrusive<StringVal>(end_of_data - data, data);
}

StringValPtr to_string_val(const data_chunk_t buf) { return to_string_val(buf.length, buf.data); }

static data_chunk_t get_data_chunk(String* s) {
    data_chunk_t b;
    b.length = s->Len();
    b.data = (const char*)s->Bytes();
    return b;
}

int fputs(data_chunk_t b, FILE* fp) {
    for ( int i = 0; i < b.length; ++i )
        if ( fputc(b.data[i], fp) == EOF )
            return EOF;
    return 0;
}

void MIME_Mail::Undelivered(int len) {
    cur_entity_id =
        file_mgr->Gap(cur_entity_len, len, analyzer->GetAnalyzerTag(), analyzer->Conn(), is_orig, cur_entity_id);
}

bool istrequal(data_chunk_t s, const char* t) {
    int len = strlen(t);

    if ( s.length != len )
        return false;

    return strncasecmp(s.data, t, len) == 0;
}

int MIME_count_leading_lws(int len, const char* data) {
    int i;
    for ( i = 0; i < len; ++i )
        if ( ! is_lws(data[i]) )
            break;
    return i;
}

int MIME_count_trailing_lws(int len, const char* data) {
    int i;
    for ( i = 0; i < len; ++i )
        if ( ! is_lws(data[len - 1 - i]) )
            break;
    return i;
}

// See RFC 2822, page 11
int MIME_skip_comments(int len, const char* data) {
    if ( len == 0 || data[0] != '(' )
        return 0;

    int par = 0;
    for ( int i = 0; i < len; ++i ) {
        switch ( data[i] ) {
            case '(': ++par; break;

            case ')':
                --par;
                if ( par == 0 )
                    return i + 1;
                break;

            case '\\': ++i; break;
        }
    }

    return len;
}

// Skip over lws and comments, but not tspecials. Do not use this
// function in quoted-string or comments.
int MIME_skip_lws_comments(int len, const char* data) {
    int i = 0;
    while ( i < len ) {
        if ( is_lws(data[i]) )
            ++i;
        else {
            if ( data[i] == '(' )
                i += MIME_skip_comments(len - i, data + i);
            else
                return i;
        }
    }

    return len;
}

int MIME_get_field_name(int len, const char* data, data_chunk_t* name) {
    int i = MIME_skip_lws_comments(len, data);
    while ( i < len ) {
        int j;
        if ( MIME_is_field_name_char(data[i]) ) {
            name->data = data + i;

            for ( j = i; j < len; ++j )
                if ( ! MIME_is_field_name_char(data[j]) )
                    break;

            name->length = j - i;
            return j;
        }

        j = MIME_skip_lws_comments(len - i, data + i);
        i += (j > 0) ? j : 1;
    }

    return -1;
}

// See RFC 2045, page 12.
static bool MIME_is_tspecial(char ch, bool is_boundary = false) {
    if ( is_boundary )
        return ch == '"';
    else
        return ch == '(' || ch == ')' || ch == '<' || ch == '>' || ch == '@' || ch == ',' || ch == ';' || ch == ':' ||
               ch == '\\' || ch == '"' || ch == '/' || ch == '[' || ch == ']' || ch == '?' || ch == '=';
}

bool MIME_is_field_name_char(char ch) { return ch >= 33 && ch <= 126 && ch != ':'; }

static bool MIME_is_token_char(char ch, bool is_boundary = false) {
    return ch >= 33 && ch <= 126 && ! MIME_is_tspecial(ch, is_boundary);
}

// See RFC 2045, page 12.
// A token is composed of characters that are not SPACE, CTLs or tspecials
int MIME_get_token(int len, const char* data, data_chunk_t* token, bool is_boundary) {
    int i = 0;

    if ( ! is_boundary )
        i = MIME_skip_lws_comments(len, data);

    while ( i < len ) {
        int j;

        if ( MIME_is_token_char(data[i], is_boundary) ) {
            token->data = (data + i);
            for ( j = i; j < len; ++j ) {
                if ( ! MIME_is_token_char(data[j], is_boundary) )
                    break;
            }

            token->length = j - i;
            return j;
        }

        j = MIME_skip_lws_comments(len - i, data + i);
        i += (j > 0) ? j : 1;
    }

    return -1;
}

int MIME_get_slash_token_pair(int len, const char* data, data_chunk_t* first, data_chunk_t* second) {
    int offset;
    const char* data_start = data;

    offset = MIME_get_token(len, data, first);
    if ( offset < 0 ) {
        // DEBUG_MSG("first token missing in slash token pair");
        return -1;
    }

    data += offset;
    len -= offset;

    offset = MIME_skip_lws_comments(len, data);
    if ( offset < 0 || offset >= len || data[offset] != '/' ) {
        // DEBUG_MSG("/ not found in slash token pair");
        return -1;
    }

    ++offset;
    data += offset;
    len -= offset;

    offset = MIME_get_token(len, data, second);
    if ( offset < 0 ) {
        // DEBUG_MSG("second token missing in slash token pair");
        return -1;
    }

    data += offset;
    len -= offset;

    return data - data_start;
}

// See RFC 2822, page 13.
int MIME_get_quoted_string(int len, const char* data, data_chunk_t* str) {
    int offset = MIME_skip_lws_comments(len, data);

    len -= offset;
    data += offset;

    if ( len <= 0 || *data != '"' )
        return -1;

    for ( int i = 1; i < len; ++i ) {
        switch ( data[i] ) {
            case '"':
                str->data = data + 1;
                str->length = i - 1;
                return offset + i + 1;

            case '\\': ++i; break;
        }
    }

    return -1;
}

int MIME_get_value(int len, const char* data, String*& buf, bool is_boundary) {
    int offset = 0;

    if ( ! is_boundary ) // For boundaries, simply accept everything.
        offset = MIME_skip_lws_comments(len, data);

    len -= offset;
    data += offset;

    if ( len > 0 && *data == '"' ) {
        data_chunk_t str;
        int end = MIME_get_quoted_string(len, data, &str);
        if ( end < 0 )
            return -1;

        buf = MIME_decode_quoted_pairs(str);
        return offset + end;
    }

    else {
        data_chunk_t str;
        int end = MIME_get_token(len, data, &str, is_boundary);
        if ( end < 0 )
            return -1;

        buf = new String((const u_char*)str.data, str.length, true);
        return offset + end;
    }
}

// Decode each quoted-pair: a '\' followed by a character by the
// quoted character. The decoded string is returned.

String* MIME_decode_quoted_pairs(data_chunk_t buf) {
    const char* data = buf.data;
    char* dest = new char[buf.length + 1];
    int j = 0;
    for ( int i = 0; i < buf.length; ++i )
        if ( data[i] == '\\' ) {
            if ( ++i < buf.length )
                dest[j++] = data[i];
            else {
                // a trailing '\' -- don't know what
                // to do with it -- ignore it.
            }
        }
        else
            dest[j++] = data[i];
    dest[j] = 0;

    return new String(true, (byte_vec)dest, j);
}

MIME_Multiline::MIME_Multiline() { line = nullptr; }

MIME_Multiline::~MIME_Multiline() {
    delete line;
    delete_strings(buffer);
}

void MIME_Multiline::append(int len, const char* data) { buffer.push_back(new String((const u_char*)data, len, true)); }

String* MIME_Multiline::get_concatenated_line() {
    if ( buffer.empty() )
        return nullptr;

    delete line;
    line = concatenate(buffer);

    return line;
}

MIME_Header::MIME_Header(MIME_Multiline* hl) {
    lines = hl;
    name = value = value_token = rest_value = null_data_chunk;

    String* s = hl->get_concatenated_line();
    int len = s->Len();
    const char* data = (const char*)s->Bytes();

    int offset = MIME_get_field_name(len, data, &name);
    if ( offset < 0 )
        return;

    len -= offset;
    data += offset;
    offset = MIME_skip_lws_comments(len, data);

    if ( offset < len && data[offset] == ':' ) {
        value.length = len - offset - 1;
        value.data = data + offset + 1;
        while ( value.length && isspace(*value.data) ) {
            --value.length;
            ++value.data;
        }
    }
    else
        // malformed header line
        name = null_data_chunk;
}

MIME_Header::~MIME_Header() { delete lines; }

int MIME_Header::get_first_token() {
    if ( MIME_get_token(value.length, value.data, &value_token) >= 0 ) {
        rest_value.data = value_token.data + value_token.length;
        rest_value.length = value.data + value.length - rest_value.data;
        return 1;
    }
    else {
        value_token = rest_value = null_data_chunk;
        return 0;
    }
}

data_chunk_t MIME_Header::get_value_token() {
    if ( ! is_null_data_chunk(value_token) )
        return value_token;
    get_first_token();
    return value_token;
}

data_chunk_t MIME_Header::get_value_after_token() {
    if ( ! is_null_data_chunk(rest_value) )
        return rest_value;
    get_first_token();
    return rest_value;
}

MIME_Entity::MIME_Entity(MIME_Message* output_message, MIME_Entity* parent_entity) {
    init();
    parent = parent_entity;
    message = output_message;
    if ( parent ) {
        content_encoding = parent->ContentTransferEncoding();
        depth = parent->Depth() + 1;
    }

    want_all_headers = (bool)mime_all_headers;
}

void MIME_Entity::init() {
    in_header = 1;
    end_of_data = 0;

    current_header_line = nullptr;
    current_field_type = MIME_FIELD_OTHER;

    need_to_parse_parameters = 0;

    content_type_str = make_intrusive<StringVal>("TEXT");
    content_subtype_str = make_intrusive<StringVal>("PLAIN");

    content_encoding_str = nullptr;
    multipart_boundary = nullptr;
    content_type = CONTENT_TYPE_TEXT;
    content_subtype = CONTENT_SUBTYPE_PLAIN;
    content_encoding = CONTENT_ENCODING_OTHER;

    parent = nullptr;
    current_child_entity = nullptr;

    base64_decoder = nullptr;

    depth = 0;
    data_buf_length = 0;
    data_buf_data = nullptr;
    data_buf_offset = -1;

    message = nullptr;
    delay_adding_implicit_CRLF = false;
    want_all_headers = false;
}

MIME_Entity::~MIME_Entity() {
    if ( ! end_of_data )
        reporter->AnalyzerError(message ? message->GetAnalyzer() : nullptr,
                                "missing MIME_Entity::EndOfData() before ~MIME_Entity");

    delete current_header_line;
    delete content_encoding_str;
    delete multipart_boundary;

    for ( auto& header : headers )
        delete header;
    headers.clear();

    delete base64_decoder;
}

void MIME_Entity::Deliver(int len, const char* data, bool trailing_CRLF) {
    if ( in_header ) {
        if ( len == 0 || *data == '\0' ) { // an empty line at the end of header fields
            FinishHeader();
            in_header = 0;
            SubmitAllHeaders();

            // Note: it's possible that we are in the
            // trailer of a chunked transfer (see HTTP.cc).
            // In this case, end_of_data will be set in
            // HTTP_Entity::SubmitAllHeaders(), and we
            // should not begin a new body.

            if ( ! end_of_data )
                BeginBody();
        }

        else if ( is_lws(*data) )
            // linear whitespace - a continuing header line
            ContHeader(len, data);
        else
            NewHeader(len, data);
    }
    else {
        if ( ! mime_header_only && data )
            NewDataLine(len, data, trailing_CRLF);
    }
}

void MIME_Entity::BeginBody() {
    if ( content_encoding == CONTENT_ENCODING_BASE64 )
        StartDecodeBase64();

    if ( content_type == CONTENT_TYPE_MESSAGE )
        BeginChildEntity();
}

void MIME_Entity::EndOfData() {
    if ( end_of_data )
        return;

    end_of_data = 1;

    if ( in_header ) {
        FinishHeader();
        in_header = 0;
        SubmitAllHeaders();
        message->SubmitEvent(MIME_EVENT_ILLEGAL_FORMAT, "entity body missing");
    }

    else {
        if ( current_child_entity != nullptr ) {
            if ( content_type == CONTENT_TYPE_MULTIPART )
                IllegalFormat("multipart closing boundary delimiter missing");
            EndChildEntity();
        }

        if ( content_encoding == CONTENT_ENCODING_BASE64 )
            FinishDecodeBase64();

        FlushData();
    }

    message->EndEntity(this);
}

void MIME_Entity::NewDataLine(int len, const char* data, bool trailing_CRLF) {
    if ( content_type == CONTENT_TYPE_MULTIPART ) {
        switch ( CheckBoundaryDelimiter(len, data) ) {
            case MULTIPART_BOUNDARY:
                if ( current_child_entity != nullptr )
                    EndChildEntity();
                if ( ! end_of_data )
                    BeginChildEntity();
                return;

            case MULTIPART_CLOSING_BOUNDARY:
                if ( current_child_entity != nullptr )
                    EndChildEntity();
                EndOfData();
                return;
        }
    }

    if ( content_type == CONTENT_TYPE_MULTIPART || content_type == CONTENT_TYPE_MESSAGE ) {
        // Here we ignore the difference among 7bit, 8bit and
        // binary encoding, and thus do not need to decode
        // before passing the data to child.

        if ( current_child_entity != nullptr )
            // Data before the first or after the last
            // boundary delimiter are ignored
            current_child_entity->Deliver(len, data, trailing_CRLF);
    }
    else {
        if ( mime_decode_data )
            DecodeDataLine(len, data, trailing_CRLF);
    }
}

void MIME_Entity::NewHeader(int len, const char* data) {
    FinishHeader();

    if ( len == 0 )
        return;

    ASSERT(! is_lws(*data));

    current_header_line = new MIME_Multiline();
    current_header_line->append(len, data);
}

void MIME_Entity::ContHeader(int len, const char* data) {
    if ( current_header_line == nullptr ) {
        IllegalFormat("first header line starts with linear whitespace");

        // shall we try it as a new header or simply ignore this line?
        int ws = MIME_count_leading_lws(len, data);
        NewHeader(len - ws, data + ws);
        return;
    }

    current_header_line->append(len, data);
}

void MIME_Entity::FinishHeader() {
    if ( current_header_line == nullptr )
        return;

    MIME_Header* h = new MIME_Header(current_header_line);
    current_header_line = nullptr;

    if ( ! is_null_data_chunk(h->get_name()) ) {
        ParseMIMEHeader(h);
        SubmitHeader(h);

        if ( want_all_headers )
            headers.push_back(h);
        else
            delete h;
    }
    else
        delete h;
}

int MIME_Entity::LookupMIMEHeaderName(data_chunk_t name) {
    // A linear lookup should be fine for now.
    // header names are case-insensitive (RFC 822, 2822, 2045).

    for ( int i = 0; MIMEHeaderName[i] != nullptr; ++i )
        if ( istrequal(name, MIMEHeaderName[i]) )
            return i;
    return -1;
}

void MIME_Entity::ParseMIMEHeader(MIME_Header* h) {
    if ( h == nullptr )
        return;

    current_field_type = LookupMIMEHeaderName(h->get_name());

    switch ( current_field_type ) {
        case MIME_CONTENT_TYPE: ParseContentTypeField(h); break;

        case MIME_CONTENT_TRANSFER_ENCODING: ParseContentEncodingField(h); break;
    }
}

bool MIME_Entity::ParseContentTypeField(MIME_Header* h) {
    data_chunk_t val = h->get_value();
    int len = val.length;
    const char* data = val.data;

    data_chunk_t ty;
    data_chunk_t subty;
    int offset = MIME_get_slash_token_pair(len, data, &ty, &subty);

    if ( offset < 0 ) {
        IllegalFormat("media type/subtype not found in content type");
        return false;
    }
    data += offset;
    len -= offset;

    content_type_str = make_intrusive<StringVal>(ty.length, ty.data);
    content_type_str->ToUpper();
    content_subtype_str = make_intrusive<StringVal>(subty.length, subty.data);
    content_subtype_str->ToUpper();

    ParseContentType(ty, subty);

    // Proceed to parameters.
    if ( need_to_parse_parameters )
        ParseFieldParameters(len, data);

    if ( content_type == CONTENT_TYPE_MULTIPART && ! multipart_boundary ) {
        IllegalFormat(
            "boundary delimiter is not specified for a multipart entity -- content is "
            "treated as type application/octet-stream");
        content_type = CONTENT_TYPE_OTHER;
        content_subtype = CONTENT_SUBTYPE_OTHER;
    }

    return true;
}

bool MIME_Entity::ParseContentEncodingField(MIME_Header* h) {
    data_chunk_t enc;

    enc = h->get_value_token();
    if ( is_null_data_chunk(enc) ) {
        IllegalFormat("encoding type not found in content encoding");
        return false;
    }

    delete content_encoding_str;
    content_encoding_str = new String((const u_char*)enc.data, enc.length, true);
    ParseContentEncoding(enc);

    if ( need_to_parse_parameters ) {
        data_chunk_t val = h->get_value_after_token();
        if ( ! is_null_data_chunk(val) )
            ParseFieldParameters(val.length, val.data);
    }

    return true;
}

bool MIME_Entity::ParseFieldParameters(int len, const char* data) {
    data_chunk_t attr;

    while ( true ) {
        int offset = MIME_skip_lws_comments(len, data);
        if ( offset < 0 || offset >= len || data[offset] != ';' )
            break;

        ++offset;
        data += offset;
        len -= offset;

        offset = MIME_get_token(len, data, &attr);
        if ( offset < 0 ) {
            IllegalFormat("attribute name not found in parameter specification");
            return false;
        }

        data += offset;
        len -= offset;

        offset = MIME_skip_lws_comments(len, data);
        if ( offset < 0 || offset >= len || data[offset] != '=' ) {
            IllegalFormat("= not found in parameter specification");
            continue;
        }

        ++offset;
        data += offset;
        len -= offset;

        String* val = nullptr;

        if ( current_field_type == MIME_CONTENT_TYPE && content_type == CONTENT_TYPE_MULTIPART &&
             istrequal(attr, "boundary") ) {
            // token or quoted-string (and some lenience for characters
            // not explicitly allowed by the RFC, but encountered in the wild)
            offset = MIME_get_value(len, data, val, true);

            if ( ! val ) {
                IllegalFormat("Could not parse multipart boundary");
                continue;
            }

            data_chunk_t vd = get_data_chunk(val);
            delete multipart_boundary;
            multipart_boundary = new String((const u_char*)vd.data, vd.length, true);
        }
        else
            // token or quoted-string
            offset = MIME_get_value(len, data, val);

        if ( offset < 0 ) {
            IllegalFormat("value not found in parameter specification");
            delete val;
            continue;
        }

        data += offset;
        len -= offset;
        delete val;
    }

    return true;
}

void MIME_Entity::ParseContentType(data_chunk_t type, data_chunk_t sub_type) {
    int i;
    for ( i = 0; MIMEContentTypeName[i]; ++i )
        if ( istrequal(type, MIMEContentTypeName[i]) )
            break;

    content_type = i;

    for ( i = 0; MIMEContentSubtypeName[i]; ++i )
        if ( istrequal(sub_type, MIMEContentSubtypeName[i]) )
            break;

    content_subtype = i;

    switch ( content_type ) {
        case CONTENT_TYPE_MULTIPART:
        case CONTENT_TYPE_MESSAGE: need_to_parse_parameters = 1; break;

        default: need_to_parse_parameters = 0; break;
    }
}

void MIME_Entity::ParseContentEncoding(data_chunk_t encoding_mechanism) {
    int i;
    for ( i = 0; MIMEContentEncodingName[i]; ++i )
        if ( istrequal(encoding_mechanism, MIMEContentEncodingName[i]) )
            break;

    content_encoding = i;
}

int MIME_Entity::CheckBoundaryDelimiter(int len, const char* data) {
    if ( ! multipart_boundary ) {
        reporter->Warning("boundary delimiter was not specified for a multipart message\n");
        DEBUG_MSG("headers of the MIME entity for debug:\n");
        DebugPrintHeaders();
        return NOT_MULTIPART_BOUNDARY;
    }

    if ( len >= 2 && data[0] == '-' && data[1] == '-' ) {
        len -= 2;
        data += 2;

        data_chunk_t delim = get_data_chunk(multipart_boundary);

        int i;
        for ( i = 0; i < len && i < delim.length; ++i )
            if ( data[i] != delim.data[i] )
                return NOT_MULTIPART_BOUNDARY;

        if ( i < delim.length )
            return NOT_MULTIPART_BOUNDARY;

        len -= i;
        data += i;

        if ( len >= 2 && data[0] == '-' && data[1] == '-' )
            return MULTIPART_CLOSING_BOUNDARY;
        else
            return MULTIPART_BOUNDARY;
    }

    return NOT_MULTIPART_BOUNDARY;
}

// trailing_CRLF indicates whether an implicit CRLF sequence follows data
// (the CRLF sequence is not included in data).

void MIME_Entity::DecodeDataLine(int len, const char* data, bool trailing_CRLF) {
    if ( ! mime_submit_data )
        return;

    switch ( content_encoding ) {
        case CONTENT_ENCODING_QUOTED_PRINTABLE: DecodeQuotedPrintable(len, data); break;

        case CONTENT_ENCODING_BASE64: DecodeBase64(len, data); break;

        case CONTENT_ENCODING_7BIT:
        case CONTENT_ENCODING_8BIT:
        case CONTENT_ENCODING_BINARY:
        case CONTENT_ENCODING_OTHER: DecodeBinary(len, data, trailing_CRLF); break;
    }
    FlushData();
}

void MIME_Entity::DecodeBinary(int len, const char* data, bool trailing_CRLF) {
    if ( delay_adding_implicit_CRLF ) {
        delay_adding_implicit_CRLF = false;
        DataOctet(CR);
        DataOctet(LF);
    }

    DataOctets(len, data);

    if ( trailing_CRLF ) {
        if ( Parent() && Parent()->MIMEContentType() == mime::CONTENT_TYPE_MULTIPART ) {
            // For multipart body content, we want to keep all implicit CRLFs
            // except for the last because that one belongs to the multipart
            // boundary delimiter, not the content.  Simply delaying the
            // addition of implicit CRLFs until another chunk of content
            // data comes in is a way to prevent the CRLF before the final
            // message boundary from being accidentally added to the content.
            delay_adding_implicit_CRLF = true;
        }
        else {
            DataOctet(CR);
            DataOctet(LF);
        }
    }
}

void MIME_Entity::DecodeQuotedPrintable(int len, const char* data) {
    // Ignore trailing HT and SP.
    int i;
    for ( i = len - 1; i >= 0; --i )
        if ( data[i] != HT && data[i] != SP )
            break;

    int end_of_line = i;
    int soft_line_break = 0;

    for ( i = 0; i <= end_of_line; ++i ) {
        if ( data[i] == '=' ) {
            if ( i == end_of_line )
                soft_line_break = 1;
            else {
                int legal = 0;
                if ( i + 2 < len ) {
                    int a = util::decode_hex(data[i + 1]);
                    int b = util::decode_hex(data[i + 2]);

                    if ( a >= 0 && b >= 0 ) {
                        DataOctet((a << 4) + b);
                        legal = 1;
                        i += 2;
                    }
                }

                if ( ! legal ) {
                    // Follows suggestions for a robust
                    // decoder. See RFC 2045 page 22.
                    IllegalEncoding("= is not followed by two hexadecimal digits in quoted-printable encoding");
                    DataOctet(data[i]);
                }
            }
        }

        else if ( (data[i] >= 33 && data[i] <= 60) ||
                  // except controls, whitespace and '='
                  (data[i] >= 62 && data[i] <= 126) || (data[i] == HT || data[i] == SP) )
            DataOctet(data[i]);

        else {
            IllegalEncoding(util::fmt("control characters in quoted-printable encoding: %d", (int)(data[i])));
            DataOctet(data[i]);
        }
    }

    if ( ! soft_line_break ) {
        DataOctet(CR);
        DataOctet(LF);
    }
}

void MIME_Entity::DecodeBase64(int len, const char* data) {
    int rlen;
    char rbuf[128];

    while ( len > 0 ) {
        rlen = 128;
        char* prbuf = rbuf;
        int decoded = base64_decoder->Decode(len, data, &rlen, &prbuf);
        DataOctets(rlen, rbuf);
        len -= decoded;
        data += decoded;
    }
}

void MIME_Entity::StartDecodeBase64() {
    if ( base64_decoder ) {
        reporter->InternalWarning("previous MIME Base64 decoder not released");
        delete base64_decoder;
    }

    analyzer::Analyzer* analyzer = message->GetAnalyzer();

    if ( ! analyzer ) {
        reporter->InternalWarning("no analyzer associated with MIME message");
        return;
    }

    base64_decoder = new zeek::detail::Base64Converter(analyzer->Conn());
}

void MIME_Entity::FinishDecodeBase64() {
    if ( ! base64_decoder )
        return;

    int rlen = 128;
    char rbuf[128];
    char* prbuf = rbuf;

    if ( base64_decoder->Done(&rlen, &prbuf) ) { // some remaining data
        if ( rlen > 0 )
            DataOctets(rlen, rbuf);
    }

    delete base64_decoder;
    base64_decoder = nullptr;
}

bool MIME_Entity::GetDataBuffer() {
    int ret = message->RequestBuffer(&data_buf_length, &data_buf_data);
    if ( ! ret || data_buf_length == 0 || data_buf_data == nullptr ) {
        // reporter->InternalError("cannot get data buffer from MIME_Message", "");
        return false;
    }

    data_buf_offset = 0;
    return true;
}

void MIME_Entity::DataOctet(char ch) {
    if ( data_buf_offset < 0 && ! GetDataBuffer() )
        return;

    data_buf_data[data_buf_offset] = ch;

    ++data_buf_offset;
    if ( data_buf_offset == data_buf_length ) {
        SubmitData(data_buf_length, data_buf_data);
        data_buf_offset = -1;
    }
}

void MIME_Entity::SubmitData(int len, const char* buf) { message->SubmitData(len, buf); }

void MIME_Entity::DataOctets(int len, const char* data) {
    while ( len > 0 ) {
        if ( data_buf_offset < 0 && ! GetDataBuffer() )
            return;

        int n = std::min(data_buf_length - data_buf_offset, len);
        memcpy(data_buf_data + data_buf_offset, data, n);
        data += n;
        data_buf_offset += n;
        len -= n;

        if ( data_buf_offset == data_buf_length ) {
            SubmitData(data_buf_length, data_buf_data);
            data_buf_offset = -1;
        }
    }
}

void MIME_Entity::FlushData() {
    if ( data_buf_offset > 0 ) {
        SubmitData(data_buf_offset, data_buf_data);
        data_buf_offset = -1;
    }
}

void MIME_Entity::SubmitHeader(MIME_Header* h) { message->SubmitHeader(h); }

void MIME_Entity::SubmitAllHeaders() { message->SubmitAllHeaders(headers); }

void MIME_Entity::BeginChildEntity() {
    ASSERT(current_child_entity == nullptr);

    // If the maximum depth for analysis is reached, don't create a new
    // child entity. Instead, its header/body will be delivered to the
    // current entity.
    if ( zeek::BifConst::MIME::max_depth > 0 && Depth() >= zeek::BifConst::MIME::max_depth ) {
        if ( message->GetAnalyzer() ) {
            const char* addl = zeek::util::fmt("%" PRIu64, Depth());
            message->GetAnalyzer()->Weird("exceeded_mime_max_depth", addl);
        }

        return;
    }

    current_child_entity = NewChildEntity();
    message->BeginEntity(current_child_entity);
}

void MIME_Entity::EndChildEntity() {
    ASSERT(current_child_entity != nullptr);

    current_child_entity->EndOfData();
    delete current_child_entity;
    current_child_entity = nullptr;
}

void MIME_Entity::IllegalFormat(const char* explanation) {
    message->SubmitEvent(MIME_EVENT_ILLEGAL_FORMAT, explanation);
}

void MIME_Entity::IllegalEncoding(const char* explanation) {
    message->SubmitEvent(MIME_EVENT_ILLEGAL_ENCODING, explanation);
}

void MIME_Entity::DebugPrintHeaders() {
#ifdef DEBUG_BRO
    for ( MIME_Header* h : headers ) {
        DEBUG_fputs(h->get_name(), stderr);
        DEBUG_MSG(":\"");
        DEBUG_fputs(h->get_value(), stderr);
        DEBUG_MSG("\"\n");
    }
#endif
}

RecordValPtr MIME_Message::ToHeaderVal(MIME_Header* h) {
    static auto mime_header_rec = id::find_type<RecordType>("mime_header_rec");
    auto header_record = make_intrusive<RecordVal>(mime_header_rec);
    header_record->Assign(0, to_string_val(h->get_name()));
    auto upper_hn = to_string_val(h->get_name());
    upper_hn->ToUpper();
    header_record->Assign(1, std::move(upper_hn));
    header_record->Assign(2, to_string_val(h->get_value()));
    return header_record;
}

TableValPtr MIME_Message::ToHeaderTable(MIME_HeaderList& hlist) {
    static auto mime_header_list = id::find_type<TableType>("mime_header_list");
    auto t = make_intrusive<TableVal>(mime_header_list);

    for ( size_t i = 0; i < hlist.size(); ++i ) {
        auto index = val_mgr->Count(i + 1); // index starting from 1
        MIME_Header* h = hlist[i];
        t->Assign(std::move(index), ToHeaderVal(h));
    }

    return t;
}

MIME_Mail::MIME_Mail(analyzer::Analyzer* mail_analyzer, bool orig, int buf_size) : MIME_Message(mail_analyzer) {
    analyzer = mail_analyzer;

    min_overlap_length = zeek::detail::mime_segment_overlap_length;
    max_chunk_length = zeek::detail::mime_segment_length;
    is_orig = orig;

    int length = buf_size;

    if ( min_overlap_length < 0 )
        min_overlap_length = 0;

    if ( max_chunk_length < min_overlap_length + 32 )
        max_chunk_length = min_overlap_length + 32;

    if ( length < max_chunk_length )
        length = max_chunk_length;

    buffer_start = data_start = 0;
    data_buffer = new String(true, new u_char[length + 1], length);

    if ( mime_content_hash ) {
        compute_content_hash = 1;
        md5_hash = zeek::detail::hash_init(detail::Hash_MD5);
    }
    else
        compute_content_hash = 0;

    content_hash_length = 0;

    top_level = new MIME_Entity(this, nullptr); // to be changed to MIME_Mail
    BeginEntity(top_level);
}

void MIME_Mail::Done() {
    top_level->EndOfData();

    SubmitAllData();

    if ( compute_content_hash && mime_content_hash ) {
        u_char* digest = new u_char[16];
        zeek::detail::hash_final(md5_hash, digest);
        md5_hash = nullptr;

        analyzer->EnqueueConnEvent(mime_content_hash, analyzer->ConnVal(), val_mgr->Count(content_hash_length),
                                   make_intrusive<StringVal>(new String(true, digest, 16)));
    }

    MIME_Message::Done();

    file_mgr->EndOfFile(analyzer->GetAnalyzerTag(), analyzer->Conn());
}

MIME_Mail::~MIME_Mail() {
    if ( md5_hash )
        zeek::detail::hash_state_free(md5_hash);

    delete_strings(all_content);
    delete data_buffer;
    delete top_level;
}

void MIME_Mail::BeginEntity(MIME_Entity* /* entity */) {
    cur_entity_len = 0;
    cur_entity_id.clear();

    if ( mime_begin_entity )
        analyzer->EnqueueConnEvent(mime_begin_entity, analyzer->ConnVal());

    buffer_start = data_start = 0;
    ASSERT(entity_content.size() == 0);
}

void MIME_Mail::EndEntity(MIME_Entity* /* entity */) {
    if ( mime_entity_data ) {
        String* s = concatenate(entity_content);

        analyzer->EnqueueConnEvent(mime_entity_data, analyzer->ConnVal(), val_mgr->Count(s->Len()),
                                   make_intrusive<StringVal>(s));

        if ( ! mime_all_data )
            delete_strings(entity_content);
        else
            entity_content.clear();
    }

    if ( mime_end_entity )
        analyzer->EnqueueConnEvent(mime_end_entity, analyzer->ConnVal());

    if ( ! cur_entity_id.empty() )
        file_mgr->EndOfFile(cur_entity_id);
    else
        file_mgr->EndOfFile(analyzer->GetAnalyzerTag(), analyzer->Conn());

    cur_entity_id.clear();
}

void MIME_Mail::SubmitHeader(MIME_Header* h) {
    if ( mime_one_header )
        analyzer->EnqueueConnEvent(mime_one_header, analyzer->ConnVal(), ToHeaderVal(h));
}

void MIME_Mail::SubmitAllHeaders(MIME_HeaderList& hlist) {
    if ( mime_all_headers )
        analyzer->EnqueueConnEvent(mime_all_headers, analyzer->ConnVal(), ToHeaderTable(hlist));
}

void MIME_Mail::SubmitData(int len, const char* buf) {
    if ( buf != (char*)data_buffer->Bytes() + buffer_start ) {
        reporter->AnalyzerError(GetAnalyzer(), "MIME buffer misalignment");
        return;
    }

    if ( compute_content_hash ) {
        content_hash_length += len;
        zeek::detail::hash_update(md5_hash, (const u_char*)buf, len);
    }

    if ( mime_entity_data || mime_all_data ) {
        String* s = new String((const u_char*)buf, len, false);

        if ( mime_entity_data )
            entity_content.push_back(s);
        if ( mime_all_data )
            all_content.push_back(s);
    }

    if ( mime_segment_data ) {
        const char* data = (char*)data_buffer->Bytes() + data_start;
        int data_len = (buf + len) - data;

        analyzer->EnqueueConnEvent(mime_segment_data, analyzer->ConnVal(), val_mgr->Count(data_len),
                                   make_intrusive<StringVal>(data_len, data));
    }

    cur_entity_id = file_mgr->DataIn(reinterpret_cast<const u_char*>(buf), len, analyzer->GetAnalyzerTag(),
                                     analyzer->Conn(), is_orig, cur_entity_id);

    cur_entity_len += len;
    buffer_start = (buf + len) - (char*)data_buffer->Bytes();
}

bool MIME_Mail::RequestBuffer(int* plen, char** pbuf) {
    data_start = buffer_start - min_overlap_length;
    if ( data_start < 0 )
        data_start = 0;

    int overlap = buffer_start - data_start;
    int buffer_end = data_start + max_chunk_length;
    if ( buffer_end > data_buffer->Len() ) {
        // Copy every thing in [data_start, buffer_start) to
        // [0, overlap).
        if ( buffer_start > data_start )
            memcpy(data_buffer->Bytes(), data_buffer->Bytes() + data_start, overlap);
        data_start = 0;
        buffer_start = overlap;
    }

    *plen = max_chunk_length - overlap;
    *pbuf = (char*)data_buffer->Bytes() + buffer_start;

    return true;
}

void MIME_Mail::SubmitAllData() {
    if ( mime_all_data ) {
        String* s = concatenate(all_content);
        delete_strings(all_content);

        analyzer->EnqueueConnEvent(mime_all_data, analyzer->ConnVal(), val_mgr->Count(s->Len()),
                                   make_intrusive<StringVal>(s));
    }
}

void MIME_Mail::SubmitEvent(int event_type, const char* detail) {
    const char* category = "";

    switch ( event_type ) {
        case MIME_EVENT_ILLEGAL_FORMAT: category = "illegal format"; break;

        case MIME_EVENT_ILLEGAL_ENCODING: category = "illegal encoding"; break;

        default: reporter->AnalyzerError(GetAnalyzer(), "unrecognized MIME_Mail event"); return;
    }

    if ( mime_event )
        analyzer->EnqueueConnEvent(mime_event, analyzer->ConnVal(), make_intrusive<StringVal>(category),
                                   make_intrusive<StringVal>(detail));
}

} // namespace zeek::analyzer::mime
