File: System\DirectoryServices\Protocols\common\DirectoryControl.cs
Web Access
Project: src\src\libraries\System.DirectoryServices.Protocols\src\System.DirectoryServices.Protocols.csproj (System.DirectoryServices.Protocols)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Collections;
using System.ComponentModel;
using System.Diagnostics;
using System.Formats.Asn1;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Security.Principal;
using System.Text;
 
namespace System.DirectoryServices.Protocols
{
    public enum ExtendedDNFlag
    {
        HexString = 0,
        StandardString = 1
    }
 
    [Flags]
    public enum SecurityMasks
    {
        None = 0,
        Owner = 1,
        Group = 2,
        Dacl = 4,
        Sacl = 8
    }
 
    [Flags]
    public enum DirectorySynchronizationOptions : long
    {
        None = 0,
        ObjectSecurity = 0x1,
        ParentsFirst = 0x0800,
        PublicDataOnly = 0x2000,
        IncrementalValues = 0x80000000
    }
 
    public enum SearchOption
    {
        DomainScope = 1,
        PhantomRoot = 2
    }
 
    internal static class UtilityHandle
    {
        private static readonly ConnectionHandle s_handle = new ConnectionHandle();
 
        public static ConnectionHandle GetHandle() => s_handle;
    }
 
    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
    public class SortKey
    {
        private string _name;
        private string _rule;
        private bool _order;
 
        public SortKey()
        {
        }
 
        public SortKey(string attributeName, string matchingRule, bool reverseOrder)
        {
            AttributeName = attributeName;
            _rule = matchingRule;
            _order = reverseOrder;
        }
 
        public string AttributeName
        {
            get => _name;
            set => _name = value ?? throw new ArgumentNullException(nameof(value));
        }
 
        public string MatchingRule
        {
            get => _rule;
            set => _rule = value;
        }
 
        public bool ReverseOrder
        {
            get => _order;
            set => _order = value;
        }
    }
 
    // The encoding and decoding of LDAP directory controls interprets BER INTEGER values as 32-bit signed integers.
    // Although BER INTEGERS can exceed this data type's maximum value, previous versions of DirectoryControl (and
    // its derived classes) encoded and decoded these types by passing the "i" format specifier to BerConverter. The
    // .NET Framework continues to do so. There is therefore historical precedent to justify limiting BER INTEGER
    // values to this data type when using AsnDecoder and AsnWriter.
    public class DirectoryControl
    {
        // Scratch buffer allocations with sizes which are below this threshold should be made on the stack.
        // This is based on the Active Directory schema. The largest attribute name here is msDS-FailedInteractiveLogonCountAtLastSuccessfulLogon,
        // which is 53 characters long. This is rounded up to the nearest power of two.
        internal const int AttributeNameStackAllocationThreshold = 64;
 
        internal static readonly UTF8Encoding s_utf8Encoding = new(false, true);
 
        internal byte[] _directoryControlValue;
 
        [ThreadStatic]
        private static AsnWriter? t_writer;
 
        internal static AsnWriter GetWriter()
        {
            t_writer ??= new AsnWriter(AsnEncodingRules.BER);
            t_writer.Reset();
 
            return t_writer;
        }
 
        public DirectoryControl(string type, byte[] value, bool isCritical, bool serverSide)
        {
            ArgumentNullException.ThrowIfNull(type);
 
            Type = type;
 
            if (value != null)
            {
                _directoryControlValue = value.AsSpan().ToArray();
            }
            IsCritical = isCritical;
            ServerSide = serverSide;
        }
 
        public virtual byte[] GetValue()
        {
            if (_directoryControlValue == null)
            {
                return Array.Empty<byte>();
            }
 
            return _directoryControlValue.AsSpan().ToArray();
        }
 
        public string Type { get; }
 
        public bool IsCritical { get; set; }
 
        public bool ServerSide { get; set; }
 
