/*
 * Script file support library
 *
 * Copyright (C) 2021-2024 Michał Trojnara <Michal.Trojnara@stunnel.org>
 */

#include "osslsigncode.h"
#include "helpers.h"
#include "utf.h"

typedef enum {
    comment_hash,
    comment_xml,
    comment_c,
    comment_js,
    comment_not_found
} COMMENT_STYLE;

typedef struct {
    const char *extension;
    COMMENT_STYLE comment;
} SCRIPT_FORMAT;

const SCRIPT_FORMAT supported_formats[] = {
    {".ps1",    comment_hash},
    {".ps1xml", comment_xml},
    {".psc1",   comment_xml},
    {".psd1",   comment_hash},
    {".psm1",   comment_hash},
    {".cdxml",  comment_xml},
    {".mof",    comment_c},
    {".js",     comment_js},
    {NULL,      comment_not_found},
};

#define header_hash "SIG # Begin signature block"
#define footer_hash "SIG # End signature block"
#define header_js "Begin signature block"
#define footer_js "End signature block"

typedef struct {
    const char *open, *close, *header, *footer;
} SCRIPT_COMMENT;

const SCRIPT_COMMENT comment_text[] = {
    [comment_hash] = {"# ", "", header_hash, footer_hash},
    [comment_xml]  = {"<!-- ", " -->", header_hash, footer_hash},
    [comment_c]    = {"/* ", " */", header_hash, footer_hash},
    [comment_js]   = {"// SIG // ", "", header_js, footer_js}
};

struct script_ctx_st {
    const SCRIPT_COMMENT *comment_text;
    int utf;
    uint32_t sigpos;
    uint32_t fileend;
};

#define LINE_MAX_LEN 100

/* FILE_FORMAT method prototypes */
static FILE_FORMAT_CTX *script_ctx_new(GLOBAL_OPTIONS *options, BIO *hash, BIO *outdata);
static ASN1_OBJECT *script_spc_sip_info_get(u_char **p, int *plen, FILE_FORMAT_CTX *ctx);
static PKCS7 *script_pkcs7_contents_get(FILE_FORMAT_CTX *ctx, BIO *hash, const EVP_MD *md);
static int script_hash_length_get(FILE_FORMAT_CTX *ctx);
static u_char *script_digest_calc(FILE_FORMAT_CTX *ctx, const EVP_MD *md);
static int script_verify_digests(FILE_FORMAT_CTX *ctx, PKCS7 *p7);
static PKCS7 *script_pkcs7_extract(FILE_FORMAT_CTX *ctx);
static PKCS7 *script_pkcs7_extract_to_nest(FILE_FORMAT_CTX *ctx);
static int script_remove_pkcs7(FILE_FORMAT_CTX *ctx, BIO *hash, BIO *outdata);
static int script_process_data(FILE_FORMAT_CTX *ctx, BIO *hash, BIO *outdata);
static PKCS7 *script_pkcs7_signature_new(FILE_FORMAT_CTX *ctx, BIO *hash);
static int script_append_pkcs7(FILE_FORMAT_CTX *ctx, BIO *outdata, PKCS7 *p7);
static void script_bio_free(BIO *hash, BIO *outdata);
static void script_ctx_cleanup(FILE_FORMAT_CTX *ctx);
static int script_is_detaching_supported(void);

FILE_FORMAT file_format_script = {
    .ctx_new        = script_ctx_new,
    .data_blob_get  = script_spc_sip_info_get,
    .pkcs7_contents_get = script_pkcs7_contents_get,
    .hash_length_get = script_hash_length_get,
    .digest_calc    = script_digest_calc,
    .verify_digests = script_verify_digests,
    .pkcs7_extract  = script_pkcs7_extract,
    .pkcs7_extract_to_nest = script_pkcs7_extract_to_nest,
    .remove_pkcs7   = script_remove_pkcs7,
    .process_data   = script_process_data,
    .pkcs7_signature_new = script_pkcs7_signature_new,
    .append_pkcs7   = script_append_pkcs7,
    .bio_free       = script_bio_free,
    .ctx_cleanup    = script_ctx_cleanup,
    .is_detaching_supported = script_is_detaching_supported
};

