/*
Copyright (C) 2014 John E. Davis

This file is part of the S-Lang Library.

The S-Lang Library is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License as
published by the Free Software Foundation; either version 2 of the
License, or (at your option) any later version.

The S-Lang Library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
General Public License for more details.

You should have received a copy of the GNU General Public License
along with this library; if not, write to the Free Software
Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307,
USA.
*/
#include <stdio.h>
#include <string.h>
#include <ctype.h>
#include <slang.h>

SLANG_MODULE(base64);

static int Base64_Type_Id = 0;
typedef struct _Base64_Type Base64_Type;
struct _Base64_Type
{
   int type;
#define B64_TYPE_ENCODER	1
#define B64_TYPE_DECODER	2
   SLang_Name_Type *callback;
   SLang_Any_Type *callback_data;
   unsigned char *buffer;		       /* malloced buffer_size + overhead */
#define B64_ENCODE_BUFFER_SIZE 76    /* multiple of 4 */
#define B64_DECODE_BUFFER_SIZE 512
   unsigned int buffer_size;
   unsigned int num_buffered;
   unsigned char smallbuf[4];
   unsigned int smallbuf_len;
#define B64_CLOSED	0x1
#define B64_INVALID	0x2
   int flags;
};

static int check_b64_type (Base64_Type *b64, int type, int err)
{
   if (type && (b64->type != type))
     {
	if (err)
	  SLang_verror (SL_InvalidParm_Error, "Expected a base64 %s type",
			(type == B64_TYPE_ENCODER) ? "encoder" : "decoder");
	return -1;
     }

   if (b64->flags & (B64_INVALID|B64_CLOSED))
     {
	if (err)
	  SLang_verror (SL_InvalidParm_Error, "Base64 encoder is invalid or closed");
	return -1;
     }
   return 0;
}

static void b64_partial_free (Base64_Type *b64)
{
   if (b64->callback_data != NULL) SLang_free_anytype (b64->callback_data);
   b64->callback_data = NULL;
   if (b64->callback != NULL) SLang_free_function (b64->callback);
   b64->callback = NULL;
   if (b64->buffer != NULL) SLfree ((char *)b64->buffer);
   b64->buffer = NULL;
   b64->flags |= B64_INVALID;
}

static int create_b64_buffer (Base64_Type *b64)
{
   b64->num_buffered = 0;
   if (NULL == (b64->buffer = (unsigned char *)SLmalloc (b64->buffer_size + 1)))
     return -1;
   return 0;
}

static int execute_callback (Base64_Type *b64)
{
   SLang_BString_Type *b;

   if (NULL == (b = SLbstring_create_malloced (b64->buffer, b64->num_buffered, 0)))
     return -1;

   if (-1 == create_b64_buffer (b64))
     {
	SLbstring_free (b);
	return -1;
     }

   if ((-1 == SLang_start_arg_list ())
       || (-1 == SLang_push_anytype (b64->callback_data))
       || (-1 == SLang_push_bstring (b))
       || (-1 == SLang_end_arg_list ())
       || (-1 == SLexecute_function (b64->callback)))
     {
	b64->flags |= B64_INVALID;
	SLbstring_free (b);
	return -1;
     }
   SLbstring_free (b);
   return 0;
}

static void free_b64_type (Base64_Type *b64)
{
   if (b64 == NULL)
     return;
   b64_partial_free (b64);
   SLfree ((char *)b64);
}

/* rfc1521:
 *
 * The encoding process represents 24-bit groups of input bits as output
 *  strings of 4 encoded characters. Proceeding from left to right, a
 *  24-bit input group is formed by concatenating 3 8-bit input groups.
 *  These 24 bits are then treated as 4 concatenated 6-bit groups, each
 *  of which is translated into a single digit in the base64 alphabet.
 *  When encoding a bit stream via the base64 encoding, the bit stream
 *  must be presumed to be ordered with the most-significant-bit first.
 */

static char Base64_Bit_Mapping[64] =
{
   'A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P',
   'Q','R','S','T','U','V','W','X','Y','Z','a','b','c','d','e','f',
   'g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v',
   'w','x','y','z','0','1','2','3','4','5','6','7','8','9','+','/',
};

