//
// ASN1.cs: Abstract Syntax Notation 1 - micro-parser and generator
//
// Authors:
//	Sebastien Pouliot  <sebastien@ximian.com>
//	Jesper Pedersen  <jep@itplus.dk>
//
// (C) 2002, 2003 Motus Technologies Inc. (http://www.motus.com)
// Copyright (C) 2004 Novell, Inc (http://www.novell.com)
// (C) 2004 IT+ A/S (http://www.itplus.dk)
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
// 
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
// 
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//

using System;
using System.Collections;
using System.IO;
using System.Text;

namespace Mono.Security
{

	// References:
	// a.	ITU ASN.1 standards (free download)
	//	http://www.itu.int/ITU-T/studygroups/com17/languages/

#if INSIDE_CORLIB
	internal
#else
	public
#endif
	class ASN1
	{

		private byte m_nTag;
		private byte[] m_aValue;
		private ArrayList elist;

		public ASN1() : this(0x00, null) { }

		public ASN1(byte tag) : this(tag, null) { }

		public ASN1(byte tag, byte[] data)
		{
			m_nTag = tag;
			m_aValue = data;
		}

		public ASN1(byte[] data)
		{
			m_nTag = data[0];

			int nLenLength = 0;
			int nLength = data[1];

			if (nLength > 0x80)
			{
				// composed length
				nLenLength = nLength - 0x80;
				nLength = 0;
				for (int i = 0; i < nLenLength; i++)
				{
					nLength *= 256;
					nLength += data[i + 2];
				}
			}
			else if (nLength == 0x80)
			{
				// undefined length encoding
				throw new NotSupportedException("Undefined length encoding.");
			}

			m_aValue = new byte[nLength];
			Buffer.BlockCopy(data, (2 + nLenLength), m_aValue, 0, nLength);

			if ((m_nTag & 0x20) == 0x20)
			{
				int nStart = 0;
				Decode(m_aValue, ref nStart, m_aValue.Length);
			}
		}

		public int Count
		{
			get
			{
				if (elist == null)
					return 0;
				return elist.Count;
			}
		}

		public byte Tag
		{
			get { return m_nTag; }
		}

		public int Length
		{
			get
			{
				if (m_aValue != null)
					return m_aValue.Length;
				else
					return 0;
			}
		}

		public byte[] Value
		{
			get
			{
				if (m_aValue == null)
					GetBytes();
				return (byte[])m_aValue.Clone();
			}
			set
			{
				if (value != null)
					m_aValue = (byte[])value.Clone();
			}
		}

		private bool CompareArray(byte[] array1, byte[] array2)
		{
			bool bResult = (array1.Length == array2.Length);
			if (bResult)
			{
				for (int i = 0; i < array1.Length; i++)
				{
					if (array1[i] != array2[i])
						return false;
				}
			}
			return bResult;
		}

		public bool Equals(byte[] asn1)
		{
			return CompareArray(this.GetBytes(), asn1);
		}

		public bool CompareValue(byte[] value)
		{
			return CompareArray(m_aValue, value);
		}

		public ASN1 Add(ASN1 asn1)
		{
			if (asn1 != null)
			{
				if (elist == null)
					elist = new ArrayList();
				elist.Add(asn1);
			}
			return asn1;
		}