        internal static void TransformControls(DirectoryControl[] controls)
        {
            Span<byte> attributeNameScratchSpace = stackalloc byte[AttributeNameStackAllocationThreshold];
 
            try
            {
                for (int i = 0; i < controls.Length; i++)
                {
                    Debug.Assert(controls[i] != null);
                    Debug.Assert(controls[i]._directoryControlValue != null);
 
                    byte[] value = controls[i]._directoryControlValue;
                    Span<byte> asnSpan = value;
                    bool asnReadSuccessful;
 
                    if (controls[i].Type == "1.2.840.113556.1.4.319")
                    {
                        // The control is a PageResultResponseControl. The structure of its value is described as a realSearchControlValue structure in RFC 2696.
                        byte[] cookie;
 
                        AsnDecoder.ReadSequence(asnSpan, AsnEncodingRules.BER, out int sequenceContentOffset, out int sequenceContentLength, out int bytesConsumed);
                        ThrowUnless(sequenceContentLength > 0);
                        ThrowUnless(bytesConsumed == asnSpan.Length);
 
                        asnSpan = asnSpan.Slice(sequenceContentOffset, sequenceContentLength);
                        asnReadSuccessful = AsnDecoder.TryReadInt32(asnSpan, AsnEncodingRules.BER, out int size, out bytesConsumed);
                        ThrowUnless(asnReadSuccessful);
                        asnSpan = asnSpan.Slice(bytesConsumed);
 
                        // The remaining bytes in the control are expected to be the cookie (an octet string.)
                        // A cookie with length 0 will be sent when paged search is done. In this situation, the ASN.1 tag will still consume two bytes.
                        cookie = AsnDecoder.ReadOctetString(asnSpan, AsnEncodingRules.BER, out bytesConsumed);
                        asnSpan = asnSpan.Slice(bytesConsumed);
                        ThrowUnless(asnSpan.IsEmpty);
 
                        PageResultResponseControl pageControl = new PageResultResponseControl(size, cookie, controls[i].IsCritical, value);
                        controls[i] = pageControl;
                    }
                    else if (controls[i].Type == "1.2.840.113556.1.4.1504")
                    {
                        // The control is an AsqResponseControl. The structure of its value is described as an ASQResponseValue in MS-ADTS section 3.1.1.3.4.1.18.
                        ResultCode result;
 
                        AsnDecoder.ReadSequence(asnSpan, AsnEncodingRules.BER, out int sequenceContentOffset, out int sequenceContentLength, out int bytesConsumed);
                        ThrowUnless(sequenceContentLength > 0);
                        ThrowUnless(bytesConsumed == asnSpan.Length);
 
                        result = AsnDecoder.ReadEnumeratedValue<ResultCode>(asnSpan.Slice(sequenceContentOffset, sequenceContentLength), AsnEncodingRules.BER, out bytesConsumed);
                        ThrowUnless(bytesConsumed == sequenceContentLength);
 
                        AsqResponseControl asq = new AsqResponseControl(result, controls[i].IsCritical, value);
                        controls[i] = asq;
                    }
                    else if (controls[i].Type == "1.2.840.113556.1.4.841")
                    {
                        // The control is a DirSyncResponseControl. The structure of its value is described as a DirSyncResponseValue in MS-ADTS section 3.1.1.3.4.1.3.
                        byte[] dirsyncCookie;
 
                        AsnDecoder.ReadSequence(asnSpan, AsnEncodingRules.BER, out int sequenceContentOffset, out int sequenceContentLength, out int bytesConsumed);
                        ThrowUnless(sequenceContentLength > 0);
                        ThrowUnless(bytesConsumed == asnSpan.Length);
                        asnSpan = asnSpan.Slice(sequenceContentOffset, sequenceContentLength);
 
                        asnReadSuccessful = AsnDecoder.TryReadInt32(asnSpan, AsnEncodingRules.BER, out int moreResults, out bytesConsumed);
                        ThrowUnless(asnReadSuccessful);
                        asnSpan = asnSpan.Slice(bytesConsumed);
 
                        asnReadSuccessful = AsnDecoder.TryReadInt32(asnSpan, AsnEncodingRules.BER, out int count, out bytesConsumed);
                        ThrowUnless(asnReadSuccessful);
                        asnSpan = asnSpan.Slice(bytesConsumed);
 
                        dirsyncCookie = AsnDecoder.ReadOctetString(asnSpan, AsnEncodingRules.BER, out bytesConsumed);
                        ThrowUnless(asnSpan.Length == bytesConsumed);
 
                        DirSyncResponseControl dirsync = new DirSyncResponseControl(dirsyncCookie, moreResults != 0, count, controls[i].IsCritical, value);
                        controls[i] = dirsync;
                    }
                    else if (controls[i].Type == "1.2.840.113556.1.4.474")
                    {
                        // The control is a SortResponseControl. The structure of its value is described as a SortResult in RFC 2891.
                        ResultCode result;
                        string attribute = null;
 
                        AsnDecoder.ReadSequence(asnSpan, AsnEncodingRules.BER, out int sequenceContentOffset, out int sequenceContentLength, out int bytesConsumed);
                        ThrowUnless(sequenceContentLength > 0);
                        ThrowUnless(bytesConsumed == asnSpan.Length);
                        asnSpan = asnSpan.Slice(sequenceContentOffset, sequenceContentLength);
 
                        result = AsnDecoder.ReadEnumeratedValue<ResultCode>(asnSpan, AsnEncodingRules.BER, out bytesConsumed);
                        asnSpan = asnSpan.Slice(bytesConsumed);
 
                        // If present, the remaining bytes in the control are expected to be an octet string.
                        if (!asnSpan.IsEmpty)
                        {
                            // Attribute name is optional: AD for example never returns attribute name
                            scoped Span<byte> attributeNameBuffer;
 
                            if (asnSpan.Length <= AttributeNameStackAllocationThreshold)
                            {
                                asnReadSuccessful = AsnDecoder.TryReadOctetString(asnSpan, attributeNameScratchSpace,
                                    AsnEncodingRules.BER, out bytesConsumed, out int octetStringLength, SortResponseControl.AttributeNameTag);
                                Debug.Assert(asnReadSuccessful);
                                attributeNameBuffer = attributeNameScratchSpace.Slice(0, octetStringLength);
                            }
                            else
                            {
                                attributeNameBuffer = AsnDecoder.ReadOctetString(asnSpan, AsnEncodingRules.BER, out bytesConsumed, SortResponseControl.AttributeNameTag);
                            }
                            asnSpan = asnSpan.Slice(bytesConsumed);
 
                            attribute = s_utf8Encoding.GetString(attributeNameBuffer);
                        }
 
                        ThrowUnless(asnSpan.IsEmpty);
 
                        SortResponseControl sort = new SortResponseControl(result, attribute, controls[i].IsCritical, value);
                        controls[i] = sort;
                    }
                    else if (controls[i].Type == "2.16.840.1.113730.3.4.10")
                    {
                        // The control is a VlvResponseControl. The structure of its value is described as a VLVResponseValue in MS-ADTS 3.1.1.3.4.1.17.
                        ResultCode result;
                        byte[] context = null;
 
                        AsnDecoder.ReadSequence(asnSpan, AsnEncodingRules.BER, out int sequenceContentOffset, out int sequenceContentLength, out int bytesConsumed);
                        ThrowUnless(sequenceContentLength > 0);
                        ThrowUnless(bytesConsumed == asnSpan.Length);
                        asnSpan = asnSpan.Slice(sequenceContentOffset, sequenceContentLength);
 
                        asnReadSuccessful = AsnDecoder.TryReadInt32(asnSpan, AsnEncodingRules.BER, out int position, out bytesConsumed);
                        ThrowUnless(asnReadSuccessful);
                        asnSpan = asnSpan.Slice(bytesConsumed);
 
                        asnReadSuccessful = AsnDecoder.TryReadInt32(asnSpan, AsnEncodingRules.BER, out int count, out bytesConsumed);
                        ThrowUnless(asnReadSuccessful);
                        asnSpan = asnSpan.Slice(bytesConsumed);
 
                        result = AsnDecoder.ReadEnumeratedValue<ResultCode>(asnSpan, AsnEncodingRules.BER, out bytesConsumed);
                        asnSpan = asnSpan.Slice(bytesConsumed);
 
                        // If present, the remaining bytes in the control are expected to be an octet string.
                        if (!asnSpan.IsEmpty)
                        {
                            // The user expects cookie with length 0 as paged search is done. In this situation, there'll still be two bytes
                            // for the ASN.1 tag.
                            context = AsnDecoder.ReadOctetString(asnSpan, AsnEncodingRules.BER, out bytesConsumed);
                            asnSpan = asnSpan.Slice(bytesConsumed);
                        }
                        ThrowUnless(asnSpan.IsEmpty);
 
                        VlvResponseControl vlv = new VlvResponseControl(position, count, context, result, controls[i].IsCritical, value);
                        controls[i] = vlv;
                    }
                }
            }
            catch (AsnContentException asnEx)
            {
                throw new BerConversionException(SR.BerConversionError, asnEx);
            }
            catch (DecoderFallbackException decEx)
            {
                throw new BerConversionException(SR.BerConversionError, decEx);
            }
        }
 
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        private static void ThrowUnless(bool condition)
        {
            if (!condition)
            {
                throw new BerConversionException();
            }
        }
    }
 