/* helper functions */
static SCRIPT_CTX *script_ctx_get(char *indata, uint32_t filesize, const SCRIPT_COMMENT *comment, int utf);
static int write_commented(FILE_FORMAT_CTX *ctx, BIO *outdata, const char *data, size_t length);
static int write_in_encoding(FILE_FORMAT_CTX *ctx, BIO *outdata, const char *line, size_t length);
static size_t utf8_to_utf16(const char *data, size_t len, uint16_t **out_utf16);
static size_t utf16_to_utf8(const uint16_t *data, size_t len, char **out_utf8);
static BIO *script_digest_calc_bio(FILE_FORMAT_CTX *ctx, const EVP_MD *md);
static int script_digest_convert(BIO *hash, FILE_FORMAT_CTX *ctx, size_t len);
static int script_write_bio(BIO *data, char *indata, size_t len);
static int script_check_file(FILE_FORMAT_CTX *ctx);

/*
 * Allocate and return a script file format context.
 * [in, out] options: structure holds the input data
 * [out] hash: message digest BIO
 * [in] outdata: outdata file BIO (unused)
 * [returns] pointer to script file format context
 */
static FILE_FORMAT_CTX *script_ctx_new(GLOBAL_OPTIONS *options, BIO *hash, BIO *outdata)
{
    FILE_FORMAT_CTX *ctx;
    SCRIPT_CTX *script_ctx;
    const SCRIPT_FORMAT *fmt;
    uint32_t filesize;
    const uint8_t utf16_bom[] = {0xff, 0xfe};
    size_t name_len;
    int utf;

    /* squash the unused parameter warning */
    (void)outdata;

    /* find out whether our format is supported */
    name_len = strlen(options->infile);
    for (fmt = supported_formats; fmt->comment != comment_not_found; fmt++) {
        size_t ext_len = strlen(fmt->extension);
        if(name_len > ext_len && !strcasecmp(options->infile + name_len - ext_len, fmt->extension))
            break;
    }
    if (fmt->comment == comment_not_found)
        return NULL;
    printf("Script file format: %s\n", fmt->extension);

    filesize = get_file_size(options->infile);
    if (filesize == 0)
        return NULL; /* FAILED */

    options->indata = map_file(options->infile, filesize);
    if (!options->indata) {
        return NULL; /* FAILED */
    }
    utf = memcmp(options->indata, utf16_bom, sizeof utf16_bom) ? 8 : 16;

    /* initialize script context */
    script_ctx = script_ctx_get(options->indata, filesize, comment_text + fmt->comment, utf);
    if (!script_ctx) {
        unmap_file(options->indata, filesize);
        return NULL; /* FAILED */
    }

    /* initialize file format context */
    ctx = OPENSSL_malloc(sizeof(FILE_FORMAT_CTX));
    memset(ctx, 0, sizeof(FILE_FORMAT_CTX));
    ctx->format = &file_format_script;
    ctx->options = options;
    ctx->script_ctx = script_ctx;

    if (hash)
        BIO_push(hash, BIO_new(BIO_s_null()));

    /* FIXME: user interface logic belongs to osslsigncode.c */
    if (options->pagehash == 1)
        printf("Warning: -ph option is only valid for PE files\n");
    if (options->jp >= 0)
        printf("Warning: -jp option is only valid for CAB files\n");
    return ctx;
}

/*
 * Allocate and return SpcSipInfo object.
 * Subject Interface Package (SIP) is an internal Microsoft API for
 * transforming arbitrary files into a digestible stream.
 * These ClassIDs are found in the indirect data section and identify
 * the type of processor needed to validate the signature.
 * https://github.com/sassoftware/relic/blob/620d0b75ec67c0158a8a9120950abe04327d922f/lib/authenticode/structs.go#L154
 * [out] p: SpcSipInfo data
 * [out] plen: SpcSipInfo data length
 * [in] ctx: structure holds input and output data
 * [returns] pointer to ASN1_OBJECT structure corresponding to SPC_SIPINFO_OBJID
 */
