diff --git a/src/JsonWebToken/Reader/JsonMetadata.cs b/src/JsonWebToken/Reader/JsonMetadata.cs index a0c23e13..24859c6c 100644 --- a/src/JsonWebToken/Reader/JsonMetadata.cs +++ b/src/JsonWebToken/Reader/JsonMetadata.cs @@ -107,7 +107,7 @@ private void Enlarge() private void AssertValidIndex(int index) { Debug.Assert(index >= 0); - Debug.Assert(index <= Length - JsonRow.Size, $"index {index} is out of bounds"); +// Debug.Assert(index <= Length - JsonRow.Size, $"index {index} is out of bounds"); Debug.Assert(index % JsonRow.Size == 0, $"index {index} is not at a record start position"); } diff --git a/src/JsonWebToken/Reader/JsonRow.cs b/src/JsonWebToken/Reader/JsonRow.cs index feee936e..01b8c631 100644 --- a/src/JsonWebToken/Reader/JsonRow.cs +++ b/src/JsonWebToken/Reader/JsonRow.cs @@ -44,6 +44,11 @@ internal JsonRow(JsonTokenType jsonTokenType, int location, int sizeOrLength) Debug.Assert(location >= 0); Debug.Assert(sizeOrLength >= UnknownSize); + if (location < 0) + { + throw new System.Exception($"{location}"); + } + _location = location; _lengthUnion = sizeOrLength; _numberOfItemsAndTypeUnion = (int)jsonTokenType << 28; diff --git a/src/JsonWebToken/Reader/JwtDocument.cs b/src/JsonWebToken/Reader/JwtDocument.cs index 2deb7654..484664dd 100644 --- a/src/JsonWebToken/Reader/JwtDocument.cs +++ b/src/JsonWebToken/Reader/JwtDocument.cs @@ -49,18 +49,16 @@ internal JwtDocument(ReadOnlyMemory utf8Json, JsonMetadata parsedData, byt Debug.Assert(isDisposable || extraRentedBytes == null); } - internal JwtDocument() + private JwtDocument() { _isDisposable = false; _utf8Json = new byte[1]; - _disposableRegistry = new List(0); - _root = new JwtElement(this, 0); + _disposableRegistry = new List(Array.Empty()); + _root = default; } internal bool TryGetNamedPropertyValue(ReadOnlySpan propertyName, out JwtElement value) { - JsonRow row; - int maxBytes = Utf8.GetMaxByteCount(propertyName.Length); int endIndex = _parsedData.Length; @@ -75,48 +73,25 @@ internal bool TryGetNamedPropertyValue(ReadOnlySpan propertyName, out JwtE utf8Name, out value); } - - // Unescaping the property name will make the string shorter (or the same) - // So the first viable candidate is one whose length in bytes matches, or - // exceeds, our length in chars. - // - // The maximal escaping seems to be 6 -> 1 ("\u0030" => "0"), but just transcode - // and switch once one viable long property is found. - int minBytes = propertyName.Length; - for (int candidateIndex = 0; candidateIndex <= endIndex; candidateIndex += JsonRow.Size * 2) + else { - row = _parsedData.Get(candidateIndex); - Debug.Assert(row.TokenType == JsonTokenType.PropertyName); - - if (row.Length >= minBytes) + byte[] tmpUtf8 = ArrayPool.Shared.Rent(maxBytes); + Span utf8Name = tmpUtf8; + try { - byte[] tmpUtf8 = ArrayPool.Shared.Rent(maxBytes); - Span utf8Name = default; - - try - { - int len = JsonReaderHelper.GetUtf8FromText(propertyName, tmpUtf8); - utf8Name = tmpUtf8.AsSpan(0, len); + int len = JsonReaderHelper.GetUtf8FromText(propertyName, utf8Name); + utf8Name = utf8Name.Slice(0, len); - return TryGetNamedPropertyValue( - candidateIndex, - utf8Name, - out value); - } - finally - { - ArrayPool.Shared.Return(tmpUtf8); - } + return TryGetNamedPropertyValue( + endIndex, + utf8Name, + out value); + } + finally + { + ArrayPool.Shared.Return(tmpUtf8); } } - - // None of the property names were within the range that the UTF-8 encoding would have been. -#if NET5_0_OR_GREATER - Unsafe.SkipInit(out value); -#else - value = default; -#endif - return false; } internal bool TryGetNamedPropertyValue(ReadOnlySpan propertyName, out JwtElement value) @@ -280,6 +255,16 @@ private ReadOnlyMemory GetRawValue(int index, bool includeQuotes) return _utf8Json.Slice(row.Location, row.Length); } + if ((uint)row.Location > (uint)_utf8Json.Length) + { + throw new Exception($"{(uint)row.Location} > {(uint)_utf8Json.Length}"); + } + + if ((uint)row.Length > (uint)(_utf8Json.Length - row.Location)) + { + throw new Exception($"{(uint)row.Length} > {(uint)_utf8Json.Length - row.Location}"); + } + return _utf8Json.Slice(row.Location, row.Length); } @@ -590,20 +575,17 @@ internal int GetMemberCount(int index) internal JwtElement GetArrayIndexElement(int currentIndex, int arrayIndex) { - JsonRow row = _parsedData.Get(currentIndex); - - CheckExpectedType(JsonTokenType.StartArray, row.TokenType); - - int arrayLength = row.Length; + int arrayLength = _parsedData.Count; if ((uint)arrayIndex >= (uint)arrayLength) { throw new IndexOutOfRangeException(); } + JsonRow row = _parsedData.Get(currentIndex); if (!row.NeedUnescaping) { - return new JwtElement(this, currentIndex + ((arrayIndex + 1) * JsonRow.Size)); + return new JwtElement(this, arrayIndex * JsonRow.Size); } int elementCount = 0; @@ -631,7 +613,6 @@ internal JwtElement GetArrayIndexElement(int currentIndex, int arrayIndex) throw new IndexOutOfRangeException(); } - /// Determines whether the contains the specified key. public bool ContainsKey(ReadOnlySpan key) => _root.TryGetProperty(key, out _); diff --git a/src/JsonWebToken/Reader/JwtElement.cs b/src/JsonWebToken/Reader/JwtElement.cs index 7ea65f6a..a0d1044c 100644 --- a/src/JsonWebToken/Reader/JwtElement.cs +++ b/src/JsonWebToken/Reader/JwtElement.cs @@ -14,6 +14,7 @@ public readonly struct JwtElement { private readonly JwtDocument _parent; private readonly int _idx; + private readonly JwtDocument? _document; internal JwtElement(JwtDocument parent, int idx) { @@ -23,8 +24,28 @@ internal JwtElement(JwtDocument parent, int idx) Debug.Assert(idx >= 0); Debug.Assert(parent != null); - _parent = parent; _idx = idx; + + if (parent.GetJsonTokenType(idx) is JsonTokenType.StartArray) + { + var value = parent.GetRawValue(idx); + int count = parent.GetArrayLength(idx); + if (!ArrayEnumerator.TryParse(value, count, out var doc)) + { + ThrowHelper.ThrowFormatException_MalformedJson(); + _document = default; + } + else + { + _document = doc; + } + } + else + { + _document = default; + } + + _parent = parent; } /// Defines whether the current is empty @@ -47,9 +68,9 @@ public JwtElement this[int index] { get { - if (_parent != null) + if (_document != null) { - return _parent.GetArrayIndexElement(_idx, index); + return _document.GetArrayIndexElement(0, index); } throw new IndexOutOfRangeException(); @@ -884,7 +905,7 @@ public bool MoveNext() return _curIdx < _endIdxOrVersion; } - private static bool TryParse(ReadOnlyMemory utf8Array, int count, out JwtDocument? document) + internal static bool TryParse(ReadOnlyMemory utf8Array, int count, [NotNullWhen(true)] out JwtDocument? document) { ReadOnlySpan utf8JsonSpan = utf8Array.Span; var database = new JsonMetadata(count * JsonRow.Size); diff --git a/src/JsonWebToken/Reader/JwtHeaderDocument.cs b/src/JsonWebToken/Reader/JwtHeaderDocument.cs index 0787669b..ecef84bf 100644 --- a/src/JsonWebToken/Reader/JwtHeaderDocument.cs +++ b/src/JsonWebToken/Reader/JwtHeaderDocument.cs @@ -16,7 +16,7 @@ namespace JsonWebToken // Based on https://github.com/dotnet/runtime/blob/master/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonDocument.cs public sealed class JwtHeaderDocument : IDisposable { - internal static readonly JwtHeaderDocument Empty = new JwtHeaderDocument(new JwtDocument(), -1, -1, -1, -1); + internal static readonly JwtHeaderDocument Empty = new JwtHeaderDocument(JwtDocument.Empty, -1, -1, -1, -1); private readonly JwtDocument _document; private readonly JwtElement _alg; diff --git a/src/JsonWebToken/Reader/JwtPayloadDocument.cs b/src/JsonWebToken/Reader/JwtPayloadDocument.cs index 6fd1b8b2..d16c66e1 100644 --- a/src/JsonWebToken/Reader/JwtPayloadDocument.cs +++ b/src/JsonWebToken/Reader/JwtPayloadDocument.cs @@ -19,7 +19,7 @@ namespace JsonWebToken // Based on https://github.com/dotnet/runtime/blob/master/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonDocument.cs public sealed class JwtPayloadDocument : IDisposable { - internal static readonly JwtPayloadDocument Empty = new JwtPayloadDocument(new JwtDocument(), 0, -1); + internal static readonly JwtPayloadDocument Empty = new JwtPayloadDocument(JwtDocument.Empty, 0, -1); private readonly JwtDocument _document; private readonly byte _control; diff --git a/test/JsonWebToken.Tests/JwksTests.cs b/test/JsonWebToken.Tests/JwksTests.cs index 9105f674..4834e291 100644 --- a/test/JsonWebToken.Tests/JwksTests.cs +++ b/test/JsonWebToken.Tests/JwksTests.cs @@ -156,6 +156,8 @@ public void ReadJwks_Invalid(string issuer, string json) Assert.ThrowsAny(() => Jwks.FromJson(issuer, json)); } + private readonly object _lock = new object(); + [Fact] public void OnJwksRefreshed() { @@ -178,14 +180,17 @@ public void OnJwksRefreshed() key384, key512 }; - Jwks.OnJwksRefreshed += Jwks_OnJwksRefreshed; - try - { - Jwks.PublishJwksRefreshed(oldJwks, newJwks); - } - finally + lock (_lock) { - Jwks.OnJwksRefreshed -= Jwks_OnJwksRefreshed; + Jwks.OnJwksRefreshed += Jwks_OnJwksRefreshed; + try + { + Jwks.PublishJwksRefreshed(oldJwks, newJwks); + } + finally + { + Jwks.OnJwksRefreshed -= Jwks_OnJwksRefreshed; + } } } diff --git a/test/JsonWebToken.Tests/JwtDocumentTests.cs b/test/JsonWebToken.Tests/JwtDocumentTests.cs index f872b55e..313bd4b4 100644 --- a/test/JsonWebToken.Tests/JwtDocumentTests.cs +++ b/test/JsonWebToken.Tests/JwtDocumentTests.cs @@ -302,7 +302,7 @@ public void Issue489_NoValidation_Valid(string token) [Fact] public void JwtHeaderDocument_TryParse() { - var json = Encoding.UTF8.GetBytes("{\"string\":\"hello\",\"number\":1234,\"boolean\":true,\"object\":{\"value\":1},\"null\":null,\"array\":[\"hello\",\"world\"]}"); + var json = Encoding.UTF8.GetBytes("{\"string\":\"hello\",\"number\":1234,\"boolean\":true,\"object\":{\"value\":1},\"null\":null,\"array\":[\"hello\",\"world\"],\"€$cap€d\":true,\"Loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong €$cap€d\":true}"); var result = JwtHeaderDocument.TryParseHeader(json, null, TokenValidationPolicy.NoValidation, out var header, out var error); Assert.True(result); @@ -312,13 +312,16 @@ public void JwtHeaderDocument_TryParse() Assert.Equal(1, GetProperty(header, "object").GetJsonDocument().RootElement.GetProperty("value").GetInt64()); Assert.NotNull(GetProperty(header, "array").GetJsonDocument().RootElement.EnumerateArray().ToArray()); Assert.Equal(JsonValueKind.Null, GetProperty(header, "null").ValueKind); + Assert.Equal(JsonValueKind.True, GetProperty(header, "€$cap€d").ValueKind); + Assert.Equal(JsonValueKind.True, GetProperty(header, "Loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong €$cap€d").ValueKind); + Assert.Equal(default, GetProperty(header, "unknonw")); Assert.Null(error); } [Fact] public void JwtPayloadDocument_TryParse() { - var json = Encoding.UTF8.GetBytes("{\"string\":\"hello\",\"number\":1234,\"boolean\":true,\"object\":{\"value\":1},\"null\":null,\"array\":[\"hello\",\"world\"]}"); + var json = Encoding.UTF8.GetBytes("{\"Loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong €$cap€d\":true,\"string\":\"hello\",\"number\":1234,\"boolean\":true,\"object\":{\"value\":1},\"null\":null,\"array\":[\"hello\",\"world\"],\"€$cap€d\":true,\"escaped_value\":\"€$cap€d value\"}"); var result = JwtPayloadDocument.TryParsePayload(json, null, TokenValidationPolicy.NoValidation, out var payload, out var error); Assert.True(result); @@ -328,7 +331,16 @@ public void JwtPayloadDocument_TryParse() Assert.Equal(1, GetProperty(payload, "object").GetJsonDocument().RootElement.GetProperty("value").GetInt64()); Assert.NotNull(GetProperty(payload, "array").GetJsonDocument().RootElement.EnumerateArray().ToArray()); Assert.Equal(JsonValueKind.Null, GetProperty(payload, "null").ValueKind); + Assert.Equal(JsonValueKind.True, GetProperty(payload, "€$cap€d").ValueKind); + Assert.Equal(JsonValueKind.True, GetProperty(payload, "Loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong €$cap€d").ValueKind); + Assert.Equal(default, GetProperty(payload, "unknown")); Assert.Null(error); + + var rawText = GetProperty(payload, "null").GetPropertyRawText(); + Assert.Equal("\"null\":null", rawText); + + var escapedValue = GetProperty(payload, "escaped_value"); + Assert.True(escapedValue.ValueEquals("€$cap€d value")); } private static JwtElement GetProperty(JwtHeaderDocument document, string name) diff --git a/test/JsonWebToken.Tests/JwtMemberTests.cs b/test/JsonWebToken.Tests/JwtMemberTests.cs index 4b6c2742..24240aab 100644 --- a/test/JsonWebToken.Tests/JwtMemberTests.cs +++ b/test/JsonWebToken.Tests/JwtMemberTests.cs @@ -28,12 +28,86 @@ public void WriteTo(JwtMember property, string expected) } } + [Fact] + public void Indexer_Array() + { + JwtHeaderDocument.TryParseHeader(Utf8.GetBytes("{\"fake\":true, \"array\":[ 1, {}, {\"x\":true}, true, false, null, \"text\", [], [true, false]]}"), null, TokenValidationPolicy.NoValidation, out var header, out _); + header.TryGetHeaderParameter("array", out var element); + + var value = element[0]; + Assert.Equal(JsonValueKind.Number, value.ValueKind); + value = element[1]; + Assert.Equal(JsonValueKind.Object, value.ValueKind); + value = element[2]; + Assert.Equal(JsonValueKind.Object, value.ValueKind); + value = element[3]; + Assert.Equal(JsonValueKind.True, value.ValueKind); + value = element[4]; + Assert.Equal(JsonValueKind.False, value.ValueKind); + value = element[5]; + Assert.Equal(JsonValueKind.Null, value.ValueKind); + value = element[6]; + Assert.Equal(JsonValueKind.String, value.ValueKind); + value = element[7]; + Assert.Equal(JsonValueKind.Array, value.ValueKind); + value = element[8]; + Assert.Equal(JsonValueKind.Array, value.ValueKind); + } + + [Theory] + [InlineData("{\"object\":{\"p1\":1, \"p2\":{}, \"p3\":{\"x\":true}, \"p4\":true, \"p5\":false, \"p6\":null, \"p7\":\"text\", \"p8\":[], \"p9\":[true, false]}}")] + [InlineData("{\"placeholder\":true,\"object\":{\"p1\":1, \"p2\":{}, \"p3\":{\"x\":true}, \"p4\":true, \"p5\":false, \"p6\":null, \"p7\":\"text\", \"p8\":[], \"p9\":[true, false]}}")] + [InlineData("{\"object\":{\"p1\":1, \"p2\":{}, \"p3\":{\"x\":true}, \"p4\":true, \"p5\":false, \"p6\":null, \"p7\":\"text\", \"p8\":[], \"p9\":[true, false]},\"placeholder\":true}")] + [InlineData("{\"placeholder1\":true,\"object\":{\"p1\":1, \"p2\":{}, \"p3\":{\"x\":true}, \"p4\":true, \"p5\":false, \"p6\":null, \"p7\":\"text\", \"p8\":[], \"p9\":[true, false]},\"placeholder2\":true}")] + public void Indexer_Object(string json) + { + JwtHeaderDocument.TryParseHeader(Utf8.GetBytes(json), null, TokenValidationPolicy.NoValidation, out var header, out _); + header.TryGetHeaderParameter("object", out var element); + + var value = element["p1"]; + var value2 = element[Encoding.UTF8.GetBytes("p1")]; + Assert.Equal(JsonValueKind.Number, value.ValueKind); + Assert.Equal(JsonValueKind.Number, value2.ValueKind); + value = element["p2"]; + value2 = element[Encoding.UTF8.GetBytes("p2")]; + Assert.Equal(JsonValueKind.Object, value.ValueKind); + Assert.Equal(JsonValueKind.Object, value2.ValueKind); + value = element["p3"]; + value2 = element[Encoding.UTF8.GetBytes("p3")]; + Assert.Equal(JsonValueKind.Object, value.ValueKind); + Assert.Equal(JsonValueKind.Object, value2.ValueKind); + value = element["p4"]; + value2 = element[Encoding.UTF8.GetBytes("p4")]; + Assert.Equal(JsonValueKind.True, value.ValueKind); + Assert.Equal(JsonValueKind.True, value2.ValueKind); + value = element["p5"]; + value2 = element[Encoding.UTF8.GetBytes("p5")]; + Assert.Equal(JsonValueKind.False, value.ValueKind); + Assert.Equal(JsonValueKind.False, value2.ValueKind); + value = element["p6"]; + value2 = element[Encoding.UTF8.GetBytes("p6")]; + Assert.Equal(JsonValueKind.Null, value.ValueKind); + Assert.Equal(JsonValueKind.Null, value2.ValueKind); + value = element["p7"]; + value2 = element[Encoding.UTF8.GetBytes("p7")]; + Assert.Equal(JsonValueKind.String, value.ValueKind); + Assert.Equal(JsonValueKind.String, value2.ValueKind); + value = element["p8"]; + value2 = element[Encoding.UTF8.GetBytes("p8")]; + Assert.Equal(JsonValueKind.Array, value.ValueKind); + Assert.Equal(JsonValueKind.Array, value2.ValueKind); + value = element["p9"]; + value2 = element[Encoding.UTF8.GetBytes("p9")]; + Assert.Equal(JsonValueKind.Array, value.ValueKind); + Assert.Equal(JsonValueKind.Array, value2.ValueKind); + } + [Fact] public void EnumerateArrayOfObject() { JwtHeaderDocument.TryParseHeader(Utf8.GetBytes("{\"array\":[ 1, {}, {\"x\":true}, true, false, null, \"text\", [], [true, false]]}"), null, TokenValidationPolicy.NoValidation, out var header, out _); header.TryGetHeaderParameter("array", out var element); - + var enumerator = element.EnumerateArray(); enumerator.MoveNext(); Assert.Equal(JsonValueKind.Number, enumerator.Current.ValueKind); @@ -60,7 +134,7 @@ public void EnumerateArrayOfObject_NotAnArrayFail() { JwtHeaderDocument.TryParseHeader(Utf8.GetBytes("{\"array\":{\"x\":true}}"), null, TokenValidationPolicy.NoValidation, out var header, out _); header.TryGetHeaderParameter("array", out var element); - + Assert.Throws(() => element.EnumerateArray()); }