From fa7a2afd14cf728376ba425bda9ad49ab9945f9a Mon Sep 17 00:00:00 2001 From: Edward Neal <55035479+edwardneal@users.noreply.github.com> Date: Fri, 30 Aug 2024 07:44:30 +0100 Subject: [PATCH 1/4] Added tests for server response DirectoryControls --- .../tests/AsqResponseControlTests.cs | 168 +++++++++++ .../tests/DirSyncResponseControlTests.cs | 234 +++++++++++++++ .../tests/PageResultResponseControlTests.cs | 207 ++++++++++++++ .../tests/SortResponseControlTests.cs | 223 +++++++++++++++ ...m.DirectoryServices.Protocols.Tests.csproj | 5 + .../tests/VlvResponseControlTests.cs | 269 ++++++++++++++++++ 6 files changed, 1106 insertions(+) create mode 100644 src/libraries/System.DirectoryServices.Protocols/tests/AsqResponseControlTests.cs create mode 100644 src/libraries/System.DirectoryServices.Protocols/tests/DirSyncResponseControlTests.cs create mode 100644 src/libraries/System.DirectoryServices.Protocols/tests/PageResultResponseControlTests.cs create mode 100644 src/libraries/System.DirectoryServices.Protocols/tests/SortResponseControlTests.cs create mode 100644 src/libraries/System.DirectoryServices.Protocols/tests/VlvResponseControlTests.cs 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..04983dd497ad6b --- /dev/null +++ b/src/libraries/System.DirectoryServices.Protocols/tests/DirSyncResponseControlTests.cs @@ -0,0 +1,234 @@ +// 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 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 } }; + } + + 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} }; + + // {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..bddc0569c8d1d9 --- /dev/null +++ b/src/libraries/System.DirectoryServices.Protocols/tests/PageResultResponseControlTests.cs @@ -0,0 +1,207 @@ +// 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 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 } }; + } + + 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} }; + + // {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..8bf51945aeb12f --- /dev/null +++ b/src/libraries/System.DirectoryServices.Protocols/tests/SortResponseControlTests.cs @@ -0,0 +1,223 @@ +// 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 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 + }, (ResultCode)0x40, string.Empty }; + yield return new object[] { new byte[] { 0x30, 0x05, + 0x0A, 0x01, 0x7F, + 0x04, 0x00 + }, (ResultCode)0x7F, string.Empty }; + + // {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 + }, (ResultCode)0x40, string.Empty }; + yield return new object[] { new byte[] { 0x30, 0x84, 0x00, 0x00, 0x00, 0x09, + 0x0A, 0x01, 0x7F, + 0x04, 0x84, 0x00, 0x00, 0x00, 0x00 + }, (ResultCode)0x7F, string.Empty }; + } + + 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 + yield return new object[] { new byte[] { 0x30, 0x07, + 0x0A, 0x01, 0x40, + 0x80, 0x80, 0x80, 0x80 + }, (ResultCode)0x40, string.Empty }; + + // {e}, four-byte length. Trailing data within the sequence is interpreted as an empty string + yield return new object[] { new byte[] { 0x30, 0x84, 0x00, 0x00, 0x00, 0x07, + 0x0A, 0x01, 0x40, + 0x80, 0x80, 0x80, 0x80 + }, (ResultCode)0x40, string.Empty }; + + // {ea}, single-byte length. Octet string length extending beyond the end of the sequence (but within 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, 0x80, + 0x80, 0x80, 0x80 + }, (ResultCode)0x40, null }; + + // {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 + 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, 0x80, + 0x80, 0x80, 0x80 + }, (ResultCode)0x40, null }; + + // {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 } }; + } + + [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)); + } + + 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..a5880ce4d97e90 --- /dev/null +++ b/src/libraries/System.DirectoryServices.Protocols/tests/VlvResponseControlTests.cs @@ -0,0 +1,269 @@ +// 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 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. + // {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, Array.Empty() }; + + // {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, Array.Empty() }; + + // {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; + } + } + } +} From d34ff414ae10d297ee93c895bd7c064b286bca28 Mon Sep 17 00:00:00 2001 From: Edward Neal <55035479+edwardneal@users.noreply.github.com> Date: Sat, 31 Aug 2024 08:32:03 +0100 Subject: [PATCH 2/4] Addressing platform inconsistencies * There are looser rules in OpenLDAP when an octet string exceeds the length of its containing sequence. * Windows 8.1 and below treats trailing 0x80 bytes differently to every other platform. * Added a test which covers an octet string containing invalid Unicode bytes. --- .../tests/DirSyncResponseControlTests.cs | 52 ++++++++++++++----- .../tests/PageResultResponseControlTests.cs | 46 ++++++++++++---- .../tests/SortResponseControlTests.cs | 52 ++++++++++++++++--- .../tests/VlvResponseControlTests.cs | 8 ++- 4 files changed, 125 insertions(+), 33 deletions(-) diff --git a/src/libraries/System.DirectoryServices.Protocols/tests/DirSyncResponseControlTests.cs b/src/libraries/System.DirectoryServices.Protocols/tests/DirSyncResponseControlTests.cs index 04983dd497ad6b..b670477c499e42 100644 --- a/src/libraries/System.DirectoryServices.Protocols/tests/DirSyncResponseControlTests.cs +++ b/src/libraries/System.DirectoryServices.Protocols/tests/DirSyncResponseControlTests.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Reflection; +using System.Runtime.InteropServices; using Xunit; namespace System.DirectoryServices.Protocols.Tests @@ -111,6 +112,26 @@ public static IEnumerable NonconformantControlValues() 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() @@ -144,19 +165,24 @@ public static IEnumerable InvalidControlValues() 0x02, 0x01, 0x40, 0x04, 0x84, 0x00, 0x00, 0x00, 0x00} }; - // {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 } }; + // 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, diff --git a/src/libraries/System.DirectoryServices.Protocols/tests/PageResultResponseControlTests.cs b/src/libraries/System.DirectoryServices.Protocols/tests/PageResultResponseControlTests.cs index bddc0569c8d1d9..bb02931f17c487 100644 --- a/src/libraries/System.DirectoryServices.Protocols/tests/PageResultResponseControlTests.cs +++ b/src/libraries/System.DirectoryServices.Protocols/tests/PageResultResponseControlTests.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Reflection; +using System.Runtime.InteropServices; using Xunit; namespace System.DirectoryServices.Protocols.Tests @@ -97,6 +98,24 @@ public static IEnumerable NonconformantControlValues() 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() @@ -122,17 +141,22 @@ public static IEnumerable InvalidControlValues() 0x02, 0x01, 0x40, 0x04, 0x84, 0x00, 0x00, 0x00, 0x00} }; - // {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 } }; + // 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, diff --git a/src/libraries/System.DirectoryServices.Protocols/tests/SortResponseControlTests.cs b/src/libraries/System.DirectoryServices.Protocols/tests/SortResponseControlTests.cs index 8bf51945aeb12f..04a55a7da15792 100644 --- a/src/libraries/System.DirectoryServices.Protocols/tests/SortResponseControlTests.cs +++ b/src/libraries/System.DirectoryServices.Protocols/tests/SortResponseControlTests.cs @@ -3,6 +3,9 @@ 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 @@ -83,16 +86,22 @@ public static IEnumerable NonconformantControlValues() }, (ResultCode)0x40, null }; // {e}, single-byte length. Trailing data after the end of the sequence + // In this scenario, OpenLDAP and Windows 10 or above will return null. Anything before Windows 10 will return + // an empty string. This is likely because the trailing [0x80, 0x80, 0x80, 0x80] is being interpreted as a TLV + // with a length of 0x80. A length of 0x80 has bit 7 set (indicating a long-form encoding) but indicates to the + // parser that the next zero (!) bytes contain the actual length of the value. Windows <10 appears to treat this + // as a zero-length value, OpenLDAP and Windows >10 appears to treat this as a BER element which is lacking a length. yield return new object[] { new byte[] { 0x30, 0x03, 0x0A, 0x01, 0x40, 0x80, 0x80, 0x80, 0x80 - }, (ResultCode)0x40, null }; + }, (ResultCode)0x40, RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && Environment.OSVersion.Version.Major < 10 ? string.Empty : null }; // {e}, four-byte length. Trailing data after the end of the sequence + // The comment on the test case above also applies here. yield return new object[] { new byte[] { 0x30, 0x84, 0x00, 0x00, 0x00, 0x03, 0x0A, 0x01, 0x40, 0x80, 0x80, 0x80, 0x80 - }, (ResultCode)0x40, null }; + }, (ResultCode)0x40, RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && Environment.OSVersion.Version.Major < 10 ? string.Empty : null }; // {e}, single-byte length. Trailing data within the sequence is interpreted as an empty string yield return new object[] { new byte[] { 0x30, 0x07, @@ -106,19 +115,24 @@ public static IEnumerable NonconformantControlValues() 0x80, 0x80, 0x80, 0x80 }, (ResultCode)0x40, string.Empty }; - // {ea}, single-byte length. Octet string length extending beyond the end of the sequence (but within the buffer.) Result of the "a" format specifier is 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, 0x80, + 0x04, 0x06, 0x6E, 0x61, 0x6D, 0x65, 0x31, 0x31, 0x80, 0x80, 0x80 - }, (ResultCode)0x40, null }; + }, (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, 0x80, + 0x04, 0x84, 0x00, 0x00, 0x00, 0x06, 0x6E, 0x61, 0x6D, 0x65, 0x31, 0x31, 0x80, 0x80, 0x80 - }, (ResultCode)0x40, null }; + }, (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, @@ -161,6 +175,21 @@ public static IEnumerable InvalidControlValues() 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) @@ -180,6 +209,15 @@ public void InvalidResponseControlThrowsException(byte[] value) 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); diff --git a/src/libraries/System.DirectoryServices.Protocols/tests/VlvResponseControlTests.cs b/src/libraries/System.DirectoryServices.Protocols/tests/VlvResponseControlTests.cs index a5880ce4d97e90..07283a7cc6a0db 100644 --- a/src/libraries/System.DirectoryServices.Protocols/tests/VlvResponseControlTests.cs +++ b/src/libraries/System.DirectoryServices.Protocols/tests/VlvResponseControlTests.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Reflection; +using System.Runtime.InteropServices; using Xunit; namespace System.DirectoryServices.Protocols.Tests @@ -129,6 +130,9 @@ public static IEnumerable NonconformantControlValues() }, 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, @@ -136,7 +140,7 @@ public static IEnumerable NonconformantControlValues() 0x0A, 0x01, 0x40, 0x04, 0x06, 0xC0, 0xC1, 0xC2, 0xC3, 0xC4, 0x80, 0x80, 0x80, 0x80 - }, 0x00, 0x20, (ResultCode)0x40, Array.Empty() }; + }, 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, @@ -145,7 +149,7 @@ public static IEnumerable NonconformantControlValues() 0x0A, 0x01, 0x40, 0x04, 0x84, 0x00, 0x00, 0x00, 0x06, 0xC0, 0xC1, 0xC2, 0xC3, 0xC4, 0x80, 0x80, 0x80, 0x80 - }, 0x00, 0x20, (ResultCode)0x40, Array.Empty() }; + }, 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, From 10ae098668dadf0665f17f200ed69b701278df9d Mon Sep 17 00:00:00 2001 From: Edward Neal <55035479+edwardneal@users.noreply.github.com> Date: Sat, 31 Aug 2024 16:53:10 +0100 Subject: [PATCH 3/4] Handle differing .NET Framework behaviour Also handle changes to the "a" format string: Winldap in Windows 10 is stricter when a SEQUENCE's contents overflow its length --- .../tests/SortResponseControlTests.cs | 55 +++++++++++++++---- 1 file changed, 44 insertions(+), 11 deletions(-) diff --git a/src/libraries/System.DirectoryServices.Protocols/tests/SortResponseControlTests.cs b/src/libraries/System.DirectoryServices.Protocols/tests/SortResponseControlTests.cs index 04a55a7da15792..3c5c5cbea6a099 100644 --- a/src/libraries/System.DirectoryServices.Protocols/tests/SortResponseControlTests.cs +++ b/src/libraries/System.DirectoryServices.Protocols/tests/SortResponseControlTests.cs @@ -48,11 +48,19 @@ public static IEnumerable ConformantControlValues() 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, @@ -66,21 +74,29 @@ public static IEnumerable ConformantControlValues() 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. + // {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. + // {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 }; @@ -91,35 +107,52 @@ public static IEnumerable NonconformantControlValues() // with a length of 0x80. A length of 0x80 has bit 7 set (indicating a long-form encoding) but indicates to the // parser that the next zero (!) bytes contain the actual length of the value. Windows <10 appears to treat this // as a zero-length value, OpenLDAP and Windows >10 appears to treat this as a BER element which is lacking a length. + // This behavior is unique to .NET yield return new object[] { new byte[] { 0x30, 0x03, 0x0A, 0x01, 0x40, 0x80, 0x80, 0x80, 0x80 +#if NET }, (ResultCode)0x40, RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && Environment.OSVersion.Version.Major < 10 ? string.Empty : null }; +#else + }, (ResultCode)0x40, null }; +#endif // {e}, four-byte length. Trailing data after the end of the sequence - // The comment on the test case above also applies here. + // The comment on the test case above also applies here yield return new object[] { new byte[] { 0x30, 0x84, 0x00, 0x00, 0x00, 0x03, 0x0A, 0x01, 0x40, 0x80, 0x80, 0x80, 0x80 +#if NET }, (ResultCode)0x40, RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && Environment.OSVersion.Version.Major < 10 ? string.Empty : null }; +#else + }, (ResultCode)0x40, null }; +#endif - // {e}, single-byte length. Trailing data within the sequence is interpreted as an empty string + // {e}, single-byte length. Trailing data within the sequence is interpreted as null yield return new object[] { new byte[] { 0x30, 0x07, 0x0A, 0x01, 0x40, 0x80, 0x80, 0x80, 0x80 - }, (ResultCode)0x40, string.Empty }; +#if NET + }, (ResultCode)0x40, RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && Environment.OSVersion.Version.Major >= 10 ? string.Empty : null }; +#else + }, (ResultCode)0x40, null }; +#endif - // {e}, four-byte length. Trailing data within the sequence is interpreted as an empty string + // {e}, four-byte length. Trailing data within the sequence is interpreted as null yield return new object[] { new byte[] { 0x30, 0x84, 0x00, 0x00, 0x00, 0x07, 0x0A, 0x01, 0x40, 0x80, 0x80, 0x80, 0x80 - }, (ResultCode)0x40, string.Empty }; +#if NET + }, (ResultCode)0x40, RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && Environment.OSVersion.Version.Major >= 10 ? string.Empty : null }; +#else + }, (ResultCode)0x40, null }; +#endif // {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. + // we can verify the results yield return new object[] { new byte[] { 0x30, 0x0A, 0x0A, 0x01, 0x40, 0x04, 0x06, 0x6E, 0x61, 0x6D, 0x65, 0x31, 0x31, @@ -127,7 +160,7 @@ public static IEnumerable NonconformantControlValues() }, (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. + // 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, @@ -177,13 +210,13 @@ public static IEnumerable InvalidControlValues() public static IEnumerable InvalidUnicodeText() { - // {ea}, single-byte length. Octet string contains a trailing 0x80 (which is invalid Unicode.) + // {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.) + // {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 From cbd51352f9232c1d679d4cb60b1148bb6cb79253 Mon Sep 17 00:00:00 2001 From: Edward Neal <55035479+edwardneal@users.noreply.github.com> Date: Sat, 31 Aug 2024 20:14:19 +0100 Subject: [PATCH 4/4] Update SortResponseControlTests.cs --- .../tests/SortResponseControlTests.cs | 31 +++---------------- 1 file changed, 4 insertions(+), 27 deletions(-) diff --git a/src/libraries/System.DirectoryServices.Protocols/tests/SortResponseControlTests.cs b/src/libraries/System.DirectoryServices.Protocols/tests/SortResponseControlTests.cs index 3c5c5cbea6a099..0a17dfb27a3380 100644 --- a/src/libraries/System.DirectoryServices.Protocols/tests/SortResponseControlTests.cs +++ b/src/libraries/System.DirectoryServices.Protocols/tests/SortResponseControlTests.cs @@ -102,51 +102,28 @@ public static IEnumerable NonconformantControlValues() }, (ResultCode)0x40, null }; // {e}, single-byte length. Trailing data after the end of the sequence - // In this scenario, OpenLDAP and Windows 10 or above will return null. Anything before Windows 10 will return - // an empty string. This is likely because the trailing [0x80, 0x80, 0x80, 0x80] is being interpreted as a TLV - // with a length of 0x80. A length of 0x80 has bit 7 set (indicating a long-form encoding) but indicates to the - // parser that the next zero (!) bytes contain the actual length of the value. Windows <10 appears to treat this - // as a zero-length value, OpenLDAP and Windows >10 appears to treat this as a BER element which is lacking a length. - // This behavior is unique to .NET yield return new object[] { new byte[] { 0x30, 0x03, 0x0A, 0x01, 0x40, 0x80, 0x80, 0x80, 0x80 -#if NET - }, (ResultCode)0x40, RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && Environment.OSVersion.Version.Major < 10 ? string.Empty : null }; -#else }, (ResultCode)0x40, null }; -#endif // {e}, four-byte length. Trailing data after the end of the sequence - // The comment on the test case above also applies here yield return new object[] { new byte[] { 0x30, 0x84, 0x00, 0x00, 0x00, 0x03, 0x0A, 0x01, 0x40, 0x80, 0x80, 0x80, 0x80 -#if NET - }, (ResultCode)0x40, RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && Environment.OSVersion.Version.Major < 10 ? string.Empty : null }; -#else }, (ResultCode)0x40, null }; -#endif - // {e}, single-byte length. Trailing data within the sequence is interpreted as 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 -#if NET - }, (ResultCode)0x40, RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && Environment.OSVersion.Version.Major >= 10 ? string.Empty : null }; -#else - }, (ResultCode)0x40, null }; -#endif + }, (ResultCode)0x40, RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? string.Empty : null }; - // {e}, four-byte length. Trailing data within the sequence is interpreted as 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 -#if NET - }, (ResultCode)0x40, RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && Environment.OSVersion.Version.Major >= 10 ? string.Empty : null }; -#else - }, (ResultCode)0x40, null }; -#endif + }, (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