////////////////////////////////////////////////////////////////////////////
//
// Copyright (C) DSTC Pty Ltd (ACN 052 372 577) 1993, 1994, 1995.
// Unpublished work.  All Rights Reserved.
//
// The software contained on this media is the property of the
// DSTC Pty Ltd.  Use of this software is strictly in accordance
// with the license agreement in the accompanying LICENSE.DOC
// file. If your distribution of this software does not contain
// a LICENSE.DOC file then you have no rights to use this
// software in any manner and should contact DSTC at the address
// below to determine an appropriate licensing arrangement.
//
//      DSTC Pty Ltd
//      Level 7, GP South
//      University of Queensland
//      St Lucia, 4072
//      Australia
//      Tel: +61 7 3365 4310
//      Fax: +61 7 3365 4311
//      Email: jcsi@dstc.qut.edu.au
//
// This software is being provided "AS IS" without warranty of
// any kind.  In no event shall DSTC Pty Ltd be liable for
// damage of any kind arising out of or in connection with
// the use or performance of this software.
//
////////////////////////////////////////////////////////////////////////////

package com.dstc.security.asn1;

import java.io.IOException;

/**
 * <p>An abstract class for an ASN.1 structure.
 *
 * <p>Currently supports only DER encoding, but arbitrary BER (including
 * indefinite-length) decoding. A current limitation soon to be removed
 * is that tag numbers need to be less than 31.
 *
 * @version 0.98, 98/07/01
 * @author Ming Yung
 */
public abstract class Asn1
{
  //Tag classes -- bits 8 and 7
  public static final byte TAG_CLASS_UNIVERSAL = 0x00;
  public static final byte TAG_CLASS_APPLICATION = 0x40;
  public static final byte TAG_CLASS_CONTEXT = (byte) 0x80;
  public static final byte TAG_CLASS_PRIVATE = (byte) 0xC0;
  public static final byte MASK_CLASS = (byte) 0xC0;

  //Primitive or Constructed -- bit 6
  protected static final byte TAG_PRIMITIVE = 0x00;
  protected static final byte TAG_CONSTRUCTED = 0x20;
  protected static final byte MASK_CONSTRUCTED = 0x20;

  //Tag numbers
  protected static final byte TAG_BOOLEAN = 0x01;
  protected static final byte TAG_INTEGER = 0x02;
  protected static final byte TAG_BITSTRING = 0x03;
  protected static final byte TAG_OCTETSTRING = 0x04;
  protected static final byte TAG_NULL = 0x05;
  protected static final byte TAG_OID = 0x06;
  protected static final byte TAG_ENUMERATED = 0x0A;
  protected static final byte TAG_SEQUENCE = 0x10;
  protected static final byte TAG_SETOF = 0x11;
  protected static final byte TAG_UTF8STRING = 0x12;
  protected static final byte TAG_PRINTABLESTRING = 0x13;
  protected static final byte TAG_T61STRING = 0x14;
  protected static final byte TAG_IA5STRING = 0x16;
  protected static final byte TAG_UTCTIME = 0x17;
  protected static final byte TAG_GENERALIZEDTIME = 0x18;
  protected static final byte TAG_VISIBLESTRING = 0x1A;
  protected static final byte TAG_GENERALSTRING = 0x1B;
  protected static final byte TAG_BMPSTRING = 0x1E;
  protected static final byte MASK_NUMBER = 0x1F;

  protected int depth = 0;
  protected int tagNum;
  protected byte classType = TAG_CLASS_UNIVERSAL;
  protected byte[] encoded = null;

  /**
   * Decodes a supplied DER encoding
   */
  public static Asn1 decode(byte encoded[]) throws Asn1Exception
  {
    int marks[] = {0, 0};
    Asn1 retval = Asn1.decode(encoded, marks);
    retval.encoded = new byte[marks[1]];
    System.arraycopy(encoded, 0, retval.encoded, 0,
                     retval.encoded.length);
    return retval;
  }

  public static Asn1 decode(byte encoded[],
      int marks[]) throws Asn1Exception
  {
    //Test bit 6. If set is constructed
    if ((encoded[marks[0]] & MASK_CONSTRUCTED) == TAG_CONSTRUCTED)
    {
      return Constructed.decode(encoded, marks);
    }
    else
    {
      return Primitive.decode(encoded, marks);
    }
  }

