Click here to Skip to main content
15,886,110 members
Articles / Programming Languages / C#

An RFC 2253 Compliant Distinguished Name Parser

Rate me:
Please Sign up or sign in to vote.
4.87/5 (9 votes)
27 Mar 2009CPOL3 min read 100.9K   1.7K   26  
A set of classes to parse and manipulate LDAP distinguished names
/******************************************************************************
*
* CPI.DirectoryServices.DN.cs
*
* By: Pete Everett (pete@CynicalPirate.com)
*
* (C) 2005 Pete Everett (http://www.CynicalPirate.com)
*
*******************************************************************************/

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

namespace CPI.DirectoryServices
{
	/// <summary>
	/// A distinguished name (DN) is a name that uniquely identifies an object in an LDAP
	/// directory by using the relative distinguished name (RDN) of the object itself, plus
	/// the names of its container object.  A DN identifies the object as well as its location 
	/// in a tree.  An example of a distinguished name is CN=Pete,OU=People,DC=example,DC=com
	/// </summary>
	public class DN
	{
		# region Enumerations

		private enum ParserState {LookingForSeparator, InQuotedString};
		
		# endregion
		
		# region Static Data Members

		private static EscapeChars defaultEscapeChars = EscapeChars.ControlChars | EscapeChars.SpecialChars;
		
		# endregion
		
		# region Data Members
		
		private RDNList rDNs;
		private EscapeChars escapeChars;
		private int hashCode;
		
		# endregion
		
		# region Properties
		
		/// <summary>
		/// Gets or sets the categories of special characters that will be 
		/// escaped with a backslash when the DN is printed as a string.
		/// </summary>
		public EscapeChars CharsToEscape
		{
			get
			{
				return escapeChars;
			}
			set
			{
				escapeChars = value;
			}
		}
		
		/// <summary>
		/// Gets a list of all of the relative distinguished names that
		/// make up this distinguished name.
		/// </summary>
		public RDNList RDNs
		{
			get
			{
				return rDNs;
			}
		}
		
		/// <summary>
		/// Gets a DN object representing the object that contains the current DN.
		/// </summary>
		public DN Parent
		{
			get
			{
				if (rDNs.Length >= 1)
				{
					RDN[] parentRDNs = new RDN[rDNs.Length - 1];
					
					for (int i = 0; i < parentRDNs.Length; i++)
					{
						parentRDNs[i] = rDNs[i + 1];
					}
					
					return new DN(new RDNList(parentRDNs), this.CharsToEscape);
				}
				else
				{
					throw new InvalidOperationException("Can't get the parent of an empty DN");
				}
			}
		}
		
		# endregion
		
		# region Constructors
		
		/// <summary>
		/// Constructs a new DN object based on a string representation of an LDAP distinguished name.
		/// </summary>
		/// <param name="dnString">a string representation of a distinguished name</param>
		public DN(string dnString):this(dnString, DefaultEscapeChars) {}
		
		/// <summary>
		/// Constructs a new DN object based on a string representation of an LDAP distinguished name.
		/// </summary>
		/// <param name="dnString">a string representation of a distinguished name</param>
		/// <param name="escapeChars">the categories of special characters to be escaped when the DN is printed as a string</param>
		public DN(string dnString, EscapeChars escapeChars)
		{
            if (dnString == null)
            {
                throw new ArgumentNullException("dnString");
            }

			this.escapeChars = escapeChars;
		
			ParseDN(dnString);
			
			GenerateHashCode();
		}
		
		private DN(RDNList rdnList, EscapeChars escapeChars)
		{
			this.escapeChars = escapeChars;
		
			rDNs = rdnList;
			
			GenerateHashCode();
		}
		
		# endregion
		
		# region Methods
		
		/// <summary>
		/// Determines whether the specified object is equal to the current DN
		/// </summary>
		/// <param name="obj">the object to compare to the current DN</param>
		/// <returns>true if the specified object equals the current DN; false otherwise</returns>
		public override bool Equals(object obj)
		{
            if (object.ReferenceEquals(obj, null))
            {
                return false;
            }

			if (hashCode != obj.GetHashCode())
				return false;
		
			if (obj is DN)
			{
				DN dnObj = (DN)obj;
				
				if (dnObj.rDNs.Length == this.rDNs.Length)
				{
					for (int i = 0; i < this.rDNs.Length; i++)
					{
						if (!(dnObj.rDNs[i].Equals(this.rDNs[i])))
							return false;	
					}
					return true;
				}
				else
				{
					return false;
				}
			}
			else
			{
				return false;
			}
		}

