• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

zorbathut / dec / 16260897978

14 Jul 2025 07:08AM UTC coverage: 90.121% (+0.03%) from 90.087%
16260897978

push

github

zorbathut
Fix: Thread contention error when running Dec on multiple threads simultaneously.

5218 of 5790 relevant lines covered (90.12%)

218918.62 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

87.97
/src/Serialization.cs
1
using System;
2
using System.Collections;
3
using System.Collections.Generic;
4
using System.ComponentModel;
5
using System.ComponentModel.Design;
6
using System.Linq;
7
using System.Reflection;
8

9
namespace Dec
10
{
11
    /// <summary>
12
    /// Internal serialization utilities.
13
    /// </summary>
14
    internal static class Serialization
15
    {
16
        // Initialize it to empty in order to support Recorder operations without Dec initialization.
17
        // At some point we'll figure out how to support Converters at that point as well.
18
        internal static bool ConverterInitialized = false;
12✔
19
        internal static System.Collections.Concurrent.ConcurrentDictionary<Type, Converter> ConverterObjects = new System.Collections.Concurrent.ConcurrentDictionary<Type, Converter>();
12✔
20
        internal static System.Collections.Concurrent.ConcurrentDictionary<Type, Type> ConverterGenericPrototypes = new System.Collections.Concurrent.ConcurrentDictionary<Type, Type>();
12✔
21

22
        internal class ConverterNullableString<T> : ConverterString<T?> where T : struct
23
        {
24
            private ConverterString<T> child;
25

26
            public ConverterNullableString(ConverterString<T> child)
30✔
27
            {
30✔
28
                this.child = child;
30✔
29
            }
30✔
30

31
            public override string Write(T? input)
32
            {
×
33
                if (!input.HasValue)
×
34
                {
×
35
                    Dbg.Err("Internal error: ConverterNullableString.Write called with null value; this should never happen");
×
36
                    return "";
×
37
                }
38

39
                return child.Write(input.Value);
×
40
            }
×
41

42
            public override T? Read(string input, Context context)
43
            {
30✔
44
                // if we're null, we must have already handled this elsewhere
45
                return child.Read(input, context);
30✔
46
            }
30✔
47
        }
48

49
        internal class ConverterNullableRecord<T> : ConverterRecord<T?> where T : struct
50
        {
51
            private ConverterRecord<T> child;
52

53
            public ConverterNullableRecord(ConverterRecord<T> child)
30✔
54
            {
30✔
55
                this.child = child;
30✔
56
            }
30✔
57

58
            public override void Record(ref T? input, Recorder recorder)
59
            {
30✔
60
                if (recorder.Mode == Recorder.Direction.Write)
30✔
61
                {
×
62
                    if (!input.HasValue)
×
63
                    {
×
64
                        Dbg.Err("Internal error: ConverterNullableRecord called with null value in write mode; this should never happen");
×
65
                        return;
×
66
                    }
67

68
                    var value = input.Value;
×
69
                    child.Record(ref value, recorder);
×
70
                }
×
71
                else if (recorder.Mode == Recorder.Direction.Read)
30✔
72
                {
30✔
73
                    T value = default;
30✔
74
                    child.Record(ref value, recorder);
30✔
75
                    input = value;
30✔
76
                }
30✔
77
                else
78
                {
×
79
                    Dbg.Err("Internal error: ConverterNullableRecord called with unknown mode; this should never happen");
×
80
                }
×
81
            }
30✔
82
        }
83

84
        internal class ConverterNullableFactory<T> : ConverterFactory<T?> where T : struct
85
        {
86
            private ConverterFactory<T> child;
87

88
            public ConverterNullableFactory(ConverterFactory<T> child)
30✔
89
            {
30✔
90
                this.child = child;
30✔
91
            }
30✔
92

93
            public override T? Create(Recorder recorder)
94
            {
30✔
95
                return child.Create(recorder);
30✔
96
            }
30✔
97

98
            public override void Read(ref T? input, Recorder recorder)
99
            {
30✔
100
                if (!input.HasValue)
30✔
101
                {
×
102
                    Dbg.Err("Internal error: ConverterNullableFactory.Read called with null value; this should never happen");
×
103
                    return;
×
104
                }
105

106
                var value = input.Value;
30✔
107
                child.Read(ref value, recorder);
30✔
108
                input = value;
30✔
109
            }
30✔
110

111
            public override void Write(T? input, Recorder recorder)
112
            {
×
113
                if (!input.HasValue)
×
114
                {
×
115
                    Dbg.Err("Internal error: ConverterNullableFactory.Write called with null value; this should never happen");
×
116
                    return;
×
117
                }
118

119
                child.Write(input.Value, recorder);
×
120
            }
×
121
        }
122

123
        internal static Converter ConverterFor(Type inputType)
124
        {
6,494,616✔
125
            if (ConverterObjects.TryGetValue(inputType, out var converter))
6,494,616✔
126
            {
6,431,846✔
127
                return converter;
6,431,846✔
128
            }
129

130
            // check for Nullable
131
            if (inputType.IsConstructedGenericType && inputType.GetGenericTypeDefinition() == typeof(Nullable<>))
62,770✔
132
            {
498✔
133
                var nullableType = inputType.GenericTypeArguments[0];
498✔
134

135
                // handle all types of converters
136
                var originalConverter = ConverterFor(nullableType);
498✔
137
                if (originalConverter is ConverterString nullableStringConverter)
498✔
138
                {
30✔
139
                    var nullableConverterType = typeof(ConverterNullableString<>).MakeGenericType(nullableType);
30✔
140
                    var nullableConverter = (ConverterString)Activator.CreateInstance(nullableConverterType, new object[] { originalConverter });
30✔
141
                    ConverterObjects[inputType] = nullableConverter;
30✔
142
                    return nullableConverter;
30✔
143
                }
144
                else if (originalConverter is ConverterRecord nullableRecordConverter)
468✔
145
                {
30✔
146
                    var nullableConverterType = typeof(ConverterNullableRecord<>).MakeGenericType(nullableType);
30✔
147
                    var nullableConverter = (ConverterRecord)Activator.CreateInstance(nullableConverterType, new object[] { originalConverter });
30✔
148
                    ConverterObjects[inputType] = nullableConverter;
30✔
149
                    return nullableConverter;
30✔
150
                }
151
                else if (originalConverter is ConverterFactory nullableFactoryConverter)
438✔
152
                {
30✔
153
                    var nullableConverterType = typeof(ConverterNullableFactory<>).MakeGenericType(nullableType);
30✔
154
                    var nullableConverter = (ConverterFactory)Activator.CreateInstance(nullableConverterType, new object[] { originalConverter });
30✔
155
                    ConverterObjects[inputType] = nullableConverter;
30✔
156
                    return nullableConverter;
30✔
157
                }
158
                else if (originalConverter != null)
408✔
159
                {
×
160
                    Dbg.Err($"Found converter {originalConverter} which is not a string, record, or factory converter. This is not allowed.");
×
161
                }
×
162
            }
408✔
163

164
            if (inputType.IsConstructedGenericType)
62,680✔
165
            {
12,132✔
166
                var genericType = inputType.GetGenericTypeDefinition();
12,132✔
167
                if (ConverterGenericPrototypes.TryGetValue(genericType, out var converterType))
12,132✔
168
                {
60✔
169
                    // construct `prototype` with the same generic arguments that `type` has
170
                    var concreteConverterType = converterType.MakeGenericType(inputType.GenericTypeArguments);
60✔
171
                    converter = (Converter)concreteConverterType.CreateInstanceSafe("converter", null);
60✔
172

173
                    // yes, do this even if it's null
174
                    ConverterObjects[inputType] = converter;
60✔
175

176
                    return converter;
60✔
177
                }
178
                else
179
                {
12,072✔
180
                    // stub it out so we can do the fast path next time
181
                    ConverterObjects[inputType] = null;
12,072✔
182
                }
12,072✔
183
            }
12,072✔
184

185
            var factoriedConverter = Config.ConverterFactory?.Invoke(inputType);
62,620✔
186
            ConverterObjects[inputType] = factoriedConverter;   // cache this so we don't generate a million of them
62,620✔
187
            return factoriedConverter;
62,620✔
188
        }
6,494,616✔
189

190

191
        internal static void Initialize()
192
        {
29,124✔
193
            if (ConverterInitialized)
29,124✔
194
            {
9,270✔
195
                return;
9,270✔
196
            }
197

198
            // this is here just so we don't keep thrashing if something breaks
199
            ConverterInitialized = true;
19,854✔
200

201
            ConverterObjects = new System.Collections.Concurrent.ConcurrentDictionary<Type, Converter>();
19,854✔
202

203
            IEnumerable<Type> conversionTypes;
204
            if (Config.TestParameters == null)
19,854✔
205
            {
6✔
206
                conversionTypes = UtilReflection.GetAllUserTypes().Where(t => t.IsSubclassOf(typeof(Converter)));
8,529✔
207
            }
6✔
208
            else if (Config.TestParameters.explicitConverters != null)
19,848✔
209
            {
4,104✔
210
                conversionTypes = Config.TestParameters.explicitConverters;
4,104✔
211
            }
4,104✔
212
            else
213
            {
15,744✔
214
                conversionTypes = Enumerable.Empty<Type>();
15,744✔
215
            }
15,744✔
216

217
            foreach (var type in conversionTypes)
61,476✔
218
            {
966✔
219
                if (type.IsAbstract)
966✔
220
                {
6✔
221
                    Dbg.Err($"Found converter {type} which is abstract. This is not allowed.");
6✔
222
                    continue;
6✔
223
                }
224

225
                if (type.IsGenericType)
960✔
226
                {
36✔
227
                    var baseConverterType = type;
36✔
228
                    while (baseConverterType.BaseType != typeof(ConverterString) && baseConverterType.BaseType != typeof(ConverterRecord) && baseConverterType.BaseType != typeof(ConverterFactory))
72✔
229
                    {
36✔
230
                        baseConverterType = baseConverterType.BaseType;
36✔
231
                    }
36✔
232

233
                    // we are now, presumably, at ConverterString<T> or ConverterRecord<T> or ConverterFactory<T>
234
                    // this *really* needs more error checking
235
                    Type converterTarget = baseConverterType.GenericTypeArguments[0];
36✔
236

237
                    if (!converterTarget.IsGenericType)
36✔
238
                    {
6✔
239
                        Dbg.Err($"Found generic converter {type} which is not referring to a generic constructed type.");
6✔
240
                        continue;
6✔
241
                    }
242

243
                    converterTarget = converterTarget.GetGenericTypeDefinition();
30✔
244
                    if (ConverterGenericPrototypes.ContainsKey(converterTarget))
30✔
245
                    {
×
246
                        Dbg.Err($"Found multiple converters for {converterTarget}: {ConverterGenericPrototypes[converterTarget]} and {type}");
×
247
                    }
×
248

249
                    ConverterGenericPrototypes[converterTarget] = type;
30✔
250
                    continue;
30✔
251
                }
252

253
                var converter = (Converter)type.CreateInstanceSafe("converter", null);
924✔
254
                if (converter != null && (converter is ConverterString || converter is ConverterRecord || converter is ConverterFactory))
924✔
255
                {
918✔
256
                    Type convertedType = converter.GetConvertedTypeHint();
918✔
257
                    if (ConverterObjects.ContainsKey(convertedType))
918✔
258
                    {
6✔
259
                        Dbg.Err($"Found multiple converters for {convertedType}: {ConverterObjects[convertedType]} and {type}");
6✔
260
                    }
6✔
261

262
                    ConverterObjects[convertedType] = converter;
918✔
263
                    continue;
918✔
264
                }
265
            }
6✔
266
        }
29,124✔
267

268
        internal static object GenerateResultFallback(object model, Type type)
269
        {
324✔
270
            if (model != null)
324✔
271
            {
6✔
272
                return model;
6✔
273
            }
274
            else if (type.IsValueType)
318✔
275
            {
198✔
276
                // We don't need Safe here because all value types are required to have a default constructor.
277
                return Activator.CreateInstance(type);
198✔
278
            }
279
            else
280
            {
120✔
281
                return null;
120✔
282
            }
283
        }
324✔
284

285
        internal enum ParseMode
286
        {
287
            Default,
288
            Replace,
289
            Patch,
290
            Append,
291

292
            // Dec-only
293
            Create,
294
            CreateOrReplace,
295
            CreateOrPatch,
296
            CreateOrIgnore,
297
            Delete,
298
            ReplaceIfExists,
299
            PatchIfExists,
300
            DeleteIfExists,
301
        }
302
        internal static ParseMode ParseModeFromString(Context context, string str)
303
        {
3,760,854✔
304
            if (str == null)
3,760,854✔
305
            {
3,753,042✔
306
                return ParseMode.Default;
3,753,042✔
307
            }
308
            else if (str == "replace")
7,812✔
309
            {
1,224✔
310
                return ParseMode.Replace;
1,224✔
311
            }
312
            else if (str == "patch")
6,588✔
313
            {
2,616✔
314
                return ParseMode.Patch;
2,616✔
315
            }
316
            else if (str == "append")
3,972✔
317
            {
1,140✔
318
                return ParseMode.Append;
1,140✔
319
            }
320
            else if (str == "create")
2,832✔
321
            {
384✔
322
                return ParseMode.Create;
384✔
323
            }
324
            else if (str == "createOrReplace")
2,448✔
325
            {
216✔
326
                return ParseMode.CreateOrReplace;
216✔
327
            }
328
            else if (str == "createOrPatch")
2,232✔
329
            {
240✔
330
                return ParseMode.CreateOrPatch;
240✔
331
            }
332
            else if (str == "createOrIgnore")
1,992✔
333
            {
120✔
334
                return ParseMode.CreateOrIgnore;
120✔
335
            }
336
            else if (str == "delete")
1,872✔
337
            {
288✔
338
                return ParseMode.Delete;
288✔
339
            }
340
            else if (str == "replaceIfExists")
1,584✔
341
            {
192✔
342
                return ParseMode.ReplaceIfExists;
192✔
343
            }
344
            else if (str == "patchIfExists")
1,392✔
345
            {
216✔
346
                return ParseMode.PatchIfExists;
216✔
347
            }
348
            else if (str == "deleteIfExists")
1,176✔
349
            {
168✔
350
                return ParseMode.DeleteIfExists;
168✔
351
            }
352
            else
353
            {
1,008✔
354
                Dbg.Err($"{context}: Invalid `{str}` mode!");
1,008✔
355

356
                return ParseMode.Default;
1,008✔
357
            }
358
        }
3,760,854✔
359

360
        internal enum ParseCommand
361
        {
362
            Replace,
363
            Patch,
364
            Append,
365
        }
366
        internal static List<(ParseCommand command, ReaderNodeParseable node)> CompileOrders(UtilType.ParseModeCategory modeCategory, List<ReaderNodeParseable> nodes)
367
        {
1,824,498✔
368
            var orders = new List<(ParseCommand command, ReaderNodeParseable payload)>();
1,824,498✔
369

370
            if (modeCategory == UtilType.ParseModeCategory.Dec)
1,824,498✔
371
            {
×
372
                Dbg.Err($"Internal error: CompileOrders called with Dec mode category, this should never happen! Please report it.");
×
373
                return orders;
×
374
            }
375

376
            foreach (var node in nodes)
9,122,490✔
377
            {
1,824,498✔
378
                var context = node.GetContext();
1,824,498✔
379
                var s_parseMode = ParseModeFromString(context, node.GetMetadata(ReaderNodeParseable.Metadata.Mode));
1,824,498✔
380

381
                ParseCommand s_parseCommand;
382

383
                switch (modeCategory)
1,824,498✔
384
                {
385
                    case UtilType.ParseModeCategory.Object:
386
                        switch (s_parseMode)
1,138,390✔
387
                        {
388
                            default:
389
                                Dbg.Err($"{context}: Invalid mode {s_parseMode} provided for an Object-type parse, defaulting to Patch");
288✔
390
                                goto case ParseMode.Default;
288✔
391

392
                            case ParseMode.Default:
393
                            case ParseMode.Patch:
394
                                s_parseCommand = ParseCommand.Patch;
1,138,390✔
395
                                break;
1,138,390✔
396
                        }
397
                        break;
1,138,390✔
398
                    case UtilType.ParseModeCategory.OrderedContainer:
399
                        switch (s_parseMode)
159,810✔
400
                        {
401
                            default:
402
                                Dbg.Err($"{context}: Invalid mode {s_parseMode} provided for an ordered-container-type parse, defaulting to Replace");
96✔
403
                                goto case ParseMode.Default;
96✔
404

405
                            case ParseMode.Default:
406
                            case ParseMode.Replace:
407
                                s_parseCommand = ParseCommand.Replace;
159,648✔
408
                                break;
159,648✔
409

410
                            case ParseMode.Append:
411
                                s_parseCommand = ParseCommand.Append;
162✔
412
                                break;
162✔
413
                        }
414
                        break;
159,810✔
415
                    case UtilType.ParseModeCategory.UnorderedContainer:
416
                        switch (s_parseMode)
43,542✔
417
                        {
418
                            default:
419
                                Dbg.Err($"{context}: Invalid mode {s_parseMode} provided for an unordered-container-type parse, defaulting to Replace");
×
420
                                goto case ParseMode.Default;
×
421

422
                            case ParseMode.Default:
423
                            case ParseMode.Replace:
424
                                s_parseCommand = ParseCommand.Replace;
43,062✔
425
                                break;
43,062✔
426

427
                            case ParseMode.Patch:
428
                                s_parseCommand = ParseCommand.Patch;
288✔
429
                                break;
288✔
430

431
                            case ParseMode.Append:
432
                                s_parseCommand = ParseCommand.Append;
192✔
433
                                break;
192✔
434
                        }
435

436
                        break;
43,542✔
437
                    case UtilType.ParseModeCategory.Value:
438
                        switch (s_parseMode)
482,756✔
439
                        {
440
                            default:
441
                                Dbg.Err($"{context}: Invalid mode {s_parseMode} provided for a value-type parse, defaulting to Replace");
96✔
442
                                goto case ParseMode.Default;
96✔
443

444
                            case ParseMode.Default:
445
                            case ParseMode.Replace:
446
                                s_parseCommand = ParseCommand.Replace;
482,756✔
447
                                break;
482,756✔
448
                        }
449
                        break;
482,756✔
450
                    default:
451
                        Dbg.Err($"{context}: Internal error, unknown mode category {modeCategory}, please report");
×
452
                        s_parseCommand = ParseCommand.Patch;  // . . . I guess?
×
453
                        break;
×
454
                }
455

456
                if (s_parseCommand == ParseCommand.Replace)
1,824,498✔
457
                {
685,466✔
458
                    orders.Clear();
685,466✔
459
                }
685,466✔
460

461
                orders.Add((s_parseCommand, node));
1,824,498✔
462
            }
1,824,498✔
463

464
            return orders;
1,824,498✔
465
        }
1,824,498✔
466

467
        internal static List<ReaderFileDec.ReaderDec> CompileDecOrders(List<ReaderFileDec.ReaderDec> decs)
468
        {
55,272✔
469
            var orders = new List<ReaderFileDec.ReaderDec>();
55,272✔
470
            bool everExisted = false;
55,272✔
471
            foreach (var item in decs)
279,048✔
472
            {
56,616✔
473
                // this is also horribly inefficient
474
                var s_parseMode = ParseModeFromString(item.context, item.nodeFactory(null).GetMetadata(ReaderNodeParseable.Metadata.Mode));
56,616✔
475

476
                switch (s_parseMode)
56,616✔
477
                {
478
                    default:
479
                        Dbg.Err($"{item.context}: Invalid mode {s_parseMode} provided for a Dec-type parse, defaulting to Create");
24✔
480
                        goto case ParseMode.Default;
24✔
481

482
                    case ParseMode.Default:
483
                    case ParseMode.Create:
484
                        if (orders.Count != 0)
55,272✔
485
                        {
96✔
486
                            Dbg.Err($"{item.context}: Create mode used when a Dec already exists, falling back to Patch");
96✔
487
                            goto case ParseMode.Patch;
96✔
488
                        }
489
                        orders.Add(item);
55,176✔
490
                        everExisted = true;
55,176✔
491
                        break;
55,176✔
492

493
                    case ParseMode.Replace:
494
                        if (orders.Count == 0)
120✔
495
                        {
48✔
496
                            Dbg.Err($"{item.context}: Replace mode used when a Dec doesn't exist, falling back to Create");
48✔
497
                            goto case ParseMode.Create;
48✔
498
                        }
499
                        orders.Clear();
72✔
500
                        orders.Add(item);
72✔
501
                        break;
72✔
502

503
                    case ParseMode.Patch:
504
                        if (orders.Count == 0)
504✔
505
                        {
72✔
506
                            Dbg.Err($"{item.context}: Patch mode used when a Dec doesn't exist, falling back to Create");
72✔
507
                            goto case ParseMode.Create;
72✔
508
                        }
509
                        orders.Add(item);
432✔
510
                        break;
432✔
511

512
                    case ParseMode.CreateOrReplace:
513
                        // doesn't matter if we have a thing or not
514
                        orders.Clear();
96✔
515
                        orders.Add(item);
96✔
516
                        everExisted = true;
96✔
517
                        break;
96✔
518

519
                    case ParseMode.CreateOrPatch:
520
                        // doesn't matter if we have a thing or not
521
                        orders.Add(item);
96✔
522
                        everExisted = true;
96✔
523
                        break;
96✔
524

525
                    case ParseMode.CreateOrIgnore:
526
                        if (orders.Count == 0)
96✔
527
                        {
24✔
528
                            orders.Add(item);
24✔
529
                            everExisted = true;
24✔
530
                        }
24✔
531
                        break;
96✔
532

533
                    case ParseMode.Delete:
534
                        if (!everExisted)
288✔
535
                        {
24✔
536
                            Dbg.Err($"{item.context}: Delete mode used when a Dec doesn't exist; did you want deleteIfExists?");
24✔
537
                        }
24✔
538
                        orders.Clear();
288✔
539
                        break;
288✔
540

541
                    case ParseMode.ReplaceIfExists:
542
                        if (orders.Count != 0)
96✔
543
                        {
72✔
544
                            orders.Clear();
72✔
545
                            orders.Add(item);
72✔
546
                        }
72✔
547
                        break;
96✔
548

549
                    case ParseMode.PatchIfExists:
550
                        if (orders.Count != 0)
96✔
551
                        {
72✔
552
                            orders.Add(item);
72✔
553
                        }
72✔
554
                        break;
96✔
555

556
                    case ParseMode.DeleteIfExists:
557
                        orders.Clear();
168✔
558
                        break;
168✔
559
                }
560
            }
56,616✔
561

562
            return orders;
55,272✔
563
        }
55,272✔
564

565
        internal static object ParseElement(List<ReaderNodeParseable> nodes, Type type, object original,
566
            ReaderGlobals globals, Recorder.Settings recSettings, FieldInfo fieldInfo = null, bool isRootDec = false,
567
            bool hasReferenceId = false, bool asThis = false,
568
            List<(ParseCommand command, ReaderNodeParseable node)> ordersOverride = null)
569
        {
1,878,408✔
570
            var result = ParseElement_Worker(nodes, type, original, globals, recSettings, fieldInfo, isRootDec, hasReferenceId, asThis, ordersOverride);
1,878,408✔
571

572
            // I just really don't want to put this code at the end of *every single return*, that would be insane
573
            // we don't allow dec references, we've already got those!
574
            if (globals.decPathLookup != null && result != null)
1,878,408✔
575
            {
597,844✔
576
                var resultType = result.GetType();
597,844✔
577

578
                // I really feel like whatever I'm expressing here must exist elsewhere in the codebase, but I can't find it
579
                if (!resultType.IsValueType && resultType != typeof(string) && resultType != typeof(Type) && resultType != TypeSystemRuntimeType && !typeof(Dec).IsAssignableFrom(resultType))
597,844✔
580
                {
133,754✔
581
                    // these paths *should* all match up, so we're just choosing one
582
                    var newPath = nodes[0].GetContext().path;
133,754✔
583

584
                    if (newPath == null)
133,754✔
585
                    {
×
586
                        Dbg.Err("Internal error; missing path somehow? Please report this, thanks!");
×
587
                    }
×
588
                    else
589
                    {
133,754✔
590
                        // right now we strictly overwrite previous instances of the result
591
                        // this is not a great solution because it's very error-prone
592
                        // the problem is that the inheritance/patch system has a tendency to spam this function repeatedly with the same object
593
                        // I'm currently not sure how to deal with this, so . . . I'm not! I'm just doing it the bad way.
594
                        globals.decPathLookup[result] = newPath;
133,754✔
595
                    }
133,754✔
596
                }
133,754✔
597
            }
597,844✔
598

599
            return result;
1,878,408✔
600
        }
1,878,408✔
601

602
        internal static T ParseElementTyped<T>(List<ReaderNodeParseable> nodes, Type type, object original, ReaderGlobals globals, Recorder.Settings recSettings, FieldInfo fieldInfo = null, bool isRootDec = false, bool hasReferenceId = false, bool asThis = false, List<(ParseCommand command, ReaderNodeParseable node)> ordersOverride = null)
603
        {
4,212✔
604
            var result = ParseElement(nodes, type, original, globals, recSettings, fieldInfo, isRootDec, hasReferenceId, asThis, ordersOverride);
4,212✔
605
            if (result != null)
4,212✔
606
            {
4,146✔
607
                return (T)result;
4,146✔
608
            }
609
            else
610
            {
66✔
611
                return default;
66✔
612
            }
613
        }
4,212✔
614

615
        internal static object ParseElement_Worker(List<ReaderNodeParseable> nodes, Type type, object original, ReaderGlobals globals, Recorder.Settings recSettings, FieldInfo fieldInfo = null, bool isRootDec = false, bool hasReferenceId = false, bool asThis = false, List<(ParseCommand command, ReaderNodeParseable node)> ordersOverride = null)
616
        {
1,878,408✔
617
            if (nodes == null || nodes.Count == 0)
1,878,408✔
618
            {
×
619
                Dbg.Err("Internal error, Dec failed to provide nodes to ParseElement. Please report this!");
×
620
                return original;
×
621
            }
622

623
            if (!globals.allowReflection && nodes.Count > 1)
1,878,408✔
624
            {
×
625
                Dbg.Err("Internal error, multiple nodes provided for recorder-mode behavior. Please report this!");
×
626
            }
×
627

628
            // We keep the original around in case of error, but do all our manipulation on a result object.
629
            object result = original;
1,878,408✔
630

631
            // Verify our Shared flags as the *very* first step to ensure nothing gets past us.
632
            // In theory this should be fine with Flexible; Flexible only happens on an outer wrapper that was shared, and therefore was null, and therefore this is default also
633
            if (recSettings.shared == Recorder.Settings.Shared.Allow)
1,878,408✔
634
            {
970,188✔
635
                if (!type.CanBeShared())
970,188✔
636
                {
120✔
637
                    // If shared, make sure our input is null and our type is appropriate for sharing
638
                    Dbg.Wrn($"{nodes[0].GetContext()}: Value type `{type}` tagged as Shared in recorder, this is meaningless but harmless");
120✔
639
                }
120✔
640
                else if (original != null && !hasReferenceId)
970,068✔
641
                {
30✔
642
                    // We need to create objects without context if it's shared, so we kind of panic in this case
643
                    Dbg.Err($"{nodes[0].GetContext()}: Shared `{type}` provided with non-null default object, this may result in unexpected behavior");
30✔
644
                }
30✔
645
            }
970,188✔
646

647
            // The next thing we do is parse all our attributes. This is because we want to verify that there are no attributes being ignored.
648

649
            // Validate all combinations here
650
            // This could definitely be more efficient and skip at least one traversal pass
651
            foreach (var s_node in nodes)
9,396,108✔
652
            {
1,880,442✔
653
                string nullAttribute = s_node.GetMetadata(ReaderNodeParseable.Metadata.Null);
1,880,442✔
654
                string refAttribute = s_node.GetMetadata(ReaderNodeParseable.Metadata.Ref);
1,880,442✔
655
                string classAttribute = s_node.GetMetadata(ReaderNodeParseable.Metadata.Class);
1,880,442✔
656
                string modeAttribute = s_node.GetMetadata(ReaderNodeParseable.Metadata.Mode);
1,880,442✔
657

658
                // Some of these are redundant and that's OK
659
                if (nullAttribute != null && (refAttribute != null || classAttribute != null || modeAttribute != null))
1,880,442✔
660
                {
216✔
661
                    Dbg.Err($"{s_node.GetContext()}: Null element may not have ref, class, or mode specified; guessing wildly at intentions");
216✔
662
                }
216✔
663
                else if (refAttribute != null && (nullAttribute != null || classAttribute != null || modeAttribute != null))
1,880,226✔
664
                {
90✔
665
                    Dbg.Err($"{s_node.GetContext()}: Ref element may not have null, class, or mode specified; guessing wildly at intentions");
90✔
666
                }
90✔
667
                else if (classAttribute != null && (nullAttribute != null || refAttribute != null))
1,880,136✔
668
                {
×
669
                    Dbg.Err($"{s_node.GetContext()}: Class-specified element may not have null or ref specified; guessing wildly at intentions");
×
670
                }
×
671
                else if (modeAttribute != null && (nullAttribute != null || refAttribute != null))
1,880,136✔
672
                {
×
673
                    Dbg.Err($"{s_node.GetContext()}: Mode-specified element may not have null or ref specified; guessing wildly at intentions");
×
674
                }
×
675

676
                var unrecognized = s_node.GetMetadataUnrecognized();
1,880,442✔
677
                if (unrecognized != null)
1,880,442✔
678
                {
78✔
679
                    Dbg.Err($"{s_node.GetContext()}: Has unknown attributes {unrecognized}");
78✔
680
                }
78✔
681
            }
1,880,442✔
682

683
            // Doesn't mean anything outside recorderMode, so we check it for validity just in case
684
            string refKey;
685
            ReaderNode refKeyNode = null; // stored entirely for error reporting
1,878,408✔
686
            if (!globals.allowRefs)
1,878,408✔
687
            {
739,860✔
688
                refKey = null;
739,860✔
689
                foreach (var s_node in nodes)
3,703,368✔
690
                {
741,894✔
691
                    string nodeRefAttribute = s_node.GetMetadata(ReaderNodeParseable.Metadata.Ref);
741,894✔
692
                    if (nodeRefAttribute != null)
741,894✔
693
                    {
240✔
694
                        Dbg.Err($"{s_node.GetContext()}: Found a reference tag while not evaluating Recorder mode, ignoring it");
240✔
695
                    }
240✔
696
                }
741,894✔
697
            }
739,860✔
698
            else
699
            {
1,138,548✔
700
                (refKey, refKeyNode) = nodes.Select(node => (node.GetMetadata(ReaderNodeParseable.Metadata.Ref), node)).Where(anp => anp.Item1 != null).LastOrDefault();
3,415,644✔
701
            }
1,138,548✔
702

703
            // First figure out type. We actually need type to be set before we can properly analyze and validate the mode flags.
704
            // If we're in an asThis block, it refers to the outer item, not the inner item; just skip this entirely
705
            bool isNull = false;
1,878,408✔
706
            if (!asThis)
1,878,408✔
707
            {
1,877,706✔
708
                string classAttribute = null;
1,877,706✔
709
                ReaderNode classAttributeNode = null; // stored entirely for error reporting
1,877,706✔
710
                bool replaced = false;
1,877,706✔
711
                foreach (var s_node in nodes)
9,392,598✔
712
                {
1,879,740✔
713
                    // However, we do need to watch for Replace, because that means we should nuke the class attribute and start over.
714
                    string modeAttribute = s_node.GetMetadata(ReaderNodeParseable.Metadata.Mode);
1,879,740✔
715
                    ParseMode s_parseMode = ParseModeFromString(s_node.GetContext(), modeAttribute);
1,879,740✔
716
                    if (s_parseMode == ParseMode.Replace)
1,879,740✔
717
                    {
624✔
718
                        // we also should maybe be doing this if we're a list, map, or set?
719
                        classAttribute = null;
624✔
720
                        replaced = true;
624✔
721
                    }
624✔
722

723
                    // if we get nulled, we kill the class tag and basically treat it like a delete
724
                    // but we also reset the null tag on every entry
725
                    isNull = false;
1,879,740✔
726
                    string nullAttribute = s_node.GetMetadata(ReaderNodeParseable.Metadata.Null);
1,879,740✔
727
                    if (nullAttribute != null)
1,879,740✔
728
                    {
300,518✔
729
                        if (!bool.TryParse(nullAttribute, out bool nullValue))
300,518✔
730
                        {
×
731
                            Dbg.Err($"{s_node.GetContext()}: Invalid `null` attribute");
×
732
                        }
×
733
                        else if (nullValue)
300,518✔
734
                        {
300,470✔
735
                            isNull = true;
300,470✔
736
                        }
300,470✔
737
                    }
300,518✔
738

739
                    // update the class based on whatever this says
740
                    string localClassAttribute = s_node.GetMetadata(ReaderNodeParseable.Metadata.Class);
1,879,740✔
741
                    if (localClassAttribute != null)
1,879,740✔
742
                    {
249,698✔
743
                        classAttribute = localClassAttribute;
249,698✔
744
                        classAttributeNode = s_node;
249,698✔
745
                    }
249,698✔
746
                }
1,879,740✔
747

748
                if (classAttribute != null)
1,877,706✔
749
                {
249,698✔
750
                    var possibleType = (Type)ParseString(classAttribute, typeof(Type), null, classAttributeNode.GetContext());
249,698✔
751
                    if (!type.IsAssignableFrom(possibleType))
249,698✔
752
                    {
24✔
753
                        Dbg.Err($"{classAttributeNode.GetContext()}: Explicit type {classAttribute} cannot be assigned to expected type {type}");
24✔
754
                    }
24✔
755
                    else if (!replaced && result != null && result.GetType() != possibleType)
249,674✔
756
                    {
24✔
757
                        Dbg.Err($"{classAttributeNode.GetContext()}: Explicit type {classAttribute} does not match already-provided instance {type}");
24✔
758
                    }
24✔
759
                    else
760
                    {
249,650✔
761
                        type = possibleType;
249,650✔
762
                    }
249,650✔
763
                }
249,698✔
764
            }
1,877,706✔
765

766
            var converter = ConverterFor(type);
1,878,408✔
767

768
            // Now we traverse the Mode attributes as prep for our final parse pass.
769
            // ordersOverride makes `nodes` admittedly a little unnecessary.
770
            List<(ParseCommand command, ReaderNodeParseable node)> orders = ordersOverride ?? CompileOrders(type.CalculateSerializationModeCategory(converter, isRootDec), nodes);
1,878,408✔
771

772
            // Gather info
773
            bool hasChildren = false;
1,878,408✔
774
            ReaderNode hasChildrenNode = null;
1,878,408✔
775
            bool hasText = false;
1,878,408✔
776
            ReaderNode hasTextNode = null;
1,878,408✔
777
            foreach (var (_, node) in orders)
9,396,108✔
778
            {
1,880,442✔
779
                if (!hasChildren && node.HasChildren())
1,880,442✔
780
                {
578,678✔
781
                    hasChildren = true;
578,678✔
782
                    hasChildrenNode = node;
578,678✔
783
                }
578,678✔
784
                if (!hasText && node.GetText() != null)
1,880,442✔
785
                {
430,034✔
786
                    hasText = true;
430,034✔
787
                    hasTextNode = node;
430,034✔
788
                }
430,034✔
789
            }
1,880,442✔
790

791
            // Actually handle our attributes
792
            if (refKey != null)
1,878,408✔
793
            {
429,192✔
794
                // Ref is the highest priority, largely because I think it's cool
795

796
                // First we check if this is a valid Dec path ref; those don't require .Shared()
797
                if (Database.DecPathLookupReverse.TryGetValue(refKey, out var defPathRef))
429,192✔
798
                {
270✔
799
                    // check types
800
                    if (!type.IsAssignableFrom(defPathRef.GetType()))
270✔
801
                    {
×
802
                        Dbg.Err($"{refKeyNode.GetContext()}: Dec path reference object [{refKey}] is of type {defPathRef.GetType()}, which cannot be converted to expected type {type}");
×
803
                        return result;
×
804
                    }
805

806
                    // if it's a conflict, be unhappy
807
                    if (Database.DecPathLookupInvalid.Contains(refKey))
270✔
808
                    {
×
809
                        Dbg.Err($"{refKeyNode.GetContext()}: Deserializes improper Dec path reference [{refKey}]; this should probably not have been serialized in the first place, doing our best though");
×
810
                    }
×
811
                    else if (Database.DecPathLookupConflicts.Contains(refKey))
270✔
812
                    {
×
813
                        Dbg.Err($"{refKeyNode.GetContext()}: Multiple objects claiming Dec path reference [{refKey}]; this should probably not have been serialized in the first place, doing our best though");
×
814
                    }
×
815

816
                    // toot
817
                    return defPathRef;
270✔
818
                }
819

820
                if (recSettings.shared == Recorder.Settings.Shared.Deny)
428,922✔
821
                {
72✔
822
                    Dbg.Err($"{refKeyNode.GetContext()}: Found a reference in a non-.Shared() context; this should happen only if you've removed the .Shared() tag since the file was generated, or if you hand-wrote a file that is questionably valid. Using the reference anyway but this might produce unexpected results");
72✔
823
                }
72✔
824

825
                if (globals.refs == null)
428,922✔
826
                {
48✔
827
                    Dbg.Err($"{refKeyNode.GetContext()}: Found a reference object {refKey} before refs are initialized (is this being used in a ConverterFactory<>.Create()?)");
48✔
828
                    return result;
48✔
829
                }
830

831
                if (!globals.refs.ContainsKey(refKey))
428,874✔
832
                {
6✔
833
                    Dbg.Err($"{refKeyNode.GetContext()}: Found a reference object {refKey} without a valid reference mapping");
6✔
834
                    return result;
6✔
835
                }
836

837
                object refObject = globals.refs[refKey];
428,868✔
838
                if (refObject == null && !type.IsValueType)
428,868✔
839
                {
96✔
840
                    // okay, good enough
841
                    return refObject;
96✔
842
                }
843

844
                if (!type.IsAssignableFrom(refObject.GetType()))
428,772✔
845
                {
30✔
846
                    Dbg.Err($"{refKeyNode.GetContext()}: Reference object {refKey} is of type {refObject.GetType()}, which cannot be converted to expected type {type}");
30✔
847
                    return result;
30✔
848
                }
849

850
                return refObject;
428,742✔
851
            }
852
            else if (isNull)
1,449,216✔
853
            {
300,440✔
854
                return null;
300,440✔
855

856
                // Note: It may seem wrong that we can return null along with a non-null model.
857
                // The problem is that this is meant to be able to override defaults. If the default is an object, explicitly setting it to null *should* clear the object out.
858
                // If we actually need a specific object to be returned, for whatever reason, the caller has to do the comparison.
859
            }
860

861
            // Basic early validation
862

863
            if (hasChildren && hasText)
1,148,776✔
864
            {
18✔
865
                Dbg.Err($"{hasChildrenNode.GetContext()} / {hasTextNode.GetContext()}: Cannot have both text and child nodes in XML - this is probably a typo, maybe you have the wrong number of close tags or added text somewhere you didn't mean to?");
18✔
866

867
                // we'll just fall through and try to parse anyway, though
868
            }
18✔
869

870
            if (typeof(Dec).IsAssignableFrom(type) && hasChildren && !isRootDec)
1,148,776✔
871
            {
×
872
                Dbg.Err($"{hasChildrenNode.GetContext()}: Defining members of an item of type {type}, derived from Dec.Dec, is not supported within an outer Dec. Either reference a {type} defined independently or remove {type}'s inheritance from Dec.");
×
873
                return null;
×
874
            }
875

876
            // Defer off to converters, whatever they feel like doing
877
            if (converter != null)
1,148,776✔
878
            {
726✔
879
                // string converter
880
                if (converter is ConverterString converterString)
726✔
881
                {
312✔
882
                    foreach (var (parseCommand, node) in orders)
1,560✔
883
                    {
312✔
884
                        switch (parseCommand)
312✔
885
                        {
886
                            case ParseCommand.Replace:
887
                                // easy, done
888
                                break;
312✔
889

890
                            default:
891
                                Dbg.Err($"{node.GetContext()}: Internal error, got invalid mode {parseCommand}");
×
892
                                break;
×
893
                        }
894

895
                        if (hasChildren)
312✔
896
                        {
18✔
897
                            Dbg.Err($"{node.GetContext()}: String converter {converter.GetType()} called with child XML nodes, which will be ignored");
18✔
898
                        }
18✔
899

900
                        // We actually accept "no text" here, though, empty-string might be valid!
901

902
                        // context might be null; that's OK at the moment
903
                        try
904
                        {
312✔
905
                            result = converterString.ReadObj(node.GetText() ?? "", node.GetContext());
312✔
906
                        }
210✔
907
                        catch (Exception e)
102✔
908
                        {
102✔
909
                            Dbg.Ex(new ConverterReadException(node.GetContext(), converter, e));
102✔
910

911
                            result = GenerateResultFallback(result, type);
102✔
912
                        }
102✔
913
                    }
312✔
914
                }
312✔
915
                else if (converter is ConverterRecord converterRecord)
414✔
916
                {
282✔
917
                    foreach (var (parseCommand, node) in orders)
1,410✔
918
                    {
282✔
919
                        switch (parseCommand)
282✔
920
                        {
921
                            case ParseCommand.Patch:
922
                                // easy, done
923
                                break;
282✔
924

925
                            case ParseCommand.Replace:
926
                                result = null;
×
927
                                break;
×
928

929
                            default:
930
                                Dbg.Err($"{node.GetContext()}: Internal error, got invalid mode {parseCommand}");
×
931
                                break;
×
932
                        }
933

934
                        bool isNullable = type.IsConstructedGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>);
282✔
935

936
                        if (result == null && !isNullable)
282✔
937
                        {
144✔
938
                            result = type.CreateInstanceSafe("converterrecord", node);
144✔
939
                        }
144✔
940

941
                        // context might be null; that's OK at the moment
942
                        if (result != null || isNullable)
282✔
943
                        {
282✔
944
                            var recorderReader = new RecorderReader(node, globals, trackUsage: true);
282✔
945
                            try
946
                            {
282✔
947
                                object returnedResult = converterRecord.RecordObj(result, recorderReader);
282✔
948

949
                                if (!type.IsValueType && result != returnedResult)
264✔
950
                                {
×
951
                                    Dbg.Err($"{node.GetContext()}: Converter {converterRecord.GetType()} changed object instance, this is disallowed");
×
952
                                }
×
953
                                else
954
                                {
264✔
955
                                    // for value types, this is fine
956
                                    result = returnedResult;
264✔
957
                                }
264✔
958

959
                                recorderReader.ReportUnusedFields();
264✔
960
                            }
264✔
961
                            catch (Exception e)
18✔
962
                            {
18✔
963
                                Dbg.Ex(new ConverterReadException(node.GetContext(), converter, e));
18✔
964

965
                                // no fallback needed, we already have a result
966
                            }
18✔
967
                        }
282✔
968
                    }
282✔
969
                }
282✔
970
                else if (converter is ConverterFactory converterFactory)
132✔
971
                {
132✔
972
                    foreach (var (parseCommand, node) in orders)
660✔
973
                    {
132✔
974
                        switch (parseCommand)
132✔
975
                        {
976
                            case ParseCommand.Patch:
977
                                // easy, done
978
                                break;
132✔
979

980
                            case ParseCommand.Replace:
981
                                result = null;
×
982
                                break;
×
983

984
                            default:
985
                                Dbg.Err($"{node.GetContext()}: Internal error, got invalid mode {parseCommand}");
×
986
                                break;
×
987
                        }
988

989
                        var recorderReader = new RecorderReader(node, globals, disallowShared: true, trackUsage: true);
132✔
990
                        if (result == null)
132✔
991
                        {
126✔
992
                            try
993
                            {
126✔
994
                                result = converterFactory.CreateObj(recorderReader);
126✔
995
                            }
108✔
996
                            catch (Exception e)
18✔
997
                            {
18✔
998
                                Dbg.Ex(new ConverterReadException(node.GetContext(), converter, e));
18✔
999
                            }
18✔
1000
                        }
126✔
1001

1002
                        // context might be null; that's OK at the moment
1003
                        if (result != null)
132✔
1004
                        {
114✔
1005
                            recorderReader.AllowShared(globals);
114✔
1006
                            try
1007
                            {
114✔
1008
                                result = converterFactory.ReadObj(result, recorderReader);
114✔
1009
                                recorderReader.ReportUnusedFields();
96✔
1010
                            }
96✔
1011
                            catch (Exception e)
18✔
1012
                            {
18✔
1013
                                Dbg.Ex(new ConverterReadException(node.GetContext(), converter, e));
18✔
1014

1015
                                // no fallback needed, we already have a result
1016
                            }
18✔
1017
                        }
114✔
1018
                    }
132✔
1019
                }
132✔
1020
                else
1021
                {
×
1022
                    Dbg.Err($"Somehow ended up with an unsupported converter {converter.GetType()}");
×
1023
                }
×
1024

1025
                return result;
726✔
1026
            }
1027

1028
            // All our standard text-using options
1029
            // Placed before IRecordable just in case we have a Dec that is IRecordable
1030
            if ((typeof(Dec).IsAssignableFrom(type) && !isRootDec) ||
1,148,050✔
1031
                    type == typeof(Type) ||
1,148,050✔
1032
                    type == typeof(string) ||
1,148,050✔
1033
                    type.IsPrimitive ||
1,148,050✔
1034
                    (TypeDescriptor.GetConverter(type)?.CanConvertFrom(typeof(string)) ?? false)   // this is last because it's slow
1,148,050✔
1035
                )
1,148,050✔
1036
            {
477,830✔
1037
                foreach (var (parseCommand, node) in orders)
2,389,150✔
1038
                {
477,830✔
1039
                    switch (parseCommand)
477,830✔
1040
                    {
1041
                        case ParseCommand.Replace:
1042
                            // easy, done
1043
                            break;
477,830✔
1044

1045
                        default:
1046
                            Dbg.Err($"{node.GetContext()}: Internal error, got invalid mode {parseCommand}");
×
1047
                            break;
×
1048
                    }
1049

1050
                    if (hasChildren)
477,830✔
1051
                    {
24✔
1052
                        Dbg.Err($"{node.GetContext()}: Child nodes are not valid when parsing {type}");
24✔
1053
                    }
24✔
1054

1055
                    result = ParseString(node.GetText(), type, result, node.GetContext());
477,830✔
1056
                }
477,830✔
1057

1058
                return result;
477,830✔
1059
            }
1060

1061
            // Special case: IRecordables
1062
            IRecordable recordableBuffered = null;
670,220✔
1063
            if (typeof(IRecordable).IsAssignableFrom(type))
670,220✔
1064
            {
471,756✔
1065
                // we're going to need to make one anyway so let's just go ahead and do that
1066
                IRecordable recordable = null;
471,756✔
1067

1068
                if (result != null)
471,756✔
1069
                {
248,130✔
1070
                    recordable = (IRecordable)result;
248,130✔
1071
                }
248,130✔
1072
                else if (recSettings.factories == null)
223,626✔
1073
                {
222,390✔
1074
                    recordable = (IRecordable)type.CreateInstanceSafe("recordable", orders[0].node);
222,390✔
1075
                }
222,390✔
1076
                else
1077
                {
1,236✔
1078
                    recordable = recSettings.CreateRecordableFromFactory(type, "recordable", orders[0].node);
1,236✔
1079
                }
1,236✔
1080

1081
                // we hold on to this so that, *if* we end up not using this object, we can optionally reuse it later for reflection
1082
                // in an ideal world we wouldn't create it at all in the first place, but we need to create it to call IConditionalRecordable's function
1083
                recordableBuffered = recordable;
471,756✔
1084

1085
                var conditionalRecordable = recordable as IConditionalRecordable;
471,756✔
1086
                if (conditionalRecordable == null || conditionalRecordable.ShouldRecord(nodes[0].UserSettings))
471,756✔
1087
                {
471,738✔
1088
                    foreach (var (parseCommand, node) in orders)
2,358,690✔
1089
                    {
471,738✔
1090
                        switch (parseCommand)
471,738✔
1091
                        {
1092
                            case ParseCommand.Patch:
1093
                                // easy, done
1094
                                break;
471,738✔
1095

1096
                            default:
1097
                                Dbg.Err($"{node.GetContext()}: Internal error, got invalid mode {parseCommand}");
×
1098
                                break;
×
1099
                        }
1100

1101
                        if (recordable != null)
471,738✔
1102
                        {
471,714✔
1103
                            var recorderReader = new RecorderReader(node, globals, trackUsage: true);
471,714✔
1104
                            recordable.Record(recorderReader);
471,714✔
1105
                            recorderReader.ReportUnusedFields();
471,714✔
1106

1107
                            // TODO: support indices if this is within the Dec system?
1108
                        }
471,714✔
1109
                    }
471,738✔
1110

1111
                    result = recordable;
471,738✔
1112
                    return result;
471,738✔
1113
                }
1114

1115
                // otherwise we just fall through
1116
            }
18✔
1117

1118
            // Special case: byte[] arrays with base64 encoding
1119
            if (type == typeof(byte[]) && hasText && !hasChildren)
198,482✔
1120
            {
456✔
1121
                // This is a byte array encoded as base64 text
1122
                foreach (var (parseCommand, node) in orders)
2,280✔
1123
                {
456✔
1124
                    switch (parseCommand)
456✔
1125
                    {
1126
                        case ParseCommand.Replace:
1127
                            // easy, done
1128
                            break;
456✔
1129

1130
                        default:
1131
                            Dbg.Err($"{node.GetContext()}: Internal error, got invalid mode {parseCommand}");
×
1132
                            break;
×
1133
                    }
1134

1135
                    try
1136
                    {
456✔
1137
                        byte[] decodedArray = Convert.FromBase64String(node.GetText());
456✔
1138

1139
                        // Check if we can reuse the existing array
1140
                        if (result != null && result.GetType() == type && ((byte[])result).Length == decodedArray.Length)
408✔
1141
                        {
168✔
1142
                            // Copy into existing array for reference preservation
1143
                            Array.Copy(decodedArray, (byte[])result, decodedArray.Length);
168✔
1144
                        }
168✔
1145
                        else
1146
                        {
240✔
1147
                            // Use this as the new array
1148
                            result = decodedArray;
240✔
1149
                        }
240✔
1150
                    }
408✔
1151
                    catch (FormatException)
48✔
1152
                    {
48✔
1153
                        Dbg.Err($"{node.GetContext()}: Invalid base64 string for byte array");
48✔
1154

1155
                        if (result == null)
48✔
1156
                        {
24✔
1157
                            result = new byte[0];   // kind of an ugly fallback
24✔
1158
                        }
24✔
1159
                    }
48✔
1160
                }
456✔
1161

1162
                return result;
456✔
1163
            }
1164

1165
            // Nothing past this point even supports text, so let's just get angry and break stuff.
1166
            if (hasText)
198,026✔
1167
            {
96✔
1168
                Dbg.Err($"{hasTextNode.GetContext()}: Text detected in a situation where it is invalid; will be ignored");
96✔
1169
                return result;
96✔
1170
            }
1171

1172
            // Special case: Lists
1173
            if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(List<>))
197,930✔
1174
            {
55,878✔
1175
                foreach (var (parseCommand, node) in orders)
279,390✔
1176
                {
55,878✔
1177
                    switch (parseCommand)
55,878✔
1178
                    {
1179
                        case ParseCommand.Replace:
1180
                            // If you have a default list, but specify it in XML, we assume this is a full override. Clear the original list to cut down on GC churn.
1181
                            // TODO: Is some bozo going to store the same "constant" global list on init, then be surprised when we re-use the list instead of creating a new one? Detect this and yell about it I guess.
1182
                            // If you are reading this because you're the bozo, [insert angry emoji here], but also feel free to be annoyed that I haven't fixed it yet despite realizing it's a problem. Ping me on Discord, I'll take care of it, sorry 'bout that.
1183
                            if (result != null)
55,782✔
1184
                            {
504✔
1185
                                ((IList)result).Clear();
504✔
1186
                            }
504✔
1187
                            break;
55,782✔
1188

1189
                        case ParseCommand.Append:
1190
                            // we're good
1191
                            break;
96✔
1192

1193
                        default:
1194
                            Dbg.Err($"{node.GetContext()}: Internal error, got invalid mode {parseCommand}");
×
1195
                            break;
×
1196
                    }
1197

1198
                    // List<> handling
1199
                    Type referencedType = type.GetGenericArguments()[0];
55,878✔
1200

1201
                    var list = (IList)(result ?? Activator.CreateInstance(type));
55,878✔
1202

1203
                    node.ParseList(list, referencedType, globals, recSettings);
55,878✔
1204

1205
                    result = list;
55,878✔
1206
                }
55,878✔
1207

1208
                return result;
55,878✔
1209
            }
1210

1211
            // Special case: Arrays
1212
            if (type.IsArray)
142,052✔
1213
            {
4,032✔
1214
                Type referencedType = type.GetElementType();
4,032✔
1215

1216
                foreach (var (parseCommand, node) in orders)
20,160✔
1217
                {
4,032✔
1218
                    Array array = null;
4,032✔
1219
                    int startOffset = 0;
4,032✔
1220

1221
                    // This is a bit extra-complicated because we can't append stuff after the fact, we need to figure out what our length is when we create the object.
1222
                    switch (parseCommand)
4,032✔
1223
                    {
1224
                        case ParseCommand.Replace:
1225
                        {
3,990✔
1226
                            // This is a full override, so we're going to create it here.
1227
                            // It is actually vitally important that we fall back on the model when possible, because the Recorder Ref system requires it.
1228
                            bool match = result != null && result.GetType() == type;
3,990✔
1229
                            var arrayDimensions = node.GetArrayDimensions(type.GetArrayRank());
3,990✔
1230
                            if (match)
3,990✔
1231
                            {
582✔
1232
                                array = (Array)result;
582✔
1233
                                if (array.Rank != type.GetArrayRank())
582✔
1234
                                {
×
1235
                                    match = false;
×
1236
                                }
×
1237
                                else
1238
                                {
582✔
1239
                                    for (int i = 0; i < array.Rank; i++)
1,536✔
1240
                                    {
594✔
1241
                                        if (array.GetLength(i) != arrayDimensions[i])
594✔
1242
                                        {
408✔
1243
                                            match = false;
408✔
1244
                                            break;
408✔
1245
                                        }
1246
                                    }
186✔
1247
                                }
582✔
1248
                            }
582✔
1249

1250
                            if (!match)
3,990✔
1251
                            {
3,816✔
1252
                                // Otherwise just make a new one, no harm done.
1253
                                array = Array.CreateInstance(referencedType, arrayDimensions);
3,816✔
1254
                            }
3,816✔
1255

1256
                            break;
3,990✔
1257
                        }
1258

1259
                        case ParseCommand.Append:
1260
                        {
66✔
1261
                            if (result == null)
66✔
1262
                            {
24✔
1263
                                goto case ParseCommand.Replace;
24✔
1264
                            }
1265

1266
                            // This is jankier; we create it here with the intended final length, then copy the elements over, all because arrays can't be resized
1267
                            // (yes, I know, that's the point of arrays, I'm not complaining, just . . . grumbling a little)
1268
                            var oldArray = (Array)result;
42✔
1269
                            startOffset = oldArray.Length;
42✔
1270
                            var arrayDimensions = node.GetArrayDimensions(type.GetArrayRank());
42✔
1271
                            arrayDimensions[0] += startOffset;
42✔
1272
                            array = Array.CreateInstance(referencedType, arrayDimensions);
42✔
1273
                            if (arrayDimensions.Length == 1)
42✔
1274
                            {
24✔
1275
                                oldArray.CopyTo(array, 0);
24✔
1276
                            }
24✔
1277
                            else
1278
                            {
18✔
1279
                                // oy
1280
                                void CopyArray(Array source, Array destination, int[] indices, int rank = 0)
1281
                                {
72✔
1282
                                    if (rank < source.Rank)
72✔
1283
                                    {
54✔
1284
                                        for (int i = 0; i < source.GetLength(rank); i++)
216✔
1285
                                        {
54✔
1286
                                            indices[rank] = i;
54✔
1287
                                            CopyArray(source, destination, indices, rank + 1);
54✔
1288
                                        }
54✔
1289
                                    }
54✔
1290
                                    else
1291
                                    {
18✔
1292
                                        destination.SetValue(source.GetValue(indices), indices);
18✔
1293
                                    }
18✔
1294
                                }
72✔
1295

1296
                                {
18✔
1297
                                    int[] indices = new int[arrayDimensions.Length];
18✔
1298
                                    CopyArray(oldArray, array, indices, 0);
18✔
1299
                                }
18✔
1300
                            }
18✔
1301

1302
                            break;
42✔
1303
                        }
1304

1305
                        default:
1306
                            Dbg.Err($"{node.GetContext()}: Internal error, got invalid mode {parseCommand}");
×
1307
                            array = null; // just to break the unassigned-local-variable
×
1308
                            break;
×
1309
                    }
1310

1311
                    node.ParseArray(array, referencedType, globals, recSettings, startOffset);
4,032✔
1312

1313
                    result = array;
4,032✔
1314
                }
4,032✔
1315

1316
                return result;
4,032✔
1317
            }
1318

1319
            // Special case: Dictionaries
1320
            if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Dictionary<,>))