		public virtual byte[] GetBytes()
		{
			byte[] val = null;

			if (Count > 0)
			{
				int esize = 0;
				ArrayList al = new ArrayList();
				foreach (ASN1 a in elist)
				{
					byte[] item = a.GetBytes();
					al.Add(item);
					esize += item.Length;
				}
				val = new byte[esize];
				int pos = 0;
				for (int i = 0; i < elist.Count; i++)
				{
					byte[] item = (byte[])al[i];
					Buffer.BlockCopy(item, 0, val, pos, item.Length);
					pos += item.Length;
				}
			}
			else if (m_aValue != null)
			{
				val = m_aValue;
			}

			byte[] der;
			int nLengthLen = 0;

			if (val != null)
			{
				int nLength = val.Length;
				// special for length > 127
				if (nLength > 127)
				{
					if (nLength <= Byte.MaxValue)
					{
						der = new byte[3 + nLength];
						Buffer.BlockCopy(val, 0, der, 3, nLength);
						nLengthLen = 0x81;
						der[2] = (byte)(nLength);
					}
					else if (nLength <= UInt16.MaxValue)
					{
						der = new byte[4 + nLength];
						Buffer.BlockCopy(val, 0, der, 4, nLength);
						nLengthLen = 0x82;
						der[2] = (byte)(nLength >> 8);
						der[3] = (byte)(nLength);
					}
					else if (nLength <= 0xFFFFFF)
					{
						// 24 bits
						der = new byte[5 + nLength];
						Buffer.BlockCopy(val, 0, der, 5, nLength);
						nLengthLen = 0x83;
						der[2] = (byte)(nLength >> 16);
						der[3] = (byte)(nLength >> 8);
						der[4] = (byte)(nLength);
					}
					else
					{
						// max (Length is an integer) 32 bits
						der = new byte[6 + nLength];
						Buffer.BlockCopy(val, 0, der, 6, nLength);
						nLengthLen = 0x84;
						der[2] = (byte)(nLength >> 24);
						der[3] = (byte)(nLength >> 16);
						der[4] = (byte)(nLength >> 8);
						der[5] = (byte)(nLength);
					}
				}
				else
				{
					// basic case (no encoding)
					der = new byte[2 + nLength];
					Buffer.BlockCopy(val, 0, der, 2, nLength);
					nLengthLen = nLength;
				}
				if (m_aValue == null)
					m_aValue = val;
			}
			else
				der = new byte[2];

			der[0] = m_nTag;
			der[1] = (byte)nLengthLen;

			return der;
		}

		// Note: Recursive
		protected void Decode(byte[] asn1, ref int anPos, int anLength)
		{
			byte nTag;
			int nLength;
			byte[] aValue;

			// minimum is 2 bytes (tag + length of 0)
			while (anPos < anLength - 1)
			{
				DecodeTLV(asn1, ref anPos, out nTag, out nLength, out aValue);
				// sometimes we get trailing 0
				if (nTag == 0)
					continue;

				ASN1 elm = Add(new ASN1(nTag, aValue));

				if ((nTag & 0x20) == 0x20)
				{
					int nConstructedPos = anPos;
					elm.Decode(asn1, ref nConstructedPos, nConstructedPos + nLength);
				}
				anPos += nLength; // value length
			}
		}

		// TLV : Tag - Length - Value
		protected void DecodeTLV(byte[] asn1, ref int pos, out byte tag, out int length, out byte[] content)
		{
			tag = asn1[pos++];
			length = asn1[pos++];

			// special case where L contains the Length of the Length + 0x80
			if ((length & 0x80) == 0x80)
			{
				int nLengthLen = length & 0x7F;
				length = 0;
				for (int i = 0; i < nLengthLen; i++)
					length = length * 256 + asn1[pos++];
			}

			content = new byte[length];
			Buffer.BlockCopy(asn1, pos, content, 0, length);
		}

		public ASN1 this[int index]
		{
			get
			{
				try
				{
					if ((elist == null) || (index >= elist.Count))
						return null;
					return (ASN1)elist[index];
				}
				catch (ArgumentOutOfRangeException)
				{
					return null;
				}
			}
		}

		public ASN1 Element(int index, byte anTag)
		{
			try
			{
				if ((elist == null) || (index >= elist.Count))
					return null;

				ASN1 elm = (ASN1)elist[index];
				if (elm.Tag == anTag)
					return elm;
				else
					return null;
			}
			catch (ArgumentOutOfRangeException)
			{
				return null;
			}
		}

		public override string ToString()
		{
			StringBuilder hexLine = new StringBuilder();

			// Add tag
			hexLine.AppendFormat("Tag: {0} {1}", m_nTag.ToString("X2"), Environment.NewLine);

			// Add length
			hexLine.AppendFormat("Length: {0} {1}", Value.Length, Environment.NewLine);

			// Add value
			hexLine.Append("Value: ");
			hexLine.Append(Environment.NewLine);
			for (int i = 0; i < Value.Length; i++)
			{
				hexLine.AppendFormat("{0} ", Value[i].ToString("X2"));
				if ((i + 1) % 16 == 0)
					hexLine.AppendFormat(Environment.NewLine);
			}
			return hexLine.ToString();
		}

		public void SaveToFile(string filename)
		{
			if (filename == null)
				throw new ArgumentNullException("filename");

			using (FileStream fs = File.Create(filename))
			{
				byte[] data = GetBytes();
				fs.Write(data, 0, data.Length);
			}
		}
	}
}