static ASN1_OBJECT *script_spc_sip_info_get(u_char **p, int *plen, FILE_FORMAT_CTX *ctx)
{
    const u_char SpcUUIDSipInfoPs[] = {
        0x1f, 0xcc, 0x3b, 0x60, 0x59, 0x4b, 0x08, 0x4e,
        0xb7, 0x24, 0xd2, 0xc6, 0x29, 0x7e, 0xf3, 0x51
    };
    ASN1_OBJECT *dtype;
    SpcSipInfo *si = SpcSipInfo_new();

    /* squash the unused parameter warning */
    (void)ctx;

    ASN1_INTEGER_set(si->a, 65536);
    ASN1_INTEGER_set(si->b, 0);
    ASN1_INTEGER_set(si->c, 0);
    ASN1_INTEGER_set(si->d, 0);
    ASN1_INTEGER_set(si->e, 0);
    ASN1_INTEGER_set(si->f, 0);
    ASN1_OCTET_STRING_set(si->string, SpcUUIDSipInfoPs, sizeof SpcUUIDSipInfoPs);
    *plen = i2d_SpcSipInfo(si, NULL);
    *p = OPENSSL_malloc((size_t)*plen);
    i2d_SpcSipInfo(si, p);
    *p -= *plen;
    dtype = OBJ_txt2obj(SPC_SIPINFO_OBJID, 1);
    SpcSipInfo_free(si);
    return dtype; /* OK */
}

/*
 * Allocate and return a data content to be signed.
 * [in] ctx: structure holds input and output data
 * [in] hash: message digest BIO
 * [in] md: message digest algorithm
 * [returns] data content
 */
static PKCS7 *script_pkcs7_contents_get(FILE_FORMAT_CTX *ctx, BIO *hash, const EVP_MD *md)
{
    ASN1_OCTET_STRING *content;
    BIO *bhash;

    /* squash the unused parameter warning */
    (void)hash;

    bhash = script_digest_calc_bio(ctx, md);
    if (!bhash) {
        return NULL; /* FAILED */
    }
    content = spc_indirect_data_content_get(bhash, ctx);
    BIO_free_all(bhash);
    return pkcs7_set_content(content);
}

static int script_hash_length_get(FILE_FORMAT_CTX *ctx)
{
    return EVP_MD_size(ctx->options->md);
}

/*
 * Compute a simple sha1/sha256 message digest of the MSI file
 * for use with a catalog file.
 * [in] ctx: structure holds input and output data
 * [in] md: message digest algorithm
 * [returns] pointer to calculated message digest
 */
static u_char *script_digest_calc(FILE_FORMAT_CTX *ctx, const EVP_MD *md)
{
    u_char *mdbuf;
    BIO *hash = BIO_new(BIO_f_md());

#if defined(__GNUC__)
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wcast-qual"
#endif
    if (!BIO_set_md(hash, md)) {
        fprintf(stderr, "Unable to set the message digest of BIO\n");
        BIO_free_all(hash);
        return NULL; /* FAILED */
    }
#if defined(__GNUC__)
#pragma GCC diagnostic pop
#endif
    BIO_push(hash, BIO_new(BIO_s_null()));
    if (!script_write_bio(hash, ctx->options->indata, ctx->script_ctx->fileend)) {
        BIO_free_all(hash);
        return NULL; /* FAILED */
    }
    mdbuf = OPENSSL_malloc((size_t)EVP_MD_size(md));
    BIO_gets(hash, (char*)mdbuf, EVP_MD_size(md));
    BIO_free_all(hash);
    return mdbuf; /* OK */
}

/*
 * Calculate the hash and compare to PKCS#7 signedData.
 * [in] ctx: structure holds input and output data
 * [in] p7: PKCS#7 signature
 * [returns] 0 on error or 1 on success
 */