138,020✔
1321
            {
28,518✔
1322
                foreach (var (parseCommand, node) in orders)
142,590✔
1323
                {
28,518✔
1324
                    bool permitPatch = false;
28,518✔
1325
                    switch (parseCommand)
28,518✔
1326
                    {
1327
                        case ParseCommand.Replace:
1328
                            // If you have a default dict, but specify it in XML, we assume this is a full override. Clear the original list to cut down on GC churn.
1329
                            // TODO: Is some bozo going to store the same "constant" global dict on init, then be surprised when we re-use the dict instead of creating a new one? Detect this and yell about it I guess.
1330
                            // If you are reading this because you're the bozo, [insert angry emoji here], but also feel free to be annoyed that I haven't fixed it yet despite realizing it's a problem. Ping me on Discord, I'll take care of it, sorry 'bout that.
1331
                            if (result != null)
28,230✔
1332
                            {
582✔
1333
                                ((IDictionary)result).Clear();
582✔
1334
                            }
582✔
1335
                            break;
28,230✔
1336

1337
                        case ParseCommand.Patch:
1338
                            if (original != null)
192✔
1339
                            {
168✔
1340
                                permitPatch = true;
168✔
1341
                            }
168✔
1342
                            break;
192✔
1343

1344
                        case ParseCommand.Append:
1345
                            // nothing needs to be done, our existing dupe checking will solve it
1346
                            break;
96✔
1347

1348
                        default:
1349
                            Dbg.Err($"{node.GetContext()}: Internal error, got invalid mode {parseCommand}");
×
1350
                            break;
×
1351
                    }
1352

1353
                    // Dictionary<> handling
1354
                    Type keyType = type.GetGenericArguments()[0];
28,518✔
1355
                    Type valueType = type.GetGenericArguments()[1];
28,518✔
1356

1357
                    var dict = (IDictionary)(result ?? Activator.CreateInstance(type));
28,518✔
1358

1359
                    node.ParseDictionary(dict, keyType, valueType, globals, recSettings, permitPatch);
28,518✔
1360

1361
                    result = dict;
28,518✔
1362
                }
28,518✔
1363

1364
                return result;
28,518✔
1365
            }
