-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathKeyCodec.cs
More file actions
214 lines (191 loc) · 7.57 KB
/
Copy pathKeyCodec.cs
File metadata and controls
214 lines (191 loc) · 7.57 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
203
204
205
206
207
208
209
210
211
212
213
214
using System.Buffers.Binary;
using System.Text;
using static KeyLoad.Storage.KeyCodecReadPrimitives;
namespace KeyLoad.Storage;
/// <summary>Represents an indexed key component which is absent rather than explicitly null.</summary>
public sealed class MissingValue
{
private MissingValue() { }
/// <summary>Gets the singleton missing-value marker.</summary>
public static MissingValue Instance { get; } = new();
}
/// <summary>Version 1 binary keys; strings use UTF-8 byte order and zero escaping.</summary>
public static class KeyCodec
{
/// <summary>Gets the version byte written at the start of every encoded key.</summary>
public const byte Version = 1;
private const int MaximumComponents = 256;
private const string TooManyComponentsMessage = "Key component count exceeds the supported limit.";
private const string InvalidTextMessage = "Key text is not valid Unicode.";
private static readonly UTF8Encoding Utf8 = new(false, true);
/// <summary>Encodes the supplied components into a version 1 sortable binary key.</summary>
/// <param name="components">The key components, in their comparison order.</param>
/// <returns>The encoded key bytes.</returns>
/// <exception cref="ArgumentNullException"><paramref name="components"/> is null.</exception>
/// <exception cref="KeyLoadException">A component is unsupported or cannot be encoded.</exception>
public static byte[] Encode(params object?[] components)
{
ArgumentNullException.ThrowIfNull(components);
if (components.Length > MaximumComponents)
{
throw Errors.Fail(ErrorCode.ResourceExhausted, TooManyComponentsMessage);
}
using var stream = new MemoryStream();
stream.WriteByte(Version);
foreach (var component in components)
{
Write(stream, component);
}
return stream.ToArray();
}
/// <summary>Decodes a version 1 sortable binary key into its components.</summary>
/// <param name="key">The encoded key bytes.</param>
/// <returns>The decoded components in their original order.</returns>
/// <exception cref="KeyLoadException">The key is unsupported or malformed.</exception>
public static object?[] Decode(ReadOnlySpan<byte> key)
{
if (key.IsEmpty || key[0] != Version)
{
throw Errors.Fail(ErrorCode.FormatUnsupported, "Unknown key codec version.");
}
var values = new List<object?>();
var offset = 1;
while (offset < key.Length)
{
if (values.Count == MaximumComponents)
{
throw BadKey();
}
var tag = key[offset++];
object? value = tag switch
{
0x10 => MissingValue.Instance,
0x11 => null,
0x20 => ReadByte(key, ref offset) switch { 0 => false, 1 => true, _ => throw BadKey() },
0x30 => unchecked((long)(ReadUInt64(key, ref offset) ^ (1UL << 63))),
0x31 => KeyCodecDecimal.Read(key, ref offset),
0x32 => ReadDouble(key, ref offset),
0x40 => ReadTimestamp(key, ref offset),
0x50 => ReadText(key, ref offset),
0x60 => ReadEscaped(key, ref offset),
_ => throw BadKey()
};
values.Add(value);
}
return values.ToArray();
}
private static void Write(Stream stream, object? value)
{
switch (value)
{
case MissingValue:
WriteMarker(stream, 0x10);
break;
case null:
WriteMarker(stream, 0x11);
break;
case bool boolean:
WriteBoolean(stream, boolean);
break;
case int integer:
Write(stream, (long)integer);
break;
case long integer:
WriteInt64(stream, integer);
break;
case decimal number:
KeyCodecDecimal.Write(stream, number);
break;
case double number:
WriteDouble(stream, number);
break;
case DateTimeOffset time:
WriteDateTimeOffset(stream, time);
break;
case string text:
WriteText(stream, text);
break;
case byte[] bytes:
WriteBinary(stream, bytes);
break;
case Guid id:
Write(stream, id.ToString("N"));
break;
default:
throw Errors.Fail(ErrorCode.UnsupportedCapability, "This type is not supported by key codec v1.");
}
}
private static void WriteMarker(Stream stream, byte marker) => stream.WriteByte(marker);
private static void WriteBoolean(Stream stream, bool value)
{
stream.WriteByte(0x20);
stream.WriteByte(value ? (byte)1 : (byte)0);
}
private static void WriteInt64(Stream stream, long value)
{
Span<byte> buffer = stackalloc byte[sizeof(ulong)];
stream.WriteByte(0x30);
BinaryPrimitives.WriteUInt64BigEndian(buffer, unchecked((ulong)value) ^ (1UL << 63));
stream.Write(buffer);
}
private static void WriteDouble(Stream stream, double value)
{
if (!double.IsFinite(value))
{
throw Errors.Fail(ErrorCode.Validation, "Indexed numbers must be finite.");
}
Span<byte> buffer = stackalloc byte[sizeof(ulong)];
stream.WriteByte(0x32);
var bits = BitConverter.DoubleToUInt64Bits(value == 0 ? 0 : value);
BinaryPrimitives.WriteUInt64BigEndian(buffer, (bits & (1UL << 63)) != 0 ? ~bits : bits ^ (1UL << 63));
stream.Write(buffer);
}
private static void WriteDateTimeOffset(Stream stream, DateTimeOffset value)
{
Span<byte> buffer = stackalloc byte[sizeof(ulong)];
stream.WriteByte(0x40);
BinaryPrimitives.WriteUInt64BigEndian(buffer, unchecked((ulong)value.UtcTicks) ^ (1UL << 63));
stream.Write(buffer);
}
private static void WriteText(Stream stream, string value)
{
stream.WriteByte(0x50);
try
{
WriteEscaped(stream, Utf8.GetBytes(value));
}
catch (EncoderFallbackException)
{
throw Errors.Fail(ErrorCode.Validation, InvalidTextMessage);
}
}
private static void WriteBinary(Stream stream, byte[] value)
{
stream.WriteByte(0x60);
WriteEscaped(stream, value);
}
private static void WriteEscaped(Stream stream, ReadOnlySpan<byte> bytes)
{
foreach (var b in bytes)
{
stream.WriteByte(b);
if (b == 0)
{
stream.WriteByte(0xFF);
}
}
stream.WriteByte(0);
stream.WriteByte(0);
}
}
/// <summary>Compares encoded keys using unsigned lexicographic byte ordering.</summary>
public sealed class BinaryKeyComparer : IComparer<byte[]>
{
/// <summary>Gets the shared binary key comparer.</summary>
public static BinaryKeyComparer Instance { get; } = new();
/// <summary>Compares two byte arrays lexicographically; null is treated as an empty array.</summary>
/// <param name="x">The first key to compare.</param>
/// <param name="y">The second key to compare.</param>
/// <returns>A negative value when <paramref name="x"/> precedes <paramref name="y"/>, zero when they are equal, or a positive value otherwise.</returns>
public int Compare(byte[]? x, byte[]? y) => x.AsSpan().SequenceCompareTo(y.AsSpan());
}