		/// <summary>
		/// Serves as a hash function, suitable for use in hashing algorithms and data structures like a hash table.
		/// </summary>
		/// <returns>a 32-bit integer representing the hash code of the current object</returns>
		public override int GetHashCode()
		{
			return hashCode;
		}

		private void GenerateHashCode()
		{
			// start with a made-up seed
			hashCode = 0x28f527b4;
			
			for (int i = 0; i < this.rDNs.Length; i++)
			{
				hashCode ^= this.rDNs[i].GetHashCode();
			}
		}
		
		/// <summary>
		/// Returns a string that represents the current DN.
		/// </summary>
		/// <returns>a string that represents the current DN</returns>
		public override string ToString()
		{
			return ToString(this.escapeChars);
		}
		
		/// <summary>
		/// Returns a string that represents the current DN.
		/// </summary>
		/// <param name="escapeChars">the categories of characters to be escaped</param>
		/// <returns>a string that represents the current DN</returns>
		public string ToString(EscapeChars escapeChars)
		{
			StringBuilder ReturnValue = new StringBuilder();
			
			foreach (RDN rdn in RDNs)
			{
				ReturnValue.Append(rdn.ToString(escapeChars));
				ReturnValue.Append(",");
			}
			
			// Remove the trailing comma
			if (ReturnValue.Length > 0)
				ReturnValue.Length--;
				
			return ReturnValue.ToString();
		}

		
		private void ParseDN(string dnString)
		{
			// If the string has nothing in it, that's allowed.  Just return an empty array.
			if (dnString.Length == 0)
			{
				rDNs = new RDNList(new RDN[0]);
				return;  // That was easy...
			}
			
			// Break the DN down into its component RDNs.
			// Don't check the validity of the RDNs; just find the separators.
			
			ArrayList rawRDNs = new ArrayList();
			ParserState state = ParserState.LookingForSeparator;
			StringBuilder rawRDN = new StringBuilder();
			
			
			for (int position = 0; position < dnString.Length; ++position)
			{
				switch(state)
				{
					# region case ParserState.LookingForSeparator:
					case ParserState.LookingForSeparator:
						// If we find a separator character, we've hit the end of an RDN.
						// We'll store the RDN, and we'll check to see if the RDN is actually
						// valid later.
						if (dnString[position] == ',' || dnString[position] == ';')
						{
							rawRDNs.Add(rawRDN.ToString()); // Add the string to the list of raw RDNs
							rawRDN.Length = 0;              // Clear the StringBuilder to prepare for the next RDN
						}
						else
						{
							// Add the character to our temporary RDN string
							rawRDN.Append(dnString[position]);

							// If we find an escape character, store character that follows it,
							// but don't consider it as a possible separator character.  If the
							// string ends with an escape character, that's bad, and we should
							// throw an exception
							if (dnString[position] == '\\')
							{
								try
								{
									rawRDN.Append(dnString[++position]);
								}
								catch (IndexOutOfRangeException)
								{
									throw new ArgumentException("Invalid DN: DNs aren't allowed to end with an escape character.", dnString);
								}
							
							}
								// If we find a quote, we'll change state so that we look for the closing quote
								// and ignore any separator characters within.
							else if (dnString[position] == '"')
							{
								state = ParserState.InQuotedString;
							}
						}
						
						break;
						
					# endregion
					
					# region case ParserState.InQuotedString:
					
					case ParserState.InQuotedString:
						// Store the character
						rawRDN.Append(dnString[position]);
						
						// You're allowed to escape special characters in a quoted string, but not required
						// to.  But if there's an escaped quote, we need to take special care to make sure
						// that we don't mistake that for the end of the quoted string.
						if (dnString[position] == '\\')
						{
							try
							{
								rawRDN.Append(dnString[++position]);
							}
							catch (IndexOutOfRangeException)
							{
								throw new ArgumentException("Invalid DN: DNs aren't allowed to end with an escape character.", dnString);
							}
						}
						else if (dnString[position] == '"')
							state = ParserState.LookingForSeparator;
						break;
						
					# endregion
				}
			}
			
			// Take the last RDN and add it to the list
			rawRDNs.Add(rawRDN.ToString());
			
			// Check parser's end state
			if (state == ParserState.InQuotedString)
				throw new ArgumentException("Invalid DN: Unterminated quoted string.", dnString);
			
			RDN[] results = new RDN[rawRDNs.Count];

			for (int i = 0; i < results.Length; i++)
			{
				results[i] = new RDN(rawRDNs[i].ToString());
			}	
			
			rDNs = new RDNList(results);
		}
		
