/* SPDX-License-Identifier: GPL-2.0-only */

#include <b64_decode.h>
#include <console/console.h>

/*
 * Translation Table to decode base64 ASCII stream into binary. Borrowed from
 *
 * http://base64.sourceforge.net/b64.c.
 *
 */
static const char cd64[] = "|$$$}rstuvwxyz{$$$$$$$>?@ABCDEFGHIJKLMN"
	"OPQRSTUVW$$$$$$XYZ[\\]^_`abcdefghijklmnopq";

struct buffer_descriptor {
	const uint8_t *input_buffer;
	size_t data_size;
	size_t input_index;
};

#define isalnum(c) ((((c) >= 'a') && ((c) <= 'z')) || \
		    (((c) >= 'A') && ((c) <= 'Z')) || \
		    (((c) >= '0') && ((c) <= '9')))

/*
 * On each invocation this function returns the next valid base64 character
 * from the encoded message, ignoring padding and line breaks.
 *
 * Once all input is consumed, 0 is returned on all following invocations. In
 * case any other than expected characters is found in the encoded message, -1
 * is returned for error.
 */
static int get_next_char(struct buffer_descriptor *bd)
{
	uint8_t c;

	/*
	 * The canonical base64 encoded messages include the following
	 * characters:
	 * - '0..9A..Za..z+/' to represent 64 values
	 * - '=' for padding
	 * - '<CR><LF>' to split the message into lines.
	 */
	while (bd->input_index < bd->data_size) {
		c = bd->input_buffer[bd->input_index++];

		switch (c) {
		case '=':
		case 0xa:
		case 0xd:
			continue;

		default:
			break;
		}

		if (!isalnum(c) && (c != '+') && (c != '/'))
			return -1;

		return c;
	}

	return 0;
}

/*
** decode
**
** decode a base64 encoded stream discarding padding and line breaks.
*/
size_t b64_decode(const uint8_t *input_data,
		  size_t input_length,
		  uint8_t *output_data)
{
	struct buffer_descriptor bd;
	unsigned int interim = 0;
	size_t output_size = 0;
	/* count of processed input bits, modulo log2(64) */
	unsigned int bit_count = 0;

	/*
	 * Keep the context on the stack to make things easier if this needs
	 * to run with CAR.
	 */
	bd.input_buffer = input_data;
	bd.data_size = input_length;
	bd.input_index = 0;

	while (1) { /* Until input is exhausted. */
		int v = get_next_char(&bd);

		if (v < 0) {
			printk(BIOS_ERR,
			       "Incompatible character at offset %zd.\n",
			       bd.input_index);
			return 0;
		}

		if (!v)
			break;

		/*
		 * v is guaranteed to be in the proper range for cd64, the
		 * result is a 6 bit number.
		 */
		v = cd64[v - 43] - 62;

		if (bit_count >= 2) {
			/*
			 * Once 6 more bits are added to the output, there is
			 * going to be at least a full byte.
			 *
			 * 'remaining_bits' is the exact number of bits which
			 * need to be added to the output to have another full
			 * byte ready.
			 */
			int remaining_bits = 8 - bit_count;

			interim <<= remaining_bits;
			interim |= v >> (6 - remaining_bits);

			/* Pass the new full byte to the output. */
			output_data[output_size++] = interim & 0xff;

			interim = v;
			bit_count -= 2;
		} else {
			interim <<= 6;
			interim |= v;
			bit_count += 6;
		}
	}

	return output_size;
}