1366

1367
            // Special case: HashSet
1368
            if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(HashSet<>))
109,502✔
1369
            {
1,212✔
1370
                foreach (var (parseCommand, node) in orders)
6,060✔
1371
                {
1,212✔
1372
                    bool permitPatch = false;
1,212✔
1373
                    switch (parseCommand)
1,212✔
1374
                    {
1375
                        case ParseCommand.Replace:
1376
                            // If you have a default set, but specify it in XML, we assume this is a full override. Clear the original set to cut down on GC churn.
1377
                            // TODO: Is some bozo going to store the same "constant" global set on init, then be surprised when we re-use the set instead of creating a new one? Detect this and yell about it I guess.
1378
                            // If you are reading this because you're the bozo, [insert angry emoji here], but also feel free to be annoyed that I haven't fixed it yet despite realizing it's a problem. Ping me on Discord, I'll take care of it, sorry 'bout that.
1379
                            if (result != null)
1,020✔
1380
                            {
540✔
1381
                                // Did you know there's no non-generic interface that HashSet<> supports that includes a Clear function?
1382
                                // Fun fact:
1383
                                // That thing I just wrote!
1384
                                var clearFunction = result.GetType().GetMethod("Clear");
540✔
1385
                                clearFunction.Invoke(result, null);
540✔
1386
                            }
540✔
1387
                            break;
1,020✔
1388

1389
                        case ParseCommand.Patch:
1390
                            if (original != null)
96✔
1391
                            {
72✔
1392
                                permitPatch = true;
72✔
1393
                            }
72✔
1394
                            break;
96✔
1395

1396
                        case ParseCommand.Append:
1397
                            // nothing needs to be done, our existing dupe checking will solve it
1398
                            break;
96✔
1399

1400
                        default:
1401
                            Dbg.Err($"{node.GetContext()}: Internal error, got invalid mode {parseCommand}");
×
1402
                            break;
×
1403
                    }
1404

1405
                    Type keyType = type.GetGenericArguments()[0];
1,212✔
1406

1407
                    var set = result ?? Activator.CreateInstance(type);
1,212✔
1408

1409
                    node.ParseHashset(set, keyType, globals, recSettings, permitPatch);
1,212✔
1410

1411
                    result = set;
1,212✔
1412
                }
1,212✔
1413

1414
                return result;
1,212✔
1415
            }
