/*
   sshutf8.c

   Author: Markku-Juhani Saarinen <mjos@ssh.fi>

   Copyright (C) 1997 SSH Communications Security Oy, Espoo, Finland
   All rights reserved.

   Character set conversion routines. Complies with draft-yergeay-utf8-01.txt,
   (which will obsolete RFC 2044, the current UTF-8 spec).
*/

/*
 * $Log: sshutf8.c,v $
 * $EndLog$
 */

#include "sshincludes.h"
#include "sshutf8.h"

/* the standard data types for native representations of 16- and 32-bit
   unicode data */

#ifndef SSH_CHARSET_NATIVE_UNICODE16
#define SSH_CHARSET_NATIVE_UNICODE16 unsigned short int
#endif /* SSH_CHARSET_NATIVE_UNICODE16 */

#ifndef SSH_CHARSET_NATIVE_UNICODE32
#define SSH_CHARSET_NATIVE_UNICODE32 unsigned int
#endif /* SSH_CHARSET_NATIVE_UNICODE32 */

/* An unknown character */
#define SSH_CHARACTER_UNKNOWN 0xffffffff

struct SshChrConvRec 
{
  SshCharsetEncoding input_encoding;
  SshCharsetEncoding output_encoding;
  SshChUCS4 partial[8];
  int resume_pt;
  size_t partial_size;
  unsigned char *input_buf;
  size_t input_pos;
  size_t input_len;
  unsigned char *output_buf;
  size_t output_max;
  size_t output_pos;
  Boolean resuming;
  Boolean output_not_full;
};

/* Initialize a context for charset conversion 
   (this way the conversion can be done in parts).
   Returns NULL on failure. */

SshChrConv ssh_charset_init(SshCharsetEncoding input_encoding,
			    SshCharsetEncoding output_encoding)
{
  SshChrConv ctx;

  if (input_encoding < SSH_CHARSET_FIRST || 
      input_encoding > SSH_CHARSET_LAST)
    return NULL;
  if (output_encoding < SSH_CHARSET_FIRST ||
      output_encoding > SSH_CHARSET_LAST)
    return NULL;

  ctx = ssh_xcalloc(1, sizeof(struct SshChrConvRec));
  ctx->input_encoding = input_encoding;
  ctx->output_encoding = output_encoding;
  ctx->partial_size = 0;
  ctx->resume_pt = 0;
  ctx->input_buf = NULL;
  ctx->input_pos = 0;
  ctx->input_len = 0;
  ctx->output_buf = NULL;
  ctx->output_max = 0;
  ctx->output_pos = 0;
  ctx->resuming = FALSE;
  ctx->output_not_full = FALSE;

  return ctx;
}


/* Free the charset conversion context */

void ssh_charset_free(SshChrConv ctx)
{
  if (ctx != NULL)
    ssh_xfree(ctx);
}

/* Get a byte with buffering. Return TRUE, when the input buffer is empty. */

Boolean ssh_charset_get_byte(SshChrConv ctx, SshChUCS4 *bp)
{
  SshChUCS4 ch;

  /* see if we're resuming.. */

  if (ctx->resuming)
    {
      if (ctx->resume_pt < ctx->partial_size)
	{
	  *bp = (SshChUCS4) ctx->partial[ctx->resume_pt++];
	  return FALSE;
	}
      else
	{
	  ctx->resuming = FALSE;
	  ctx->resume_pt = 0;
	}
    }
  
  /* not resuming; check if our input buffer is empty.. */

  if (ctx->input_pos >= ctx->input_len)
    return TRUE;

  /* ok, read a byte and increase the resume buffer */

  ch = ((SshChUCS4) (ctx->input_buf)[ctx->input_pos++]) & 0xff;
  if (ctx->partial_size >= 8)
    {
#if 0 
      /* we can not call fatal here */
      ssh_fatal("ssh_charset_get_byte: partial buffer overfull.");
#endif
      /* mark the buffer as "read" */
      ctx->input_pos = ctx->input_len;
      
    }
      
  ctx->partial[ctx->partial_size++] = ch;

  *bp = ch;
  return FALSE;
}

/* try to resume from the point after the last flush */

void ssh_charset_resume(SshChrConv ctx)
{
  if (ctx->partial_size > 0)
    {
      ctx->resuming = TRUE;
      ctx->resume_pt = 0;
    }
}

/* flush the partial buffer */

void ssh_charset_flush(SshChrConv ctx)
{
  ctx->partial_size = 0;
}

/* put out a byte. return TRUE if this can not be performed. */

Boolean ssh_charset_put_byte(SshChrConv ctx, SshChUCS4 b)
{
  b &= 0xff;
  if (!(ctx->output_not_full))
    return TRUE;
  (ctx->output_buf)[ctx->output_pos++] = (unsigned char) b;
  ctx->output_not_full = ctx->output_pos < ctx->output_max;
  return FALSE;
}

