using System;
using System.Linq;
using System.Collections.Generic;
using System.Text;
namespace Unosquare.Swan.Networking.Ldap {
///
/// Represents a single entry in a directory, consisting of
/// a distinguished name (DN) and zero or more attributes.
/// An instance of
/// LdapEntry is created in order to add an entry to a directory, and
/// instances of LdapEntry are returned on a search by enumerating an
/// LdapSearchResults.
///
///
///
public class LdapEntry {
private readonly LdapAttributeSet _attrs;
///
/// Initializes a new instance of the class.
/// Constructs a new entry with the specified distinguished name and set
/// of attributes.
///
/// The distinguished name of the new entry. The
/// value is not validated. An invalid distinguished
/// name will cause operations using this entry to fail.
/// The initial set of attributes assigned to the
/// entry.
public LdapEntry(String dn = null, LdapAttributeSet attrs = null) {
this.DN = dn ?? String.Empty;
this._attrs = attrs ?? new LdapAttributeSet();
}
///
/// Returns the distinguished name of the entry.
///
///
/// The dn.
///
public String DN {
get;
}
///
/// Returns the attributes matching the specified attrName.
///
/// The name of the attribute or attributes to return.
///
/// The attribute matching the name.
///
public LdapAttribute GetAttribute(String attrName) => this._attrs[attrName];
///
/// Returns the attribute set of the entry.
/// All base and subtype variants of all attributes are
/// returned. The LdapAttributeSet returned may be
/// empty if there are no attributes in the entry.
///
///
/// The attribute set of the entry.
///
public LdapAttributeSet GetAttributeSet() => this._attrs;
///
/// Returns an attribute set from the entry, consisting of only those
/// attributes matching the specified subtypes.
/// The getAttributeSet method can be used to extract only
/// a particular language variant subtype of each attribute,
/// if it exists. The "subtype" may be, for example, "lang-ja", "binary",
/// or "lang-ja;phonetic". If more than one subtype is specified, separated
/// with a semicolon, only those attributes with all of the named
/// subtypes will be returned. The LdapAttributeSet returned may be
/// empty if there are no matching attributes in the entry.
///
/// One or more subtype specification(s), separated
/// with semicolons. The "lang-ja" and
/// "lang-en;phonetic" are valid subtype
/// specifications.
///
/// An attribute set from the entry with the attributes that
/// match the specified subtypes or an empty set if no attributes
/// match.
///
public LdapAttributeSet GetAttributeSet(String subtype) => this._attrs.GetSubset(subtype);
}
///
/// The name and values of one attribute of a directory entry.
/// LdapAttribute objects are used when searching for, adding,
/// modifying, and deleting attributes from the directory.
/// LdapAttributes are often used in conjunction with an
/// LdapAttributeSet when retrieving or adding multiple
/// attributes to an entry.
///
public class LdapAttribute {
private readonly String _baseName; // cn of cn;lang-ja;phonetic
private readonly String[] _subTypes; // lang-ja of cn;lang-ja
private Object[] _values; // Array of byte[] attribute values
///
/// Initializes a new instance of the class.
/// Constructs an attribute with no values.
///
/// Name of the attribute.
/// Attribute name cannot be null.
public LdapAttribute(String attrName) {
this.Name = attrName ?? throw new ArgumentNullException(nameof(attrName));
this._baseName = GetBaseName(attrName);
this._subTypes = GetSubtypes(attrName);
}
///
/// Initializes a new instance of the class.
/// Constructs an attribute with a single value.
///
/// Name of the attribute.
/// Value of the attribute as a string.
/// Attribute value cannot be null.
public LdapAttribute(String attrName, String attrString)
: this(attrName) => this.Add(Encoding.UTF8.GetSBytes(attrString));
///
/// Returns the values of the attribute as an array of bytes.
///
///
/// The byte value array.
///
public SByte[][] ByteValueArray {
get {
if(this._values == null) {
return new SByte[0][];
}
Int32 size = this._values.Length;
SByte[][] bva = new SByte[size][];
// Deep copy so application cannot change values
for(Int32 i = 0, u = size; i < u; i++) {
bva[i] = new SByte[((SByte[])this._values[i]).Length];
Array.Copy((Array)this._values[i], 0, bva[i], 0, bva[i].Length);
}
return bva;
}
}
///
/// Returns the values of the attribute as an array of strings.
///
///
/// The string value array.
///
public String[] StringValueArray {
get {
if(this._values == null) {
return new String[0];
}
Int32 size = this._values.Length;
String[] sva = new String[size];
for(Int32 j = 0; j < size; j++) {
sva[j] = Encoding.UTF8.GetString((SByte[])this._values[j]);
}
return sva;
}
}
///
/// Returns the the first value of the attribute as an UTF-8 string.
///
///
/// The string value.
///
public String StringValue => this._values == null ? null : Encoding.UTF8.GetString((SByte[])this._values[0]);
///
/// Returns the first value of the attribute as a byte array or null.
///
///
/// The byte value.
///
public SByte[] ByteValue {
get {
if(this._values == null) {
return null;
}
// Deep copy so app can't change the value
SByte[] bva = new SByte[((SByte[])this._values[0]).Length];
Array.Copy((Array)this._values[0], 0, bva, 0, bva.Length);
return bva;
}
}
///
/// Returns the language subtype of the attribute, if any.
/// For example, if the attribute name is cn;lang-ja;phonetic,
/// this method returns the string, lang-ja.
///
///
/// The language subtype.
///
public String LangSubtype => this._subTypes?.FirstOrDefault(t => t.StartsWith("lang-"));
///
/// Returns the name of the attribute.
///
///
/// The name.
///
public String Name {
get;
}
internal String Value {
set {
this._values = null;
this.Add(Encoding.UTF8.GetSBytes(value));
}
}
///
/// Extracts the subtypes from the specified attribute name.
/// For example, if the attribute name is cn;lang-ja;phonetic,
/// this method returns an array containing lang-ja and phonetic.
///
/// Name of the attribute from which to extract
/// the subtypes.
///
/// An array subtypes or null if the attribute has none.
///
/// Attribute name cannot be null.
public static String[] GetSubtypes(String attrName) {
if(attrName == null) {
throw new ArgumentException("Attribute name cannot be null");
}
Tokenizer st = new Tokenizer(attrName, ";");
String[] subTypes = null;
Int32 cnt = st.Count;
if(cnt > 0) {
_ = st.NextToken(); // skip over basename
subTypes = new String[cnt - 1];
Int32 i = 0;
while(st.HasMoreTokens()) {
subTypes[i++] = st.NextToken();
}
}
return subTypes;
}
///
/// Returns the base name of the specified attribute name.
/// For example, if the attribute name is cn;lang-ja;phonetic,
/// this method returns cn.
///
/// Name of the attribute from which to extract the
/// base name.
/// The base name of the attribute.
/// Attribute name cannot be null.
public static String GetBaseName(String attrName) {
if(attrName == null) {
throw new ArgumentException("Attribute name cannot be null");
}
Int32 idx = attrName.IndexOf(';');
return idx == -1 ? attrName : attrName.Substring(0, idx - 0);
}
///
/// Clones this instance.
///
/// A cloned instance.
public LdapAttribute Clone() {
Object newObj = this.MemberwiseClone();
if(this._values != null) {
Array.Copy(this._values, 0, ((LdapAttribute)newObj)._values, 0, this._values.Length);
}
return (LdapAttribute)newObj;
}
///
/// Adds a value to the attribute.
///
/// Value of the attribute as a String.
/// Attribute value cannot be null.
public void AddValue(String attrString) {
if(attrString == null) {
throw new ArgumentException("Attribute value cannot be null");
}
this.Add(Encoding.UTF8.GetSBytes(attrString));
}
///
/// Adds a byte-formatted value to the attribute.
///
/// Value of the attribute as raw bytes.
/// Note: If attrBytes represents a string it should be UTF-8 encoded.
/// Attribute value cannot be null.
public void AddValue(SByte[] attrBytes) {
if(attrBytes == null) {
throw new ArgumentException("Attribute value cannot be null");
}
this.Add(attrBytes);
}
///
/// Adds a base64 encoded value to the attribute.
/// The value will be decoded and stored as bytes. String
/// data encoded as a base64 value must be UTF-8 characters.
///
/// The base64 value of the attribute as a String.
/// Attribute value cannot be null.
public void AddBase64Value(String attrString) {
if(attrString == null) {
throw new ArgumentException("Attribute value cannot be null");
}
this.Add(Convert.FromBase64String(attrString).ToSByteArray());
}
///
/// Adds a base64 encoded value to the attribute.
/// The value will be decoded and stored as bytes. Character
/// data encoded as a base64 value must be UTF-8 characters.
///
/// The base64 value of the attribute as a StringBuffer.
/// The start index of base64 encoded part, inclusive.
/// The end index of base encoded part, exclusive.
/// attrString.
public void AddBase64Value(StringBuilder attrString, Int32 start, Int32 end) {
if(attrString == null) {
throw new ArgumentNullException(nameof(attrString));
}
this.Add(Convert.FromBase64String(attrString.ToString(start, end)).ToSByteArray());
}
///
/// Adds a base64 encoded value to the attribute.
/// The value will be decoded and stored as bytes. Character
/// data encoded as a base64 value must be UTF-8 characters.
///
/// The base64 value of the attribute as an array of
/// characters.
/// attrChars.
public void AddBase64Value(Char[] attrChars) {
if(attrChars == null) {
throw new ArgumentNullException(nameof(attrChars));
}
this.Add(Convert.FromBase64CharArray(attrChars, 0, attrChars.Length).ToSByteArray());
}
///
/// Returns the base name of the attribute.
/// For example, if the attribute name is cn;lang-ja;phonetic,
/// this method returns cn.
///
///
/// The base name of the attribute.
///
public String GetBaseName() => this._baseName;
///
/// Extracts the subtypes from the attribute name.
/// For example, if the attribute name is cn;lang-ja;phonetic,
/// this method returns an array containing lang-ja and phonetic.
///
///
/// An array subtypes or null if the attribute has none.
///
public String[] GetSubtypes() => this._subTypes;
///
/// Reports if the attribute name contains the specified subtype.
/// For example, if you check for the subtype lang-en and the
/// attribute name is cn;lang-en, this method returns true.
///
///
/// The single subtype to check for.
///
///
/// True, if the attribute has the specified subtype;
/// false, if it doesn't.
///
public Boolean HasSubtype(String subtype) {
if(subtype == null) {
throw new ArgumentNullException(nameof(subtype));
}
return this._subTypes != null && this._subTypes.Any(t => String.Equals(t, subtype, StringComparison.OrdinalIgnoreCase));
}
///
/// Reports if the attribute name contains all the specified subtypes.
/// For example, if you check for the subtypes lang-en and phonetic
/// and if the attribute name is cn;lang-en;phonetic, this method
/// returns true. If the attribute name is cn;phonetic or cn;lang-en,
/// this method returns false.
///
///
/// An array of subtypes to check for.
///
///
/// True, if the attribute has all the specified subtypes;
/// false, if it doesn't have all the subtypes.
///
public Boolean HasSubtypes(String[] subtypes) {
if(subtypes == null) {
throw new ArgumentNullException(nameof(subtypes));
}
for(Int32 i = 0; i < subtypes.Length; i++) {
foreach(String sub in this._subTypes) {
if(sub == null) {
throw new ArgumentException($"subtype at array index {i} cannot be null");
}
if(String.Equals(sub, subtypes[i], StringComparison.OrdinalIgnoreCase)) {
return true;
}
}
}
return false;
}
///
/// Removes a string value from the attribute.
///
/// Value of the attribute as a string.
/// Note: Removing a value which is not present in the attribute has
/// no effect.
/// attrString.
public void RemoveValue(String attrString) {
if(attrString == null) {
throw new ArgumentNullException(nameof(attrString));
}
this.RemoveValue(Encoding.UTF8.GetSBytes(attrString));
}
///
/// Removes a byte-formatted value from the attribute.
///
/// Value of the attribute as raw bytes.
/// Note: If attrBytes represents a string it should be UTF-8 encoded.
/// Note: Removing a value which is not present in the attribute has
/// no effect.
///
/// attrBytes.
public void RemoveValue(SByte[] attrBytes) {
if(attrBytes == null) {
throw new ArgumentNullException(nameof(attrBytes));
}
for(Int32 i = 0; i < this._values.Length; i++) {
if(!Equals(attrBytes, (SByte[])this._values[i])) {
continue;
}
if(i == 0 && this._values.Length == 1) {
// Optimize if first element of a single valued attr
this._values = null;
return;
}
if(this._values.Length == 1) {
this._values = null;
} else {
Int32 moved = this._values.Length - i - 1;
Object[] tmp = new Object[this._values.Length - 1];
if(i != 0) {
Array.Copy(this._values, 0, tmp, 0, i);
}
if(moved != 0) {
Array.Copy(this._values, i + 1, tmp, i, moved);
}
this._values = tmp;
}
break;
}
}
///
/// Returns the number of values in the attribute.
///
///
/// The number of values in the attribute.
///
public Int32 Size() => this._values?.Length ?? 0;
///
/// Compares this object with the specified object for order.
/// Ordering is determined by comparing attribute names using the method Compare() of the String class.
///
/// The LdapAttribute to be compared to this object.
///
/// Returns a negative integer, zero, or a positive
/// integer as this object is less than, equal to, or greater than the
/// specified object.
///
public Int32 CompareTo(Object attribute)
=> String.Compare(this.Name, ((LdapAttribute)attribute).Name, StringComparison.Ordinal);
///
/// Returns a string representation of this LdapAttribute.
///
///
/// a string representation of this LdapAttribute.
///
/// NullReferenceException.
public override String ToString() {
StringBuilder result = new StringBuilder("LdapAttribute: ");
_ = result.Append("{type='" + this.Name + "'");
if(this._values != null) {
_ = result
.Append(", ")
.Append(this._values.Length == 1 ? "value='" : "values='");
for(Int32 i = 0; i < this._values.Length; i++) {
if(i != 0) {
_ = result.Append("','");
}
if(((SByte[])this._values[i]).Length == 0) {
continue;
}
String sval = Encoding.UTF8.GetString((SByte[])this._values[i]);
if(sval.Length == 0) {
// didn't decode well, must be binary
_ = result.Append("
/// Adds an object to this object's list of attribute values.
///
/// Ultimately all of this attribute's values are treated
/// as binary data so we simplify the process by requiring
/// that all data added to our list is in binary form.
/// Note: If attrBytes represents a string it should be UTF-8 encoded.
private void Add(SByte[] bytes) {
if(this._values == null) {
this._values = new Object[] { bytes };
} else {
// Duplicate attribute values not allowed
if(this._values.Any(t => Equals(bytes, (SByte[])t))) {
return; // Duplicate, don't add
}
Object[] tmp = new Object[this._values.Length + 1];
Array.Copy(this._values, 0, tmp, 0, this._values.Length);
tmp[this._values.Length] = bytes;
this._values = tmp;
}
}
private static Boolean Equals(SByte[] e1, SByte[] e2) {
// If same object, they compare true
if(e1 == e2) {
return true;
}
// If either but not both are null, they compare false
if(e1 == null || e2 == null) {
return false;
}
// If arrays have different length, they compare false
Int32 length = e1.Length;
if(e2.Length != length) {
return false;
}
// If any of the bytes are different, they compare false
for(Int32 i = 0; i < length; i++) {
if(e1[i] != e2[i]) {
return false;
}
}
return true;
}
}
///
/// A set of LdapAttribute objects.
/// An LdapAttributeSet is a collection of LdapAttribute
/// classes as returned from an LdapEntry on a search or read
/// operation. LdapAttributeSet may be also used to construct an entry
/// to be added to a directory.
///
///
///
public class LdapAttributeSet : Dictionary {
///
/// Initializes a new instance of the class.
///
public LdapAttributeSet()
: base(StringComparer.OrdinalIgnoreCase) {
// placeholder
}
///
/// Creates a new attribute set containing only the attributes that have
/// the specified subtypes.
/// For example, suppose an attribute set contains the following
/// attributes:
/// - cn
- cn;lang-ja
- sn;phonetic;lang-ja
- sn;lang-us
/// Calling the getSubset method and passing lang-ja as the
/// argument, the method returns an attribute set containing the following
/// attributes:.
/// - cn;lang-ja
- sn;phonetic;lang-ja
///
/// Semi-colon delimited list of subtypes to include. For
/// example:
/// - "lang-ja" specifies only Japanese language subtypes
- "binary" specifies only binary subtypes
-
/// "binary;lang-ja" specifies only Japanese language subtypes
/// which also are binary
///
/// Note: Novell eDirectory does not currently support language subtypes.
/// It does support the "binary" subtype.
///
/// An attribute set containing the attributes that match the
/// specified subtype.
///
public LdapAttributeSet GetSubset(String subtype) {
// Create a new tempAttributeSet
LdapAttributeSet tempAttributeSet = new LdapAttributeSet();
foreach(KeyValuePair kvp in this) {
if(kvp.Value.HasSubtype(subtype)) {
_ = tempAttributeSet.Add(kvp.Value.Clone());
}
}
return tempAttributeSet;
}
///
/// Returns true if this set contains an attribute of the same name
/// as the specified attribute.
///
/// Object of type LdapAttribute.
///
/// true if this set contains the specified attribute.
///
public Boolean Contains(Object attr) => this.ContainsKey(((LdapAttribute)attr).Name);
///
/// Adds the specified attribute to this set if it is not already present.
/// If an attribute with the same name already exists in the set then the
/// specified attribute will not be added.
///
/// Object of type LdapAttribute.
///
/// true if the attribute was added.
///
public Boolean Add(LdapAttribute attr) {
String name = attr.Name;
if(this.ContainsKey(name)) {
return false;
}
this[name] = attr;
return true;
}
///
/// Removes the specified object from this set if it is present.
/// If the specified object is of type LdapAttribute, the
/// specified attribute will be removed. If the specified object is of type
/// string, the attribute with a name that matches the string will
/// be removed.
///
/// The entry.
///
/// true if the object was removed.
///
public Boolean Remove(LdapAttribute entry) => this.Remove(entry.Name);
///
/// Returns a that represents this instance.
///
///
/// A that represents this instance.
///
public override String ToString() {
StringBuilder retValue = new StringBuilder("LdapAttributeSet: ");
Boolean first = true;
foreach(KeyValuePair attr in this) {
if(!first) {
_ = retValue.Append(" ");
}
first = false;
_ = retValue.Append(attr);
}
return retValue.ToString();
}
}
}