/*  The output stream (encoded bytes) must be represented in lines of no
 *  more than 76 characters each.  All line breaks or other characters
 *  not found in Table 1 must be ignored by decoding software.  In base64
 *  data, characters other than those in Table 1, line breaks, and other
 *  white space probably indicate a transmission error, about which a
 *  warning message or even a message rejection might be appropriate
 *  under some circumstances.
 */

static int b64_encode_triplet (Base64_Type *b64, unsigned char *str)
{
   unsigned char *encode_buf;
   unsigned char ch0, ch1, ch2;

   encode_buf = b64->buffer + b64->num_buffered;

   ch0 = str[0];
   ch1 = str[1];
   ch2 = str[2];
   encode_buf[0] = Base64_Bit_Mapping[ch0>>2];
   encode_buf[1] = Base64_Bit_Mapping[((ch0&0x3)<<4) | (ch1>>4)];
   encode_buf[2] = Base64_Bit_Mapping[((ch1&0xF)<<2) | (ch2>>6)];
   encode_buf[3] = Base64_Bit_Mapping[ch2&0x3F];
   b64->num_buffered += 4;
   if (b64->num_buffered < b64->buffer_size)
     return 0;
   encode_buf[4] = 0;
   return execute_callback (b64);
}

static int b64_encode_accumulate (Base64_Type *b64, unsigned char *line, unsigned int len)
{
   unsigned char *linemax;
   unsigned int i;

   linemax = line + len;

   i = b64->smallbuf_len;
   if (i && (i < 3))
     {
	if (line < linemax)
	  b64->smallbuf[i++] = *line++;
	if ((i < 3) && (line < linemax))
	  b64->smallbuf[i++] = *line++;

	if (i < 3)
	  {
	     b64->smallbuf_len = i;
	     return 0;
	  }
	if (-1 == b64_encode_triplet (b64, b64->smallbuf))
	  return -1;
	b64->smallbuf_len = 0;
     }

   while (line + 3 <= linemax)
     {
	if (-1 == b64_encode_triplet (b64, line))
	  return -1;
	line += 3;
     }

   i = 0;
   while (line < linemax)
     b64->smallbuf[i++] = *line++;
   b64->smallbuf_len = i;
   return 0;
}

static void b64_encoder_accumulate_intrin (Base64_Type *b64, SLang_BString_Type *bstr)
{
   unsigned char *data;
   SLstrlen_Type len;

   if (-1 == check_b64_type (b64, B64_TYPE_ENCODER, 1))
     return;

   if (NULL == (data = SLbstring_get_pointer (bstr, &len)))
     return;

   (void) b64_encode_accumulate (b64, data, len);
}

static void b64_encoder_close_intrin (Base64_Type *b64)
{
   if (-1 == check_b64_type (b64, B64_TYPE_ENCODER, 0))
     goto close_encoder;

   /* Handle the padding */
   if (b64->smallbuf_len)
     {
	unsigned char *encode_buf = b64->buffer + b64->num_buffered;
	unsigned char ch0, ch1;

	ch0 = b64->smallbuf[0];
	encode_buf[0] = Base64_Bit_Mapping[ch0>>2];
	if (b64->smallbuf_len > 1)
	  {
	     ch1 = b64->smallbuf[1];
	     encode_buf[1] = Base64_Bit_Mapping[((ch0&0x3)<<4) | (ch1>>4)];
	     encode_buf[2] = Base64_Bit_Mapping[((ch1&0xF)<<2)];
	  }
	else
	  {
	     encode_buf[1] = Base64_Bit_Mapping[((ch0&0x3)<<4)];
	     encode_buf[2] = '=';
	  }
	encode_buf[3] = '=';
	b64->num_buffered += 4;
	b64->smallbuf_len = 0;
	if (b64->num_buffered >= b64->buffer_size)
	  (void) execute_callback (b64);
     }

   if (b64->num_buffered)
     (void) execute_callback (b64);

close_encoder:
   b64_partial_free (b64);
   b64->flags |= B64_CLOSED;
}

static unsigned char Base64_Decode_Map [256] =
{
   255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,
   255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,
   255,255,255,255,255,255,255,255,255,255,255, 62,255,255,255, 63,
   52, 53, 54, 55, 56, 57, 58, 59, 60, 61,255,255,255,255,255,255,
   255,  0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14,
   15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25,255,255,255,255,255,
   255, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
   41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51,255,255,255,255,255,
   255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,
   255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,
   255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,
   255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,
   255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,
   255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,
   255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,
   255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255
};