1416

1417
            // Special case: Stack
1418
            if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Stack<>))
108,290✔
1419
            {
66✔
1420
                // Stack<> handling
1421
                // Again, no sensible non-generic interface to use, so we're stuck with reflection
1422

1423
                foreach (var (parseCommand, node) in orders)
330✔
1424
                {
66✔
1425
                    switch (parseCommand)
66✔
1426
                    {
1427
                        case ParseCommand.Replace:
1428
                            // If you have a default stack, but specify it in XML, we assume this is a full override. Clear the original stack to cut down on GC churn.
1429
                            // TODO: Is some bozo going to store the same "constant" global stack on init, then be surprised when we re-use the stack instead of creating a new one? Detect this and yell about it I guess.
1430
                            // If you are reading this because you're the bozo, [insert angry emoji here], but also feel free to be annoyed that I haven't fixed it yet despite realizing it's a problem. Ping me on Discord, I'll take care of it, sorry 'bout that.
1431
                            if (result != null)
66✔
1432
                            {
6✔
1433
                                var clearFunction = result.GetType().GetMethod("Clear");
6✔
1434
                                clearFunction.Invoke(result, null);
6✔
1435
                            }
6✔
1436
                            break;
66✔
1437

1438
                        case ParseCommand.Append:
1439
                            break;
×
1440

1441
                        // There definitely starts being an argument for prepend.
1442

1443
                        default:
1444
                            Dbg.Err($"{node.GetContext()}: Internal error, got invalid mode {parseCommand}");
×
1445
                            break;
×
1446
                    }
1447

1448
                    Type keyType = type.GetGenericArguments()[0];
66✔
1449

1450
                    var set = result ?? Activator.CreateInstance(type);
66✔
1451

1452
                    node.ParseStack(set, keyType, globals, recSettings);
66✔
1453

1454
                    result = set;
66✔
1455
                }
66✔
1456

1457
                return result;
66✔
1458
            }