/* Convert max. input_len bytes starting from input_buf. The output is 
   written to output_buf which is an array capable of holding output_max 
   bytes. The actual number of bytes written is returned. 

   This function can not fail. */

size_t ssh_charset_convert(SshChrConv ctx,
			   void *input_buf, size_t input_len,
			   void *output_buf, size_t output_max)
{
  int i, utf8size;
  SshChUCS4 ch, t;

  /* used for native representation conversions. */

  union {
    SSH_CHARSET_NATIVE_UNICODE16 x;
    unsigned char y[sizeof(SSH_CHARSET_NATIVE_UNICODE16)];
  } union_u16;

  union {
    SSH_CHARSET_NATIVE_UNICODE32 x;
    unsigned char y[sizeof(SSH_CHARSET_NATIVE_UNICODE32)];
  } union_u32;

  /* set the new buffer to the context */

  ctx->input_buf = input_buf;
  ctx->input_pos = 0;
  ctx->input_len = input_len;
  ssh_charset_resume(ctx);
  ctx->output_buf = output_buf;
  ctx->output_pos = 0;
  ctx->output_max = output_max;
  ctx->output_not_full = output_max > 0;

  /* ok, now loop over the entire data */

  while (ctx->output_not_full)
    {

      /* read a character into ch */

      switch(ctx->input_encoding)
	{
	  
	  /* no conversion for these */
	  
	case SSH_CHARSET_USASCII:
	case SSH_CHARSET_ISO_LATIN_1:
	  if (ssh_charset_get_byte(ctx, &ch))
	    goto done;
	  break;
	  
	  /* utf-8 conversion */
	  
	case SSH_CHARSET_UTF8:
	  
	  if (ssh_charset_get_byte(ctx, &ch))
	    goto done;
	  if (ch < 0x80)
	    break;
	  
	  /* ok, not a single character, try to determine size */
	  
	  t = ch;
	  for (utf8size = 0; utf8size < 8 && 
		 ((t << utf8size) & 0x80) != 0; utf8size++);
	  
	  if (utf8size < 2 || utf8size > 6)
	    {
	      ch = SSH_CHARACTER_UNKNOWN;
	      break;
	    }
	  
	  /* mask out extra bits and roll the first byte to the correct
	     position */
	  
	  ch = (ch & (0x7f >> utf8size)) << (6 * (utf8size-1));
	  
	  /* ok, now read consecutive bytes that make up the UCS4 character */
	
	  for (i = utf8size-2; i >= 0; i--)
	    {
	      if (ssh_charset_get_byte(ctx, &t))
		goto done;
	      if ((t & 0xc0) != 0x80)
		{
		  ch = SSH_CHARACTER_UNKNOWN;
		  break;
		}
	      ch |= (t & 0x3f) << (6 * i);
	    }
	  

	  /* check that the character is in appropriate range. */
	  
	  if (ch != SSH_CHARACTER_UNKNOWN)
	    {
	      switch (utf8size)
		{
		case 2:
		  if (ch < 0x80 || ch > 0x7ff)
		    ch = SSH_CHARACTER_UNKNOWN;
		  break;
		  
		case 3:
		  if (ch < 0x800 || ch > 0xfff)
		    ch = SSH_CHARACTER_UNKNOWN;
		  break;

		case 4:
		  if (ch < 0x10000 || ch > 0x1fffff)
		    ch = SSH_CHARACTER_UNKNOWN;
		  break;

		case 5:
		  if (ch < 0x200000 || ch > 0x3ffffff)
		    ch = SSH_CHARACTER_UNKNOWN;
		  break;

		case 6:
		  if (ch < 0x4000000)
		    ch = SSH_CHARACTER_UNKNOWN;
		  break;
		  
		default:
		  break;
		}
	    }
	  break;  /* utf-8 done */
	  

	  /* 16-bit unicode in native byte order */
	  
	case SSH_CHARSET_UNICODE16:
	  
	  for (i = 0; i < sizeof(union_u16.x); i++)
	    {
	      if (ssh_charset_get_byte(ctx, &t))
		goto done;
	      union_u16.y[i] = (unsigned char) t;
	    }
	  ch = union_u16.x;
	  break;


	  /* 16-bit unicode in network byte order (two big endian bytes) */
	  
	case SSH_CHARSET_UNICODE16_NBO:
      
	  if (ssh_charset_get_byte(ctx, &t))
	    goto done;
	  if (ssh_charset_get_byte(ctx, &ch))
	    goto done;
	  ch = (t << 8) | ch;
	  break;


	  /* 32-bit unicode in native byte order */

	case SSH_CHARSET_UNICODE32:
	  
	  for (i = 0; i < sizeof(union_u32.x); i++)
	    {
	      if (ssh_charset_get_byte(ctx, &t))
		goto done;
	      union_u32.y[i] = (unsigned char) t;
	    }
	  ch = union_u32.x;
	  break;
	  
	  /* 32-bit unicode in network byte order (four big endian bytes) */

	case SSH_CHARSET_UNICODE32_NBO:
	  ch = 0;
	  for (i = 0; i < 4; i++)
	    {
	      if (ssh_charset_get_byte(ctx, &t))
		goto done;
	      ch |= t << ((3-i) * 8);
	    }
	  break;
	  
	  /* input encoding not recognized. */
	  
	default:
	  /* We can not call fatal here.. we'll simply bail out */
#if 0
	  ssh_fatal("ssh_charset_convert: Illegal input encoding %d",
		    ctx->input_encoding);
#endif
	  /* mark the whole buffer as "read" */
	  ctx->input_pos = ctx->input_len;	
	}


      
      /* ok, we have a character. now try to write it out */

      switch(ctx->output_encoding)
	{

	case SSH_CHARSET_USASCII:
	  if (ch >= 0x80)
	    ch = '?';
	  if (ssh_charset_put_byte(ctx, ch))
	    goto done;
	  break;

	case SSH_CHARSET_ISO_LATIN_1:
	  if (ch >= 0x100)
	    ch = '?';
	  if (ssh_charset_put_byte(ctx, ch))
	    goto done;
	  break;
	  

	  /* utf-8 encoding */

	case SSH_CHARSET_UTF8:
	  
	  /* the 7-bit common case */

	  if (ch < 0x80)
	    {	      
	      if (ssh_charset_put_byte(ctx, ch))
		goto done;
	      break;
	    }

	  /* determine the size of the utf8 representation (1..6 bytes) */

	  utf8size = 6;
	  if (ch < 0x4000000) 
	    utf8size--;
	  if (ch < 0x200000)
	    utf8size--;
	  if (ch < 0x10000)
	    utf8size--;
	  if (ch < 0x800)
	    utf8size--;
	  
	  /* write the first byte */
	  
	  t = (0xfc << (6 - utf8size)) | (ch >> (6 * (utf8size - 1)));
	  if (ssh_charset_put_byte(ctx, t))
	    goto done;

	  /* ok, now write consecutive bytes */

	  for (i = utf8size - 2; i >= 0; i--)
	    {
	      t = 0x80 | ((ch >> (6 * i)) & 0x3f);
	      if (ssh_charset_put_byte(ctx, t))
		goto done;
	    }
	  break; /* utf-8 */


	  /* 16-bit unicode in native byte order */
	 
	case SSH_CHARSET_UNICODE16:

	  if (ch > 0x10000)
	    ch = '?';
	  union_u16.x = (SSH_CHARSET_NATIVE_UNICODE16) ch;
	  for (i = 0; i < sizeof(union_u16.x); i++)
	    {
	      if (ssh_charset_put_byte(ctx, union_u16.y[i]))
		goto done;
	    }  
	  break;
	  

	  /* 16-bit unicode in network byte order */

	case SSH_CHARSET_UNICODE16_NBO:
	  
	  if (ch > 0x10000)
	    ch = '?';
	  if (ssh_charset_put_byte(ctx, (ch >> 8) & 0xff))
	    goto done;
	  if (ssh_charset_put_byte(ctx, ch & 0xff))
	    goto done;
	  break;


	  /* 32-bit unicode in native byte order */

	case SSH_CHARSET_UNICODE32:

	  union_u32.x = (SSH_CHARSET_NATIVE_UNICODE16) ch;
	  for (i = 0; i < sizeof(union_u32.x); i++)
	    {
	      if (ssh_charset_put_byte(ctx, union_u32.y[i]))
		goto done;
	    }  
	  break;


	  /* 32-bit unicode in network byte order */

	case SSH_CHARSET_UNICODE32_NBO:

	  for (i = 0; i < 4; i++)
	    {
	      if (ssh_charset_put_byte(ctx, 
				       (ch >> (8 * (3 - i))) & 0xff))
		goto done;
	    }
	  break;
      
	  /* unknown encoding.. */
	  
	default:
#if 0	  
	  ssh_fatal("ssh_charset_convert: Illegal output encoding %d",
		     ctx->output_encoding);
#endif
	  /* mark the buffer as "read" */
	  ctx->input_pos = ctx->input_len;
	}
      
      /* the character has been read and written. it can be discarded. */

      ssh_charset_flush(ctx);
    }

  /* apparently either one of the buffers ran out */

done:

  return ctx->output_pos;
}