static int b64_decode_quartet (Base64_Type *b64, unsigned char *str4)
{
   unsigned char b0, b1, b2, b3;
   unsigned char bytes_buf[3], *bytes;
   unsigned int bad;
   unsigned int n;

   if (0xFF == (b0 = Base64_Decode_Map[str4[0]]))
     {
	bad = 0;
	goto return_error;
     }
   if (0xFF == (b1 = Base64_Decode_Map[str4[1]]))
     {
	bad = 1;
	goto return_error;
     }

   n = 3;

   b2 = Base64_Decode_Map[str4[2]];
   b3 = Base64_Decode_Map[str4[3]];
   if ((b2 == 0xFF) || (b3 == 0xFF))
     {
	if (b2 == 0xFF)
	  {
	     if ('=' != str4[2])
	       {
		  bad = 2;
		  goto return_error;
	       }
	     n = 1;
	  }
	else n = 2;

	if (str4[3] != '=')
	  {
	     SLang_verror (SL_Data_Error, "Illegally padded base64 sequence seen");
	     return -1;
	  }
     }

   if (b64->num_buffered + n < b64->buffer_size)
     bytes = b64->buffer + b64->num_buffered;
   else
     bytes = bytes_buf;

   bytes[0] = (b0 << 2) | (b1>>4);
   if (n > 1)
     {
	bytes[1] = (b1 << 4) | (b2 >> 2);
	if (n > 2)
	  bytes[2] = (b2 << 6) | b3;
     }

   if (bytes != bytes_buf)
     {
	b64->num_buffered += n;
	return 0;
     }

   while (n && (b64->num_buffered < b64->buffer_size))
     {
	b64->buffer[b64->num_buffered++] = *bytes++;
	n--;
     }
   if (b64->num_buffered == b64->buffer_size)
     {
	if (-1 == execute_callback (b64))
	  return -1;
     }
   while (n)
     {
	b64->buffer[b64->num_buffered++] = *bytes++;
	n--;
     }
   return 0;

return_error:
   SLang_verror (SL_Data_Error, "Invalid character (0x%X) found in base64-encoded stream", str4[bad]);
   return -1;
}

static void b64_decoder_accumulate_intrin (Base64_Type *b64, SLFUTURE_CONST char *str)
{
   unsigned int i;
   unsigned char ch;
   unsigned char *buf4;
   if (-1 == check_b64_type (b64, B64_TYPE_DECODER, 1))
     return;

#define NEXT_CHAR \
   while (isspace(*str)) str++; ch = *str++

   NEXT_CHAR;
   if (ch == 0)
     return;

   i = b64->smallbuf_len;
   buf4 = b64->smallbuf;
   if (i && (i < 4))
     {
	buf4[i++] = ch;
	NEXT_CHAR;

	if ((i < 4) && (ch != 0))
	  {
	     buf4[i++] = ch;
	     NEXT_CHAR;
	  }
	if ((i < 4) && (ch != 0))
	  {
	     buf4[i++] = ch;
	     NEXT_CHAR;
	  }
	if (i < 4)
	  {
	     b64->smallbuf_len = i;
	     return;
	  }
	if (-1 == b64_decode_quartet (b64, buf4))
	  return;

	b64->smallbuf_len = 0;
     }

   while (1)
     {
	if (ch == 0)
	  {
	     i = 0;
	     break;
	  }
	buf4[0] = ch;
	NEXT_CHAR;
	if (ch == 0)
	  {
	     i = 1;
	     break;
	  }
	buf4[1] = ch;
	NEXT_CHAR;
	if (ch == 0)
	  {
	     i = 2;
	     break;
	  }
	buf4[2] = ch;
	NEXT_CHAR;
	if (ch == 0)
	  {
	     i = 3;
	     break;
	  }
	buf4[3] = ch;
	if (-1 == b64_decode_quartet (b64, buf4))
	  return;
	NEXT_CHAR;
     }

   b64->smallbuf_len = i;
}

