/*
 * Base 64 decoding functions
 *
 * SPDX-FileType: SOURCE
 * SPDX-FileCopyrightText: Michael Bäuerle
 * SPDX-License-Identifier: BSD-2-Clause
 */

#include <assert.h>

#include "libbasexx-0/base64_decode.h"


#define BXX0_I_DECODE_MAPPING_SIZE  128U


/* ========================================================================== */
/*
 * Mapping table access
 *
 * Independent of execution character set.
 *
 * Return bit group corresponding to Base 64 alphabet US-ASCII character "c".
 * Return 0x80 for the Pad Character.
 * Return 0xFF for other Non-Alphabet Characters.
 */
static unsigned char bxx0_i_base64_mapping(const unsigned char c)
{
    static const unsigned char mapping[BXX0_I_DECODE_MAPPING_SIZE] =
    {
        0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
        0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
        0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
        0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
        0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
        0xFF, 0xFF, 0xFF, 0x3E, 0xFF, 0xFF, 0xFF, 0x3F,
        0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A, 0x3B,
        0x3C, 0x3D, 0xFF, 0xFF, 0xFF, 0x80, 0xFF, 0xFF,
        0xFF, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06,
        0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E,
        0x0F, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16,
        0x17, 0x18, 0x19, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
        0xFF, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, 0x20,
        0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28,
        0x29, 0x2A, 0x2B, 0x2C, 0x2D, 0x2E, 0x2F, 0x30,
        0x31, 0x32, 0x33, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
    };

    /* Check would be required for 256 elements too if (CHAR_BIT > 8) */
    return (BXX0_I_DECODE_MAPPING_SIZE > c) ? mapping[c] : 0xFF;
}


/* ========================================================================== */
/*
 * Write bit group "bg" into "quad" at index pointed to by "i_quad"
 *
 * The index at "i_quad" must be lower than four and is incremented after write
 * access.
 *
 * Return nonzero if quad is completely populated.
 */
static unsigned char bxx0_i_base64_quad_add(unsigned char *quad, size_t *i_quad,
                                            const unsigned char bg)
{
    assert(4U > *i_quad);
    quad[(*i_quad)++] = bg;

    if (4U == *i_quad)
        return 1;
    else
        return 0;
}


/* ========================================================================== */
/*
 * Combine four bit groups from "quad" and write three octets to "triple"
 */
static void bxx0_i_base64_combine(unsigned char       *triple,
                                  const unsigned char *quad)
{
    triple[0]  =  (quad[0] & 0x3F) << 2;
    triple[0] |= ((quad[1] & 0x30) >> 4);

    triple[1]  =  (quad[1] & 0x0F) << 4;
    triple[1] |= ((quad[2] & 0x3C) >> 2);

    triple[2]  =  (quad[2] & 0x03) << 6;
    triple[2] |=  (quad[3] & 0x3F);
}


/* ========================================================================== */
/*
 * Check unused bits of last bit group in "quad" to be zero
 *
 * The index "i_quad" must be two or three.
 *
 * Return code suitable for API.
 */
static signed char bxx0_i_base64_check(const unsigned char *quad,
                                       const size_t         i_quad,
                                       const unsigned char  flags,
                                       signed char          ret)
{
    unsigned char invpad = 0;

    assert((2U == i_quad) || (3U == i_quad));
    if ((2U == i_quad) && (quad[1] & 0x0F))
        invpad = 1;
    if ((3U == i_quad) && (quad[2] & 0x03))
        invpad = 1;

    if (invpad)
    {
        if (BXX0_BASE64_DECODE_FLAG_INVTAIL & flags)
            ret |= BXX0_BASE64_DECODE_FLAG_INVTAIL;
        else
            return BXX0_BASE64_DECODE_ERROR_TAIL;
    }

    return ret;
}


/* ========================================================================== */
/*
 * Read tail (padded quad) from "quad" and write to "out" at index "i_out"
 *
 * The value for "padding" must be one or two (all zero bit groups at the end).
 */
static void bxx0_i_base64_tail(unsigned char       *out , size_t       *i_out,
                               const unsigned char *quad, const size_t  padding)
{
    const size_t  octets    = 3U - padding;
    unsigned char triple[3] = { 0, 0, 0 };

    bxx0_i_base64_combine(triple, quad);

    assert((1U == octets) || (2U == octets));
    out[*i_out]          = triple[0];
    if (2U == octets)
        out[*i_out + 1U] = triple[1];

    *i_out += octets;
}


/* ========================================================================== */
/*
 * Handling of Pad character (in processing loop)
 *
 * Increment "padding".
 * Append an all zero bit group to "quad" at index i_quad".
 *
 * Return code suitable for API.
 */
