diff --git a/src/libraries/System.DirectoryServices.Protocols/tests/AsqResponseControlTests.cs b/src/libraries/System.DirectoryServices.Protocols/tests/AsqResponseControlTests.cs new file mode 100644 index 00000000000000..4a2a0c86fda5d9 --- /dev/null +++ b/src/libraries/System.DirectoryServices.Protocols/tests/AsqResponseControlTests.cs @@ -0,0 +1,168 @@ +// 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.Generic; +using System.Reflection; +using Xunit; + +namespace System.DirectoryServices.Protocols.Tests +{ + [ConditionalClass(typeof(DirectoryServicesTestHelpers), nameof(DirectoryServicesTestHelpers.IsWindowsOrLibLdapIsInstalled))] + public class AsqResponseControlTests + { + private const string ControlOid = "1.2.840.113556.1.4.1504"; + + private static MethodInfo s_transformControlsMethod = typeof(DirectoryControl) + .GetMethod("TransformControls", BindingFlags.NonPublic | BindingFlags.Static); + + public static IEnumerable ConformantControlValues() + { + // {e}, single-byte length. ENUMERATED varies between zero & non-zero + yield return new object[] { new byte[] { 0x30, 0x03, + 0x0A, 0x01, 0x00 + }, (ResultCode)0x00 }; + yield return new object[] { new byte[] { 0x30, 0x03, + 0x0A, 0x01, 0x7F + }, (ResultCode)0x7F }; + + // {e}, four-byte length. ENUMERATED varies between zero & non-zero + yield return new object[] { new byte[] { 0x30, 0x84, 0x00, 0x00, 0x00, 0x03, + 0x0A, 0x01, 0x00 + }, (ResultCode)0x00 }; + yield return new object[] { new byte[] { 0x30, 0x84, 0x00, 0x00, 0x00, 0x03, + 0x0A, 0x01, 0x7F + }, (ResultCode)0x7F }; + } + + public static IEnumerable NonconformantControlValues() + { + // {i}, single-byte length. ASN.1 type of INTEGER rather than ENUMERATED + yield return new object[] { new byte[] { 0x30, 0x03, + 0x02, 0x01, 0x00 + }, (ResultCode)0x00 }; + yield return new object[] { new byte[] { 0x30, 0x03, + 0x02, 0x01, 0x7F + }, (ResultCode)0x7F }; + + // {i}, four-byte length. ASN.1 type of INTEGER rather than ENUMERATED + yield return new object[] { new byte[] { 0x30, 0x84, 0x00, 0x00, 0x00, 0x03, + 0x02, 0x01, 0x00 + }, (ResultCode)0x00 }; + yield return new object[] { new byte[] { 0x30, 0x84, 0x00, 0x00, 0x00, 0x03, + 0x02, 0x01, 0x7F + }, (ResultCode)0x7F }; + + // {e}, single-byte length. Trailing data after the end of the sequence + yield return new object[] { new byte[] { 0x30, 0x03, + 0x0A, 0x01, 0x00, + 0x80, 0x80, 0x80, 0x80 + }, (ResultCode)0x00 }; + yield return new object[] { new byte[] { 0x30, 0x03, + 0x0A, 0x01, 0x7F, + 0x80, 0x80, 0x80, 0x80 + }, (ResultCode)0x7F }; + + // {e}, four-byte length. Trailing data after the end of the sequence + yield return new object[] { new byte[] { 0x30, 0x84, 0x00, 0x00, 0x00, 0x03, + 0x0A, 0x01, 0x00, + 0x80, 0x80, 0x80, 0x80 + }, (ResultCode)0x00 }; + yield return new object[] { new byte[] { 0x30, 0x84, 0x00, 0x00, 0x00, 0x03, + 0x0A, 0x01, 0x7F, + 0x80, 0x80, 0x80, 0x80 + }, (ResultCode)0x7F }; + + // {e}, single-byte length. Trailing data within the sequence + yield return new object[] { new byte[] { 0x30, 0x07, + 0x0A, 0x01, 0x00, + 0x80, 0x80, 0x80, 0x80 + }, (ResultCode)0x00 }; + yield return new object[] { new byte[] { 0x30, 0x07, + 0x0A, 0x01, 0x7F, + 0x80, 0x80, 0x80, 0x80 + }, (ResultCode)0x7F }; + + // {e}, four-byte length. Trailing data within the sequence + yield return new object[] { new byte[] { 0x30, 0x84, 0x00, 0x00, 0x00, 0x07, + 0x0A, 0x01, 0x00, + 0x80, 0x80, 0x80, 0x80 + }, (ResultCode)0x00 }; + yield return new object[] { new byte[] { 0x30, 0x84, 0x00, 0x00, 0x00, 0x07, + 0x0A, 0x01, 0x7F, + 0x80, 0x80, 0x80, 0x80 + }, (ResultCode)0x7F }; + } + + public static IEnumerable InvalidControlValues() + { + // e, not wrapped in an ASN.1 SEQUENCE + yield return new object[] { new byte[] { 0x02, 0x01, 0x00 } }; + + // {e}, single-byte length, sequence length extending beyond the end of the buffer + yield return new object[] { new byte[] { 0x30, 0x04, + 0x0A, 0x01, 0x00 } }; + + // {e}, four-byte length, sequence length extending beyond the end of the buffer + yield return new object[] { new byte[] { 0x30, 0x84, 0x00, 0x00, 0x00, 0x04, + 0x0A, 0x01, 0x00 } }; + } + + [Theory] + [MemberData(nameof(ConformantControlValues))] + public void ConformantResponseControlParsedSuccessfully(byte[] value, ResultCode expectedResultCode) + => VerifyResponseControl(value, expectedResultCode); + + [Theory] + [MemberData(nameof(NonconformantControlValues))] + public void NonconformantResponseControlParsedSuccessfully(byte[] value, ResultCode expectedResultCode) + => VerifyResponseControl(value, expectedResultCode); + + [Theory] + [MemberData(nameof(InvalidControlValues))] + public void InvalidResponseControlThrowsException(byte[] value) + { + DirectoryControl control = new(ControlOid, value, true, true); + + Assert.Throws(() => TransformResponseControl(control, false)); + } + + private static void VerifyResponseControl(byte[] value, ResultCode expectedResultCode) + { + DirectoryControl control = new(ControlOid, value, true, true); + AsqResponseControl castControl = TransformResponseControl(control, true) as AsqResponseControl; + + Assert.Equal(expectedResultCode, castControl.Result); + } + + private static DirectoryControl TransformResponseControl(DirectoryControl control, bool assertControlProperties) + { + DirectoryControl[] controls = [control]; + DirectoryControl resultantControl; + + try + { + s_transformControlsMethod.Invoke(null, new object[] { controls }); + } + catch (TargetInvocationException tie) + { + throw tie.InnerException; + } + + resultantControl = controls[0]; + + if (assertControlProperties) + { + Assert.Equal(control.Type, resultantControl.Type); + Assert.Equal(control.IsCritical, resultantControl.IsCritical); + Assert.Equal(control.ServerSide, resultantControl.ServerSide); + Assert.Equal(control.GetValue(), resultantControl.GetValue()); + + return Assert.IsType(resultantControl); + } + else + { + return resultantControl; + } + } + } +} diff --git a/src/libraries/System.DirectoryServices.Protocols/tests/DirSyncResponseControlTests.cs b/src/libraries/System.DirectoryServices.Protocols/tests/DirSyncResponseControlTests.cs new file mode 100644 index 00000000000000..b670477c499e42 --- /dev/null +++ b/src/libraries/System.DirectoryServices.Protocols/tests/DirSyncResponseControlTests.cs @@ -0,0 +1,260 @@ +// 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.Generic; +using System.Reflection; +using System.Runtime.InteropServices; +using Xunit; + +namespace System.DirectoryServices.Protocols.Tests +{ + [ConditionalClass(typeof(DirectoryServicesTestHelpers), nameof(DirectoryServicesTestHelpers.IsWindowsOrLibLdapIsInstalled))] + public class DirSyncResponseControlTests + { + private const string ControlOid = "1.2.840.113556.1.4.841"; + + private static MethodInfo s_transformControlsMethod = typeof(DirectoryControl) + .GetMethod("TransformControls", BindingFlags.NonPublic | BindingFlags.Static); + + public static IEnumerable ConformantControlValues() + { + // {iiO}, single-byte length + // Varying combinations of a zero & non-zero value for the first INTEGER, and a populated & zero-length OCTET STRING + yield return new object[] { new byte[] { 0x30, 0x0D, + 0x02, 0x01, 0x00, + 0x02, 0x01, 0x40, + 0x04, 0x05, 0xC0, 0xC1, 0xC2, 0xC3, 0xC4 + }, false, 0x40, new byte[] { 0xC0, 0xC1, 0xC2, 0xC3, 0xC4 } }; + yield return new object[] { new byte[] { 0x30, 0x0D, + 0x02, 0x01, 0xFF, + 0x02, 0x01, 0x40, + 0x04, 0x05, 0xC0, 0xC1, 0xC2, 0xC3, 0xC4 + }, true, 0x40, new byte[] { 0xC0, 0xC1, 0xC2, 0xC3, 0xC4 } }; + yield return new object[] { new byte[] { 0x30, 0x08, + 0x02, 0x01, 0x00, + 0x02, 0x01, 0x40, + 0x04, 0x00 + }, false, 0x40, Array.Empty() }; + yield return new object[] { new byte[] { 0x30, 0x08, + 0x02, 0x01, 0xFF, + 0x02, 0x01, 0x40, + 0x04, 0x00 + }, true, 0x40, Array.Empty() }; + + // {iiO}, four-byte length + // Varying combinations of a zero & non-zero value for the first INTEGER, and a populated & zero-length OCTET STRING + yield return new object[] { new byte[] { 0x30, 0x84, 0x00, 0x00, 0x00, 0x11, + 0x02, 0x01, 0x00, + 0x02, 0x01, 0x40, + 0x04, 0x84, 0x00, 0x00, 0x00, 0x05, 0xC0, 0xC1, 0xC2, 0xC3, 0xC4 + }, false, 0x40, new byte[] { 0xC0, 0xC1, 0xC2, 0xC3, 0xC4 } }; + yield return new object[] { new byte[] { 0x30, 0x84, 0x00, 0x00, 0x00, 0x11, + 0x02, 0x01, 0xFF, + 0x02, 0x01, 0x40, + 0x04, 0x84, 0x00, 0x00, 0x00, 0x05, 0xC0, 0xC1, 0xC2, 0xC3, 0xC4 + }, true, 0x40, new byte[] { 0xC0, 0xC1, 0xC2, 0xC3, 0xC4 } }; + yield return new object[] { new byte[] { 0x30, 0x84, 0x00, 0x00, 0x00, 0x0C, + 0x02, 0x01, 0x00, + 0x02, 0x01, 0x40, + 0x04, 0x84, 0x00, 0x00, 0x00, 0x00 + }, false, 0x40, Array.Empty() }; + yield return new object[] { new byte[] { 0x30, 0x84, 0x00, 0x00, 0x00, 0x0C, + 0x02, 0x01, 0xFF, + 0x02, 0x01, 0x40, + 0x04, 0x84, 0x00, 0x00, 0x00, 0x00 + }, true, 0x40, Array.Empty() }; + } + + public static IEnumerable NonconformantControlValues() + { + // {eeO}, single-byte length. ASN.1 type of ENUMERATED rather than INTEGER + yield return new object[] { new byte[] { 0x30, 0x0D, + 0x0A, 0x01, 0xFF, + 0x0A, 0x01, 0x40, + 0x04, 0x05, 0xC0, 0xC1, 0xC2, 0xC3, 0xC4 + }, true, 0x40, new byte[] { 0xC0, 0xC1, 0xC2, 0xC3, 0xC4 } }; + + // {eeO}, four-byte length. ASN.1 type of ENUMERATED rather than INTEGER + yield return new object[] { new byte[] { 0x30, 0x84, 0x00, 0x00, 0x00, 0x11, + 0x0A, 0x01, 0xFF, + 0x0A, 0x01, 0x40, + 0x04, 0x84, 0x00, 0x00, 0x00, 0x05, 0xC0, 0xC1, 0xC2, 0xC3, 0xC4 + }, true, 0x40, new byte[] { 0xC0, 0xC1, 0xC2, 0xC3, 0xC4 } }; + + // {iiO}, single-byte length. Trailing data after the end of the sequence + yield return new object[] { new byte[] { 0x30, 0x0D, + 0x02, 0x01, 0xFF, + 0x02, 0x01, 0x40, + 0x04, 0x05, 0xC0, 0xC1, 0xC2, 0xC3, 0xC4, + 0x80, 0x80, 0x80, 0x80 + }, true, 0x40, new byte[] { 0xC0, 0xC1, 0xC2, 0xC3, 0xC4 } }; + + // {iiO}, four-byte length. Trailing data after the end of the sequence + yield return new object[] { new byte[] { 0x30, 0x84, 0x00, 0x00, 0x00, 0x11, + 0x02, 0x01, 0xFF, + 0x02, 0x01, 0x40, + 0x04, 0x84, 0x00, 0x00, 0x00, 0x05, 0xC0, 0xC1, 0xC2, 0xC3, 0xC4, + 0x80, 0x80, 0x80, 0x80 + }, true, 0x40, new byte[] { 0xC0, 0xC1, 0xC2, 0xC3, 0xC4 } }; + + // {iiO}, single-byte length. Trailing data within the sequence (after the octet string) + yield return new object[] { new byte[] { 0x30, 0x11, + 0x02, 0x01, 0xFF, + 0x02, 0x01, 0x40, + 0x04, 0x05, 0xC0, 0xC1, 0xC2, 0xC3, 0xC4, + 0x80, 0x80, 0x80, 0x80 + }, true, 0x40, new byte[] { 0xC0, 0xC1, 0xC2, 0xC3, 0xC4 } }; + + // {iiO}, four-byte length. Trailing data within the sequence (after the octet string) + yield return new object[] { new byte[] { 0x30, 0x84, 0x00, 0x00, 0x00, 0x15, + 0x02, 0x01, 0xFF, + 0x02, 0x01, 0x40, + 0x04, 0x84, 0x00, 0x00, 0x00, 0x05, 0xC0, 0xC1, 0xC2, 0xC3, 0xC4, + 0x80, 0x80, 0x80, 0x80 + }, true, 0x40, new byte[] { 0xC0, 0xC1, 0xC2, 0xC3, 0xC4 } }; + + // Windows will treat these values as invalid. OpenLDAP has slightly looser parsing rules around octet string lengths. + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + // {iiO}, single-byte length. Octet string length extending beyond the end of the sequence (but within the buffer) + yield return new object[] { new byte[] { 0x30, 0x0D, + 0x02, 0x01, 0x00, + 0x02, 0x01, 0x40, + 0x04, 0x06, 0xC0, 0xC1, 0xC2, 0xC3, 0xC4, 0x80, + 0x80, 0x80, 0x80 + }, false, 0x40, new byte[] { 0xC0, 0xC1, 0xC2, 0xC3, 0xC4, 0x80 } }; + + // {iiO}, four-byte length. Octet string length extending beyond the end of the sequence (but within the buffer) + yield return new object[] { new byte[] { 0x30, 0x84, 0x00, 0x00, 0x00, 0x11, + 0x02, 0x01, 0x00, + 0x02, 0x01, 0x40, + 0x04, 0x84, 0x00, 0x00, 0x00, 0x06, 0xC0, 0xC1, 0xC2, 0xC3, 0xC4, 0x80, + 0x80, 0x80, 0x80 + }, false, 0x40, new byte[] { 0xC0, 0xC1, 0xC2, 0xC3, 0xC4, 0x80 } }; + } + } + + public static IEnumerable InvalidControlValues() + { + // i, not wrapped in an ASN.1 SEQUENCE + yield return new object[] { new byte[] { 0x02, 0x01, 0x00 } }; + + // ii, not wrapped in an ASN.1 SEQUENCE + yield return new object[] { new byte[] { 0x02, 0x01, 0x00, + 0x02, 0x01, 0x40 } }; + + // iiO, single-byte length, not wrapped in an ASN.1 SEQUENCE + yield return new object[] { new byte[] { 0x02, 0x01, 0x00, + 0x02, 0x01, 0x40, + 0x04, 0x05, 0xC0, 0xC1, 0xC2, 0xC3, 0xC4} }; + + // iiO, four-byte length, not wrapped in an ASN.1 SEQUENCE + yield return new object[] { new byte[] { 0x02, 0x01, 0x00, + 0x02, 0x01, 0x40, + 0x04, 0x84, 0x00, 0x00, 0x00, 0x05, 0xC0, 0xC1, 0xC2, 0xC3, 0xC4} }; + + // {iiO}, single-byte length, sequence length extending beyond the end of the buffer + yield return new object[] { new byte[] { 0x30, 0x09, + 0x02, 0x01, 0x00, + 0x02, 0x01, 0x40, + 0x04, 0x00} }; + + // {iiO}, four-byte length, sequence length extending beyond the end of the buffer + yield return new object[] { new byte[] { 0x30, 0x84, 0x00, 0x00, 0x00, 0x0D, + 0x02, 0x01, 0x00, + 0x02, 0x01, 0x40, + 0x04, 0x84, 0x00, 0x00, 0x00, 0x00} }; + + // Only Windows treats these values as invalid. These values are present in NonconformantControlValues to prove + // the OpenLDAP behavior. + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + // {iiO}, single-byte length. Octet string length extending beyond the end of the sequence (but within the buffer) + yield return new object[] { new byte[] { 0x30, 0x0D, + 0x02, 0x01, 0x00, + 0x02, 0x01, 0x40, + 0x04, 0x06, 0xC0, 0xC1, 0xC2, 0xC3, 0xC4, 0x80, + 0x80, 0x80, 0x80 } }; + + // {iiO}, four-byte length. Octet string length extending beyond the end of the sequence (but within the buffer) + yield return new object[] { new byte[] { 0x30, 0x84, 0x00, 0x00, 0x00, 0x11, + 0x02, 0x01, 0x00, + 0x02, 0x01, 0x40, + 0x04, 0x84, 0x00, 0x00, 0x00, 0x06, 0xC0, 0xC1, 0xC2, 0xC3, 0xC4, 0x80, + 0x80, 0x80, 0x80 } }; + } + + // {iiO}, single-byte length. Octet string length extending beyond the end of the buffer + yield return new object[] { new byte[] { 0x30, 0x0D, + 0x02, 0x01, 0x00, + 0x02, 0x01, 0x40, + 0x04, 0x06, 0xC0, 0xC1, 0xC2, 0xC3, 0xC4 } }; + + // {iiO}, four-byte length. Octet string length extending beyond the end of the buffer + yield return new object[] { new byte[] { 0x30, 0x84, 0x00, 0x00, 0x00, 0x11, + 0x02, 0x01, 0x00, + 0x02, 0x01, 0x40, + 0x04, 0x84, 0x00, 0x00, 0x00, 0x06, 0xC0, 0xC1, 0xC2, 0xC3, 0xC4 } }; + } + + [Theory] + [MemberData(nameof(ConformantControlValues))] + public void ConformantResponseControlParsedSuccessfully(byte[] value, bool moreData, int resultSize, byte[] cookie) + => VerifyResponseControl(value, moreData, resultSize, cookie); + + [Theory] + [MemberData(nameof(NonconformantControlValues))] + public void NonconformantResponseControlParsedSuccessfully(byte[] value, bool moreData, int resultSize, byte[] cookie) + => VerifyResponseControl(value, moreData, resultSize, cookie); + + [Theory] + [MemberData(nameof(InvalidControlValues))] + public void InvalidResponseControlThrowsException(byte[] value) + { + DirectoryControl control = new(ControlOid, value, true, true); + + Assert.Throws(() => TransformResponseControl(control, false)); + } + + private static void VerifyResponseControl(byte[] value, bool moreData, int resultSize, byte[] cookie) + { + DirectoryControl control = new(ControlOid, value, true, true); + DirSyncResponseControl castControl = TransformResponseControl(control, true) as DirSyncResponseControl; + + Assert.Equal(moreData, castControl.MoreData); + Assert.Equal(resultSize, castControl.ResultSize); + Assert.Equal(cookie, castControl.Cookie); + } + + private static DirectoryControl TransformResponseControl(DirectoryControl control, bool assertControlProperties) + { + DirectoryControl[] controls = [control]; + DirectoryControl resultantControl; + + try + { + s_transformControlsMethod.Invoke(null, new object[] { controls }); + } + catch (TargetInvocationException tie) + { + throw tie.InnerException; + } + + resultantControl = controls[0]; + + if (assertControlProperties) + { + Assert.Equal(control.Type, resultantControl.Type); + Assert.Equal(control.IsCritical, resultantControl.IsCritical); + Assert.Equal(control.ServerSide, resultantControl.ServerSide); + Assert.Equal(control.GetValue(), resultantControl.GetValue()); + + return Assert.IsType(resultantControl); + } + else + { + return resultantControl; + } + } + } +} diff --git a/src/libraries/System.DirectoryServices.Protocols/tests/PageResultResponseControlTests.cs b/src/libraries/System.DirectoryServices.Protocols/tests/PageResultResponseControlTests.cs new file mode 100644 index 00000000000000..bb02931f17c487 --- /dev/null +++ b/src/libraries/System.DirectoryServices.Protocols/tests/PageResultResponseControlTests.cs @@ -0,0 +1,231 @@ +// 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.Generic; +using System.Reflection; +using System.Runtime.InteropServices; +using Xunit; + +namespace System.DirectoryServices.Protocols.Tests +{ + [ConditionalClass(typeof(DirectoryServicesTestHelpers), nameof(DirectoryServicesTestHelpers.IsWindowsOrLibLdapIsInstalled))] + public class PageResultResponseControlTests + { + private const string ControlOid = "1.2.840.113556.1.4.319"; + + private static MethodInfo s_transformControlsMethod = typeof(DirectoryControl) + .GetMethod("TransformControls", BindingFlags.NonPublic | BindingFlags.Static); + + public static IEnumerable ConformantControlValues() + { + // {iO}, single-byte length + // Varying combinations of a zero & non-zero value for the first INTEGER, and a populated & zero-length OCTET STRING + yield return new object[] { new byte[] { 0x30, 0x0A, + 0x02, 0x01, 0x00, + 0x04, 0x05, 0xC0, 0xC1, 0xC2, 0xC3, 0xC4 + }, 0x00, new byte[] { 0xC0, 0xC1, 0xC2, 0xC3, 0xC4 } }; + yield return new object[] { new byte[] { 0x30, 0x0A, + 0x02, 0x01, 0x40, + 0x04, 0x05, 0xC0, 0xC1, 0xC2, 0xC3, 0xC4 + }, 0x40, new byte[] { 0xC0, 0xC1, 0xC2, 0xC3, 0xC4 } }; + yield return new object[] { new byte[] { 0x30, 0x05, + 0x02, 0x01, 0x00, + 0x04, 0x00 + }, 0x00, Array.Empty() }; + yield return new object[] { new byte[] { 0x30, 0x05, + 0x02, 0x01, 0x40, + 0x04, 0x00 + }, 0x40, Array.Empty() }; + + // {iO}, four-byte length + // Varying combinations of a zero & non-zero value for the first INTEGER, and a populated & zero-length OCTET STRING + yield return new object[] { new byte[] { 0x30, 0x84, 0x00, 0x00, 0x00, 0x0E, + 0x02, 0x01, 0x00, + 0x04, 0x84, 0x00, 0x00, 0x00, 0x05, 0xC0, 0xC1, 0xC2, 0xC3, 0xC4 + }, 0x00, new byte[] { 0xC0, 0xC1, 0xC2, 0xC3, 0xC4 } }; + yield return new object[] { new byte[] { 0x30, 0x84, 0x00, 0x00, 0x00, 0x0E, + 0x02, 0x01, 0x40, + 0x04, 0x84, 0x00, 0x00, 0x00, 0x05, 0xC0, 0xC1, 0xC2, 0xC3, 0xC4 + }, 0x40, new byte[] { 0xC0, 0xC1, 0xC2, 0xC3, 0xC4 } }; + yield return new object[] { new byte[] { 0x30, 0x84, 0x00, 0x00, 0x00, 0x09, + 0x02, 0x01, 0x00, + 0x04, 0x84, 0x00, 0x00, 0x00, 0x00 + }, 0x00, Array.Empty() }; + yield return new object[] { new byte[] { 0x30, 0x84, 0x00, 0x00, 0x00, 0x09, + 0x02, 0x01, 0x40, + 0x04, 0x84, 0x00, 0x00, 0x00, 0x00 + }, 0x40, Array.Empty() }; + } + + public static IEnumerable NonconformantControlValues() + { + // {eO}, single-byte length. ASN.1 type of ENUMERATED rather than INTEGER + yield return new object[] { new byte[] { 0x30, 0x0A, + 0x0A, 0x01, 0x40, + 0x04, 0x05, 0xC0, 0xC1, 0xC2, 0xC3, 0xC4 + }, 0x40, new byte[] { 0xC0, 0xC1, 0xC2, 0xC3, 0xC4 } }; + + // {eO}, four-byte length. ASN.1 type of ENUMERATED rather than INTEGER + yield return new object[] { new byte[] { 0x30, 0x84, 0x00, 0x00, 0x00, 0x0E, + 0x0A, 0x01, 0x40, + 0x04, 0x84, 0x00, 0x00, 0x00, 0x05, 0xC0, 0xC1, 0xC2, 0xC3, 0xC4 + }, 0x40, new byte[] { 0xC0, 0xC1, 0xC2, 0xC3, 0xC4 } }; + + // {iO}, single-byte length. Trailing data after the end of the sequence + yield return new object[] { new byte[] { 0x30, 0x0A, + 0x02, 0x01, 0x40, + 0x04, 0x05, 0xC0, 0xC1, 0xC2, 0xC3, 0xC4, + 0x80, 0x80, 0x80, 0x80 + }, 0x40, new byte[] { 0xC0, 0xC1, 0xC2, 0xC3, 0xC4 } }; + + // {iO}, four-byte length. Trailing data after the end of the sequence + yield return new object[] { new byte[] { 0x30, 0x84, 0x00, 0x00, 0x00, 0x0E, + 0x02, 0x01, 0x40, + 0x04, 0x84, 0x00, 0x00, 0x00, 0x05, 0xC0, 0xC1, 0xC2, 0xC3, 0xC4, + 0x80, 0x80, 0x80, 0x80 + }, 0x40, new byte[] { 0xC0, 0xC1, 0xC2, 0xC3, 0xC4 } }; + + // {iO}, single-byte length. Trailing data within the sequence (after the octet string) + yield return new object[] { new byte[] { 0x30, 0x0E, + 0x02, 0x01, 0x40, + 0x04, 0x05, 0xC0, 0xC1, 0xC2, 0xC3, 0xC4, + 0x80, 0x80, 0x80, 0x80 + }, 0x40, new byte[] { 0xC0, 0xC1, 0xC2, 0xC3, 0xC4 } }; + + // {iO}, four-byte length. Trailing data within the sequence (after the octet string) + yield return new object[] { new byte[] { 0x30, 0x84, 0x00, 0x00, 0x00, 0x12, + 0x0A, 0x01, 0x40, + 0x04, 0x84, 0x00, 0x00, 0x00, 0x05, 0xC0, 0xC1, 0xC2, 0xC3, 0xC4, + 0x80, 0x80, 0x80, 0x80 + }, 0x40, new byte[] { 0xC0, 0xC1, 0xC2, 0xC3, 0xC4 } }; + + // Windows will treat these values as invalid. OpenLDAP has slightly looser parsing rules around octet string lengths. + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + // {iO}, single-byte length. Octet string length extending beyond the end of the sequence (but within the buffer) + yield return new object[] { new byte[] { 0x30, 0x0A, + 0x02, 0x01, 0x40, + 0x04, 0x06, 0xC0, 0xC1, 0xC2, 0xC3, 0xC4, 0x80, + 0x80, 0x80, 0x80 + }, 0x40, new byte[] { 0xC0, 0xC1, 0xC2, 0xC3, 0xC4, 0x80 } }; + + // {iO}, four-byte length. Octet string length extending beyond the end of the sequence (but within the buffer) + yield return new object[] { new byte[] { 0x30, 0x84, 0x00, 0x00, 0x00, 0x0E, + 0x02, 0x01, 0x40, + 0x04, 0x84, 0x00, 0x00, 0x00, 0x06, 0xC0, 0xC1, 0xC2, 0xC3, 0xC4, 0x80, + 0x80, 0x80, 0x80 + }, 0x40, new byte[] { 0xC0, 0xC1, 0xC2, 0xC3, 0xC4, 0x80 } }; + } + } + + public static IEnumerable InvalidControlValues() + { + // i, not wrapped in an ASN.1 SEQUENCE + yield return new object[] { new byte[] { 0x02, 0x01, 0x40 } }; + + // iO, single-byte length, not wrapped in an ASN.1 SEQUENCE + yield return new object[] { new byte[] { 0x02, 0x01, 0x40, + 0x04, 0x05, 0xC0, 0xC1, 0xC2, 0xC3, 0xC4} }; + + // iO, four-byte length, not wrapped in an ASN.1 SEQUENCE + yield return new object[] { new byte[] { 0x02, 0x01, 0x40, + 0x04, 0x84, 0x00, 0x00, 0x00, 0x05, 0xC0, 0xC1, 0xC2, 0xC3, 0xC4} }; + + // {iO}, single-byte length, sequence length extending beyond the end of the buffer + yield return new object[] { new byte[] { 0x30, 0x06, + 0x02, 0x01, 0x40, + 0x04, 0x00} }; + + // {iO}, four-byte length, sequence length extending beyond the end of the buffer + yield return new object[] { new byte[] { 0x30, 0x84, 0x00, 0x00, 0x00, 0x0A, + 0x02, 0x01, 0x40, + 0x04, 0x84, 0x00, 0x00, 0x00, 0x00} }; + + // Only Windows treats these values as invalid. These values are present in NonconformantControlValues to prove + // the OpenLDAP behavior. + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + // {iO}, single-byte length. Octet string length extending beyond the end of the sequence (but within the buffer) + yield return new object[] { new byte[] { 0x30, 0x0A, + 0x02, 0x01, 0x40, + 0x04, 0x06, 0xC0, 0xC1, 0xC2, 0xC3, 0xC4, 0x80, + 0x80, 0x80, 0x80 } }; + + // {iO}, four-byte length. Octet string length extending beyond the end of the sequence (but within the buffer) + yield return new object[] { new byte[] { 0x30, 0x84, 0x00, 0x00, 0x00, 0x0E, + 0x02, 0x01, 0x40, + 0x04, 0x84, 0x00, 0x00, 0x00, 0x06, 0xC0, 0xC1, 0xC2, 0xC3, 0xC4, 0x80, + 0x80, 0x80, 0x80 } }; + } + + // {iO}, single-byte length. Octet string length extending beyond the end of the buffer + yield return new object[] { new byte[] { 0x30, 0x0A, + 0x02, 0x01, 0x40, + 0x04, 0x06, 0xC0, 0xC1, 0xC2, 0xC3, 0xC4 } }; + + // {iO}, four-byte length. Octet string length extending beyond the end of the buffer + yield return new object[] { new byte[] { 0x30, 0x84, 0x00, 0x00, 0x00, 0x0E, + 0x02, 0x01, 0x40, + 0x04, 0x84, 0x00, 0x00, 0x00, 0x06, 0xC0, 0xC1, 0xC2, 0xC3, 0xC4 } }; + } + + [Theory] + [MemberData(nameof(ConformantControlValues))] + public void ConformantResponseControlParsedSuccessfully(byte[] value, int totalCount, byte[] cookie) + => VerifyResponseControl(value, totalCount, cookie); + + [Theory] + [MemberData(nameof(NonconformantControlValues))] + public void NonconformantResponseControlParsedSuccessfully(byte[] value, int totalCount, byte[] cookie) + => VerifyResponseControl(value, totalCount, cookie); + + [Theory] + [MemberData(nameof(InvalidControlValues))] + public void InvalidResponseControlThrowsException(byte[] value) + { + DirectoryControl control = new(ControlOid, value, true, true); + + Assert.Throws(() => TransformResponseControl(control, false)); + } + + private static void VerifyResponseControl(byte[] value, int totalCount, byte[] cookie) + { + DirectoryControl control = new(ControlOid, value, true, true); + PageResultResponseControl castControl = TransformResponseControl(control, true) as PageResultResponseControl; + + Assert.Equal(totalCount, castControl.TotalCount); + Assert.Equal(cookie, castControl.Cookie); + } + + private static DirectoryControl TransformResponseControl(DirectoryControl control, bool assertControlProperties) + { + DirectoryControl[] controls = [control]; + DirectoryControl resultantControl; + + try + { + s_transformControlsMethod.Invoke(null, new object[] { controls }); + } + catch (TargetInvocationException tie) + { + throw tie.InnerException; + } + + resultantControl = controls[0]; + + if (assertControlProperties) + { + Assert.Equal(control.Type, resultantControl.Type); + Assert.Equal(control.IsCritical, resultantControl.IsCritical); + Assert.Equal(control.ServerSide, resultantControl.ServerSide); + Assert.Equal(control.GetValue(), resultantControl.GetValue()); + + return Assert.IsType(resultantControl); + } + else + { + return resultantControl; + } + } + } +} diff --git a/src/libraries/System.DirectoryServices.Protocols/tests/SortResponseControlTests.cs b/src/libraries/System.DirectoryServices.Protocols/tests/SortResponseControlTests.cs new file mode 100644 index 00000000000000..0a17dfb27a3380 --- /dev/null +++ b/src/libraries/System.DirectoryServices.Protocols/tests/SortResponseControlTests.cs @@ -0,0 +1,271 @@ +// 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.Generic; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Text; +using Xunit; + +namespace System.DirectoryServices.Protocols.Tests +{ + [ConditionalClass(typeof(DirectoryServicesTestHelpers), nameof(DirectoryServicesTestHelpers.IsWindowsOrLibLdapIsInstalled))] + public class SortResponseControlTests + { + private const string ControlOid = "1.2.840.113556.1.4.474"; + + private static MethodInfo s_transformControlsMethod = typeof(DirectoryControl) + .GetMethod("TransformControls", BindingFlags.NonPublic | BindingFlags.Static); + + public static IEnumerable ConformantControlValues() + { + // {e}, single-byte length + yield return new object[] { new byte[] { 0x30, 0x03, + 0x0A, 0x01, 0x40 + }, (ResultCode)0x40, null }; + yield return new object[] { new byte[] { 0x30, 0x03, + 0x0A, 0x01, 0x7F + }, (ResultCode)0x7F, null }; + + // {e}, four-byte length + yield return new object[] { new byte[] { 0x30, 0x84, 0x00, 0x00, 0x00, 0x03, + 0x0A, 0x01, 0x40 + }, (ResultCode)0x40, null }; + yield return new object[] { new byte[] { 0x30, 0x84, 0x00, 0x00, 0x00, 0x03, + 0x0A, 0x01, 0x7F + }, (ResultCode)0x7F, null }; + + // {ea}, single-byte length + yield return new object[] { new byte[] { 0x30, 0x0A, + 0x0A, 0x01, 0x40, + 0x04, 0x05, 0x6E, 0x61, 0x6D, 0x65, 0x31 + }, (ResultCode)0x40, "name1" }; + yield return new object[] { new byte[] { 0x30, 0x0A, + 0x0A, 0x01, 0x7F, + 0x04, 0x05, 0x6E, 0x61, 0x6D, 0x65, 0x31 + }, (ResultCode)0x7F, "name1" }; + yield return new object[] { new byte[] { 0x30, 0x05, + 0x0A, 0x01, 0x40, + 0x04, 0x00 +#if NET + }, (ResultCode)0x40, RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && Environment.OSVersion.Version.Major < 10 ? null : string.Empty }; +#else + }, (ResultCode)0x40, string.Empty }; +#endif + yield return new object[] { new byte[] { 0x30, 0x05, + 0x0A, 0x01, 0x7F, + 0x04, 0x00 +#if NET + }, (ResultCode)0x7F, RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && Environment.OSVersion.Version.Major < 10 ? null : string.Empty }; +#else + }, (ResultCode)0x7F, string.Empty }; +#endif + + // {ea}, four-byte length + yield return new object[] { new byte[] { 0x30, 0x84, 0x00, 0x00, 0x00, 0x0E, + 0x0A, 0x01, 0x40, + 0x04, 0x84, 0x00, 0x00, 0x00, 0x05, 0x6E, 0x61, 0x6D, 0x65, 0x31 + }, (ResultCode)0x40, "name1" }; + yield return new object[] { new byte[] { 0x30, 0x84, 0x00, 0x00, 0x00, 0x0E, + 0x0A, 0x01, 0x7F, + 0x04, 0x84, 0x00, 0x00, 0x00, 0x05, 0x6E, 0x61, 0x6D, 0x65, 0x31 + }, (ResultCode)0x7F, "name1" }; + yield return new object[] { new byte[] { 0x30, 0x84, 0x00, 0x00, 0x00, 0x09, + 0x0A, 0x01, 0x40, + 0x04, 0x84, 0x00, 0x00, 0x00, 0x00 +#if NET + }, (ResultCode)0x40, RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && Environment.OSVersion.Version.Major < 10 ? null : string.Empty }; +#else + }, (ResultCode)0x40, string.Empty }; +#endif + yield return new object[] { new byte[] { 0x30, 0x84, 0x00, 0x00, 0x00, 0x09, + 0x0A, 0x01, 0x7F, + 0x04, 0x84, 0x00, 0x00, 0x00, 0x00 +#if NET + }, (ResultCode)0x7F, RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && Environment.OSVersion.Version.Major < 10 ? null : string.Empty }; +#else + }, (ResultCode)0x7F, string.Empty }; +#endif + } + + public static IEnumerable NonconformantControlValues() + { + // {i}, single-byte length. ASN.1 type of INTEGER rather than ENUMERATED + yield return new object[] { new byte[] { 0x30, 0x03, + 0x02, 0x01, 0x40 + }, (ResultCode)0x40, null }; + + // {i}, four-byte length. ASN.1 type of INTEGER rather than ENUMERATED + yield return new object[] { new byte[] { 0x30, 0x84, 0x00, 0x00, 0x00, 0x03, + 0x02, 0x01, 0x40 + }, (ResultCode)0x40, null }; + + // {e}, single-byte length. Trailing data after the end of the sequence + yield return new object[] { new byte[] { 0x30, 0x03, + 0x0A, 0x01, 0x40, + 0x80, 0x80, 0x80, 0x80 + }, (ResultCode)0x40, null }; + + // {e}, four-byte length. Trailing data after the end of the sequence + yield return new object[] { new byte[] { 0x30, 0x84, 0x00, 0x00, 0x00, 0x03, + 0x0A, 0x01, 0x40, + 0x80, 0x80, 0x80, 0x80 + }, (ResultCode)0x40, null }; + + // {e}, single-byte length. Trailing data within the sequence is interpreted as an empty string by Windows, null by OpenLDAP + yield return new object[] { new byte[] { 0x30, 0x07, + 0x0A, 0x01, 0x40, + 0x80, 0x80, 0x80, 0x80 + }, (ResultCode)0x40, RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? string.Empty : null }; + + // {e}, four-byte length. Trailing data within the sequence is interpreted as an empty string by Windows, null by OpenLDAP + yield return new object[] { new byte[] { 0x30, 0x84, 0x00, 0x00, 0x00, 0x07, + 0x0A, 0x01, 0x40, + 0x80, 0x80, 0x80, 0x80 + }, (ResultCode)0x40, RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? string.Empty : null }; + + // {ea}, single-byte length. Octet string length extending beyond the end of the sequence (but within the buffer.) + // The result of the "a" format specifier is null on Windows, but any OS platform which uses OpenLDAP will return + // the out-of-sequence contents. This is also why the first trailing data byte is 0x31 rather than 0x80 - 0x80 is + // not a valid Unicode character, so we change it to 0x31 to avoid encountering a DecoderFallbackException before + // we can verify the results + yield return new object[] { new byte[] { 0x30, 0x0A, + 0x0A, 0x01, 0x40, + 0x04, 0x06, 0x6E, 0x61, 0x6D, 0x65, 0x31, 0x31, + 0x80, 0x80, 0x80 + }, (ResultCode)0x40, RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? null : "name11" }; + + // {ea}, four-byte length. Octet string length extending beyond the end of the sequence (but within the buffer.) Result of the "a" format specifier is null + // The comment on the test case above also applies here + yield return new object[] { new byte[] { 0x30, 0x84, 0x00, 0x00, 0x00, 0x0A, + 0x0A, 0x01, 0x40, + 0x04, 0x84, 0x00, 0x00, 0x00, 0x06, 0x6E, 0x61, 0x6D, 0x65, 0x31, 0x31, + 0x80, 0x80, 0x80 + }, (ResultCode)0x40, RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? null : "name11" }; + + // {ea}, single-byte length. Octet string length extending beyond the end of the buffer. Result of the "a" format specifier is null + yield return new object[] { new byte[] { 0x30, 0x0A, + 0x0A, 0x01, 0x40, + 0x04, 0x06, 0x6E, 0x61, 0x6D, 0x65, 0x31 + }, (ResultCode)0x40, null }; + + // {ea}, four-byte length. Octet string length extending beyond the end of the buffer. Result of the "a" format specifier is null + yield return new object[] { new byte[] { 0x30, 0x84, 0x00, 0x00, 0x00, 0x0A, + 0x0A, 0x01, 0x40, + 0x04, 0x84, 0x00, 0x00, 0x00, 0x06, 0x6E, 0x61, 0x6D, 0x65, 0x31 + }, (ResultCode)0x40, null }; + + // {ea}, single-byte length. Trailing data within the sequence (after the octet string) + yield return new object[] { new byte[] { 0x30, 0x0E, + 0x0A, 0x01, 0x40, + 0x04, 0x05, 0x6E, 0x61, 0x6D, 0x65, 0x31, + 0x80, 0x80, 0x80, 0x80 + }, (ResultCode)0x40, "name1" }; + + // {ea}, four-byte length. Trailing data within the sequence (after the octet string) + yield return new object[] { new byte[] { 0x30, 0x84, 0x00, 0x00, 0x00, 0x12, + 0x0A, 0x01, 0x40, + 0x04, 0x84, 0x00, 0x00, 0x00, 0x05, 0x6E, 0x61, 0x6D, 0x65, 0x31, + 0x80, 0x80, 0x80, 0x80 + }, (ResultCode)0x40, "name1" }; + } + + public static IEnumerable InvalidControlValues() + { + // e, not wrapped in an ASN.1 SEQUENCE + yield return new object[] { new byte[] { 0x02, 0x01, 0x40 } }; + + // {e}, single-byte length, sequence length extending beyond the end of the buffer + yield return new object[] { new byte[] { 0x30, 0x04, + 0x0A, 0x01, 0x40 } }; + + // {e}, four-byte length, sequence length extending beyond the end of the buffer + yield return new object[] { new byte[] { 0x30, 0x84, 0x00, 0x00, 0x00, 0x04, + 0x0A, 0x01, 0x40 } }; + } + + public static IEnumerable InvalidUnicodeText() + { + // {ea}, single-byte length. Octet string contains a trailing 0x80 (which is invalid Unicode) + yield return new object[] { new byte[] { 0x30, 0x0B, + 0x0A, 0x01, 0x40, + 0x04, 0x06, 0x6E, 0x61, 0x6D, 0x65, 0x31, 0x80 + } }; + + // {ea}, four-byte length. Octet string contains a trailing 0x80 (which is invalid Unicode) + yield return new object[] { new byte[] { 0x30, 0x84, 0x00, 0x00, 0x00, 0x0F, + 0x0A, 0x01, 0x40, + 0x04, 0x84, 0x00, 0x00, 0x00, 0x06, 0x6E, 0x61, 0x6D, 0x65, 0x31, 0x80 + } }; + } + + [Theory] + [MemberData(nameof(ConformantControlValues))] + public void ConformantResponseControlParsedSuccessfully(byte[] value, ResultCode expectedResultCode, string expectedAttribute) + => VerifyResponseControl(value, expectedResultCode, expectedAttribute); + + [Theory] + [MemberData(nameof(NonconformantControlValues))] + public void NonconformantResponseControlParsedSuccessfully(byte[] value, ResultCode expectedResultCode, string expectedAttribute) + => VerifyResponseControl(value, expectedResultCode, expectedAttribute); + + [Theory] + [MemberData(nameof(InvalidControlValues))] + public void InvalidResponseControlThrowsException(byte[] value) + { + DirectoryControl control = new(ControlOid, value, true, true); + + Assert.Throws(() => TransformResponseControl(control, false)); + } + + [Theory] + [MemberData(nameof(InvalidUnicodeText))] + public void InvalidUnicodeTextThrowsException(byte[] value) + { + DirectoryControl control = new(ControlOid, value, true, true); + + Assert.Throws(() => TransformResponseControl(control, false)); + } + + private static void VerifyResponseControl(byte[] value, ResultCode expectedResultCode, string expectedAttribute) + { + DirectoryControl control = new(ControlOid, value, true, true); + SortResponseControl castControl = TransformResponseControl(control, true) as SortResponseControl; + + Assert.Equal(expectedResultCode, castControl.Result); + Assert.Equal(expectedAttribute, castControl.AttributeName); + } + + private static DirectoryControl TransformResponseControl(DirectoryControl control, bool assertControlProperties) + { + DirectoryControl[] controls = [control]; + DirectoryControl resultantControl; + + try + { + s_transformControlsMethod.Invoke(null, new object[] { controls }); + } + catch (TargetInvocationException tie) + { + throw tie.InnerException; + } + + resultantControl = controls[0]; + + if (assertControlProperties) + { + Assert.Equal(control.Type, resultantControl.Type); + Assert.Equal(control.IsCritical, resultantControl.IsCritical); + Assert.Equal(control.ServerSide, resultantControl.ServerSide); + Assert.Equal(control.GetValue(), resultantControl.GetValue()); + + return Assert.IsType(resultantControl); + } + else + { + return resultantControl; + } + } + } +} diff --git a/src/libraries/System.DirectoryServices.Protocols/tests/System.DirectoryServices.Protocols.Tests.csproj b/src/libraries/System.DirectoryServices.Protocols/tests/System.DirectoryServices.Protocols.Tests.csproj index b35a0089731909..df9f61bb702269 100644 --- a/src/libraries/System.DirectoryServices.Protocols/tests/System.DirectoryServices.Protocols.Tests.csproj +++ b/src/libraries/System.DirectoryServices.Protocols/tests/System.DirectoryServices.Protocols.Tests.csproj @@ -9,10 +9,13 @@ + + + @@ -29,6 +32,7 @@ + @@ -45,6 +49,7 @@ + diff --git a/src/libraries/System.DirectoryServices.Protocols/tests/VlvResponseControlTests.cs b/src/libraries/System.DirectoryServices.Protocols/tests/VlvResponseControlTests.cs new file mode 100644 index 00000000000000..07283a7cc6a0db --- /dev/null +++ b/src/libraries/System.DirectoryServices.Protocols/tests/VlvResponseControlTests.cs @@ -0,0 +1,273 @@ +// 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.Generic; +using System.Reflection; +using System.Runtime.InteropServices; +using Xunit; + +namespace System.DirectoryServices.Protocols.Tests +{ + [ConditionalClass(typeof(DirectoryServicesTestHelpers), nameof(DirectoryServicesTestHelpers.IsWindowsOrLibLdapIsInstalled))] + public class VlvResponseControlTests + { + private const string ControlOid = "2.16.840.1.113730.3.4.10"; + + private static MethodInfo s_transformControlsMethod = typeof(DirectoryControl) + .GetMethod("TransformControls", BindingFlags.NonPublic | BindingFlags.Static); + + public static IEnumerable ConformantControlValues() + { + // {iie}, single-byte length + yield return new object[] { new byte[] { 0x30, 0x09, + 0x02, 0x01, 0x00, + 0x02, 0x01, 0x20, + 0x0A, 0x01, 0x40 + }, 0x00, 0x20, (ResultCode)0x40, Array.Empty() }; + + // {iie}, four-byte length + yield return new object[] { new byte[] { 0x30, 0x84, 0x00, 0x00, 0x00, 0x09, + 0x02, 0x01, 0x00, + 0x02, 0x01, 0x20, + 0x0A, 0x01, 0x40 + }, 0x00, 0x20, (ResultCode)0x40, Array.Empty() }; + + // {iieO}, single-byte length. Varying combinations of a populated & zero-length OCTET STRING + yield return new object[] { new byte[] { 0x30, 0x10, + 0x02, 0x01, 0x00, + 0x02, 0x01, 0x20, + 0x0A, 0x01, 0x40, + 0x04, 0x05, 0xC0, 0xC1, 0xC2, 0xC3, 0xC4 + }, 0x00, 0x20, (ResultCode)0x40, new byte[] { 0xC0, 0xC1, 0xC2, 0xC3, 0xC4 } }; + yield return new object[] { new byte[] { 0x30, 0x0B, + 0x02, 0x01, 0x00, + 0x02, 0x01, 0x20, + 0x0A, 0x01, 0x40, + 0x04, 0x00 + }, 0x00, 0x20, (ResultCode)0x40, Array.Empty() }; + + // {iieO}, four-byte length. Varying combinations of a populated & zero-length OCTET STRING + yield return new object[] { new byte[] { 0x30, 0x84, 0x00, 0x00, 0x00, 0x14, + 0x02, 0x01, 0x00, + 0x02, 0x01, 0x20, + 0x0A, 0x01, 0x40, + 0x04, 0x84, 0x00, 0x00, 0x00, 0x05, 0xC0, 0xC1, 0xC2, 0xC3, 0xC4 + }, 0x00, 0x20, (ResultCode)0x40, new byte[] { 0xC0, 0xC1, 0xC2, 0xC3, 0xC4 } }; + yield return new object[] { new byte[] { 0x30, 0x84, 0x00, 0x00, 0x00, 0x0F, + 0x02, 0x01, 0x00, + 0x02, 0x01, 0x20, + 0x0A, 0x01, 0x40, + 0x04, 0x84, 0x00, 0x00, 0x00, 0x00 + }, 0x00, 0x20, (ResultCode)0x40, Array.Empty() }; + } + + public static IEnumerable NonconformantControlValues() + { + // {eei}, single-byte length. ASN.1 types of ENUMERATED rather than INTEGER, and INTEGER rather than ENUMERATED + yield return new object[] { new byte[] { 0x30, 0x09, + 0x0A, 0x01, 0x00, + 0x0A, 0x01, 0x20, + 0x02, 0x01, 0x40 + }, 0x00, 0x20, (ResultCode)0x40, Array.Empty() }; + + // {eei}, four-byte length. ASN.1 types of ENUMERATED rather than INTEGER, and INTEGER rather than ENUMERATED + yield return new object[] { new byte[] { 0x30, 0x84, 0x00, 0x00, 0x00, 0x09, + 0x0A, 0x01, 0x00, + 0x0A, 0x01, 0x20, + 0x02, 0x01, 0x40 + }, 0x00, 0x20, (ResultCode)0x40, Array.Empty() }; + + // {eeiO}, single-byte length. ASN.1 types of ENUMERATED rather than INTEGER, and INTEGER rather than ENUMERATED + yield return new object[] { new byte[] { 0x30, 0x10, + 0x0A, 0x01, 0x00, + 0x0A, 0x01, 0x20, + 0x02, 0x01, 0x40, + 0x04, 0x05, 0xC0, 0xC1, 0xC2, 0xC3, 0xC4 + }, 0x00, 0x20, (ResultCode)0x40, new byte[] { 0xC0, 0xC1, 0xC2, 0xC3, 0xC4 } }; + + // {eeiO}, four-byte length. ASN.1 types of ENUMERATED rather than INTEGER, and INTEGER rather than ENUMERATED + yield return new object[] { new byte[] { 0x30, 0x84, 0x00, 0x00, 0x00, 0x14, + 0x0A, 0x01, 0x00, + 0x0A, 0x01, 0x20, + 0x02, 0x01, 0x40, + 0x04, 0x84, 0x00, 0x00, 0x00, 0x05, 0xC0, 0xC1, 0xC2, 0xC3, 0xC4 + }, 0x00, 0x20, (ResultCode)0x40, new byte[] { 0xC0, 0xC1, 0xC2, 0xC3, 0xC4 } }; + + // {iieO}, single-byte length. Trailing data after the end of the sequence + yield return new object[] { new byte[] { 0x30, 0x10, + 0x02, 0x01, 0x00, + 0x02, 0x01, 0x20, + 0x0A, 0x01, 0x40, + 0x04, 0x05, 0xC0, 0xC1, 0xC2, 0xC3, 0xC4, + 0x80, 0x80, 0x80, 0x80 + }, 0x00, 0x20, (ResultCode)0x40, new byte[] { 0xC0, 0xC1, 0xC2, 0xC3, 0xC4 } }; + + // {iieO}, four-byte length. Trailing data after the end of the sequence + yield return new object[] { new byte[] { 0x30, 0x84, 0x00, 0x00, 0x00, 0x14, + 0x02, 0x01, 0x00, + 0x02, 0x01, 0x20, + 0x0A, 0x01, 0x40, + 0x04, 0x84, 0x00, 0x00, 0x00, 0x05, 0xC0, 0xC1, 0xC2, 0xC3, 0xC4, + 0x80, 0x80, 0x80, 0x80 + }, 0x00, 0x20, (ResultCode)0x40, new byte[] { 0xC0, 0xC1, 0xC2, 0xC3, 0xC4 } }; + + // {iieO}, single-byte length. Trailing data within the sequence (after the end of the OCTET STRING) + yield return new object[] { new byte[] { 0x30, 0x14, + 0x02, 0x01, 0x00, + 0x02, 0x01, 0x20, + 0x0A, 0x01, 0x40, + 0x04, 0x05, 0xC0, 0xC1, 0xC2, 0xC3, 0xC4, + 0x80, 0x80, 0x80, 0x80 + }, 0x00, 0x20, (ResultCode)0x40, new byte[] { 0xC0, 0xC1, 0xC2, 0xC3, 0xC4 } }; + + // {iieO}, four-byte length. Trailing data within the sequence (after the end of the OCTET STRING) + yield return new object[] { new byte[] { 0x30, 0x84, 0x00, 0x00, 0x00, 0x18, + 0x02, 0x01, 0x00, + 0x02, 0x01, 0x20, + 0x0A, 0x01, 0x40, + 0x04, 0x84, 0x00, 0x00, 0x00, 0x05, 0xC0, 0xC1, 0xC2, 0xC3, 0xC4, + 0x80, 0x80, 0x80, 0x80 + }, 0x00, 0x20, (ResultCode)0x40, new byte[] { 0xC0, 0xC1, 0xC2, 0xC3, 0xC4 } }; + + // These cases would normally fail, but TransformControls will fall back to ignore the octet string. + // This behavior is inconsistent with other tests which cover the parsing of octet strings (which would + // throw an exception rather than return an empty array.) It is also inconsistent between Windows and + // non-Windows platforms. + // {iieO}, single-byte length. Octet string length extending beyond the end of the sequence (but within the buffer) + yield return new object[] { new byte[] { 0x30, 0x10, + 0x02, 0x01, 0x00, + 0x02, 0x01, 0x20, + 0x0A, 0x01, 0x40, + 0x04, 0x06, 0xC0, 0xC1, 0xC2, 0xC3, 0xC4, 0x80, + 0x80, 0x80, 0x80 + }, 0x00, 0x20, (ResultCode)0x40, RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? Array.Empty() : new byte[] { 0xC0, 0xC1, 0xC2, 0xC3, 0xC4, 0x80 } }; + + // {iieO}, four-byte length. Octet string length extending beyond the end of the sequence (but within the buffer) + yield return new object[] { new byte[] { 0x30, 0x84, 0x00, 0x00, 0x00, 0x14, + 0x02, 0x01, 0x00, + 0x02, 0x01, 0x20, + 0x0A, 0x01, 0x40, + 0x04, 0x84, 0x00, 0x00, 0x00, 0x06, 0xC0, 0xC1, 0xC2, 0xC3, 0xC4, 0x80, + 0x80, 0x80, 0x80 + }, 0x00, 0x20, (ResultCode)0x40, RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? Array.Empty() : new byte[] { 0xC0, 0xC1, 0xC2, 0xC3, 0xC4, 0x80 } }; + + // {iieO}, single-byte length. Octet string length extending beyond the end of the buffer + yield return new object[] { new byte[] { 0x30, 0x10, + 0x02, 0x01, 0x00, + 0x02, 0x01, 0x20, + 0x0A, 0x01, 0x40, + 0x04, 0x06, 0xC0, 0xC1, 0xC2, 0xC3, 0xC4 + }, 0x00, 0x20, (ResultCode)0x40, Array.Empty() }; + + // {iieO}, four-byte length. Octet string length extending beyond the end of the buffer + yield return new object[] { new byte[] { 0x30, 0x84, 0x00, 0x00, 0x00, 0x14, + 0x02, 0x01, 0x00, + 0x02, 0x01, 0x20, + 0x0A, 0x01, 0x40, + 0x04, 0x84, 0x00, 0x00, 0x00, 0x06, 0xC0, 0xC1, 0xC2, 0xC3, 0xC4 + }, 0x00, 0x20, (ResultCode)0x40, Array.Empty() }; + } + + public static IEnumerable InvalidControlValues() + { + // i, not wrapped in an ASN.1 SEQUENCE + yield return new object[] { new byte[] { 0x02, 0x01, 0x00 } }; + + // ii, not wrapped in an ASN.1 SEQUENCE + yield return new object[] { new byte[] { 0x02, 0x01, 0x00, + 0x02, 0x01, 0x20 } }; + + // iie, not wrapped in an ASN.1 SEQUENCE + yield return new object[] { new byte[] { 0x02, 0x01, 0x00, + 0x02, 0x01, 0x20, + 0x0A, 0x01, 0x40 } }; + + // iieO, single-byte length, not wrapped in an ASN.1 SEQUENCE + yield return new object[] { new byte[] { 0x02, 0x01, 0x00, + 0x02, 0x01, 0x20, + 0x0A, 0x01, 0x40, + 0x04, 0x05, 0xC0, 0xC1, 0xC2, 0xC3, 0xC4 } }; + + // iieO, four-byte length, not wrapped in an ASN.1 SEQUENCE + yield return new object[] { new byte[] { 0x02, 0x01, 0x00, + 0x02, 0x01, 0x20, + 0x0A, 0x01, 0x40, + 0x04, 0x84, 0x00, 0x00, 0x00, 0x05, 0xC0, 0xC1, 0xC2, 0xC3, 0xC4 } }; + + // {iieO}, single-byte length, sequence length extending beyond the end of the buffer + yield return new object[] { new byte[] { 0x30, 0x0C, + 0x02, 0x01, 0x00, + 0x02, 0x01, 0x20, + 0x0A, 0x01, 0x40, + 0x04, 0x00 } }; + + // {iieO}, four-byte length, sequence length extending beyond the end of the buffer + yield return new object[] { new byte[] { 0x30, 0x84, 0x00, 0x00, 0x00, 0x10, + 0x02, 0x01, 0x00, + 0x02, 0x01, 0x20, + 0x0A, 0x01, 0x40, + 0x04, 0x84, 0x00, 0x00, 0x00, 0x00 } }; + } + + [Theory] + [MemberData(nameof(ConformantControlValues))] + public void ConformantResponseControlParsedSuccessfully(byte[] value, int targetPosition, int contentCount, ResultCode result, byte[] contextId) + => VerifyResponseControl(value, targetPosition, contentCount, result, contextId); + + [Theory] + [MemberData(nameof(NonconformantControlValues))] + public void NonconformantResponseControlParsedSuccessfully(byte[] value, int targetPosition, int contentCount, ResultCode result, byte[] contextId) + => VerifyResponseControl(value, targetPosition, contentCount, result, contextId); + + [Theory] + [MemberData(nameof(InvalidControlValues))] + public void InvalidResponseControlThrowsException(byte[] value) + { + DirectoryControl control = new(ControlOid, value, true, true); + + Assert.Throws(() => TransformResponseControl(control, false)); + } + + private static void VerifyResponseControl(byte[] value, int targetPosition, int contentCount, ResultCode result, byte[] contextId) + { + DirectoryControl control = new(ControlOid, value, true, true); + VlvResponseControl castControl = TransformResponseControl(control, true) as VlvResponseControl; + + Assert.Equal(targetPosition, castControl.TargetPosition); + Assert.Equal(contentCount, castControl.ContentCount); + Assert.Equal(result, castControl.Result); + Assert.Equal(contextId, castControl.ContextId); + } + + private static DirectoryControl TransformResponseControl(DirectoryControl control, bool assertControlProperties) + { + DirectoryControl[] controls = [control]; + DirectoryControl resultantControl; + + try + { + s_transformControlsMethod.Invoke(null, new object[] { controls }); + } + catch (TargetInvocationException tie) + { + throw tie.InnerException; + } + + resultantControl = controls[0]; + + if (assertControlProperties) + { + Assert.Equal(control.Type, resultantControl.Type); + Assert.Equal(control.IsCritical, resultantControl.IsCritical); + Assert.Equal(control.ServerSide, resultantControl.ServerSide); + Assert.Equal(control.GetValue(), resultantControl.GetValue()); + + return Assert.IsType(resultantControl); + } + else + { + return resultantControl; + } + } + } +}