    public class AsqRequestControl : DirectoryControl
    {
        public AsqRequestControl() : base("1.2.840.113556.1.4.1504", null, true, true)
        {
        }
 
        public AsqRequestControl(string attributeName) : this()
        {
            AttributeName = attributeName;
        }
 
        public string AttributeName { get; set; }
 
        public override byte[] GetValue()
        {
            AsnWriter writer = GetWriter();
 
            /* This is as laid out in MS-ADTS, 3.1.1.3.4.1.18.
             * ASQRequestValue ::= SEQUENCE {
             *                      sourceAttribute     OCTET STRING }
             */
            using (writer.PushSequence())
            {
                writer.WriteStringAsOctetString(AttributeName, s_utf8Encoding);
            }
            _directoryControlValue = writer.Encode();
            writer.Reset();
 
            return base.GetValue();
        }
    }
 
    public class AsqResponseControl : DirectoryControl
    {
        internal AsqResponseControl(ResultCode result, bool criticality, byte[] controlValue) : base("1.2.840.113556.1.4.1504", controlValue, criticality, true)
        {
            Result = result;
        }
 
        public ResultCode Result { get; }
    }
 
    public class CrossDomainMoveControl : DirectoryControl
    {
        public CrossDomainMoveControl() : base("1.2.840.113556.1.4.521", null, true, true)
        {
        }
 