		/// <summary>
		/// Checks whether the current DN is a parent object of the specified DN.
		///
		/// For example:
		/// The DN OU=People,DC=example,DC=com
		/// contains CN=Mike,OU=Marketing,OU=People,DC=example,DC=com
		///
		/// </summary>
		/// <param name="childDN">The DN object to check against the current object</param>
		/// <returns>true if childDN is a child of the current DN; false otherwise</returns>
		public bool Contains(DN childDN)
		{
			if (childDN.rDNs.Length > this.rDNs.Length)
			{
				int Offset = childDN.rDNs.Length - this.rDNs.Length;
				
				for (int i = 0; i < this.rDNs.Length; i++)
				{
					if (childDN.rDNs[i + Offset] != this.rDNs[i])
						return false;
				}
				
				return true;
			}
			else
			{
				return false;
			}
		}
		
		/// <summary>
		/// Gets a DN object representing a child object of the current DN.
		/// </summary>
		/// <param name="childRDN">a string representing the relative path to the child object</param>
		/// <returns>a DN object representing a child object of the current DN</returns>
		public DN GetChild(string childRDN)
		{
			DN childDN = new DN(childRDN);
			
			if (childDN.rDNs.Length > 0)
			{
				RDN[] fullPath = new RDN[this.RDNs.Length + childDN.rDNs.Length];
				
				for (int i = 0; i < childDN.rDNs.Length; i++)
				{
					fullPath[i] = childDN.rDNs[i];
				}
				for (int j = 0; j < this.rDNs.Length; j++)
				{
					fullPath[j + childDN.rDNs.Length] = this.rDNs[j];
				}
				
				return new DN(new RDNList(fullPath), this.CharsToEscape);
			}
			else
			{
				return this;
			}
		}
		
		
		# endregion
		
		# region Overloaded Operators
		
		/// <summary>
		/// Checks to see whether two DN objects are equal.
		/// </summary>
		/// <param name="obj1">a DN object</param>
		/// <param name="obj2">a DN object</param>
		/// <returns>true if the two objects are equal; false otherwise</returns>
		public static bool operator == (DN obj1, DN obj2)
		{
			if (object.ReferenceEquals(obj1, null))
			{
				return (object.ReferenceEquals(obj2, null));
			}
			else
			{
				return obj1.Equals(obj2);
			}
		}
		
		/// <summary>
		/// Checks to see whether two DN objects are not equal.
		/// </summary>
		/// <param name="obj1">a DN object</param>
		/// <param name="obj2">a DN object</param>
		/// <returns>true if the two objects are not equal; false otherwise</returns>
		public static bool operator != (DN obj1, DN obj2)
		{
			return (!(obj1 == obj2));
		}
		
		# endregion
		
		# region Static Properties
		
		/// <summary>
		/// Gets or sets the categories of characters that will be 
		/// escaped by default when a DN is converted to a string
		/// </summary>
		public static EscapeChars DefaultEscapeChars
		{
			get
			{
				return defaultEscapeChars;
			}
			set
			{
				defaultEscapeChars = value;
			}
		}
		
		# endregion
	}
	
	/// <summary>
	/// Bit flag that represents different categories of special characters and 
	/// whether they should be escaped when the DN is displayed as a string.
	/// </summary>
	[Flags]
	public enum EscapeChars
	{
		/// <summary>
		/// No special characters will be escaped.
		/// </summary>
		None = 0, 
		/// <summary>
		/// Characters lower than ascii 32, such as tab and linefeed
		/// </summary>
		ControlChars = 1, 
		/// <summary>
		/// The disinguished name special characters ',', '=', '+', '<', '>', '#', ';', '\\', '"'
		/// </summary>
		SpecialChars = 2, 
		/// <summary>
		/// Any characters >= 128, which are represented as multiple bytes in UTF-8
		/// </summary>
		MultibyteChars = 4
	}


}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior)
United States United States
Pete has just recently become a corporate sell-out, working for a wholly-owned subsidiary of "The Man". He counter-balances his soul-crushing professional life by practicing circus acrobatics and watching Phineas and Ferb reruns. Ducky Momo is his friend.

Comments and Discussions