static void b64_decoder_close_intrin (Base64_Type *b64)
{
   SLFUTURE_CONST char *pad = "====";

   if (-1 == check_b64_type (b64, B64_TYPE_DECODER, 0))
     goto close_decoder;

   /* silently add pad characters if necessary */
   if (b64->smallbuf_len)
     (void) b64_decoder_accumulate_intrin (b64, pad + b64->smallbuf_len);

   if (b64->num_buffered)
     (void) execute_callback (b64);

close_decoder:
   b64_partial_free (b64);
   b64->flags |= B64_CLOSED;
}

static void new_b64_type (int type)
{
   Base64_Type *b64;
   SLang_MMT_Type *mmt;

   if (NULL == (b64 = (Base64_Type *)SLmalloc(sizeof(Base64_Type))))
     return;
   memset ((char *)b64, 0, sizeof(Base64_Type));

   b64->type = type;
   if (type == B64_TYPE_ENCODER)
     b64->buffer_size = B64_ENCODE_BUFFER_SIZE;
   else
     b64->buffer_size = B64_DECODE_BUFFER_SIZE;

   if (-1 == create_b64_buffer(b64))
     {
	SLfree ((char *)b64);
	return;
     }

   if ((-1 == SLang_pop_anytype (&b64->callback_data))
	|| (NULL == (b64->callback = SLang_pop_function ()))
	|| (NULL == (mmt = SLang_create_mmt (Base64_Type_Id, (VOID_STAR)b64))))
     {
	free_b64_type (b64);
	return;
     }

   if (-1 == SLang_push_mmt (mmt))
     SLang_free_mmt (mmt);
}

static void new_b64_encoder_intrin (void)
{
   new_b64_type (B64_TYPE_ENCODER);
}

static void new_b64_decoder_intrin (void)
{
   new_b64_type (B64_TYPE_DECODER);
}

#define DUMMY_B64_TYPE ((SLtype)-1)
static SLang_Intrin_Fun_Type Module_Intrinsics [] =
{
   MAKE_INTRINSIC_0("_base64_encoder_new", new_b64_encoder_intrin, SLANG_VOID_TYPE),
   MAKE_INTRINSIC_2("_base64_encoder_accumulate", b64_encoder_accumulate_intrin, SLANG_VOID_TYPE, DUMMY_B64_TYPE, SLANG_BSTRING_TYPE),
   MAKE_INTRINSIC_1("_base64_encoder_close", b64_encoder_close_intrin, SLANG_VOID_TYPE, DUMMY_B64_TYPE),
   MAKE_INTRINSIC_0("_base64_decoder_new", new_b64_decoder_intrin, SLANG_VOID_TYPE),
   MAKE_INTRINSIC_2("_base64_decoder_accumulate", b64_decoder_accumulate_intrin, SLANG_VOID_TYPE, DUMMY_B64_TYPE, SLANG_STRING_TYPE),
   MAKE_INTRINSIC_1("_base64_decoder_close", b64_decoder_close_intrin, SLANG_VOID_TYPE, DUMMY_B64_TYPE),
   SLANG_END_INTRIN_FUN_TABLE
};

static void destroy_b64 (SLtype type, VOID_STAR f)
{
   (void) type;
   free_b64_type ((Base64_Type *)f);
}

static int register_b64_type (void)
{
   SLang_Class_Type *cl;

   if (Base64_Type_Id != 0)
     return 0;

   if (NULL == (cl = SLclass_allocate_class ("Base64_Type")))
     return -1;

   if (-1 == SLclass_set_destroy_function (cl, destroy_b64))
     return -1;

   /* By registering as SLANG_VOID_TYPE, slang will dynamically allocate a
    * type.
    */
   if (-1 == SLclass_register_class (cl, SLANG_VOID_TYPE, sizeof (Base64_Type), SLANG_CLASS_TYPE_MMT))
     return -1;

   Base64_Type_Id = SLclass_get_class_id (cl);
   if (-1 == SLclass_patch_intrin_fun_table1 (Module_Intrinsics, DUMMY_B64_TYPE, Base64_Type_Id))
     return -1;

   return 0;
}

int init_base64_module_ns (char *ns_name)
{
   SLang_NameSpace_Type *ns = SLns_create_namespace (ns_name);
   if (ns == NULL)
     return -1;

   if (-1 == register_b64_type ())
     return -1;

   if (-1 == SLns_add_intrin_fun_table (ns, Module_Intrinsics, NULL))
     return -1;

   return 0;
}

/* This function is optional */
void deinit_base64_module (void)
{
}