1459

1460
            // Special case: Queue
1461
            if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Queue<>))
108,224✔
1462
            {
66✔
1463
                // Queue<> handling
1464
                // Again, no sensible non-generic interface to use, so we're stuck with reflection
1465

1466
                foreach (var (parseCommand, node) in orders)
330✔
1467
                {
66✔
1468
                    switch (parseCommand)
66✔
1469
                    {
1470
                        case ParseCommand.Replace:
1471
                            // If you have a default queue, but specify it in XML, we assume this is a full override. Clear the original queue to cut down on GC churn.
1472
                            // TODO: Is some bozo going to store the same "constant" global queue on init, then be surprised when we re-use the queue instead of creating a new one? Detect this and yell about it I guess.
1473
                            // If you are reading this because you're the bozo, [insert angry emoji here], but also feel free to be annoyed that I haven't fixed it yet despite realizing it's a problem. Ping me on Discord, I'll take care of it, sorry 'bout that.
1474
                            if (result != null)
66✔
1475
                            {
6✔
1476
                                var clearFunction = result.GetType().GetMethod("Clear");
6✔
1477
                                clearFunction.Invoke(result, null);
6✔
1478
                            }
6✔
1479
                            break;
66✔
1480

1481
                        case ParseCommand.Append:
1482
                            break;
×
1483

1484
                        // There definitely starts being an argument for prepend.
1485

1486
                        default:
1487
                            Dbg.Err($"{node.GetContext()}: Internal error, got invalid mode {parseCommand}");
×
1488
                            break;
×
1489
                    }
1490

1491
                    Type keyType = type.GetGenericArguments()[0];
66✔
1492

1493
                    var set = result ?? Activator.CreateInstance(type);
66✔
1494

1495
                    node.ParseQueue(set, keyType, globals, recSettings);
66✔
1496

1497
                    result = set;
66✔
1498
                }
66✔
1499

1500
                return result;
66✔
1501
            }