static signed char bxx0_base64_padding(unsigned char *out    , size_t *i_out,
                                       unsigned char *quad   , size_t *i_quad,
                                       const size_t  *len_in , size_t *i_in,
                                       size_t              *padding,
                                       const unsigned char  flags,
                                       signed char          ret)
{
    ++(*padding);  /* The main conversion loop must be able to see this */

    if (2U > *i_quad)
        return BXX0_BASE64_DECODE_ERROR_PAD;

    ret = bxx0_i_base64_check(quad, *i_quad, flags, ret);
    if (0 > ret)
        return ret;

    if (bxx0_i_base64_quad_add(quad, i_quad, 0))
    {
        bxx0_i_base64_tail(out, i_out, quad, *padding);
        *i_quad  = 0;
        *padding = 0;

        if (1U < (*len_in - *i_in))
        {
            if (BXX0_BASE64_DECODE_FLAG_CONCAT & flags)
                ret |= BXX0_BASE64_DECODE_FLAG_CONCAT;
            else
            {
                ++(*i_in);  /* Data _after_ padding is not accepted */
                return BXX0_BASE64_DECODE_ERROR_DAP;
            }
        }
    }

    return ret;
}


/* ========================================================================== */
/*
 * Handle tail without Pad characters (after processing loop)
 *
 * Return code suitable for API.
 */
static signed char bxx0_i_base64_nopadding(unsigned char *out,  size_t *i_out,
                                           unsigned char *quad, size_t *i_quad,
                                           const unsigned char flags,
                                           signed char         ret)
{
    size_t padding = 0;

    /* Do nothing on error or when no tail is present */
    if ((0 > ret) || (0U == *i_quad))
        return ret;

    if (!(BXX0_BASE64_DECODE_FLAG_NOPAD & flags))
        return ret;
    else
        ret |= BXX0_BASE64_DECODE_FLAG_NOPAD;

    if ((2U > *i_quad) || (3U < *i_quad))
        return BXX0_BASE64_DECODE_ERROR_TAIL;

    ret = bxx0_i_base64_check(quad, *i_quad, flags, ret);
    if (0 > ret)
        return ret;

    do
        ++padding;
    while (!bxx0_i_base64_quad_add(quad, i_quad, 0));

    bxx0_i_base64_tail(out, i_out, quad, padding);
    *i_quad = 0;

    return ret;
}


/* ========================================================================== */
signed char bxx0_base64_decode(unsigned char       *out, size_t *len_out,
                               const unsigned char *in , size_t *len_in,
                               const unsigned char flags)
{
    signed char   ret     = 0;  /* Return value */
    size_t        padding = 0;  /* Number of processed Pad characters */
    size_t        i_in    = 0;
    size_t        i_out   = 0;
    size_t        i_quad  = 0;
    unsigned char quad[4] = { 0, 0, 0, 0 };  /* Four 6-bit groups */

    /* Prevent overflow of output buffer */
    if (BXX0_BASE64_DECODE_LEN_OUT(*len_in) > *len_out)
        return BXX0_BASE64_DECODE_ERROR_SIZE;

    for (; *len_in > i_in; ++i_in)
    {
        const unsigned char bg = bxx0_i_base64_mapping(in[i_in]);

        if (0xC0 & bg)
        {
            /* Non-Alphabet character */
            if (0x80U == bg)
                ret = bxx0_base64_padding(out, &i_out, quad, &i_quad,
                                          len_in, &i_in, &padding, flags, ret);
            else if (BXX0_BASE64_DECODE_FLAG_IGNORE & flags)
                ret |= BXX0_BASE64_DECODE_FLAG_IGNORE;
            else
                ret = BXX0_BASE64_DECODE_ERROR_NAC;

            if (0 > ret)
                break;
        }
        else if (bxx0_i_base64_quad_add(quad, &i_quad, bg))
        {
            if (padding)
            {
                assert(0U < i_quad);
                --i_quad;
                ret = BXX0_BASE64_DECODE_ERROR_PAD;
                break;
            }
            bxx0_i_base64_combine(&out[i_out], quad);
            i_out  += 3U;
            i_quad  = 0;
        }
    }

    ret = bxx0_i_base64_nopadding(out, &i_out, quad, &i_quad, flags, ret);

    /* Handling for potential unconsumed data from last quad */
    assert(i_in >= i_quad);
    i_in -= i_quad;

    assert(*len_in  >= i_in);
    *len_in  -= i_in;
    assert(*len_out >= i_out);
    *len_out -= i_out;

    return ret;
}