        public CrossDomainMoveControl(string targetDomainController) : this()
        {
            TargetDomainController = targetDomainController;
        }
 
        public string TargetDomainController { get; set; }
 
        public override byte[] GetValue()
        {
            /* This is as laid out in MS-ADTS, 3.1.1.3.4.1.2.
             * "When sending this control to the DC, the controlValue field is set to a UTF-8 string
             * containing the fully qualified domain name of a DC in the domain to which the object
             * is to be moved. The string is not BER-encoded."
             */
            if (TargetDomainController != null)
            {
                int byteCount = s_utf8Encoding.GetByteCount(TargetDomainController);
 
                // Allocate large enough space for the '\0' character.
                _directoryControlValue = new byte[byteCount + 2];
                s_utf8Encoding.GetBytes(TargetDomainController, _directoryControlValue);
            }
 
            return base.GetValue();
        }
    }
 
    public class DomainScopeControl : DirectoryControl
    {
        public DomainScopeControl() : base("1.2.840.113556.1.4.1339", null, true, true)
        {
        }
    }
 
    public class ExtendedDNControl : DirectoryControl
    {
        private ExtendedDNFlag _flag = ExtendedDNFlag.HexString;
 
        public ExtendedDNControl() : base("1.2.840.113556.1.4.529", null, true, true)
        {
        }
 
        public ExtendedDNControl(ExtendedDNFlag flag) : this()
        {
            Flag = flag;
        }
 
        public ExtendedDNFlag Flag
        {
            get => _flag;
            set
            {
                if (value < ExtendedDNFlag.HexString || value > ExtendedDNFlag.StandardString)
                {
                    throw new InvalidEnumArgumentException(nameof(value), (int)value, typeof(ExtendedDNFlag));
                }
 
                _flag = value;
            }
        }
        public override byte[] GetValue()
        {
            AsnWriter writer = GetWriter();
 
            /* This is as laid out in MS-ADTS, 3.1.1.3.4.1.5.
             * ExtendedDNRequestValue ::= SEQUENCE {
             *                              Flag     INTEGER }
             */
            using (writer.PushSequence())
            {
                writer.WriteInteger((int)Flag);
            }
            _directoryControlValue = writer.Encode();
            writer.Reset();
 
            return base.GetValue();
        }
    }
 
    public class LazyCommitControl : DirectoryControl
    {
        public LazyCommitControl() : base("1.2.840.113556.1.4.619", null, true, true) { }
    }
 
    public class DirectoryNotificationControl : DirectoryControl
    {
        public DirectoryNotificationControl() : base("1.2.840.113556.1.4.528", null, true, true) { }
    }
 
    public class PermissiveModifyControl : DirectoryControl
    {
        public PermissiveModifyControl() : base("1.2.840.113556.1.4.1413", null, true, true) { }
    }
 
    public class SecurityDescriptorFlagControl : DirectoryControl
    {
        public SecurityDescriptorFlagControl() : base("1.2.840.113556.1.4.801", null, true, true) { }
 
        public SecurityDescriptorFlagControl(SecurityMasks masks) : this()
        {
            SecurityMasks = masks;
        }
 
        // We don't do validation to the dirsync flag here as underneath API does not check for it and we don't want to put
        // unnecessary limitation on it.
        public SecurityMasks SecurityMasks { get; set; }
 
        public override byte[] GetValue()
        {
            AsnWriter writer = GetWriter();
 
            /* This is as laid out in MS-ADTS, 3.1.1.3.4.1.11.
             * SDFlagsRequestValue ::= SEQUENCE {
             *                          Flags     INTEGER }
             */
            using (writer.PushSequence())
            {
                writer.WriteInteger((int)SecurityMasks);
            }
            _directoryControlValue = writer.Encode();
            writer.Reset();
 
            return base.GetValue();
        }
    }
 
    public class SearchOptionsControl : DirectoryControl
    {
        private SearchOption _searchOption = SearchOption.DomainScope;
        public SearchOptionsControl() : base("1.2.840.113556.1.4.1340", null, true, true) { }
 
        public SearchOptionsControl(SearchOption flags) : this()
        {
            SearchOption = flags;
        }
 
