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