1502

1503
            // Special case: A bucket of tuples
1504
            // These are all basically identical, but AFAIK there's no good way to test them all in a better way.
1505
            if (type.IsGenericType && (
108,158✔
1506
                    type.GetGenericTypeDefinition() == typeof(Tuple<>) ||
108,158✔
1507
                    type.GetGenericTypeDefinition() == typeof(Tuple<,>) ||
108,158✔
1508
                    type.GetGenericTypeDefinition() == typeof(Tuple<,,>) ||
108,158✔
1509
                    type.GetGenericTypeDefinition() == typeof(Tuple<,,,>) ||
108,158✔
1510
                    type.GetGenericTypeDefinition() == typeof(Tuple<,,,,>) ||
108,158✔
1511
                    type.GetGenericTypeDefinition() == typeof(Tuple<,,,,,>) ||
108,158✔
1512
                    type.GetGenericTypeDefinition() == typeof(Tuple<,,,,,,>) ||
108,158✔
1513
                    type.GetGenericTypeDefinition() == typeof(Tuple<,,,,,,,>) ||
108,158✔
1514
                    type.GetGenericTypeDefinition() == typeof(ValueTuple<>) ||
108,158✔
1515
                    type.GetGenericTypeDefinition() == typeof(ValueTuple<,>) ||
108,158✔
1516
                    type.GetGenericTypeDefinition() == typeof(ValueTuple<,,>) ||
108,158✔
1517
                    type.GetGenericTypeDefinition() == typeof(ValueTuple<,,,>) ||
108,158✔
1518
                    type.GetGenericTypeDefinition() == typeof(ValueTuple<,,,,>) ||
108,158✔
1519
                    type.GetGenericTypeDefinition() == typeof(ValueTuple<,,,,,>) ||
108,158✔
1520
                    type.GetGenericTypeDefinition() == typeof(ValueTuple<,,,,,,>) ||
108,158✔
1521
                    type.GetGenericTypeDefinition() == typeof(ValueTuple<,,,,,,,>)
108,158✔
1522
                    ))
108,158✔
1523
            {
1,284✔
1524
                foreach (var (parseCommand, node) in orders)
6,420✔
1525
                {
1,284✔
1526
                    switch (parseCommand)
1,284✔
1527
                    {
1528
                        case ParseCommand.Replace:
1529
                            // easy, done
1530
                            break;
1,284✔
1531

1532
                        default:
1533
                            Dbg.Err($"{node.GetContext()}: Internal error, got invalid mode {parseCommand}");
×
1534
                            break;
×
1535
                    }
1536

1537
                    int expectedCount = type.GenericTypeArguments.Length;
1,284✔
1538
                    object[] parameters = new object[expectedCount];
1,284✔
1539

1540
                    node.ParseTuple(parameters, type, fieldInfo?.GetCustomAttribute<System.Runtime.CompilerServices.TupleElementNamesAttribute>()?.TransformNames, globals, recSettings);
1,284✔
1541

1542
                    // construct!
1543
                    result = Activator.CreateInstance(type, parameters);
1,284✔
1544
                }
1,284✔
1545

1546
                return result;
1,284✔
1547
            }
1548

1549
            if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>))
106,874✔
1550
            {
168✔
1551
                // Intercept and handle appropriately
1552
                // We've already handled the `null` attribute, so we know it isn't null - just go ahead and deal with it by passing it up to a parent
1553
                // I'm not sure if this is the right approach?
1554
                result = ParseElement(nodes, type.GetGenericArguments()[0], original, globals, recSettings);
168✔
1555
                return result;
168✔
1556
            }
1557

1558
            // At this point, we're either a class or a struct, and we need to do the reflection thing
1559

1560
            // If we have refs, something has gone wrong; we should never be doing reflection inside a Record system.
1561
            // This is a really ad-hoc way of testing this and should be fixed.
1562
            // One big problem here is that I'm OK with security vulnerabilities in dec xmls. Those are either supplied by the developer or by mod authors who are intended to have full code support anyway.
1563
            // I'm less OK with security vulnerabilities in save files. Nobody expects a savefile can compromise their system.
1564
            // And the full reflection system is probably impossible to secure, whereas the Record system should be secureable.
1565
            if (!globals.allowReflection)
106,706✔
1566
            {
42✔
1567
                // just pick the first node to get something to go on
1568
                Dbg.Err($"{orders[0].node.GetContext()}: Falling back to reflection within a Record system while parsing a {type}; this is currently not allowed for security reasons. Either you shouldn't be trying to serialize this, or it should implement Dec.IRecorder (https://zorbathut.github.io/dec/release/documentation/serialization.html), or you need a Dec.Converter (https://zorbathut.github.io/dec/release/documentation/custom.html)");
42✔
1569
                return result;
42✔
1570
            }
1571

1572
            foreach (var (parseCommand, node) in orders)