        public SearchOption SearchOption
        {
            get => _searchOption;
            set
            {
                if (value < SearchOption.DomainScope || value > SearchOption.PhantomRoot)
                {
                    throw new InvalidEnumArgumentException(nameof(value), (int)value, typeof(SearchOption));
                }
 
                _searchOption = value;
            }
        }
 
        public override byte[] GetValue()
        {
            AsnWriter writer = GetWriter();
 
            /* This is as laid out in MS-ADTS, 3.1.1.3.4.1.12.
             * SearchOptionsRequestValue ::= SEQUENCE {
             *                                  Flags     INTEGER }
             */
            using (writer.PushSequence())
            {
                writer.WriteInteger((int)SearchOption);
            }
            _directoryControlValue = writer.Encode();
            writer.Reset();
 
            return base.GetValue();
        }
    }
 
    public class ShowDeletedControl : DirectoryControl
    {
        public ShowDeletedControl() : base("1.2.840.113556.1.4.417", null, true, true) { }
    }
 
    public class TreeDeleteControl : DirectoryControl
    {
        public TreeDeleteControl() : base("1.2.840.113556.1.4.805", null, true, true) { }
    }
 
    public class VerifyNameControl : DirectoryControl
    {
        private string _serverName;
        public VerifyNameControl() : base("1.2.840.113556.1.4.1338", null, true, true) { }
 
        public VerifyNameControl(string serverName) : this()
        {
            ArgumentNullException.ThrowIfNull(serverName);
 
            _serverName = serverName;
        }
 
        public VerifyNameControl(string serverName, int flag) : this(serverName)
        {
            Flag = flag;
        }
 
        public string ServerName
        {
            get => _serverName;
            set => _serverName = value ?? throw new ArgumentNullException(nameof(value));
        }
 
        public int Flag { get; set; }
 
        public override byte[] GetValue()
        {
            AsnWriter writer = GetWriter();
 
            /* This is as laid out in MS-ADTS, 3.1.1.3.4.1.16.
             * VerifyNameRequestValue ::= SEQUENCE {
             *                              Flags       INTEGER,
             *                              ServerName  OCTET STRING }
             */
            using (writer.PushSequence())
            {
                writer.WriteInteger(Flag);
 
                writer.WriteStringAsOctetString(ServerName, Encoding.Unicode);
            }
            _directoryControlValue = writer.Encode();
            writer.Reset();
 
            return base.GetValue();
        }
    }
 
    public class DirSyncRequestControl : DirectoryControl
    {
        private byte[] _dirsyncCookie;
        private int _count = 1048576;
 
        public DirSyncRequestControl() : base("1.2.840.113556.1.4.841", null, true, true) { }
        public DirSyncRequestControl(byte[] cookie) : this()
        {
            _dirsyncCookie = cookie;
        }
 
        public DirSyncRequestControl(byte[] cookie, DirectorySynchronizationOptions option) : this(cookie)
        {
            Option = option;
        }
 
        public DirSyncRequestControl(byte[] cookie, DirectorySynchronizationOptions option, int attributeCount) : this(cookie, option)
        {
            AttributeCount = attributeCount;
        }
 
        public byte[] Cookie
        {
            get
            {
                if (_dirsyncCookie == null)
                {
                    return Array.Empty<byte>();
                }
 
                return _dirsyncCookie.AsSpan().ToArray();
            }
            set => _dirsyncCookie = value;
        }
 
        // We don't do validation to the dirsync flag here as underneath API does not check for it and we don't want to put
        // unnecessary limitation on it.
        public DirectorySynchronizationOptions Option { get; set; }
 
        public int AttributeCount
        {
            get => _count;
            set
            {
                if (value < 0)
                {
                    throw new ArgumentException(SR.ValidValue, nameof(value));
                }
 
                _count = value;
            }
        }
 
        public override byte[] GetValue()
        {
            AsnWriter writer = GetWriter();
 
            /* This is as laid out in MS-ADTS, 3.1.1.3.4.1.3.
             * DirSyncRequestValue ::= SEQUENCE {
             *                          Flags       INTEGER,
             *                          MaxBytes    INTEGER,
             *                          Cookie  OCTET STRING }
             */
            using (writer.PushSequence())
            {
                writer.WriteInteger((int)Option);
                writer.WriteInteger(AttributeCount);
                writer.WriteOctetString(_dirsyncCookie ?? []);
            }
            _directoryControlValue = writer.Encode();
            writer.Reset();
 
            return base.GetValue();
        }
    }
 
    public class DirSyncResponseControl : DirectoryControl
    {
        private readonly byte[] _dirsyncCookie;
 
