/*

  silcber.c

  Author: Pekka Riikonen <priikone@silcnet.org>

  Copyright (C) 2003 - 2005 Pekka Riikonen

  This program 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; version 2 of the License.

  This program 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.

*/
/* Basic Encoding Rules (BER) encoder and decoder. */

#include "silc.h"
#include "silcber.h"

/* Encodes a BER data block into the `ber', which must already have
   sufficient space allocated.  Caller can use silc_ber_encoded_len
   function to determine how much to allocate space before calling this
   function.  If the `indefinite' is TRUE then the BER block will not
   include the length of the data in the BER block. */

SilcBool silc_ber_encode(SilcBuffer ber, SilcBerClass ber_class,
			 SilcBerEncoding encoding, SilcUInt32 tag,
			 const unsigned char *data, SilcUInt32 data_len,
			 SilcBool indefinite)
{
  int i = 0, c;
  SilcUInt32 tmp;

  if (!ber)
    return FALSE;

  /* Check that buffer is of correct size */
  if (silc_buffer_len(ber) < silc_ber_encoded_len(tag, data_len, indefinite))
    return FALSE;

  /* Put the class and encoding */
  ber->data[i] = (ber_class << 6) | (encoding << 5);

  /* Put the tag */
  if (tag < 0x1f) {
    /* Short tag */
    ber->data[i++] |= tag;
  } else {
    /* Long tag */
    ber->data[i++] |= 0x1f;

    /* Save the tag in multiple octets where 7-bits in the octet is the tag
       value and bit 8 is set, except for the last octet. */
    tmp = tag;
    c = 0;
    while (tmp) {
      c++;
      tmp >>= 7;
    }
    while (c > 1)
      ber->data[i++] = 0x80 | ((tag >> (--c * 7)) & 0x7f);
    ber->data[i++] = tag & 0x7f;
  }

  /* Put the length of data */
  if (!indefinite) {
    if (data_len < 0x80) {
      /* Use short form for less than 128 bytes */
      ber->data[i++] = data_len;
    } else {
      /* Long form */

      /* Calculate the number of octets for the length field */
      tmp = data_len;
      c = 0;
      while (tmp) {
	c++;
	tmp >>= 8;
      }
      ber->data[i++] = 0x80 | c;

      /* Put the actual length field octets, 8-bits per octet. */
      while (c > 1)
	ber->data[i++] = (data_len >> (--c * 8)) & 0xff;
      ber->data[i++] = data_len & 0xff;
    }
  } else {
    /* In indefinite form the length of data is not present in the BER */
    ber->data[i++] = 0x80;
  }

  /* Put the data */
  if (data)
    memcpy(&ber->data[i], data, data_len);

  /* Put end-of-content octets if length is indefinite */
  if (indefinite)
    ber->data[i + data_len] = ber->data[i + data_len + 1] = 0x00;

  return TRUE;
}

/* Decodesa a BER data from the buffer `ber'.  Returns the class,
   encoding and the tag number for the BER data into `ber_class',
   `encoding' and `tag'.  A pointer to the start of the data area is
   returned into `data'.  If the length of the data is available from
   the BER data the length is returned into `data_len'.  If the
   `indefinite' is TRUE then the length found in `data_len' was found
   by finding end-of-contents octets from the data.  The
   `identifier_len' is the length of the BER header, and the length
   of the entire BER object is `identifier_len' + `data_len'. */