537,292✔
1573
            {
108,698✔
1574
                if (!isRootDec)
108,698✔
1575
                {
52,886✔
1576
                    switch (parseCommand)
52,886✔
1577
                    {
1578
                        case ParseCommand.Patch:
1579
                            // easy, done
1580
                            break;
52,886✔
1581

1582
                        default:
1583
                            Dbg.Err($"{node.GetContext()}: Internal error, got invalid mode {parseCommand}");
×
1584
                            break;
×
1585
                    }
1586
                }
52,886✔
1587
                else
1588
                {
55,812✔
1589
                    if (parseCommand != ParseCommand.Patch)
55,812✔
1590
                    {
×
1591
                        Dbg.Err($"{node.GetContext()}: Mode provided for root Dec; this is currently not supported in any form");
×
1592
                    }
×
1593
                }
55,812✔
1594

1595
                // If we haven't been given a generic class from our parent, go ahead and init to defaults
1596
                if (result == null && recordableBuffered != null)
108,698✔
1597
                {
18✔
1598
                    result = recordableBuffered;
18✔
1599
                }
18✔
1600

1601
                if (result == null)
108,698✔
1602
                {
21,198✔
1603
                    // okay fine
1604
                    result = type.CreateInstanceSafe("object", node);
21,198✔
1605

1606
                    if (result == null)
21,198✔
1607
                    {
96✔
1608
                        // error already reported
1609
                        return result;
96✔
1610
                    }
1611
                }
21,102✔
1612

1613
                node.ParseReflection(result, globals, recSettings);
108,602✔
1614
            }
108,602✔
1615

1616
            // Set up our index fields; this has to happen last in case we're a struct
1617
            Index.Register(ref result);
106,568✔
1618

1619
            return result;
106,568✔
1620
        }
1,878,408✔
1621

1622
        internal static object ParseString(string text, Type type, object original, Context context)
1623
        {
979,132✔
1624
            // Special case: Converter override
1625
            // This is redundant if we're being called from ParseElement, but we aren't always.
1626
            if (ConverterFor(type) is Converter converter)
979,132✔
1627
            {
54✔
1628
                object result = original;
54✔
1629

1630
                try
1631
                {
54✔
1632
                    // string converter
1633
                    if (converter is ConverterString converterString)
54✔
1634
                    {
54✔
1635
                        // context might be null; that's OK at the moment
1636
                        try
1637
                        {
54✔
1638
                            result = converterString.ReadObj(text, context);
54✔
1639
                        }
54✔
1640
                        catch (Exception e)
×
1641
                        {
×
1642
                            Dbg.Ex(new ConverterReadException(context, converter, e));
×
1643

1644
                            result = GenerateResultFallback(result, type);
×
1645
                        }
×
1646
                    }
54✔
1647
                    else if (converter is ConverterRecord converterRecord)
×
1648
                    {
×
1649
                        // string parsing really doesn't apply here, we can't get a full Recorder context out anymore
1650
                        // in theory this could be done with RecordAsThis() but I'm just going to skip it for now
1651
                        Dbg.Err($"{context}: Attempt to string-parse with a ConverterRecord, this is currently not supported, contact developers if you need this feature");
×
1652
                    }
×
1653
                    else if (converter is ConverterFactory converterFactory)
×
1654
                    {
×
1655
                        // string parsing really doesn't apply here, we can't get a full Recorder context out anymore
1656
                        // in theory this could be done with RecordAsThis() but I'm just going to skip it for now
1657
                        Dbg.Err($"{context}: Attempt to string-parse with a ConverterFactory, this is currently not supported, contact developers if you need this feature");
×
1658
                    }
×
1659
                    else
1660
                    {
×
1661
                        Dbg.Err($"Somehow ended up with an unsupported converter {converter.GetType()}");
×
1662
                    }
×
1663
                }
54✔
1664
                catch (Exception e)
×
1665
                {
×
1666
                    Dbg.Ex(e);
×
1667
                }
×
1668

1669
                return result;
54✔
1670
            }
1671

1672
            // Special case: decs
1673
            if (typeof(Dec).IsAssignableFrom(type))
979,078✔
1674
            {
57,942✔
1675
                if (text == "" || text == null)
57,942✔
1676
                {
48,834✔
1677
                    // you reference nothing, you get the null (even if this isn't a specified type; null is null, after all)
1678
                    return null;
48,834✔
1679
                }
1680
                else
1681
                {
9,108✔
1682
                    if (type.GetDecRootType() == null)
9,108✔
1683
                    {
96✔
1684
                        Dbg.Err($"{context}: Non-hierarchy decs cannot be used as references");
96✔
1685
                        return null;
96✔
1686
                    }
1687

1688
                    Dec result = Database.Get(type, text);
9,012✔
1689
                    if (result == null)
9,012✔
1690
                    {
102✔
1691
                        if (UtilMisc.ValidateDecName(text, context))
102✔
1692
                        {
102✔
1693
                            Dbg.Err($"{context}: Couldn't find {type} named `{text}`");
102✔
1694
                        }
102✔
1695

1696
                        // If we're an invalid name, we already spat out the error
1697
                    }
102✔
1698
                    return result;
9,012✔
1699
                }
1700
            }
1701

1702
            // Special case: types
1703
            if (type == typeof(Type))
921,136✔
1704
            {
498,584✔
1705
                if (text == "")
498,584✔
1706
                {
×
1707
                    return null;
×
1708
                }
1709

1710
                return UtilType.ParseDecFormatted(text, context);
498,584✔
1711
            }
1712

1713
            // Various non-composite-type special-cases
1714
            if (text != "")
422,552✔
1715
            {
422,552✔
1716
                // If we've got text, treat us as an object of appropriate type
1717
                try
1718
                {
422,552✔
1719
                    if (type == typeof(float))
422,552✔
1720
                    {
33,888✔
1721
                        // first check the various strings, case-insensitive
1722
                        if (String.Compare(text, "nan", true) == 0)
33,888✔
1723
                        {
114✔
1724
                            return float.NaN;
114✔
1725
                        }
1726

1727
                        if (String.Compare(text, "infinity", true) == 0)
33,774✔
1728
                        {
48✔
1729
                            return float.PositiveInfinity;
48✔
1730
                        }
1731

1732
                        if (String.Compare(text, "-infinity", true) == 0)
33,726✔
1733
                        {
48✔
1734
                            return float.NegativeInfinity;
48✔
1735
                        }
1736

1737
                        if (text.StartsWith("nanbox", StringComparison.CurrentCultureIgnoreCase))
33,678✔
1738
                        {
114✔
1739
                            const int expectedFloatSize = 6 + 8;
1740

1741
                            if (type == typeof(float) && text.Length != expectedFloatSize)
114✔
1742
                            {
×
1743
                                Dbg.Err($"{context}: Found nanboxed value without the expected number of characters, expected {expectedFloatSize} but got {text.Length}");
×
1744
                                return float.NaN;
×
1745
                            }
1746

1747
                            int number = Convert.ToInt32(text.Substring(6), 16);
114✔
1748
                            return BitConverter.Int32BitsToSingle(number);
114✔
1749
                        }
1750
                    }
33,564✔
1751

1752
                    if (type == typeof(double))
422,228✔
1753
                    {
3,408✔
1754
                        // first check the various strings, case-insensitive
1755
                        if (String.Compare(text, "nan", true) == 0)
3,408✔
1756
                        {
2,178✔
1757
                            return double.NaN;
2,178✔
1758
                        }
1759

1760
                        if (String.Compare(text, "infinity", true) == 0)
1,230✔
1761
                        {
48✔
1762
                            return double.PositiveInfinity;
48✔
1763
                        }
1764

1765
                        if (String.Compare(text, "-infinity", true) == 0)
1,182✔
1766
                        {
48✔
1767
                            return double.NegativeInfinity;
48✔
1768
                        }
1769

1770
                        if (text.StartsWith("nanbox", StringComparison.CurrentCultureIgnoreCase))
1,134✔
1771
                        {
90✔
1772
                            const int expectedDoubleSize = 6 + 16;
1773

1774
                            if (type == typeof(double) && text.Length != expectedDoubleSize)
90✔
1775
                            {
×
1776
                                Dbg.Err($"{context}: Found nanboxed value without the expected number of characters, expected {expectedDoubleSize} but got {text.Length}");
×
1777
                                return double.NaN;
×
1778
                            }
1779

1780
                            long number = Convert.ToInt64(text.Substring(6), 16);
90✔
1781
                            return BitConverter.Int64BitsToDouble(number);
90✔
1782
                        }
1783
                    }
1,044✔
1784

1785
                    return TypeDescriptor.GetConverter(type).ConvertFromString(text);
419,864✔
1786
                }
1787
                catch (System.Exception e)  // I would normally not catch System.Exception, but TypeConverter is wrapping FormatException in an Exception for some reason
216✔
1788
                {
216✔
1789
                    Dbg.Err($"{context}: {e.ToString()}");
216✔
1790
                    return original;
216✔
1791
                }
1792
            }
1793
            else if (type == typeof(string))
×
1794
            {
×
1795
                // If we don't have text, and we're a string, return ""
1796
                return "";
×
1797
            }
1798
            else
1799
            {
×
1800
                // If we don't have text, and we've fallen down to this point, that's an error (and return original value I guess)
1801
                Dbg.Err($"{context}: Empty field provided for type {type}");
×
1802
                return original;
×
1803
            }
1804
        }
979,132✔
1805

1806
        internal static Type TypeSystemRuntimeType = Type.GetType("System.RuntimeType");
12✔
1807
        internal static void ComposeElement(WriterNode node, object value, Type fieldType, FieldInfo fieldInfo = null, bool isRootDec = false, bool asThis = false)