        internal DirSyncResponseControl(byte[] cookie, bool moreData, int resultSize, bool criticality, byte[] controlValue) : base("1.2.840.113556.1.4.841", controlValue, criticality, true)
        {
            _dirsyncCookie = cookie;
            MoreData = moreData;
            ResultSize = resultSize;
        }
 
        public byte[] Cookie
        {
            get
            {
                if (_dirsyncCookie == null)
                {
                    return Array.Empty<byte>();
                }
 
                return _dirsyncCookie.AsSpan().ToArray();
            }
        }
 
        public bool MoreData { get; }
 
        public int ResultSize { get; }
    }
 
    public class PageResultRequestControl : DirectoryControl
    {
        private int _size = 512;
        private byte[] _pageCookie;
 
        public PageResultRequestControl() : base("1.2.840.113556.1.4.319", null, true, true) { }
 
        public PageResultRequestControl(int pageSize) : this()
        {
            PageSize = pageSize;
        }
 
        public PageResultRequestControl(byte[] cookie) : this()
        {
            _pageCookie = cookie;
        }
 
        public int PageSize
        {
            get => _size;
            set
            {
                if (value < 0)
                {
                    throw new ArgumentException(SR.ValidValue, nameof(value));
                }
 
                _size = value;
            }
        }
 
        public byte[] Cookie
        {
            get
            {
                if (_pageCookie == null)
                {
                    return Array.Empty<byte>();
                }
 
                return _pageCookie.AsSpan().ToArray();
            }
            set => _pageCookie = value;
        }
 
        public override byte[] GetValue()
        {
            AsnWriter writer = GetWriter();
 
            /* This is as laid out in RFC2696.
             * realSearchControlValue ::= SEQUENCE {
             *                              size    INTEGER,
             *                              cookie  OCTET STRING }
             */
            using (writer.PushSequence())
            {
                writer.WriteInteger(PageSize);
                writer.WriteOctetString(_pageCookie);
            }
            _directoryControlValue = writer.Encode();
            writer.Reset();
 
            return base.GetValue();
        }
    }
 
    public class PageResultResponseControl : DirectoryControl
    {
        private readonly byte[] _pageCookie;
 
        internal PageResultResponseControl(int count, byte[] cookie, bool criticality, byte[] controlValue) : base("1.2.840.113556.1.4.319", controlValue, criticality, true)
        {
            TotalCount = count;
            _pageCookie = cookie;
        }
 
        public byte[] Cookie
        {
            get
            {
                if (_pageCookie == null)
                {
                    return Array.Empty<byte>();
                }
 
                return _pageCookie.AsSpan().ToArray();
            }
        }
 
        public int TotalCount { get; }
    }
 
    public class SortRequestControl : DirectoryControl
    {
        private static readonly Asn1Tag s_orderingRuleTag = new(TagClass.ContextSpecific, 0);
        private static readonly Asn1Tag s_reverseOrderTag = new(TagClass.ContextSpecific, 1);
 
        private SortKey[] _keys = Array.Empty<SortKey>();
        public SortRequestControl(params SortKey[] sortKeys) : base("1.2.840.113556.1.4.473", null, true, true)
        {
            ArgumentNullException.ThrowIfNull(sortKeys);
 
            for (int i = 0; i < sortKeys.Length; i++)
            {
                if (sortKeys[i] == null)
                {
                    throw new ArgumentException(SR.NullValueArray, nameof(sortKeys));
                }
            }
 
            _keys = new SortKey[sortKeys.Length];
            for (int i = 0; i < sortKeys.Length; i++)
            {
                _keys[i] = new SortKey(sortKeys[i].AttributeName, sortKeys[i].MatchingRule, sortKeys[i].ReverseOrder);
            }
        }
 
        public SortRequestControl(string attributeName, bool reverseOrder) : this(attributeName, null, reverseOrder)
        {
        }
 
        public SortRequestControl(string attributeName, string matchingRule, bool reverseOrder) : base("1.2.840.113556.1.4.473", null, true, true)
        {
            SortKey key = new SortKey(attributeName, matchingRule, reverseOrder);
            _keys = new SortKey[] { key };
        }
 
        public SortKey[] SortKeys
        {
            get
            {
                if (_keys == null)
                {
                    return Array.Empty<SortKey>();
                }
 
                SortKey[] tempKeys = new SortKey[_keys.Length];
                for (int i = 0; i < _keys.Length; i++)
                {
                    tempKeys[i] = new SortKey(_keys[i].AttributeName, _keys[i].MatchingRule, _keys[i].ReverseOrder);
                }
                return tempKeys;
            }
            set
            {
                ArgumentNullException.ThrowIfNull(value);
 
                for (int i = 0; i < value.Length; i++)
                {
                    if (value[i] == null)
                    {
                        throw new ArgumentException(SR.NullValueArray, nameof(value));
                    }
                }
 
                _keys = new SortKey[value.Length];
                for (int i = 0; i < value.Length; i++)
                {
                    _keys[i] = new SortKey(value[i].AttributeName, value[i].MatchingRule, value[i].ReverseOrder);
                }
            }
        }
 