SilcBool silc_ber_decode(SilcBuffer ber, SilcBerClass *ber_class,
			 SilcBerEncoding *encoding, SilcUInt32 *tag,
			 const unsigned char **data, SilcUInt32 *data_len,
			 SilcBool *indefinite, SilcUInt32 *identifier_len)
{
  int i = 0, c;
  SilcUInt32 t;

  if (!ber || silc_buffer_len(ber) == 0) {
    SILC_LOG_DEBUG(("Invalid data buffer"));
    return FALSE;
  }

  /* Get class */
  if (ber_class)
    *ber_class = (ber->data[0] >> 6) & 0x03;

  /* Get encoding */
  if (encoding)
    *encoding = (ber->data[0] >> 5) & 0x01;

  /* Get the tag.  Assume short tag, the most common case */
  t = ber->data[i++] & 0x1f;

  /* If the tag is over 31 then take it from next octets */
  if (t >= 0x1f) {
    if (i >= silc_buffer_len(ber)) {
      SILC_LOG_DEBUG(("Malformed BER: Not enough bytes"));
      return FALSE;
    }

    /* The tag is in next octets in 7-bits parts, parse them out.  All
       octets except the last one has bit 8 set. */
    t = 0;
    while (ber->data[i] & 0x80) {
      t <<= 7;
      t |= ber->data[i++] & 0x7f;

      if (i >= silc_buffer_len(ber)) {
	SILC_LOG_DEBUG(("Malformed BER: Not enough bytes"));
	return FALSE;
      }
    }

    /* Last 7-bits part */
    t <<= 7;
    t |= ber->data[i++] & 0x7f;
  }
  if (tag)
    *tag = t;

  if (i >= silc_buffer_len(ber)) {
    SILC_LOG_DEBUG(("Malformed BER: Not enough bytes"));
    return FALSE;
  }

  /* Get the data length and the actual data */
  if (data && data_len) {
    /* Assume short format for length */
    *data_len = ber->data[i++];
    if (indefinite)
      *indefinite = FALSE;

    /* The bit 8 is set if the length is in long format */
    if (*data_len & 0x80) {
      /* If the format is definite then this octet includes the number
	 of length octets.  If indefinite it is zero and data is ended
	 with end-of-contents octets (two zero bytes). */
      c = *data_len & 0x7f;
      if (c) {
	if (i >= silc_buffer_len(ber)) {
	  SILC_LOG_DEBUG(("Malformed BER: Not enough bytes"));
	  return FALSE;
	}

	/* Get the length from c many octects (8-bits per octet) */
	*data_len = 0;
	while (c > 0) {
	  *data_len <<= 8;
	  *data_len |= ber->data[i++] & 0xff;

	  if (i >= silc_buffer_len(ber)) {
	    SILC_LOG_DEBUG(("Malformed BER: Length is too long"));
	    return FALSE;
	  }
	  c--;
	}
      } else {
	/* It is indefinite and we attempt to find out the length by
	   finding the end-of-contents octets. */
	if (indefinite)
	  *indefinite = TRUE;
	c = i;
	while (c + 1 < silc_buffer_len(ber)) {
	  if (ber->data[c] == 0x00 && ber->data[c + 1] == 0x00)
	    break;
	  c += 2;
	}
	if (c >= silc_buffer_len(ber)) {
	  SILC_LOG_DEBUG(("Malformed BER: could not find end-of-content"));
	  return FALSE;
	}
	*data_len = c - i;
      }
    }

    if (*data_len > silc_buffer_len(ber) - i) {
      SILC_LOG_DEBUG(("Malformed BER: Length is too long"));
      return FALSE;
    }

    /* Pointer to data area */
    *data = (const unsigned char *)ber->data + i;
  }

  if (identifier_len)
    *identifier_len = i;

  return TRUE;
}

/* Calculates the length of the encoded BER data object.  This utility
   function can be used to calculate how much to allocate space before
   encoding with silc_ber_encode. */

SilcUInt32 silc_ber_encoded_len(SilcUInt32 tag, SilcUInt32 data_len,
				SilcBool indefinite)
{
  SilcUInt32 len, tmp;

  len = 1;
  if (tag >= 0x1f) {
    while (tag) {
      len++;
      tag >>= 7;
    }
  }

  len++;
  if (!indefinite) {
    if (data_len >= 0x80) {
      tmp = data_len;
      while (tmp) {
	len++;
	tmp >>= 8;
      }
    }
  } else {
    len += 2;
  }

  return len + data_len;
}
