Skip to content

Commit 4dd6870

Browse files
authored
Replace managed JSON payloads with binary protocol (#5)
1 parent 90f7d9f commit 4dd6870

7 files changed

Lines changed: 764 additions & 362 deletions

File tree

‎dotnet/src/MiniExcel.Rust/MiniExcel.Rust.csproj‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@
2929

3030
<ItemGroup>
3131
<PackageReference Include="MiniExcel" Version="[1.46.0]" />
32-
<PackageReference Include="System.Text.Json" Version="10.0.10" />
3332
</ItemGroup>
3433

3534
<ItemGroup>
Lines changed: 339 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,339 @@
1+
using System.Collections;
2+
using System.Globalization;
3+
using System.Reflection;
4+
using System.Runtime.CompilerServices;
5+
using System.Text;
6+
7+
namespace MiniExcelLibs;
8+
9+
internal static class MiniExcelBinaryPayload
10+
{
11+
private const byte Version = 1;
12+
private const int MaxDepth = 64;
13+
14+
private enum PayloadKind : byte
15+
{
16+
XlsxWrite = 1,
17+
CsvWrite = 2,
18+
TemplateValue = 3,
19+
MappedTemplate = 4
20+
}
21+
22+
private enum ValueKind : byte
23+
{
24+
Null,
25+
Boolean,
26+
Int64,
27+
UInt64,
28+
Double,
29+
String,
30+
Array,
31+
Object,
32+
Decimal
33+
}
34+
35+
internal static byte[] EncodeXlsxWrite(
36+
IReadOnlyList<string> schema,
37+
MiniExcelRustWriteOptions options)
38+
{
39+
using var stream = CreatePayload(PayloadKind.XlsxWrite, out var writer);
40+
using (writer)
41+
{
42+
WriteStrings(writer, schema);
43+
WriteString(writer, options.SheetName);
44+
WriteBoolean(writer, options.OverwriteFile);
45+
WriteBoolean(writer, options.PrintHeader);
46+
WriteBoolean(writer, options.AutoFilter);
47+
WriteBoolean(writer, options.RightToLeft);
48+
WriteBoolean(writer, options.AutoWidth);
49+
WriteBoolean(writer, options.WrapCellContents);
50+
writer.Write((byte)options.HorizontalAlignment);
51+
writer.Write((byte)options.VerticalAlignment);
52+
writer.Write((byte)options.TableStyle);
53+
WriteBoolean(writer, options.HeaderWrapText);
54+
WriteString(writer, options.HeaderBackgroundColor);
55+
writer.Write((byte)options.HeaderHorizontalAlignment);
56+
writer.Write((byte)options.HeaderVerticalAlignment);
57+
writer.Write(options.MinWidth);
58+
writer.Write(options.MaxWidth);
59+
writer.Write(options.FreezeRowCount);
60+
writer.Write(options.FreezeColumnCount);
61+
WriteString(writer, options.DateFormat);
62+
WriteString(writer, options.TimeFormat);
63+
WriteString(writer, options.DateTimeFormat);
64+
WriteString(writer, options.DurationFormat);
65+
WriteStringMap(writer, options.ColumnFormats);
66+
WriteDoubleMap(writer, options.ColumnWidths);
67+
WriteBooleanMap(writer, options.HiddenColumns);
68+
WriteStrings(
69+
writer,
70+
options.DynamicColumns
71+
.Where(column => column.Value.IsFormula)
72+
.Select(column => string.IsNullOrWhiteSpace(column.Value.Name)
73+
? column.Key
74+
: column.Value.Name!)
75+
.ToArray());
76+
}
77+
return stream.ToArray();
78+
}
79+
80+
internal static byte[] EncodeCsvWrite(
81+
IReadOnlyList<string> schema,
82+
MiniExcelRustCsvWriteOptions options)
83+
{
84+
using var stream = CreatePayload(PayloadKind.CsvWrite, out var writer);
85+
using (writer)
86+
{
87+
WriteStrings(writer, schema);
88+
writer.Write(checked((byte)options.Delimiter));
89+
writer.Write((byte)options.Encoding);
90+
WriteBoolean(writer, options.WriteBom);
91+
WriteBoolean(writer, options.PrintHeader);
92+
WriteBoolean(writer, options.OverwriteFile);
93+
}
94+
return stream.ToArray();
95+
}
96+
97+
internal static byte[] EncodeTemplateValue(object? value)
98+
{
99+
using var stream = CreatePayload(PayloadKind.TemplateValue, out var writer);
100+
using (writer)
101+
WriteValue(writer, value, 0, new HashSet<object>(ReferenceComparer.Instance));
102+
return stream.ToArray();
103+
}
104+
105+
internal static byte[] EncodeMappedTemplate(
106+
string sheetName,
107+
IEnumerable<MappedCell> cells)
108+
{
109+
var materialized = cells.ToList();
110+
using var stream = CreatePayload(PayloadKind.MappedTemplate, out var writer);
111+
using (writer)
112+
{
113+
WriteString(writer, sheetName);
114+
writer.Write(checked((uint)materialized.Count));
115+
var ancestors = new HashSet<object>(ReferenceComparer.Instance);
116+
foreach (var cell in materialized)
117+
{
118+
WriteString(writer, cell.Address);
119+
WriteBoolean(writer, cell.Formula);
120+
WriteValue(writer, cell.Value, 0, ancestors);
121+
}
122+
}
123+
return stream.ToArray();
124+
}
125+
126+
private static MemoryStream CreatePayload(PayloadKind kind, out BinaryWriter writer)
127+
{
128+
var stream = new MemoryStream();
129+
writer = new BinaryWriter(stream, Encoding.UTF8, leaveOpen: true);
130+
writer.Write(new[] { (byte)'M', (byte)'X', (byte)'B', (byte)'P' });
131+
writer.Write(Version);
132+
writer.Write((byte)kind);
133+
return stream;
134+
}
135+
136+
private static void WriteValue(
137+
BinaryWriter writer,
138+
object? value,
139+
int depth,
140+
ISet<object> ancestors)
141+
{
142+
if (depth > MaxDepth)
143+
throw new InvalidOperationException($"Template values cannot exceed {MaxDepth} nested levels.");
144+
145+
switch (value)
146+
{
147+
case null:
148+
case DBNull:
149+
writer.Write((byte)ValueKind.Null);
150+
return;
151+
case bool boolean:
152+
writer.Write((byte)ValueKind.Boolean);
153+
WriteBoolean(writer, boolean);
154+
return;
155+
case byte or sbyte or short or ushort or int or uint or long:
156+
writer.Write((byte)ValueKind.Int64);
157+
writer.Write(Convert.ToInt64(value, CultureInfo.InvariantCulture));
158+
return;
159+
case ulong unsigned:
160+
writer.Write((byte)ValueKind.UInt64);
161+
writer.Write(unsigned);
162+
return;
163+
case float or double:
164+
var number = Convert.ToDouble(value, CultureInfo.InvariantCulture);
165+
if (double.IsNaN(number) || double.IsInfinity(number))
166+
throw new ArgumentException("Template numbers must be finite.", nameof(value));
167+
writer.Write((byte)ValueKind.Double);
168+
writer.Write(number);
169+
return;
170+
case decimal decimalValue:
171+
writer.Write((byte)ValueKind.Decimal);
172+
WriteString(writer, decimalValue.ToString(CultureInfo.InvariantCulture));
173+
return;
174+
case string text:
175+
writer.Write((byte)ValueKind.String);
176+
WriteString(writer, text);
177+
return;
178+
case char character:
179+
writer.Write((byte)ValueKind.String);
180+
WriteString(writer, character.ToString());
181+
return;
182+
case DateTime dateTime:
183+
writer.Write((byte)ValueKind.String);
184+
WriteString(writer, dateTime.ToString("O", CultureInfo.InvariantCulture));
185+
return;
186+
case DateTimeOffset dateTimeOffset:
187+
writer.Write((byte)ValueKind.String);
188+
WriteString(writer, dateTimeOffset.ToString("O", CultureInfo.InvariantCulture));
189+
return;
190+
case TimeSpan timeSpan:
191+
writer.Write((byte)ValueKind.String);
192+
WriteString(writer, timeSpan.ToString("c", CultureInfo.InvariantCulture));
193+
return;
194+
case Guid or Uri:
195+
writer.Write((byte)ValueKind.String);
196+
WriteString(writer, Convert.ToString(value, CultureInfo.InvariantCulture)!);
197+
return;
198+
case byte[] bytes:
199+
writer.Write((byte)ValueKind.String);
200+
WriteString(writer, Convert.ToBase64String(bytes));
201+
return;
202+
}
203+
204+
var type = value.GetType();
205+
if (type.IsEnum)
206+
{
207+
writer.Write((byte)ValueKind.Int64);
208+
writer.Write(Convert.ToInt64(value, CultureInfo.InvariantCulture));
209+
return;
210+
}
211+
212+
if (!type.IsValueType && !ancestors.Add(value))
213+
throw new InvalidOperationException("Template values cannot contain reference cycles.");
214+
try
215+
{
216+
if (value is IDictionary dictionary)
217+
{
218+
writer.Write((byte)ValueKind.Object);
219+
writer.Write(checked((uint)dictionary.Count));
220+
foreach (DictionaryEntry entry in dictionary)
221+
{
222+
if (entry.Key is not string key)
223+
throw new ArgumentException("Template dictionary keys must be strings.", nameof(value));
224+
WriteString(writer, key);
225+
WriteValue(writer, entry.Value, depth + 1, ancestors);
226+
}
227+
return;
228+
}
229+
230+
if (value is IEnumerable sequence)
231+
{
232+
var items = sequence.Cast<object?>().ToList();
233+
writer.Write((byte)ValueKind.Array);
234+
writer.Write(checked((uint)items.Count));
235+
foreach (var item in items)
236+
WriteValue(writer, item, depth + 1, ancestors);
237+
return;
238+
}
239+
240+
var properties = type.GetProperties(BindingFlags.Instance | BindingFlags.Public)
241+
.Where(property => property.GetMethod is not null && property.GetIndexParameters().Length == 0)
242+
.Where(property => !HasJsonIgnore(property))
243+
.ToList();
244+
writer.Write((byte)ValueKind.Object);
245+
writer.Write(checked((uint)properties.Count));
246+
foreach (var property in properties)
247+
{
248+
WriteString(writer, JsonPropertyName(property));
249+
WriteValue(writer, property.GetValue(value), depth + 1, ancestors);
250+
}
251+
}
252+
finally
253+
{
254+
if (!type.IsValueType)
255+
ancestors.Remove(value);
256+
}
257+
}
258+
259+
private static bool HasJsonIgnore(MemberInfo member) =>
260+
member.CustomAttributes.Any(attribute =>
261+
attribute.AttributeType.FullName == "System.Text.Json.Serialization.JsonIgnoreAttribute");
262+
263+
private static string JsonPropertyName(MemberInfo member)
264+
{
265+
var attribute = member.CustomAttributes.FirstOrDefault(candidate =>
266+
candidate.AttributeType.FullName == "System.Text.Json.Serialization.JsonPropertyNameAttribute");
267+
return attribute?.ConstructorArguments.FirstOrDefault().Value as string ?? member.Name;
268+
}
269+
270+
private static void WriteStrings(BinaryWriter writer, IReadOnlyCollection<string> values)
271+
{
272+
writer.Write(checked((uint)values.Count));
273+
foreach (var value in values)
274+
WriteString(writer, value);
275+
}
276+
277+
private static void WriteStringMap(BinaryWriter writer, IDictionary<string, string> values)
278+
{
279+
writer.Write(checked((uint)values.Count));
280+
foreach (var value in values)
281+
{
282+
WriteString(writer, value.Key);
283+
WriteString(writer, value.Value);
284+
}
285+
}
286+
287+
private static void WriteDoubleMap(BinaryWriter writer, IDictionary<string, double> values)
288+
{
289+
writer.Write(checked((uint)values.Count));
290+
foreach (var value in values)
291+
{
292+
WriteString(writer, value.Key);
293+
writer.Write(value.Value);
294+
}
295+
}
296+
297+
private static void WriteBooleanMap(BinaryWriter writer, IDictionary<string, bool> values)
298+
{
299+
writer.Write(checked((uint)values.Count));
300+
foreach (var value in values)
301+
{
302+
WriteString(writer, value.Key);
303+
WriteBoolean(writer, value.Value);
304+
}
305+
}
306+
307+
private static void WriteString(BinaryWriter writer, string value)
308+
{
309+
var bytes = Encoding.UTF8.GetBytes(value);
310+
writer.Write(checked((uint)bytes.Length));
311+
writer.Write(bytes);
312+
}
313+
314+
private static void WriteBoolean(BinaryWriter writer, bool value) =>
315+
writer.Write((byte)(value ? 1 : 0));
316+
317+
internal readonly struct MappedCell
318+
{
319+
internal MappedCell(string address, object? value, bool formula)
320+
{
321+
Address = address;
322+
Value = value;
323+
Formula = formula;
324+
}
325+
326+
internal string Address { get; }
327+
internal object? Value { get; }
328+
internal bool Formula { get; }
329+
}
330+
331+
private sealed class ReferenceComparer : IEqualityComparer<object>
332+
{
333+
internal static readonly ReferenceComparer Instance = new();
334+
335+
public new bool Equals(object? left, object? right) => ReferenceEquals(left, right);
336+
337+
public int GetHashCode(object value) => RuntimeHelpers.GetHashCode(value);
338+
}
339+
}

0 commit comments

Comments
 (0)