diff --git a/tests/Opc.Ua.Types.Tests/Encoders/EncoderStackDepthTests.cs b/tests/Opc.Ua.Types.Tests/Encoders/EncoderStackDepthTests.cs index 08b1a2d5be..cdb996186f 100644 --- a/tests/Opc.Ua.Types.Tests/Encoders/EncoderStackDepthTests.cs +++ b/tests/Opc.Ua.Types.Tests/Encoders/EncoderStackDepthTests.cs @@ -75,6 +75,10 @@ public enum Shape private const int kBuilderStack = 256 * 1024 * 1024; private const int kUnboundedLimit = 1_000_000; + // Twice the deepest nesting of any codec and shape that fits a one + // megabyte stack before the stack check fires. + private const int kBeyondTheStackDepth = 5000; + private static readonly Codec[] s_decoderCodecs = [Codec.Binary, Codec.Json, Codec.Xml, Codec.XmlParser]; private static readonly Codec[] s_encoderCodecs = [Codec.Binary, Codec.Json, Codec.Xml]; @@ -86,6 +90,11 @@ public enum Shape Shape.ExtensionObjectChain, Shape.EncodeableTree ]; + + // A thread can get more stack than it asks for: on Linux, glibc runs it + // on the cached stack of an exited thread up to four times the requested + // size, and the stack check measures the stack the thread got. A quarter + // megabyte request can therefore run on a one megabyte stack. private static readonly int[] s_stackSizes = [kOneMegabyte, kQuarterMegabyte]; private static IEnumerable DecoderCases() @@ -140,30 +149,51 @@ public void EncodeBeyondDefaultNestingLimitDoesNotOverflowTheStack(Codec codec, public void DecodeWithUnboundedNestingLimitFailsBeforeTheStackOverflows(Codec codec, Shape shape) { // Without a usable nesting limit only the stack check stands between - // a deep message and a crash. The JSON encoder cannot write more than - // 1000 levels, which still overflowed a small stack before. - int depth = codec == Codec.Json ? 450 : 2000; - object input = EncodeOnLargeStack(codec, Build(shape, depth)); + // a deep message and a crash. The JsonEncoder cannot write more than + // 1000 levels, so the JSON input is written by hand. + object input = codec == Codec.Json + ? CreateNestedJson(shape, kBeyondTheStackDepth) + : EncodeOnLargeStack(codec, Build(shape, kBeyondTheStackDepth)); - ServiceResultException sre = RunOnThread( - kQuarterMegabyte, - () => Decode(codec, shape, input, CreateContext(kUnboundedLimit))); + foreach (int stackSize in s_stackSizes) + { + ServiceResultException sre = RunOnThread( + stackSize, + () => Decode(codec, shape, input, CreateContext(kUnboundedLimit))); - Assert.That(sre, Is.Not.Null); - Assert.That(sre.StatusCode, Is.EqualTo(StatusCodes.BadEncodingLimitsExceeded)); + Assert.That(sre, Is.Not.Null, $"Stack of {stackSize} bytes"); + Assert.That(sre.StatusCode, Is.EqualTo(StatusCodes.BadEncodingLimitsExceeded)); + } } [TestCaseSource(nameof(EncoderCases))] public void EncodeWithUnboundedNestingLimitFailsBeforeTheStackOverflows(Codec codec, Shape shape) { - object graph = Build(shape, 2000); + object graph = Build(shape, kBeyondTheStackDepth); - ServiceResultException sre = RunOnThread( - kQuarterMegabyte, - () => Encode(codec, graph, CreateContext(kUnboundedLimit))); + foreach (int stackSize in s_stackSizes) + { + ServiceResultException sre = RunOnThread( + stackSize, + () => Encode(codec, graph, CreateContext(kUnboundedLimit))); - Assert.That(sre, Is.Not.Null); - Assert.That(sre.StatusCode, Is.EqualTo(StatusCodes.BadEncodingLimitsExceeded)); + Assert.That(sre, Is.Not.Null, $"Stack of {stackSize} bytes"); + Assert.That(sre.StatusCode, Is.EqualTo(StatusCodes.BadEncodingLimitsExceeded)); + } + } + + [Test] + public void NestedJsonMatchesTheJsonEncoder([Values] Shape shape) + { + // The JSON inputs nested beyond the encoder's reach are written by + // hand. They must stay what the JsonEncoder writes, or the decoder + // tests stop exercising the encoding a peer sends. + foreach (int depth in new[] { 1, 2, 7 }) + { + string encoded = (string)EncodeOnLargeStack(Codec.Json, Build(shape, depth)); + + Assert.That(CreateNestedJson(shape, depth), Is.EqualTo(encoded), $"Depth {depth}"); + } } [TestCase(Codec.Xml)] @@ -345,6 +375,50 @@ private static object Build(Shape shape, int depth) } } + /// + /// Writes the JSON that the JsonEncoder writes for + /// at any depth. The encoder itself cannot nest deeper than 1000 JSON + /// levels. + /// + private static string CreateNestedJson(Shape shape, int depth) + { + const string int32 = "{\"UaType\":6,\"Value\":1}"; + (string head, string open, string core, string close, string tail, int levels) = shape switch + { + Shape.VariantArray => ("{\"Value\":", "{\"UaType\":24,\"Value\":[", int32, "]}", "}", depth), + Shape.DataValue => ("{\"Value\":", "{\"UaType\":23,\"Value\":", int32, "}", "}", depth), + Shape.ExtensionObjectVariant => ( + "{\"Value\":", + "{\"UaType\":22,\"Value\":{\"UaTypeId\":\"i=88901\",\"Value\":", + int32, + "}}", + "}", + depth), + Shape.ExtensionObjectChain => ( + "{\"Value\":{\"UaType\":22,\"Value\":", + "{\"UaTypeId\":\"i=88911\",\"Child\":", + "null", + "}", + "}}", + depth + 1), + _ => ("{\"Value\":", "{\"Children\":[", "{\"Children\":null}", "]}", "}", depth) + }; + + var builder = new StringBuilder( + head.Length + core.Length + tail.Length + (levels * (open.Length + close.Length))); + builder.Append(head); + for (int ii = 0; ii < levels; ii++) + { + builder.Append(open); + } + builder.Append(core); + for (int ii = 0; ii < levels; ii++) + { + builder.Append(close); + } + return builder.Append(tail).ToString(); + } + /// /// Encodes the input for a decoder test on a large stack with a limit /// that allows its depth.