static int script_verify_digests(FILE_FORMAT_CTX *ctx, PKCS7 *p7)
{
    int mdtype = -1;
    u_char mdbuf[EVP_MAX_MD_SIZE];
    u_char *cmdbuf = NULL;
    const EVP_MD *md;
    BIO *bhash;

    if (!pkcs7_get_content_digest(p7, mdbuf, &mdtype)) {
        fprintf(stderr, "Failed to extract current message digest\n\n");
        return 0; /* FAILED */
    }
    md = EVP_get_digestbynid(mdtype);
    bhash = script_digest_calc_bio(ctx, md);
    if (!bhash)
        return 0; /* FAILED */

    cmdbuf = OPENSSL_malloc((size_t)EVP_MD_size(md));
    BIO_gets(bhash, (char*)cmdbuf, EVP_MD_size(md));
    BIO_free_all(bhash);

    if (!compare_digests(mdbuf, cmdbuf, mdtype)) {
        fprintf(stderr, "Signature verification: failed\n\n");
        OPENSSL_free(cmdbuf);
        return 0; /* FAILED */
    }
    OPENSSL_free(cmdbuf);
    return 1; /* OK */
}

/*
 * Extract existing signature in DER format.
 * [in] ctx: structure holds input and output data
 * [returns] pointer to PKCS#7 structure
 */
static PKCS7 *script_pkcs7_extract(FILE_FORMAT_CTX *ctx)
{
    const char *signature_data = ctx->options->indata + ctx->script_ctx->sigpos;
    size_t signature_len = ctx->script_ctx->fileend - ctx->script_ctx->sigpos;
    size_t base64_len;
    char *ptr;
    BIO *bio_mem, *bio_b64 = NULL;
    char *base64_data = NULL;
    char *clean_base64 = NULL;
    int clean_base64_len = 0;
    const char *open_tag = ctx->script_ctx->comment_text->open;
    const char *close_tag = ctx->script_ctx->comment_text->close;
    size_t open_tag_len = strlen(open_tag);
    size_t close_tag_len = strlen(close_tag);
    size_t header_len = strlen(ctx->script_ctx->comment_text->header);
    size_t footer_len = strlen(ctx->script_ctx->comment_text->footer);
    PKCS7 *retval = NULL;

    if (!script_check_file(ctx)) {
        return NULL; /* FAILED, no signature */
    }
    /* extract Base64 signature */
    if (ctx->script_ctx->utf == 8) {
        base64_len = signature_len;
        base64_data = OPENSSL_malloc(base64_len);
        if (!base64_data)
            return NULL; /* memory allocation failed */
        memcpy(base64_data, signature_data, base64_len);
    } else {
        base64_len = utf16_to_utf8((const void *)signature_data,
            signature_len, &base64_data);
    }

    /* allocate memory for cleaned Base64 */
    clean_base64 = OPENSSL_malloc(base64_len);
    if (!clean_base64) {
        fprintf(stderr, "Malloc failed\n");
        goto cleanup;
    }

    /* copy clean Base64 data */
    for (ptr = base64_data;;) {
        /* find the opening tag */
        for(;;) {
            if (ptr + open_tag_len >= base64_data + base64_len) {
                fprintf(stderr, "Signature line too long\n");
                goto cleanup;
            }
            if (!memcmp(ptr, open_tag, (size_t)open_tag_len)) {
                ptr += open_tag_len;
                break;
            }
            ptr++;
        }
        /* process header and footer */
        if (ptr + header_len < base64_data + base64_len &&
                !memcmp(ptr, ctx->script_ctx->comment_text->header, header_len))
            ptr += header_len;
        if (ptr + footer_len <= base64_data + base64_len &&
                !memcmp(ptr, ctx->script_ctx->comment_text->footer, footer_len))
            break; /* success */

        /* copy until the closing tag */
        for(;;) {
            if (ptr + close_tag_len >= base64_data + base64_len) {
                fprintf(stderr, "Signature line too long\n");
                goto cleanup;
            }
            if (close_tag_len) {
                if (!memcmp(ptr, close_tag, (size_t)close_tag_len)) {
                    ptr += close_tag_len;
                    break;
                }
            }
            if (*ptr == '\r') {
                ptr++;
            } else if (*ptr == '\n') {
                ptr++;
                break;
            } else {
                clean_base64[clean_base64_len++] = *ptr++;
            }
        }
    }

    /* prepare for Base64 decoding */
    bio_mem = BIO_new_mem_buf(clean_base64, clean_base64_len);
    bio_b64 = BIO_new(BIO_f_base64());
    BIO_push(bio_b64, bio_mem);
    BIO_set_flags(bio_b64, BIO_FLAGS_BASE64_NO_NL);

    /* decode DER */
    retval = d2i_PKCS7_bio(bio_b64, NULL);

cleanup:
    OPENSSL_free(base64_data);
    OPENSSL_free(clean_base64);
    BIO_free_all(bio_b64);
    return retval;
}

