Repository navigation
Expand file tree
/
Copy pathJsonData.cs
More file actions
202 lines (188 loc) · 10.3 KB
/
Copy pathJsonData.cs
File metadata and controls
202 lines (188 loc) · 10.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
using System.Buffers;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
using KeyLoad.Core.ResourceExecution;
using KeyLoad.Storage;
namespace KeyLoad.Core;
/// <summary>Validates JSON and builds stable operation fingerprints and scalar paths.</summary>
public static class JsonData
{
private const int EmptyElementCount = 0;
private const int LastComponentFromEnd = 1;
private const string EmptyObjectJson = "{}";
internal const string UnsupportedScalarFieldDetail = "This field is not a scalar value.";
private const string ObjectRequiredDetail = "The JSON payload must be an object.";
private const string InvalidPayloadDetail = "The JSON payload is invalid or too deeply nested.";
private const string InvalidDocumentDetail = "A document must be an object.";
private const string ConcretePatchDetail = "Patch paths must address concrete fields.";
private const string MissingPatchParentDetail = "A patch parent is missing or is not an object.";
private const string UnsupportedArrayPatchDetail = "Array patch operations are not supported in the current protocol.";
private const string InvalidIdentifierDetail = "An identifier is missing, too long or contains control characters.";
private const string WildcardPathSegment = "*";
private const string NullJsonValue = "null";
private const int MaximumIdentifierBytes = 256;
private const char FirstControlRangeEnd = '\u001f';
private const char SecondControlRangeStart = '\u007f';
private const char SecondControlRangeEnd = '\u009f';
/// <summary>Returns canonical JSON while enforcing the serializer's default nesting and decimal-number policy.</summary>
/// <param name="json">Owned input JSON text.</param>
/// <param name="requireObject">Whether the root must be an object.</param>
/// <returns>Owned canonical text with ordinal property order.</returns>
public static string Validate(string json, bool requireObject = true)
=> ValidateCore(json, requireObject, preserveContent: false);
internal static string ValidateContent(string json)
=> ValidateCore(json, requireObject: true, preserveContent: true);
private static string ValidateCore(string json, bool requireObject, bool preserveContent)
{
ArgumentNullException.ThrowIfNull(json) /* KLD0041: The BCL null guard accepts this existing reference through its object parameter without copying its contents. */;
if (json == EmptyObjectJson)
{
return json;
}
try
{
using var document = JsonDocument.Parse(json);
if (requireObject && document.RootElement.ValueKind != JsonValueKind.Object)
{
throw Errors.Fail(ErrorCode.Validation, ObjectRequiredDetail);
}
if (preserveContent)
{
CanonicalJsonWriter.Validate(document.RootElement);
return json;
}
var bytes = new ArrayBufferWriter<byte>();
using (var writer = new Utf8JsonWriter(bytes))
{
CanonicalJsonWriter.Write(writer, document.RootElement);
}
return Encoding.UTF8.GetString(bytes.WrittenSpan);
}
catch (JsonException)
{
throw Errors.Fail(ErrorCode.Validation, InvalidPayloadDetail);
}
}
/// <summary>Hashes canonical serializer output without normalizing numeric spelling.</summary>
/// <typeparam name="T">The value's serialization type.</typeparam>
/// <param name="value">Value or null serialized with the database's canonical options.</param>
/// <returns>The exact lowercase SHA-256 fingerprint.</returns>
public static string Fingerprint<T>(T value)
{
using var document = JsonSerializer.SerializeToDocument(value, JsonDefaults.Options);
using var hash = IncrementalHash.CreateHash(HashAlgorithmName.SHA256);
using var bytes = new CanonicalHashStream(hash);
using (var writer = new Utf8JsonWriter(bytes))
{
CanonicalJsonWriter.Write(writer, document.RootElement, decimalNumbers: false);
}
return Convert.ToHexStringLower(hash.GetHashAndReset());
}
/// <summary>Reads a scalar at a validated JSON pointer.</summary>
/// <param name="document">JSON root inside its owning document lifetime.</param>
/// <param name="pathText">JSON pointer to a scalar, or empty text for the root.</param>
/// <returns>An owned scalar, null, or the distinct missing-value marker.</returns>
public static KeyComponent Scalar(JsonElement document, string pathText)
=> (Scalar(document, PathSegments(pathText))).Value.Value;
/// <summary>Reads a scalar with path segments prepared once by the operation.</summary>
/// <param name="document">JSON root inside its owning document lifetime.</param>
/// <param name="pathSegments">Decoded path segments produced by <see cref="PathSegments"/>.</param>
/// <returns>A successful scalar payload preserving JSON null and the distinct missing-value marker.</returns>
public static global::ManagedCode.Communication.Result<JsonScalarValue> Scalar(JsonElement document, ReadOnlySpan<string> pathSegments)
{
var current = document;
foreach (var part in pathSegments)
{
if (current.ValueKind == JsonValueKind.Object && current.TryGetProperty(part, out var child))
{
current = child;
}
else if (current.ValueKind == JsonValueKind.Array && TryArrayIndex(part, out var index)
&& index < current.GetArrayLength())
{
current = current[index];
}
else
{
return global::ManagedCode.Communication.Result<JsonScalarValue>.Succeed(new JsonScalarValue(KeyComponent.Missing));
}
}
var value = current.ValueKind switch
{
JsonValueKind.Null => KeyComponent.Null,
JsonValueKind.String => KeyComponent.FromString(current.GetString()),
JsonValueKind.True => KeyComponent.FromBoolean(true),
JsonValueKind.False => KeyComponent.FromBoolean(false),
JsonValueKind.Number when current.TryGetDecimal(out var number) => KeyComponent.FromDecimal(number),
_ => throw Errors.Fail(ErrorCode.UnsupportedCapability, UnsupportedScalarFieldDetail)
};
return global::ManagedCode.Communication.Result<JsonScalarValue>.Succeed(new JsonScalarValue(value));
}
/// <summary>Decodes a bounded JSON pointer into owned reusable path segments.</summary>
/// <param name="pathText">RFC 6901 pointer, or empty text for the root.</param>
/// <returns>Decoded path segments in traversal order.</returns>
public static string[] PathSegments(string pathText) => JsonPointerPaths.Parse(pathText);
/// <summary>Recognizes a concrete RFC 6901 array index without normalizing object keys.</summary>
/// <param name="segment">A decoded pointer segment, tested only when traversing an array.</param>
/// <param name="index">The representable nonnegative index when recognized.</param>
/// <returns>Whether the segment uses ASCII digits with no leading zero except zero itself.</returns>
public static bool TryArrayIndex(string segment, out int index) => JsonPointerPaths.TryArrayIndex(segment, out index);
/// <summary>Encodes concrete path segments with the existing JSON pointer escaping.</summary>
/// <param name="segments">Owned unescaped path segments.</param>
/// <returns>A slash-prefixed JSON pointer.</returns>
public static string Path(params string[] segments) => JsonPointerPaths.Encode(segments);
/// <summary>Applies field patches in order and returns canonical validated JSON.</summary>
/// <param name="json">Existing object JSON.</param>
/// <param name="patches">Validated concrete field patches.</param>
/// <returns>Owned canonical object JSON after all patches.</returns>
public static string Patch(string json, IReadOnlyList<FieldPatch> patches)
{
ArgumentNullException.ThrowIfNull(json) /* KLD0041: The BCL null guard accepts this existing reference through its object parameter without copying its contents. */;
ArgumentNullException.ThrowIfNull(patches) /* KLD0041: The BCL null guard accepts this existing reference through its object parameter without copying its contents. */;
var root = JsonNode.Parse(json) as JsonObject ?? throw Errors.Fail(ErrorCode.Validation, InvalidDocumentDetail);
foreach (var patch in patches)
{
var segments = PathSegments(patch.Path);
if (segments.Length == EmptyElementCount || segments.Any(segment => segment == WildcardPathSegment))
{
throw Errors.Fail(ErrorCode.Validation, ConcretePatchDetail);
}
JsonNode parent = root;
foreach (var segment in segments[..^LastComponentFromEnd])
{
if (parent is not JsonObject obj || obj[segment] is not { } child)
{
throw Errors.Fail(ErrorCode.Validation, MissingPatchParentDetail);
}
parent = child;
}
if (parent is not JsonObject target)
{
throw Errors.Fail(ErrorCode.UnsupportedCapability, UnsupportedArrayPatchDetail);
}
var field = segments[^LastComponentFromEnd];
if (patch.Kind == PatchKind.Remove)
{
target.Remove(field);
}
else
{
target[field] = JsonNode.Parse(Validate(patch.ValueJson ?? NullJsonValue, false));
}
}
return Validate(root.ToJsonString());
}
/// <summary>Rejects missing, oversized or control-containing database identifiers.</summary>
/// <param name="value">Identifier text to validate.</param>
public static void Identifier(string value)
{
if (string.IsNullOrWhiteSpace(value) || Encoding.UTF8.GetByteCount(value) > MaximumIdentifierBytes
|| value.AsSpan().ContainsAnyInRange(char.MinValue, FirstControlRangeEnd)
|| value.AsSpan().ContainsAnyInRange(SecondControlRangeStart, SecondControlRangeEnd))
{
throw Errors.Fail(ErrorCode.Validation, InvalidIdentifierDetail);
}
}
}