        public override byte[] GetValue()
        {
            AsnWriter writer = GetWriter();
 
            /* This is as laid out in RFC2891.
             * SortKeyList ::= SEQUENCE OF SEQUENCE {
             *                  attributeType   AttributeDescription,
             *                  orderingRule    [0] MatchingRuleId OPTIONAL,
             *                  reverseOrder    [1] BOOLEAN DEFAULT FALSE }
             **/
            using (writer.PushSequence())
            {
                for (int i = 0; i < _keys.Length; i++)
                {
                    SortKey key = _keys[i];
 
                    using (writer.PushSequence())
                    {
                        writer.WriteStringAsOctetString(key.AttributeName, s_utf8Encoding);
                        if (!string.IsNullOrEmpty(key.MatchingRule))
                        {
                            writer.WriteStringAsOctetString(key.MatchingRule, s_utf8Encoding, tag: s_orderingRuleTag);
                        }
 
                        if (key.ReverseOrder)
                        {
                            writer.WriteBoolean(key.ReverseOrder, s_reverseOrderTag);
                        }
                    }
                }
            }
            _directoryControlValue = writer.Encode();
            writer.Reset();
 
            return base.GetValue();
        }
    }
 
    public class SortResponseControl : DirectoryControl
    {
        internal static readonly Asn1Tag AttributeNameTag = new(TagClass.ContextSpecific, 0);
 
        internal SortResponseControl(ResultCode result, string attributeName, bool critical, byte[] value) : base("1.2.840.113556.1.4.474", value, critical, true)
        {
            Result = result;
            AttributeName = attributeName;
        }
 
        public ResultCode Result { get; }
 
        public string AttributeName { get; }
    }
 
    public class VlvRequestControl : DirectoryControl
    {
        private static readonly Asn1Tag s_byOffsetChoiceTag = new(TagClass.ContextSpecific, 0, true);
        private static readonly Asn1Tag s_greaterThanOrEqualChoiceTag = new(TagClass.ContextSpecific, 1, false);
 
        private int _before;
        private int _after;
        private int _offset;
        private int _estimateCount;
        private byte[] _target;
        private byte[] _context;
 
        public VlvRequestControl() : base("2.16.840.1.113730.3.4.9", null, true, true) { }
 
        public VlvRequestControl(int beforeCount, int afterCount, int offset) : this()
        {
            BeforeCount = beforeCount;
            AfterCount = afterCount;
            Offset = offset;
        }
 
        public VlvRequestControl(int beforeCount, int afterCount, string target) : this()
        {
            BeforeCount = beforeCount;
            AfterCount = afterCount;
            if (target != null)
            {
                _target = s_utf8Encoding.GetBytes(target);
            }
        }
 
        public VlvRequestControl(int beforeCount, int afterCount, byte[] target) : this()
        {
            BeforeCount = beforeCount;
            AfterCount = afterCount;
            Target = target;
        }
 
        public int BeforeCount
        {
            get => _before;
            set
            {
                if (value < 0)
                {
                    throw new ArgumentException(SR.ValidValue, nameof(value));
                }
 
                _before = value;
            }
        }
 
        public int AfterCount
        {
            get => _after;
            set
            {
                if (value < 0)
                {
                    throw new ArgumentException(SR.ValidValue, nameof(value));
                }
 
                _after = value;
            }
        }
 
        public int Offset
        {
            get => _offset;
            set
            {
                if (value < 0)
                {
                    throw new ArgumentException(SR.ValidValue, nameof(value));
                }
 
                _offset = value;
            }
        }
 
        public int EstimateCount
        {
            get => _estimateCount;
            set
            {
                if (value < 0)
                {
                    throw new ArgumentException(SR.ValidValue, nameof(value));
                }
 
                _estimateCount = value;
            }
        }
 
        public byte[] Target
        {
            get
            {
                if (_target == null)
                {
                    return Array.Empty<byte>();
                }
 
                return _target.AsSpan().ToArray();
            }
            set => _target = value;
        }
 
        public byte[] ContextId
        {
            get
            {
                if (_context == null)
                {
                    return Array.Empty<byte>();
                }
 
                return _context.AsSpan().ToArray();
            }
            set => _context = value;
        }
 