/*
 * Extract existing signature in DER format.
 * [in] ctx: structure holds input and output data
 * [returns] pointer to PKCS#7 structure
 */
static PKCS7 *script_pkcs7_extract_to_nest(FILE_FORMAT_CTX *ctx)
{
    return script_pkcs7_extract(ctx);
}

/*
 * Remove existing signature.
 * [in, out] ctx: structure holds input and output data
 * [out] hash: message digest BIO
 * [out] outdata: outdata file BIO
 * [returns] 1 on error or 0 on success
 */
static int script_remove_pkcs7(FILE_FORMAT_CTX *ctx, BIO *hash, BIO *outdata)
{
    /* squash the unused parameter warning */
    (void)hash;
    if (!script_check_file(ctx)) {
        return 1; /* FAILED, no signature */
    }
    if (!script_write_bio(outdata, ctx->options->indata, ctx->script_ctx->sigpos)) {
        return 1; /* FAILED */
    }
    return 0; /* OK */
}

/*
 * Initialize outdata file and calculate a hash (message digest) of data.
 * [in, out] ctx: structure holds input and output data
 * [out] hash: message digest BIO
 * [out] outdata: outdata file BIO
 * [returns] 1 on error or 0 on success
 */
static int script_process_data(FILE_FORMAT_CTX *ctx, BIO *hash, BIO *outdata)
{
    if (ctx->script_ctx->sigpos > 0) {
        /* Strip current signature */
        ctx->script_ctx->fileend = ctx->script_ctx->sigpos;
    }
    if (!script_write_bio(outdata, ctx->options->indata, ctx->script_ctx->fileend))
        return 0; /* FAILED */
    if (!script_digest_convert(hash, ctx, ctx->script_ctx->fileend))
        return 0; /* FAILED */
    return 1; /* OK */
}

/*
 * Create a new PKCS#7 signature.
 * [in, out] ctx: structure holds input and output data
 * [out] hash: message digest BIO
 * [returns] pointer to PKCS#7 structure
 */
static PKCS7 *script_pkcs7_signature_new(FILE_FORMAT_CTX *ctx, BIO *hash)
{
    ASN1_OCTET_STRING *content;
    PKCS7 *p7 = pkcs7_create(ctx);

    if (!p7) {
        fprintf(stderr, "Creating a new signature failed\n");
        return NULL; /* FAILED */
    }
    if (!add_indirect_data_object(p7)) {
        fprintf(stderr, "Adding SPC_INDIRECT_DATA_OBJID failed\n");
        PKCS7_free(p7);
        return NULL; /* FAILED */
    }
    content = spc_indirect_data_content_get(hash, ctx);
    if (!content) {
        fprintf(stderr, "Failed to get spcIndirectDataContent\n");
        return NULL; /* FAILED */
    }
    if (!sign_spc_indirect_data_content(p7, content)) {
        fprintf(stderr, "Failed to set signed content\n");
        PKCS7_free(p7);
        ASN1_OCTET_STRING_free(content);
        return NULL; /* FAILED */
    }
    ASN1_OCTET_STRING_free(content);
    return p7;
}

/*
 * Append signature to the outfile.
 * [in, out] ctx: structure holds input and output data
 * [out] outdata: outdata file BIO
 * [in] p7: PKCS#7 signature
 * [returns] 1 on error or 0 on success
 */