  /**
    * Returns the DER encoding
    */
  public byte[] encode()
  {
    //If cached return it
    if (this.encoded != null)
      return this.encoded;

    //Otherwise encode contents (possibly empty)
    try
    {
      byte data[] = doContents();

      //Make length octets

      byte[] lengthOctets;
      if (data == null)
      {
        lengthOctets = new byte[1];
        lengthOctets[0] = 0x00;
        encoded = new byte[1 + lengthOctets.length];
      }
      else
      {
        lengthOctets = doLength(data.length);
        //Allocate memory   (Needs modification for high tag nums)
        encoded = new byte[1 + lengthOctets.length + data.length];
        //Copy contents
        System.arraycopy(data, 0, encoded,
                         1 + lengthOctets.length, data.length);
      }

      //Copy length
      System.arraycopy(lengthOctets, 0, encoded, 1,
                       lengthOctets.length);

      //Make tag  (Needs to change order for high tag numbers)
      doTag(encoded);

      //Return encoding
      return encoded;
    }
    catch (Exception e)
    {
      e.printStackTrace();
      return null;
    }
  }

  /**
    * Sets the tag number for this Asn1 object
    */
  public void setTagNumber(int num)
  {
    this.tagNum = num;
    //the cached byte should now be invalid
    this.encoded = null;
  }

  /**
    * Returns the tag number for this Asn1 object
    */
  public int getTagNumber()
  {
    return this.tagNum;
  }

  /**
    * Sets the tag class byte for this Asn1 object
    */
  public void setTagClass(byte cl)
  {
    this.classType = cl;
    //the cached byte should now be invalid
    this.encoded = null;
  }

  /**
    * Gets the tag class byte for this Asn1 object
    */
  public byte getTagClass()
  {
    return this.classType;
  }

  protected abstract byte[] doContents() throws IOException;

  protected abstract void doTag(byte encoding[]);

  protected abstract void doDecode(byte encoding[],
                                   int marks[]) throws Asn1Exception;

  protected void doDecode(byte encoding[]) throws Asn1Exception
  {
    int marks[] = {0, 0};
    doDecode(encoding, marks);
  }

  public abstract void info(int dep);

  public void info()
  {
    info(0);
  }

  /**
    * Returns the Length Octets corresponding to a supplied length
    */
  private static final byte[] doLength(int in)
  {
    byte[] retval = new byte[0];
    if (in < 128)
    {
      //Short form of Length Octets
      retval = new byte[1];
      retval[0] = (byte)(in);
    }
    else
    {
      //Long form of Length Octets
      byte length[] = intToBytes (in);
      retval = new byte[1 + length.length];
      System.arraycopy (length, 0, retval, 1, length.length);
      retval[0] = (byte)(length.length | 0x80);
    }
    return retval;
  }

  /**
    * Returns an integer as an array of bytes, most significant first
    */
  private static final byte[] intToBytes (int in)
  {
    //Converts in to base 256
    int length = (java.lang.Integer.toHexString (in).length () + 1) / 2;
    byte bytes[] = new byte[length];
    for (int j = 0; j < length; j++)
    {
      bytes[j] = (byte)((in >>> 8 * (length - 1 - j)) & 0xff);
    }
    return bytes;
  }

  /** Interprets the Length Octets of encoded starting at offset
    * and returns the offset to Contents Octets, or -1 if contents empty
    *
    * <p> On entrance, marks[0] points to start of encoding
    *  On exit marks[0] is unchanged, marks[1] points to end of contents
    */
  protected static int decodeLengthOctets(byte encoded[], int marks[])
  {
    int retval;
    byte firstOct = encoded[marks[0] + 1]; //Needs modification for long tag

    //Check if it is an indefinite-length encoding
    if (firstOct == (byte) 0x80)
    {
      //simply point to end of array
      marks[1] = encoded.length;
      return (marks[0] + 2);
    }

    //It is definite-length. Short or long form?
    if ((firstOct & 0x80) == 0)
    {
      //Special case if content length = 0
      if (firstOct == 0)
      {
        marks[1] = marks[0] + 2;
        return -1;
      }

      //Short. Only one length octet present.
      retval = marks[0] + 2;

      //firstOct is the content length (Equals retval if length=0)
      marks[1] = retval + firstOct;
    }
    else
    {
      //Long. Additional length octets present
      int additional = firstOct & 0x7f;
      retval = marks[0] + 2 + additional;
      marks[1] = retval +
                 bytesToInt(encoded, 2 + marks[0], additional);
    }

    return retval;
  }

  protected static final int bytesToInt(byte bytes[], int offset, int len)
  {
    int retval = 0;
    for (int j = 0; j < len; j++)
    {
      retval = (retval << 8) | (bytes[j + offset] & 0xff);
    }
    return retval;
  }

  protected final void spit()
  {
    for (int i = 0; i < this.depth; i++)
      System.out.print("  ");
  }
}