        public override byte[] GetValue()
        {
            AsnWriter writer = GetWriter();
 
            /* This is as laid out in MS-ADTS, 3.1.1.3.4.1.17.
             * VLVRequestValue ::= SEQUENCE {
             *                      beforeCount     INTEGER,
             *                      afterCount      INTEGER,
             *                      CHOICE {
             *                          byoffset    [0] SEQUENCE {
             *                              offset          INTEGER,
             *                              contentCount    INTEGER },
             *                          greaterThanOrEqual  [1] AssertionValue },
             *                      contextID       OCTET STRING OPTIONAL }
             */
            using (writer.PushSequence())
            {
                // first encode the before and the after count.
                writer.WriteInteger(BeforeCount);
                writer.WriteInteger(AfterCount);
 
                // encode Target if it is not null
                if (_target != null && _target.Length > 0)
                {
                    writer.WriteOctetString(_target, s_greaterThanOrEqualChoiceTag);
                }
                else
                {
                    using (writer.PushSequence(s_byOffsetChoiceTag))
                    {
                        writer.WriteInteger(Offset);
                        writer.WriteInteger(EstimateCount);
                    }
                }
 
                // encode the contextID if present
                if (_context != null && _context.Length > 0)
                {
                    writer.WriteOctetString(_context);
                }
 
            }
            _directoryControlValue = writer.Encode();
            writer.Reset();
 
            return base.GetValue();
        }
    }
 
    public class VlvResponseControl : DirectoryControl
    {
        private readonly byte[] _context;
 
        internal VlvResponseControl(int targetPosition, int count, byte[] context, ResultCode result, bool criticality, byte[] value) : base("2.16.840.1.113730.3.4.10", value, criticality, true)
        {
            TargetPosition = targetPosition;
            ContentCount = count;
            _context = context;
            Result = result;
        }
 
        public int TargetPosition { get; }
 
        public int ContentCount { get; }
 
        public byte[] ContextId
        {
            get
            {
                if (_context == null)
                {
                    return Array.Empty<byte>();
                }
 
                return _context.AsSpan().ToArray();
            }
        }
 
        public ResultCode Result { get; }
    }
 
    [SupportedOSPlatform("windows")]
    public partial class QuotaControl : DirectoryControl
    {
        private byte[] _sid;
 
        public QuotaControl() : base("1.2.840.113556.1.4.1852", null, true, true) { }
 
        public QuotaControl(SecurityIdentifier querySid) : this()
        {
            QuerySid = querySid;
        }
 
        public override byte[] GetValue()
        {
            AsnWriter writer = GetWriter();
 
            /* This is as laid out in MS-ADTS, 3.1.1.3.4.1.19.
             * QuotaRequestValue ::= SEQUENCE {
             *                          querySID OCTET STRING }
             */
            using (writer.PushSequence())
            {
                writer.WriteOctetString(_sid);
            }
            _directoryControlValue = writer.Encode();
            writer.Reset();
 
            return base.GetValue();
        }
    }
 
    public class DirectoryControlCollection : CollectionBase
    {
        public DirectoryControlCollection()
        {
        }
 
        public DirectoryControl this[int index]
        {
            get => (DirectoryControl)List[index];
            set => List[index] = value ?? throw new ArgumentNullException(nameof(value));
        }
 
        public int Add(DirectoryControl control)
        {
            ArgumentNullException.ThrowIfNull(control);
 
            return List.Add(control);
        }
 
        public void AddRange(DirectoryControl[] controls)
        {
            ArgumentNullException.ThrowIfNull(controls);
 
            foreach (DirectoryControl control in controls)
            {
                if (control == null)
                {
                    throw new ArgumentException(SR.ContainNullControl, nameof(controls));
                }
            }
 
            InnerList.AddRange(controls);
        }
 
        public void AddRange(DirectoryControlCollection controlCollection)
        {
            ArgumentNullException.ThrowIfNull(controlCollection);
 
            int currentCount = controlCollection.Count;
            for (int i = 0; i < currentCount; i = ((i) + (1)))
            {
                Add(controlCollection[i]);
            }
        }
 
        public bool Contains(DirectoryControl value) => List.Contains(value);
 
        public void CopyTo(DirectoryControl[] array, int index) => List.CopyTo(array, index);
 
        public int IndexOf(DirectoryControl value) => List.IndexOf(value);
 
        public void Insert(int index, DirectoryControl value)
        {
            ArgumentNullException.ThrowIfNull(value);
 
            List.Insert(index, value);
        }
 
        public void Remove(DirectoryControl value) => List.Remove(value);
 
        protected override void OnValidate(object value)
        {
            ArgumentNullException.ThrowIfNull(value);
 
            if (!(value is DirectoryControl))
            {
                throw new ArgumentException(SR.Format(SR.InvalidValueType, nameof(DirectoryControl)), nameof(value));
            }
        }
    }
}