static int script_append_pkcs7(FILE_FORMAT_CTX *ctx, BIO *outdata, PKCS7 *p7)
{
    BIO *bio, *b64;
    BUF_MEM *buffer;
    size_t i;
    static const char crlf[] = {0x0d, 0x0a};
    int ret = 1;

    /* convert to BASE64 */
    b64 = BIO_new(BIO_f_base64()); /* BIO for base64 encoding */
    if (!b64)
        return 1; /* FAILED */
    BIO_set_flags(b64, BIO_FLAGS_BASE64_NO_NL);
    bio = BIO_new(BIO_s_mem()); /* BIO to hold the base64 data */
    if (!bio) {
        BIO_free(b64);
        return 1; /* FAILED */
    }
    bio = BIO_push(b64, bio); /* chain base64 BIO onto memory BIO */
    if (!i2d_PKCS7_bio(bio, p7)) {
        BIO_free_all(bio);
        return 1; /* FAILED */
    }
    (void)BIO_flush(bio);
    BIO_get_mem_ptr(bio, &buffer);
    (void)BIO_set_close(bio, BIO_NOCLOSE);

    /* split to individual lines and write to outdata */
    if (!write_commented(ctx, outdata,
            ctx->script_ctx->comment_text->header,
            strlen(ctx->script_ctx->comment_text->header)))
        goto cleanup;
    for (i = 0; i < buffer->length; i += 64) {
        if (!write_commented(ctx, outdata, buffer->data + i,
            buffer->length - i < 64 ? buffer->length - i : 64)) {
            goto cleanup;
        }
    }
    if (!write_commented(ctx, outdata,
            ctx->script_ctx->comment_text->footer,
            strlen(ctx->script_ctx->comment_text->footer)))
        goto cleanup;

    /* signtool expects CRLF terminator at the end of the text file */
    if (!write_in_encoding(ctx, outdata, crlf, sizeof crlf))
        goto cleanup;
    ret = 0;  /* OK */

cleanup:
    BUF_MEM_free(buffer);
    BIO_free_all(bio);
    return ret;
}

/*
 * Free up an entire outdata BIO chain.
 * [out] hash: message digest BIO
 * [out] outdata: outdata file BIO
 * [returns] none
 */
static void script_bio_free(BIO *hash, BIO *outdata)
{
    BIO_free_all(hash);
    BIO_free_all(outdata);
}

/*
 * Deallocate a FILE_FORMAT_CTX structure and script format specific structures.
 * [in, out] ctx: structure holds input and output data
 * [out] hash: message digest BIO
 * [out] outdata: outdata file BIO
 * [returns] none
 */
static void script_ctx_cleanup(FILE_FORMAT_CTX *ctx)
{
    unmap_file(ctx->options->indata, ctx->script_ctx->fileend);
    OPENSSL_free(ctx->script_ctx);
    OPENSSL_free(ctx);
}

static int script_is_detaching_supported(void)
{
    return 1; /* OK */
}

/*
 * Script helper functions
 */

static SCRIPT_CTX *script_ctx_get(char *indata, uint32_t filesize, const SCRIPT_COMMENT *comment, int utf)
{
    SCRIPT_CTX *script_ctx;

    const char *input_pos, *signature_pos, *ptr;
    uint32_t line[LINE_MAX_LEN], commented_header[40], cr, lf;
    size_t sig_pos = 0, line_pos = 0, commented_header_len = 0;
    size_t commented_header_size = sizeof commented_header / sizeof(uint32_t);

    utf8DecodeRune("\r", 1, &cr);
    utf8DecodeRune("\n", 1, &lf);

    /* compute runes for the commented signature header */
    for (ptr = comment->open;
            *ptr && commented_header_len < commented_header_size;
            commented_header_len++)
        ptr = utf8DecodeRune(ptr, 1, commented_header + commented_header_len);
    for (ptr = comment->header;
            *ptr && commented_header_len < commented_header_size;
            commented_header_len++)
        ptr = utf8DecodeRune(ptr, 1, commented_header + commented_header_len);
    for (ptr = comment->close;
            *ptr && commented_header_len < commented_header_size;
            commented_header_len++)
        ptr = utf8DecodeRune(ptr, 1, commented_header + commented_header_len);

    /* find the signature header */
    for (signature_pos = input_pos = indata; input_pos < indata + filesize; ) {
        const char *input_prev = input_pos;

        input_pos = utf == 8 ?
            utf8DecodeRune(input_pos,
                (size_t)(indata + filesize - input_pos),
                line + line_pos) :
            (const char *)utf16DecodeRune((const void *)input_pos,
                (size_t)(indata + filesize - input_pos)/2,
                line + line_pos);

        if (!memcmp(line + line_pos, &lf, sizeof lf)) {
            if (line_pos >= commented_header_len &&
                    !memcmp(line, commented_header, commented_header_len * sizeof(uint32_t))) {
                sig_pos = (size_t)(signature_pos - indata);
                if (!memcmp(line + line_pos - 1, &cr, sizeof cr))
                    sig_pos -= (size_t)utf / 8;
                break; /* SUCCEEDED */
            }
            line_pos = 0;
            signature_pos = input_prev; /* previous line */
        } else if (line_pos < LINE_MAX_LEN - 1) {
            line_pos++; /* we can ignore lines longer than our buffer */
        }
    }
    printf("Signature position: %zu\n", sig_pos);

    script_ctx = OPENSSL_malloc(sizeof(SCRIPT_CTX));
    script_ctx->comment_text = comment;
    script_ctx->utf = utf;
    script_ctx->fileend = filesize;
    script_ctx->sigpos = (uint32_t)sig_pos;
    return script_ctx; /* OK */
}