1808
        {
1,821,762✔
1809
            // Verify our Shared flags as the *very* first step to ensure nothing gets past us.
1810
            // In theory this should be fine with Flexible; Flexible only happens on an outer wrapper that was shared, and therefore was null, and therefore this is default also
1811
            bool canBeShared = fieldType.CanBeShared();
1,821,762✔
1812
            if (node.RecorderSettings.shared == Recorder.Settings.Shared.Allow && !asThis)
1,821,762✔
1813
            {
964,188✔
1814
                // If this is an `asThis` parameter, then we may not be writing the field type it looks like, and we're just going to trust that they're doing something sensible.
1815
                if (!canBeShared)
964,188✔
1816
                {
264✔
1817
                    // If shared, make sure our type is appropriate for sharing
1818
                    // this really needs the recorder name and the field name too
1819
                    Dbg.Wrn($"Value type `{fieldType}` tagged as Shared in recorder, this is meaningless but harmless");
264✔
1820
                }
264✔
1821
            }
964,188✔
1822

1823
            // Handle Dec types, if this isn't a root (otherwise we'd just reference ourselves and that's kind of pointless)
1824
            if (!isRootDec && value is Dec)
1,821,762✔
1825
            {
4,890✔
1826
                // Dec types are special in a few ways.
1827
                // First off, they don't include their type data, because we assume it's of a type provided by the structure.
1828
                // Second, we represent null values as an empty string, not as a null tag.
1829
                // (We'll accept the null tag if you insist, we just have a cleaner special case.)
1830
                // Null tag stuff is done further down, in the null check.
1831

1832
                var rootType = value.GetType().GetDecRootType();
4,890✔
1833
                if (!rootType.IsAssignableFrom(fieldType))
4,890✔
1834
                {
36✔
1835
                    // The user has a Dec.Dec or similar, and it has a Dec assigned to it.
1836
                    // This is a bit weird and is something we're not happy with; this means we need to include the Dec type along with it.
1837
                    // But we're OK with that, honestly. We just do that.
1838
                    // If you're saving something like this you don't get to rename Dec classes later on, but, hey, deal with it.
1839
                    // We do, however, tag it with the root type, not the derived type; this is the most general type that still lets us search things in the future.
1840
                    node.TagClass(rootType);
36✔
1841
                }
36✔
1842

1843
                node.WriteDec(value as Dec);
4,890✔
1844

1845
                return;
4,890✔
1846
            }
1847

1848
            // Everything represents "null" with an explicit XML tag, so let's just do that
1849
            // Maybe at some point we want to special-case this for the empty Dec link
1850
            if (value == null)
1,816,872✔
1851
            {
442,442✔
1852
                if (typeof(Dec).IsAssignableFrom(fieldType))
442,442✔
1853
                {
40,860✔
1854
                    node.WriteDec(null);
40,860✔
1855
                }
40,860✔
1856
                else
1857
                {
401,582✔
1858
                    node.WriteExplicitNull();
401,582✔
1859
                }
401,582✔
1860

1861
                return;
442,442✔
1862
            }
1863

1864

1865
            if (node.AllowDecPath)
1,374,430✔
1866
            {
958,164✔
1867
                // Try to snag a Dec path
1868
                var decPath = Database.DecPathLookup.TryGetValue(value);
958,164✔
1869

1870
                if (decPath != null)
958,164✔
1871
                {
666✔
1872
                    node.WriteDecPathRef(value);
666✔
1873
                    return;
666✔
1874
                }
1875
            }
957,498✔
1876

1877
            var valType = value.GetType();
1,373,764✔
1878

1879
            // This is our value's type, but we may need a little bit of tinkering to make it useful.
1880
            // The current case I know of is System.RuntimeType, which appears if we call .GetType() on a Type.
1881
            // I assume there is a complicated internal reason for this; good news, we can ignore it and just pretend it's a System.Type.
1882
            // Bad news: it's actually really hard to detect this case because System.RuntimeType is private.
1883
            // That's why we have the annoying `static` up above.
1884
            if (valType == TypeSystemRuntimeType)
1,373,764✔
1885
            {
534✔
1886
                valType = typeof(Type);
534✔
1887
            }
534✔
1888

1889
            // Do all our unreferencables first
1890
            bool unreferenceableComplete = false;
1,373,764✔
1891

1892
            if (valType.IsPrimitive)
1,373,764✔
1893
            {
327,110✔
1894
                node.WritePrimitive(value);
327,110✔
1895

1896
                unreferenceableComplete = true;
327,110✔
1897
            }
327,110✔
1898
            else if (value is System.Enum)
1,046,654✔
1899
            {
558✔
1900
                node.WriteEnum(value);
558✔
1901

1902
                unreferenceableComplete = true;
558✔
1903
            }
558✔
1904
            else if (value is string)
1,046,096✔
1905
            {
34,200✔
1906
                node.WriteString(value as string);
34,200✔
1907

1908
                unreferenceableComplete = true;
34,200✔
1909
            }
34,200✔
1910
            else if (value is Type)
1,011,896✔
1911
            {
534✔
1912
                node.WriteType(value as Type);
534✔
1913

1914
                unreferenceableComplete = true;
534✔
1915
            }
534✔
1916

1917
            // Check to see if we should make this into a ref (yes, even if we're not tagged as Shared)
1918
            // Do this *before* we do the class tagging, otherwise we may add ref/class tags to a single node, which is invalid.
1919
            // Note that it's important we don't write a reference if we had an unreferenceable; it's unnecessarily slow and some of our writer types don't support it.
1920
            if (Util.CanBeShared(valType) && !asThis)
1,373,764✔
1921
            {
975,638✔
1922
                if (node.WriteReference(value, node.Path))
975,638✔
1923
                {
182,538✔
1924
                    // The ref system has set up the appropriate tagging, so we're done!
1925
                    return;
182,538✔
1926
                }
1927

1928
                // If we support references, then this object has not previously shown up in the reference system; keep going so we finish serializing it.
1929
                // If we don't support references at all then obviously we *really* need to finish serializing it.
1930
            }
793,100✔
1931

1932
            // If we have a type that isn't the expected type, tag it. We may need this even for unreferencable value types because everything fits in an `object`.
1933
            {
1,191,226✔
1934
                bool tagClass = valType != fieldType;
1,191,226✔
1935
                if (fieldType.IsConstructedGenericType && fieldType.GetGenericTypeDefinition() == typeof(Nullable<>))
1,191,226✔
1936
                {
468✔
1937
                    // If we're a Nullable<> then we know the type, so we unwrap it a layer
1938
                    tagClass = valType != fieldType.GetGenericArguments()[0];
468✔
1939
                }
468✔
1940

1941
                if (tagClass)
1,191,226✔
1942
                {
3,560✔
1943
                    if (asThis)
3,560✔
1944
                    {
24✔
1945
                        Dbg.Err($"RecordAsThis() call attempted to add a class tag, which is currently not allowed; AsThis() calls must not be polymorphic (ask the devs for chained class tags if this is a thing you need)");
24✔
1946
                        // . . . I guess we just keep going?
1947
                    }
24✔
1948
                    else
1949
                    {
3,536✔
1950
                        node.TagClass(valType);
3,536✔
1951
                    }
3,536✔
1952
                }
3,560✔
1953
            }
1,191,226✔
1954

1955

1956
            // Did we actually write our node type? Alright, we're done.
1957
            if (unreferenceableComplete)
1,191,226✔
1958
            {
362,402✔
1959
                return;
362,402✔
1960
            }
1961

1962
            // Now we have things that *could* be references, but aren't.
1963

1964
            if (node.AllowCloning && UtilType.CanBeCloneCopied(valType))
828,824✔
1965
            {
24✔
1966
                node.WriteCloneCopy(value);
24✔
1967

1968
                return;
24✔
1969
            }
1970

1971
            if (valType == typeof(byte[]))
828,800✔
1972
            {
702✔
1973
                node.WriteByteArray(value as byte[]);
702✔
1974

1975
                return;
702✔
1976
            }
1977

1978
            if (valType.IsArray)
828,098✔
1979
            {
3,636✔
1980
                node.WriteArray(value as Array);
3,636✔
1981

1982
                return;
3,636✔
1983
            }
1984

1985
            if (valType.IsGenericType && valType.GetGenericTypeDefinition() == typeof(List<>))
824,462✔
1986
            {
36,816✔
1987
                node.WriteList(value as IList);
36,816✔
1988

1989
                return;
36,816✔
1990
            }
1991

1992
            if (valType.IsGenericType && valType.GetGenericTypeDefinition() == typeof(Dictionary<,>))
787,646✔
1993
            {
14,376✔
1994
                node.WriteDictionary(value as IDictionary);
14,376✔
1995

1996
                return;
14,376✔
1997
            }
1998

1999
            if (valType.IsGenericType && valType.GetGenericTypeDefinition() == typeof(HashSet<>))
773,270✔
2000
            {
1,410✔
2001
                node.WriteHashSet(value as IEnumerable);
1,410✔
2002

2003
                return;
1,410✔
2004
            }
2005

2006
            if (valType.IsGenericType && valType.GetGenericTypeDefinition() == typeof(Queue<>))
771,860✔
2007
            {
186✔
2008
                node.WriteQueue(value as IEnumerable);
186✔
2009

2010
                return;
186✔
2011
            }
2012

2013
            if (valType.IsGenericType && valType.GetGenericTypeDefinition() == typeof(Stack<>))
771,674✔
2014
            {
198✔
2015
                node.WriteStack(value as IEnumerable);
198✔
2016

2017
                return;
198✔
2018
            }
2019

2020
            if (valType.IsGenericType && (
771,476✔
2021
                    valType.GetGenericTypeDefinition() == typeof(Tuple<>) ||
771,476✔
2022
                    valType.GetGenericTypeDefinition() == typeof(Tuple<,>) ||
771,476✔
2023
                    valType.GetGenericTypeDefinition() == typeof(Tuple<,,>) ||
771,476✔
2024
                    valType.GetGenericTypeDefinition() == typeof(Tuple<,,,>) ||
771,476✔
2025
                    valType.GetGenericTypeDefinition() == typeof(Tuple<,,,,>) ||
771,476✔
2026
                    valType.GetGenericTypeDefinition() == typeof(Tuple<,,,,,>) ||
771,476✔
2027
                    valType.GetGenericTypeDefinition() == typeof(Tuple<,,,,,,>) ||
771,476✔
2028
                    valType.GetGenericTypeDefinition() == typeof(Tuple<,,,,,,,>)
771,476✔
2029
                ))
771,476✔
2030
            {
396✔
2031
                node.WriteTuple(value, fieldInfo?.GetCustomAttribute<System.Runtime.CompilerServices.TupleElementNamesAttribute>());
396✔
2032

2033
                return;
396✔
2034
            }
2035

2036
            if (valType.IsGenericType && (
771,080✔
2037
                    valType.GetGenericTypeDefinition() == typeof(ValueTuple<>) ||
771,080✔
2038
                    valType.GetGenericTypeDefinition() == typeof(ValueTuple<,>) ||
771,080✔
2039
                    valType.GetGenericTypeDefinition() == typeof(ValueTuple<,,>) ||
771,080✔
2040
                    valType.GetGenericTypeDefinition() == typeof(ValueTuple<,,,>) ||
771,080✔
2041
                    valType.GetGenericTypeDefinition() == typeof(ValueTuple<,,,,>) ||
771,080✔
2042
                    valType.GetGenericTypeDefinition() == typeof(ValueTuple<,,,,,>) ||
771,080✔
2043
                    valType.GetGenericTypeDefinition() == typeof(ValueTuple<,,,,,,>) ||
771,080✔
2044
                    valType.GetGenericTypeDefinition() == typeof(ValueTuple<,,,,,,,>)
771,080✔
2045
                ))
771,080✔
2046
            {
636✔
2047
                node.WriteValueTuple(value, fieldInfo?.GetCustomAttribute<System.Runtime.CompilerServices.TupleElementNamesAttribute>());
636✔
2048

2049
                return;
636✔
2050
            }
2051

2052
            if (value is IRecordable
770,444✔
2053
                && (!(value is IConditionalRecordable) || (value as IConditionalRecordable).ShouldRecord(node.UserSettings)))
770,444✔
2054
            {
702,576✔
2055
                node.WriteRecord(value as IRecordable);
702,576✔
2056

2057
                return;
702,576✔
2058
            }
2059

2060
            {
67,868✔
2061
                // Look for a converter; that's the only way to handle this before we fall back to reflection
2062
                var converter = Serialization.ConverterFor(valType);
67,868✔
2063
                if (converter != null)
67,868✔
2064
                {
1,026✔
2065
                    node.WriteConvertible(converter, value);
1,026✔
2066
                    return;
1,026✔
2067
                }
2068
            }
66,842✔
2069

2070
            if (!node.AllowReflection)
66,842✔
2071
            {
60✔
2072
                Dbg.Err($"Couldn't find a composition method for type {valType}; either you shouldn't be trying to serialize it, or it should implement Dec.IRecorder (https://zorbathut.github.io/dec/release/documentation/serialization.html), or you need a Dec.Converter (https://zorbathut.github.io/dec/release/documentation/custom.html)");
60✔
2073
                node.WriteError();
60✔
2074
                return;
60✔
2075
            }
2076

2077
            // We absolutely should not be doing reflection when in recorder mode; that way lies madness.
2078

2079
            foreach (var field in valType.GetSerializableFieldsFromHierarchy())
913,566✔
2080
            {
356,610✔
2081
                ComposeElement(node.CreateReflectionChild(field, node.RecorderSettings), field.GetValue(value), field.FieldType, fieldInfo: field);
356,610✔
2082
            }
356,610✔
2083

2084
            return;
66,782✔
2085
        }
1,821,762✔
2086

2087
        internal static void Clear()
2088
        {
35,652✔
2089
            ConverterInitialized = false;
35,652✔
2090
            ConverterObjects = new System.Collections.Concurrent.ConcurrentDictionary<Type, Converter>();
35,652✔
2091
            ConverterGenericPrototypes = new System.Collections.Concurrent.ConcurrentDictionary<Type, Type>();
35,652✔
2092
        }
35,652✔
2093
    }
2094
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc