| File: System\Text\Json\Document\JsonDocument.TryGetProperty.cs | Web Access |
| Project: src\runtime\src\libraries\System.Text.Json\src\System.Text.Json.csproj (System.Text.Json) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Buffers; using System.Diagnostics; namespace System.Text.Json { public sealed partial class JsonDocument { internal unsafe bool TryGetNamedPropertyValue(int index, ReadOnlySpan<char> propertyName, out JsonElement value) { CheckNotDisposed(); DbRow row = _parsedData.Get(index); CheckExpectedType(JsonTokenType.StartObject, row.TokenType); // Only one row means it was EndObject. if (row.NumberOfRows == 1) { value = default; return false; } int maxBytes = JsonReaderHelper.s_utf8Encoding.GetMaxByteCount(propertyName.Length); int startIndex = index + DbRow.Size; int endIndex = checked(row.NumberOfRows * DbRow.Size + index); if (maxBytes < JsonConstants.StackallocByteThreshold) { Span<byte> utf8Name = stackalloc byte[JsonConstants.StackallocByteThreshold]; int len = JsonReaderHelper.GetUtf8FromText(propertyName, utf8Name); utf8Name = utf8Name.Slice(0, len); return TryGetNamedPropertyValue( startIndex, endIndex, 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; // Move to the row before the EndObject int candidateIndex = endIndex - DbRow.Size; while (candidateIndex > index) { int passedIndex = candidateIndex; row = _parsedData.Get(candidateIndex); Debug.Assert(row.TokenType != JsonTokenType.PropertyName); // Move before the value if (row.IsSimpleValue) { candidateIndex -= DbRow.Size; } else { Debug.Assert(row.NumberOfRows > 0); candidateIndex -= DbRow.Size * (row.NumberOfRows + 1); } row = _parsedData.Get(candidateIndex); Debug.Assert(row.TokenType == JsonTokenType.PropertyName); if (row.SizeOrLength >= minBytes) { byte[] tmpUtf8 = ArrayPool<byte>.Shared.Rent(maxBytes); Span<byte> utf8Name = default; try { int len = JsonReaderHelper.GetUtf8FromText(propertyName, tmpUtf8); utf8Name = tmpUtf8.AsSpan(0, len); return TryGetNamedPropertyValue( startIndex, passedIndex + DbRow.Size, utf8Name, out value); } finally { // While property names aren't usually a secret, they also usually // aren't long enough to end up in the rented buffer transcode path. // // On the basis that this is user data, go ahead and clear it. utf8Name.Clear(); ArrayPool<byte>.Shared.Return(tmpUtf8); } } // Move to the previous value candidateIndex -= DbRow.Size; } // None of the property names were within the range that the UTF-8 encoding would have been. value = default; return false; } internal bool TryGetNamedPropertyValue(int index, ReadOnlySpan<byte> propertyName, out JsonElement value) { CheckNotDisposed(); DbRow row = _parsedData.Get(index); CheckExpectedType(JsonTokenType.StartObject, row.TokenType); // Only one row means it was EndObject. if (row.NumberOfRows == 1) { value = default; return false; } int endIndex = checked(row.NumberOfRows * DbRow.Size + index); return TryGetNamedPropertyValue( index + DbRow.Size, endIndex, propertyName, out value); } private unsafe bool TryGetNamedPropertyValue( int startIndex, int endIndex, ReadOnlySpan<byte> propertyName, out JsonElement value) { ReadOnlySpan<byte> documentSpan = _utf8Json.Span; Span<byte> utf8UnescapedStack = stackalloc byte[JsonConstants.StackallocByteThreshold]; // Move to the row before the EndObject int index = endIndex - DbRow.Size; while (index > startIndex) { DbRow row = _parsedData.Get(index); Debug.Assert(row.TokenType != JsonTokenType.PropertyName); // Move before the value if (row.IsSimpleValue) { index -= DbRow.Size; } else { Debug.Assert(row.NumberOfRows > 0); index -= DbRow.Size * (row.NumberOfRows + 1); } row = _parsedData.Get(index); Debug.Assert(row.TokenType == JsonTokenType.PropertyName); ReadOnlySpan<byte> currentPropertyName = documentSpan.Slice(row.Location, row.SizeOrLength); if (row.HasComplexChildren) { // An escaped property name will be longer than an unescaped candidate, so only unescape // when the lengths are compatible. if (currentPropertyName.Length > propertyName.Length) { int idx = currentPropertyName.IndexOf(JsonConstants.BackSlash); Debug.Assert(idx >= 0); // If everything up to where the property name has a backslash matches, keep going. if (propertyName.Length > idx && currentPropertyName.Slice(0, idx).SequenceEqual(propertyName.Slice(0, idx))) { int remaining = currentPropertyName.Length - idx; int written = 0; byte[]? rented = null; try { Span<byte> utf8Unescaped = remaining <= utf8UnescapedStack.Length ? utf8UnescapedStack : (rented = ArrayPool<byte>.Shared.Rent(remaining)); // Only unescape the part we haven't processed. JsonReaderHelper.Unescape(currentPropertyName.Slice(idx), utf8Unescaped, 0, out written); // If the unescaped remainder matches the input remainder, it's a match. if (utf8Unescaped.Slice(0, written).SequenceEqual(propertyName.Slice(idx))) { // If the property name is a match, the answer is the next element. value = new JsonElement(this, index + DbRow.Size); return true; } } finally { if (rented is not null) { rented.AsSpan(0, written).Clear(); ArrayPool<byte>.Shared.Return(rented); } } } } } else if (currentPropertyName.SequenceEqual(propertyName)) { // If the property name is a match, the answer is the next element. value = new JsonElement(this, index + DbRow.Size); return true; } // Move to the previous value index -= DbRow.Size; } value = default; return false; } } }