/* write a commented line to the bio:
 * - prepend with CRLF ("\r\n")
 * - add opening/closing comment tags
 * - adjust encoding if needed
 * [returns] 0 on error or 1 on success
 */
static int write_commented(FILE_FORMAT_CTX *ctx, BIO *outdata, const char *data, size_t length)
{
    const char *open_tag = ctx->script_ctx->comment_text->open;
    const char *close_tag = ctx->script_ctx->comment_text->close;
    size_t open_tag_len = strlen(open_tag);
    size_t close_tag_len = strlen(close_tag);
    char *line;

    /* the buffer needs to be long enough for:
     * - CRLF ("\r\n")
     * - opening tag
     * - up to 64 bytes of data
     * - closing tag
     * - trailing NUL ("\0") */
    line = OPENSSL_malloc(2 + open_tag_len + length + close_tag_len + 1);
    if (!line)
        return 0; /* memory allocation failed */
    strcpy(line, "\r\n");
    strcat(line, open_tag);
    memcpy(line + 2 + open_tag_len, data, length);
    line[2 + open_tag_len + length] = '\0';
    strcat(line, close_tag);

    /* adjust encoding */
    if (!write_in_encoding(ctx, outdata, line, strlen(line))) {
        OPENSSL_free(line);
        return 0; /* FAILED */
    }
    OPENSSL_free(line);
    return 1; /* OK */
}

/* adjust encoding if needed
 * [returns] 0 on error or 1 on success
 */
static int write_in_encoding(FILE_FORMAT_CTX *ctx, BIO *outdata, const char *line, size_t length)
{
    size_t written;
    if (ctx->script_ctx->utf == 8) {
        if (!BIO_write_ex(outdata, line, length, &written)
            || written != length) {
            return 0; /* FAILED */
        }
    } else {
        uint16_t *utf16_data = NULL;
        size_t utf16_len = utf8_to_utf16(line, length, &utf16_data);

        if (!BIO_write_ex(outdata, utf16_data, utf16_len, &written)
            || written != utf16_len) {
            OPENSSL_free(utf16_data);
            return 0; /* FAILED */
        }
        OPENSSL_free(utf16_data);
    }
    return 1; /* OK */
}

/* convert len bytes of UTF-8 to UTF-16
 * return the number of output bytes
 */
static size_t utf8_to_utf16(const char *data, size_t len, uint16_t **out_utf16)
{
    size_t utf16_len = utf8UTF16Count(data, len);
    *out_utf16 = OPENSSL_malloc(utf16_len * sizeof(uint16_t));
    if (!*out_utf16)
        return 0; /* memory allocation failed */

    const char *s = data;
    uint16_t *d = *out_utf16;
    uint32_t rune;
    size_t remaining_len = len;

    while (remaining_len > 0) {
        s = utf8DecodeRune(s, remaining_len, &rune);
        if (!s || s < data)
            break; /* invalid UTF-8 sequence */
        size_t consumed = (size_t)(s - data);

        remaining_len -= consumed;
        data = s;
        d += utf16EncodeRune(rune, d);
    }
    return (size_t)(2 * (d - *out_utf16));
}

/* convert len bytes of UTF-16 to UTF-8
 * return the number of output bytes
 */
static size_t utf16_to_utf8(const uint16_t *data, size_t len, char **out_utf8)
{
    size_t utf8_len = utf16UTF8Count(data, len/2);
    *out_utf8 = OPENSSL_malloc(utf8_len);
    if (!*out_utf8)
        return 0; /* memory allocation failed */

    const uint16_t *s = data;
    char *d = *out_utf8;
    uint32_t rune;
    size_t remaining_len = len/2;

    while (remaining_len > 0) {
        s = utf16DecodeRune(s, remaining_len, &rune);
        if (!s || s < data)
            break; /* invalid UTF-16 sequence */
        size_t consumed = (size_t)(s - data);

        remaining_len -= consumed;
        data = s;
        d += utf8EncodeRune(rune, d);
    }
    return (size_t)(d - *out_utf8);
}

/*
 * Compute a message digest value of a signed or unsigned script file.
 * [in] ctx: structure holds input and output data
 * [in] md: message digest algorithm
 * [returns] calculated message digest BIO
 */
static BIO *script_digest_calc_bio(FILE_FORMAT_CTX *ctx, const EVP_MD *md)
{
    size_t fileend;
    BIO *hash = BIO_new(BIO_f_md());

    if (ctx->script_ctx->sigpos)
        fileend = ctx->script_ctx->sigpos;
    else
        fileend = ctx->script_ctx->fileend;
#if defined(__GNUC__)
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wcast-qual"
#endif
    if (!BIO_set_md(hash, md)) {
        fprintf(stderr, "Unable to set the message digest of BIO\n");
        BIO_free_all(hash);
        return NULL; /* FAILED */
    }
#if defined(__GNUC__)
#pragma GCC diagnostic pop
#endif
    BIO_push(hash, BIO_new(BIO_s_null()));
    if (!script_digest_convert(hash, ctx, fileend)) {
        fprintf(stderr, "Unable calc a message digest value\n");
        BIO_free_all(hash);
        return NULL; /* FAILED */
    }
    return hash;
}

/*
 * Compute a message digest value
 * [in, out] hash: message digest BIO
 * [in] ctx: structure holds input and output data
 * [in] len: mapped file length
 * [returns] 0 on error or 1 on success
 */
static int script_digest_convert(BIO *hash, FILE_FORMAT_CTX *ctx, size_t len)
{
    if (ctx->script_ctx->utf == 8) { /* need to convert to UTF-16 */
        uint16_t *utf16_data = NULL;
        size_t utf16_len = utf8_to_utf16(ctx->options->indata,
            len, &utf16_data);

        if (!script_write_bio(hash, (char *)utf16_data, utf16_len)) {
            OPENSSL_free(utf16_data);
            return 0; /* FAILED */
        }
        OPENSSL_free(utf16_data);
    } else { /* already UTF-16 -> no need to convert */
        if (!script_write_bio(hash, ctx->options->indata, len)) {
            return 0; /* FAILED */
        }
    }
    return 1; /* OK */
}

/*
 * Write len bytes from data to BIO
 * [in, out] bio: message digest or outdata BIO
 * [in] indata: mapped file
 * [in] len: indata length
 * [returns] 0 on error or 1 on success
 */
static int script_write_bio(BIO *bio, char *indata, size_t len)
{
    size_t i = 0, written;

    while (len > 0) {
        if (!BIO_write_ex(bio, indata + i, len, &written))
            return 0; /* FAILED */
        len -= written;
        i += written;
    }
    return 1; /* OK */
}

/*
 * Check if the signature exists.
 * [in, out] ctx: structure holds input and output data
 * [returns] 0 on error or 1 on success
 */
static int script_check_file(FILE_FORMAT_CTX *ctx)
{
    if (!ctx) {
        fprintf(stderr, "Init error\n");
        return 0; /* FAILED */
    }
    if (ctx->script_ctx->sigpos == 0
        || ctx->script_ctx->sigpos > ctx->script_ctx->fileend) {
        fprintf(stderr, "No signature found\n");
        return 0; /* FAILED */
    }

    return 1; /* OK */
}

/*
Local Variables:
   c-basic-offset: 4
   tab-width: 4
   indent-tabs-mode: nil
End:

  vim: set ts=4 expandtab:
*/
